First web server route, use of preact for raylib html version
Some checks failed
build-story-editor / build_linux (push) Has been cancelled
build-story-editor / build_win32 (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

This commit is contained in:
anthony@rabine.fr 2025-01-09 11:13:56 +01:00
parent b2fdf5c03b
commit 432d72c80c
13 changed files with 933 additions and 189 deletions

View file

@ -30,7 +30,7 @@ MainWindow::MainWindow()
, m_nodeEditorWindow(*this)
, m_libraryWindow(*this, m_libraryManager)
, m_player(*this)
, m_webServer(m_libraryManager)
{
// VM Initialize
m_chip32_ctx.stack_size = 512;

View file

@ -1,175 +1,68 @@
#include <string.h>
#include "json.hpp"
#include "web_server.h"
#define DOCUMENT_ROOT "."
#define PORT "8081"
class FooHandler : public CivetHandler
bool HandlerBase::Reply(struct mg_connection *conn, const nlohmann::json &json)
{
public:
bool
handleGet(CivetServer *server, struct mg_connection *conn)
{
/* Handler may access the request info using mg_get_request_info */
const struct mg_request_info *req_info = mg_get_request_info(conn);
std::stringstream ss;
mg_printf(conn,
"HTTP/1.1 200 OK\r\nContent-Type: "
"text/html\r\nConnection: close\r\n\r\n");
std::string data = json.dump();
mg_printf(conn, "<html><body>\n");
mg_printf(conn, "<h2>This is the Foo GET handler!!!</h2>\n");
mg_printf(conn,
"<p>The request was:<br><pre>%s %s HTTP/%s</pre></p>\n",
req_info->request_method,
req_info->request_uri,
req_info->http_version);
mg_printf(conn, "</body></html>\n");
/* Send HTTP message header (+1 for \n) */
mg_send_http_ok(conn, "application/json; charset=utf-8", data.size() + 1);
return true;
}
bool
handlePost(CivetServer *server, struct mg_connection *conn)
{
/* Handler may access the request info using mg_get_request_info */
const struct mg_request_info *req_info = mg_get_request_info(conn);
long long rlen, wlen;
long long nlen = 0;
long long tlen = req_info->content_length;
char buf[1024];
/* Send HTTP message content */
mg_write(conn, data.c_str(), data.size());
mg_printf(conn,
"HTTP/1.1 200 OK\r\nContent-Type: "
"text/html\r\nConnection: close\r\n\r\n");
/* Add a newline. This is not required, but the result is more
* human-readable in a debugger. */
mg_write(conn, "\n", 1);
return true;
}
mg_printf(conn, "<html><body>\n");
mg_printf(conn, "<h2>This is the Foo POST handler!!!</h2>\n");
mg_printf(conn,
"<p>The request was:<br><pre>%s %s HTTP/%s</pre></p>\n",
req_info->request_method,
req_info->request_uri,
req_info->http_version);
mg_printf(conn, "<p>Content Length: %li</p>\n", (long)tlen);
mg_printf(conn, "<pre>\n");
bool LibraryManagerHandler::handleGet(CivetServer *server, struct mg_connection *conn)
{
const struct mg_request_info *req_info = mg_get_request_info(conn);
nlohmann::json json;
while (nlen < tlen) {
rlen = tlen - nlen;
if (rlen > sizeof(buf)) {
rlen = sizeof(buf);
}
rlen = mg_read(conn, buf, (size_t)rlen);
if (rlen <= 0) {
break;
}
wlen = mg_write(conn, buf, (size_t)rlen);
if (wlen != rlen) {
break;
}
nlen += wlen;
}
mg_printf(conn, "\n</pre>\n");
mg_printf(conn, "</body></html>\n");
return true;
}
#define fopen_recursive fopen
bool
handlePut(CivetServer *server, struct mg_connection *conn)
for (auto &s : m_libraryManager)
{
/* Handler may access the request info using mg_get_request_info */
const struct mg_request_info *req_info = mg_get_request_info(conn);
long long rlen, wlen;
long long nlen = 0;
long long tlen = req_info->content_length;
FILE * f;
char buf[1024];
int fail = 0;
nlohmann::json story = {
{"title", s->GetName() },
{"uuid", s->GetUuid() },
};
json.push_back(story);
#ifdef _WIN32
_snprintf(buf, sizeof(buf), "D:\\somewhere\\%s\\%s", req_info->remote_user, req_info->local_uri);
buf[sizeof(buf)-1] = 0;
if (strlen(buf)>255) {
/* Windows will not work with path > 260 (MAX_PATH), unless we use
* the unicode API. However, this is just an example code: A real
* code will probably never store anything to D:\\somewhere and
* must be adapted to the specific needs anyhow. */
fail = 1;
f = NULL;
} else {
f = fopen_recursive(buf, "wb");
}
#else
snprintf(buf, sizeof(buf), "~/somewhere/%s/%s", req_info->remote_user, req_info->local_uri);
buf[sizeof(buf)-1] = 0;
if (strlen(buf)>1020) {
/* The string is too long and probably truncated. Make sure an
* UTF-8 string is never truncated between the UTF-8 code bytes.
* This example code must be adapted to the specific needs. */
fail = 1;
f = NULL;
} else {
f = fopen_recursive(buf, "w");
}
#endif
if (!f) {
fail = 1;
} else {
while (nlen < tlen) {
rlen = tlen - nlen;
if (rlen > sizeof(buf)) {
rlen = sizeof(buf);
}
rlen = mg_read(conn, buf, (size_t)rlen);
if (rlen <= 0) {
fail = 1;
break;
}
wlen = fwrite(buf, 1, (size_t)rlen, f);
if (wlen != rlen) {
fail = 1;
break;
}
nlen += wlen;
}
fclose(f);
}
if (fail) {
mg_printf(conn,
"HTTP/1.1 409 Conflict\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n");
} else {
mg_printf(conn,
"HTTP/1.1 201 Created\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n");
}
return true;
}
};
return Reply(conn, json);
}
static const char *options[] = {
"document_root", DOCUMENT_ROOT,
"listening_ports", PORT,
0
"access_control_allow_origin", "*",
"access_control_allow_methods", "GET, POST, PUT, DELETE, OPTIONS",
"access_control_allow_headers", "Content-Type",
0
};
WebServer::WebServer()
: m_server(options)
static const std::string gRestBase = "/api/v1";
WebServer::WebServer(LibraryManager &libraryManager)
: m_libraryManager(libraryManager)
, m_server(options)
, m_libraryManagerHandler(libraryManager)
{
mg_init_library(0);
FooHandler h_foo;
m_server.addHandler("**.foo", h_foo);
printf("Browse files at http://localhost:%s/\n", PORT);
m_server.addHandler(gRestBase + "/library/list", m_libraryManagerHandler);
// printf("Browse files at http://localhost:%s/\n", PORT);
}
WebServer::~WebServer()

View file

@ -1,16 +1,169 @@
#pragma once
#include "CivetServer.h"
#include "library_manager.h"
class HandlerBase : public CivetHandler
{
public:
protected:
// Utility methods for children
bool Reply(struct mg_connection *conn, const nlohmann::json &json);
};
class LibraryManagerHandler : public HandlerBase
{
public:
LibraryManagerHandler(LibraryManager &libraryManager)
: m_libraryManager(libraryManager)
{
}
bool handleGet(CivetServer *server, struct mg_connection *conn);
bool
handlePost(CivetServer *server, struct mg_connection *conn)
{
/* Handler may access the request info using mg_get_request_info */
const struct mg_request_info *req_info = mg_get_request_info(conn);
long long rlen, wlen;
long long nlen = 0;
long long tlen = req_info->content_length;
char buf[1024];
mg_printf(conn,
"HTTP/1.1 200 OK\r\nContent-Type: "
"text/html\r\nConnection: close\r\n\r\n");
mg_printf(conn, "<html><body>\n");
mg_printf(conn, "<h2>This is the Foo POST handler!!!</h2>\n");
mg_printf(conn,
"<p>The request was:<br><pre>%s %s HTTP/%s</pre></p>\n",
req_info->request_method,
req_info->request_uri,
req_info->http_version);
mg_printf(conn, "<p>Content Length: %li</p>\n", (long)tlen);
mg_printf(conn, "<pre>\n");
while (nlen < tlen) {
rlen = tlen - nlen;
if (rlen > sizeof(buf)) {
rlen = sizeof(buf);
}
rlen = mg_read(conn, buf, (size_t)rlen);
if (rlen <= 0) {
break;
}
wlen = mg_write(conn, buf, (size_t)rlen);
if (wlen != rlen) {
break;
}
nlen += wlen;
}
mg_printf(conn, "\n</pre>\n");
mg_printf(conn, "</body></html>\n");
return true;
}
#define fopen_recursive fopen
bool
handlePut(CivetServer *server, struct mg_connection *conn)
{
/* Handler may access the request info using mg_get_request_info */
const struct mg_request_info *req_info = mg_get_request_info(conn);
long long rlen, wlen;
long long nlen = 0;
long long tlen = req_info->content_length;
FILE * f;
char buf[1024];
int fail = 0;
#ifdef _WIN32
_snprintf(buf, sizeof(buf), "D:\\somewhere\\%s\\%s", req_info->remote_user, req_info->local_uri);
buf[sizeof(buf)-1] = 0;
if (strlen(buf)>255) {
/* Windows will not work with path > 260 (MAX_PATH), unless we use
* the unicode API. However, this is just an example code: A real
* code will probably never store anything to D:\\somewhere and
* must be adapted to the specific needs anyhow. */
fail = 1;
f = NULL;
} else {
f = fopen_recursive(buf, "wb");
}
#else
snprintf(buf, sizeof(buf), "~/somewhere/%s/%s", req_info->remote_user, req_info->local_uri);
buf[sizeof(buf)-1] = 0;
if (strlen(buf)>1020) {
/* The string is too long and probably truncated. Make sure an
* UTF-8 string is never truncated between the UTF-8 code bytes.
* This example code must be adapted to the specific needs. */
fail = 1;
f = NULL;
} else {
f = fopen_recursive(buf, "w");
}
#endif
if (!f) {
fail = 1;
} else {
while (nlen < tlen) {
rlen = tlen - nlen;
if (rlen > sizeof(buf)) {
rlen = sizeof(buf);
}
rlen = mg_read(conn, buf, (size_t)rlen);
if (rlen <= 0) {
fail = 1;
break;
}
wlen = fwrite(buf, 1, (size_t)rlen, f);
if (wlen != rlen) {
fail = 1;
break;
}
nlen += wlen;
}
fclose(f);
}
if (fail) {
mg_printf(conn,
"HTTP/1.1 409 Conflict\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n");
} else {
mg_printf(conn,
"HTTP/1.1 201 Created\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n");
}
return true;
}
private:
LibraryManager &m_libraryManager;
};
class WebServer : public CivetHandler
{
public:
WebServer();
WebServer(LibraryManager &libraryManager);
~WebServer();
private:
LibraryManager &m_libraryManager;
CivetServer m_server;
LibraryManagerHandler m_libraryManagerHandler;
};

View file

@ -0,0 +1,39 @@
import apiClient from './classes/api-client.js'
import eventBus from './classes/event-bus.js';
import storage from './classes/storage.js';
import { render } from 'preact';
import { html } from 'htm/preact';
import TopMenu from './components/TopMenu.js'
import ParametersDialog from './components/ParametersDialog.js';
export function App() {
this.params = storage.getItem('server') || {
serverUrl: '127.0.0.1',
serverPort: 8081,
};
storage.setItem('server', this.params);
// try to connect to the server
apiClient.setBaseUrl(`http://${this.params.serverUrl}:${this.params.serverPort}/api/v1`);
apiClient.get('/library/list')
.then(data => {
console.log('Server is up and running', data);
eventBus.publish('server-state-changed', {connected: true});
})
.catch(error => {
console.error('Server is down', error);
eventBus.publish('server-state-changed', {connected: false});
});
return html`
<${TopMenu} />
<${ParametersDialog} />
`;
}
render(html`<${App} />`, document.getElementById('app'));

View file

@ -0,0 +1,54 @@
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
setBaseUrl(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, method = 'GET', data = null, headers = {}) {
const config = {
method,
headers: {
'Content-Type': 'application/json',
...headers
}
};
if (data) {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(`${this.baseURL}${endpoint}`, config);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Something went wrong');
}
return await response.json();
} catch (error) {
console.error('API request error:', error);
throw error;
}
}
get(endpoint, headers = {}) {
return this.request(endpoint, 'GET', null, headers);
}
post(endpoint, data, headers = {}) {
return this.request(endpoint, 'POST', data, headers);
}
put(endpoint, data, headers = {}) {
return this.request(endpoint, 'PUT', data, headers);
}
delete(endpoint, headers = {}) {
return this.request(endpoint, 'DELETE', null, headers);
}
}
// Export de l'instance ApiClient pour l'importer facilement
const apiClient = new ApiClient('127.0.0.1:8081');
export default apiClient;

View file

@ -0,0 +1,27 @@
class EventBus {
constructor() {
this.events = {};
this.id = Math.floor(Math.random() * 10000);
}
subscribe(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
unsubscribe(event, listener) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(l => l !== listener);
}
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(data));
}
}
}
export default new EventBus();

View file

@ -0,0 +1,48 @@
class Storage {
constructor(prefix = '') {
this.prefix = prefix;
}
setItem(key, value) {
try {
const data = JSON.stringify(value);
localStorage.setItem(this.prefix + key, data);
} catch (error) {
console.error('Error saving to localStorage', error);
}
}
getItem(key) {
try {
const data = localStorage.getItem(this.prefix + key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Error reading from localStorage', error);
return null;
}
}
removeItem(key) {
try {
localStorage.removeItem(this.prefix + key);
} catch (error) {
console.error('Error removing from localStorage', error);
}
}
clear() {
try {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith(this.prefix)) {
localStorage.removeItem(key);
}
});
} catch (error) {
console.error('Error clearing localStorage', error);
}
}
}
// Exemple d'utilisation
export default new Storage('ost_player_v1_');

View file

@ -0,0 +1,82 @@
import { render } from 'preact';
import { html } from 'htm/preact';
import { useState } from 'preact/hooks';
import eventBus from '../classes/event-bus.js';
function ParametersDialog() {
const [serverUrl, setServerUrl] = useState('127.0.0.1:8081');
// Function to show the modal
function showModal() {
modal.style.display = 'block';
}
// Function to hide the modal
function hideModal() {
modal.style.display = 'none';
}
// Event listener for the close button
function handleCloseClick() {
hideModal();
}
// Event listener for the submit button
function handleOkClick () {
const urlInput = document.getElementById('url-input').value;
console.log('URL entered:', urlInput);
hideModal();
}
eventBus.subscribe('show-modal', function(data) {
showModal();
});
return html`
<style>
/* Modal Styles */
.modal {
display: none; /* Hidden by default */
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
}
.close-button {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close-button:hover,
.close-button:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
<div id="modal" class="modal">
<div class="block fixed">
<span class="close-button" onClick="${handleCloseClick}"></span>
<label for="url-input">URL du serveur:</label>
<div class="wrapper block">
<input type="text" id="url-input" name="url-input" placeholder="127.0.0.1:8080" value="${serverUrl}" />
</div>
<button id="submit-button" class="block accent" onClick="${handleOkClick}">Ok</button>
</div>
</div>
`;
}
export default ParametersDialog;

View file

@ -0,0 +1,65 @@
import { html } from 'htm/preact';
import eventBus from '../classes/event-bus.js';
import { useState } from 'preact/hooks';
function MessageComponent({ show, message }) {
return html`
<div>
${show
? html`<div class="themed">
<div class="block fixed accent">
${message}
</div>
</div>`
: html`<p />`}
</div>
`;
}
function TopMenu() {
const [message, setMessage] = useState('Erreur ');
const [error, setError] = useState(false);
function handleClick() {
eventBus.publish('show-modal', { type: 'parameters' });
eventBus.subscribe('server-state-changed', function(data) {
if (data.connected) {
setMessage('Connecté au serveur');
} else {
setMessage('Serveur non trouvé');
}
});
}
return html`
<style>
.flexRow {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 12px;
width: 100%;
}
.themed {
--block-accent-color: #EA471A;
<!-- background: #abcdef; -->
}
</style>
<div class="flexRow">
<${MessageComponent} show=${error} message=${message} />
<div class="block accent" onClick="${handleClick}">
Paramètres
</div>
</div>
`;
}
export default TopMenu;

View file

@ -11,29 +11,44 @@
<title>OpenStoryTeller Web Player</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="main.css">
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/preact@10.23.1",
"preact/hooks": "https://esm.sh/preact@10.23.1/hooks",
"htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
}
}
</script>
<!-- Favicon -->
<link rel="shortcut icon" href="https://www.raylib.com/favicon.ico">
<style>
/* body {
margin: 0px;
text-align: center;
} */
canvas.emscripten { border: 0px none; background-color: black; display: inline; }
body {
line-height: 1;
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
line-height: 1.6;
}
canvas.emscripten { border: 0px none; background-color: black; display: inline; }
#canvas-container {
width: 100%;
text-align:center;
}
</style>
</head>
<body>
<main class="container-fluid">
<vertical-menu></vertical-menu>
<body style="min-width: min-content;">
<main>
<div id="app"></div>
<div id="canvas-container">
<div id="canvas-container" class="block fixed">
<canvas class=emscripten id=canvas oncontextmenu=event.preventDefault() tabindex=-1></canvas>
</div>
<p id="output" />
@ -64,7 +79,6 @@
};
</script>
<script async type="text/javascript" src="bin/story-player.js"></script>
<script src="vertical-menu.js" type="module"></script>
<script src="app.js" type="module"></script>
</body>
</html>

View file

@ -0,0 +1,54 @@
/* Extra styles to make the demo look nicer.
* Feel free to look around! */
main {
margin: 4px auto;
max-width: 800px;
color: #222;
}
@media only screen and (max-width: 824px) {
main {
margin: 4px 12px;
width: calc(100% - 24px);
}
iframe {
width: 90vw;
height: 56vw;
}
.preWrapper {
width: 100%;
}
pre {
width: 100%;
overflow-x: auto;
}
}
input,
textarea {
width: 100%;
}
h2 {
margin-top: 32px;
padding-bottom: 6px;
border-bottom: 1px solid #222;
}
h2:first-child {
margin-top: 8px;
}
h3 {
margin-top: 28px;
font-weight: normal;
}
a {
font-weight: bold;
}
input {
border: 0;
}

View file

@ -0,0 +1,342 @@
/* Basic CSS reset */
html,
body {
margin: 0;
font-family: system-ui, sans-serif;
color: #222;
background: #f8f8f8;
}
input,
textarea {
font-size: 1em;
box-sizing: border-box;
padding: 6px 8px;
}
button,
code,
kbd,
pre {
font-size: 1em;
}
code,
kbd,
pre {
font-family: 'Menlo', 'Monaco', monospace;
border-radius: 3px;
box-sizing: border-box;
padding: 2px 4px 1px 4px;
background: rgba(0, 0, 0, .1);
}
pre {
padding: 8px 12px;
}
p {
line-height: 1.5em;
}
a {
color: #222;
}
a:hover {
color: #666;
}
/* util.css
* custom utility CSS library for projects by @thesephist */
/* margins */
.m0 {margin: 0}
.mt0, .my0 {margin-top: 0}
.mt1, .my1 {margin-top: .25rem}
.mt2, .my2 {margin-top: .5rem}
.mt3, .my3 {margin-top: .75rem}
.mt4, .my4 {margin-top: 1.5rem}
.mt5, .my5 {margin-top: 2.25rem}
.mt6, .my6 {margin-top: 3.25rem}
.mb0, .my0 {margin-bottom: 0}
.mb1, .my1 {margin-bottom: .25rem}
.mb2, .my2 {margin-bottom: .5rem}
.mb3, .my3 {margin-bottom: .75rem}
.mb4, .my4 {margin-bottom: 1.5rem}
.mb5, .my5 {margin-bottom: 2.25rem}
.mb6, .my6 {margin-bottom: 3.25rem}
.ml0, .mx0 {margin-left: 0}
.ml1, .mx1 {margin-left: .25rem}
.ml2, .mx2 {margin-left: .5rem}
.ml3, .mx3 {margin-left: .75rem}
.ml4, .mx4 {margin-left: 1.5rem}
.ml5, .mx5 {margin-left: 2.25rem}
.ml6, .mx6 {margin-left: 3.25rem}
.mr0, .mx0 {margin-right: 0}
.mr1, .mx1 {margin-right: .25rem}
.mr2, .mx2 {margin-right: .5rem}
.mr3, .mx3 {margin-right: .75rem}
.mr4, .mx4 {margin-right: 1.5rem}
.mr5, .mx5 {margin-right: 2.25rem}
.mr6, .mx6 {margin-right: 3.25rem}
/* paddings */
.p0 {padding: 0}
.pt0, .py0 {padding-top: 0}
.pt1, .py1 {padding-top: .25rem}
.pt2, .py2 {padding-top: .5rem}
.pt3, .py3 {padding-top: .75rem}
.pt4, .py4 {padding-top: 1rem}
.pb0, .py0 {padding-bottom: 0}
.pb1, .py1 {padding-bottom: .25rem}
.pb2, .py2 {padding-bottom: .5rem}
.pb3, .py3 {padding-bottom: .75rem}
.pb4, .py4 {padding-bottom: 1rem}
.pl0, .px0 {padding-left: 0}
.pl1, .px1 {padding-left: .25rem}
.pl2, .px2 {padding-left: .5rem}
.pl3, .px3 {padding-left: .75rem}
.pl4, .px4 {padding-left: 1rem}
.pr0, .px0 {padding-right: 0}
.pr1, .px1 {padding-right: .25rem}
.pr2, .px2 {padding-right: .5rem}
.pr3, .px3 {padding-right: .75rem}
.pr4, .px4 {padding-right: 1rem}
/* typography */
h1, h2, h3, h4, h5, h6 {font-weight: 400}
.bold {font-weight: bold}
.clean {text-decoration: none}
.underline {text-decoration: underline}
.f0 {font-size: .75rem}
.f1 {font-size: 1rem}
.f2 {font-size: 1.25rem}
.f3 {font-size: 1.6rem}
.f4 {font-size: 2rem}
.f5 {font-size: 3rem}
.text-start {text-align: start}
.text-end {text-align: end}
.text-center {text-align: center}
/* flex & layout */
.flex {display: flex}
.flex-row {flex-direction: row}
.flex-column {flex-direction: column}
.flex-wrap {flex-wrap: wrap}
.align-center {align-items: center}
.align-start {align-items: flex-start}
.align-end {align-items: flex-end}
.justify-center {justify-content: center}
.justify-start {justify-content: flex-start}
.justify-end {justify-content: flex-end}
.fill, .fill-width {width: 100%}
.fill, .fill-height {height: 100%}
/* page widths */
.page-xs {max-width: 480px}
.page-s {max-width: 600px}
.page-m {max-width: 800px}
.page-l {max-width: 1200px}
.page-xl {max-width: 1600px}
/* shadows */
.shadow1 {box-shadow: 0 2px 3px rgba(0, 0, 0, .36)}
.shadow2 {box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .34)}
.shadow3 {box-shadow: 0 4px 7px -1px rgba(0, 0, 0, .32)}
.shadow4 {box-shadow: 0 5px 9px -2px rgba(0, 0, 0, .3)}
.shadow5 {box-shadow: 0 7px 12px -3px rgba(0, 0, 0, .28)}
/* list */
ul.list-reset,
ol.list-reset {
padding-left: 0;
list-style: none;
}
/* other utility */
.borderless {border: 0 solid transparent}
.rounded {border-radius: 4px}
.block {display: block}
.inline-block {display: inline-block}
.inline {display: inline}
.hidden {display: none}
.pointer {cursor: pointer}
.auto-center {margin-left: auto; margin-right: auto}
/* DEFAULT VARIABLES */
body {
--block-text-color: #222;
--block-background-color: #fff;
--block-accent-color: #00ae86;
--block-shadow-color: #444;
}
/* BASIC BLOCK STYLES */
.block {
display: block;
color: var(--block-text-color);
border: 3px solid var(--block-text-color);
border-radius: 3px;
padding: 4px 8px;
background: var(--block-background-color);
font-weight: bold;
cursor: pointer;
box-sizing: border-box;
position: relative;
top: -2px;
left: -2px;
transition: transform 0.2s;
margin: 8px 6px 10px 6px;
z-index: 1;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.block.wrapper,
.block.wrapper.inline {
display: inline-block;
padding: 0;
}
.block.wrapper > * {
margin: 0;
}
/* INTERACTIVE BLOCK STYLES */
.block::before {
content: "";
background: var(--block-background-color);
border: 3px solid var(--block-text-color);
border-radius: 3px;
box-sizing: border-box;
position: absolute;
top: -3px;
left: -3px;
height: calc(100% + 6px);
width: calc(100% + 6px);
z-index: -1;
}
.block:hover,
.block:focus {
transform: translate(2px, 2px);
}
.block::after {
content: "";
display: block;
box-sizing: border-box;
background: var(--block-shadow-color);
border: 3px solid var(--block-text-color);
border-radius: 3px;
height: calc(100% + 6px);
width: calc(100% + 6px);
position: absolute;
top: 3px;
left: 3px;
right: 0;
z-index: -2;
transition: transform 0.2s;
}
.block:hover::after,
.block:focus::after {
transform: translate(-2px, -3px);
}
.block:active {
color: var(--block-text-color);
transform: translate(3px, 3px);
}
.block:active::after {
transform: translate(-4px, -4px);
}
.block:focus {
outline: none;
}
.block.fixed {
cursor: initial !important;
}
/* FIXED STYLES */
.block.fixed:hover,
.block.fixed:hover::before,
.block.fixed:hover::after,
.block.fixed:active,
.block.fixed:active::before,
.block.fixed:active::after,
.block.fixed:focus,
.block.fixed:focus::before,
.block.fixed:focus::after {
transform: none !important;
}
/* ACCENT STYLES */
.block.accent {
color: var(--block-background-color);
background: var(--block-accent-color);
}
.block.accent::before {
background: var(--block-accent-color);
}
/* INLINE STYLES */
.block.inline {
display: inline-block;
font-size: 0.75em;
padding: 0 6px;
margin: 3px 2px 1px 4px;
}
.block.inline::after {
top: -1px;
left: -1px;
}
.block.inline:hover,
.block.inline:focus {
transform: translate(1px, 1px);
}
.block.inline:hover::after,
.block.inline:focus::after {
transform: translate(-1px, -1px);
}
.block.inline:active {
transform: translate(2px, 2px);
}
/* ROUND STYLES */
.block.round,
.block.round::before,
.block.round::after {
border-radius: 30px;
}
.block.round::after {
left: 1px;
}

View file

@ -1,27 +0,0 @@
class VerticalMenu extends HTMLElement {
constructor() {
super();
// Create a wrapper nav element using Bulma
this.innerHTML = `
<nav>
<ul>
<li><strong>OpenStoryTeller</strong></li>
</ul>
<ul>
<li><a href="#">Paramètres</a></li>
<li><a href="#">À propos</a></li>
</ul>
</nav>
`;
}
connectedCallback() {
}
}
// Define the new element
customElements.define('vertical-menu', VerticalMenu);