diff --git a/story-editor/src/main_window.cpp b/story-editor/src/main_window.cpp index ff1a55d..2dfc625 100644 --- a/story-editor/src/main_window.cpp +++ b/story-editor/src/main_window.cpp @@ -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; diff --git a/story-editor/src/web_server.cpp b/story-editor/src/web_server.cpp index 008e041..2fc0148 100644 --- a/story-editor/src/web_server.cpp +++ b/story-editor/src/web_server.cpp @@ -1,175 +1,68 @@ #include +#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, "\n"); - mg_printf(conn, "

This is the Foo GET handler!!!

\n"); - mg_printf(conn, - "

The request was:

%s %s HTTP/%s

\n", - req_info->request_method, - req_info->request_uri, - req_info->http_version); - mg_printf(conn, "\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, "\n"); - mg_printf(conn, "

This is the Foo POST handler!!!

\n"); - mg_printf(conn, - "

The request was:

%s %s HTTP/%s

\n", - req_info->request_method, - req_info->request_uri, - req_info->http_version); - mg_printf(conn, "

Content Length: %li

\n", (long)tlen); - mg_printf(conn, "
\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
\n"); - mg_printf(conn, "\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; - -#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; + nlohmann::json story = { + {"title", s->GetName() }, + {"uuid", s->GetUuid() }, + }; + json.push_back(story); + } -}; + 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() diff --git a/story-editor/src/web_server.h b/story-editor/src/web_server.h index eb016f6..6c75ec7 100644 --- a/story-editor/src/web_server.h +++ b/story-editor/src/web_server.h @@ -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, "\n"); + mg_printf(conn, "

This is the Foo POST handler!!!

\n"); + mg_printf(conn, + "

The request was:

%s %s HTTP/%s

\n", + req_info->request_method, + req_info->request_uri, + req_info->http_version); + mg_printf(conn, "

Content Length: %li

\n", (long)tlen); + mg_printf(conn, "
\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
\n"); + mg_printf(conn, "\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; }; diff --git a/story-player-raylib/app.js b/story-player-raylib/app.js new file mode 100644 index 0000000..206e2ab --- /dev/null +++ b/story-player-raylib/app.js @@ -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')); diff --git a/story-player-raylib/classes/api-client.js b/story-player-raylib/classes/api-client.js new file mode 100644 index 0000000..c0fc121 --- /dev/null +++ b/story-player-raylib/classes/api-client.js @@ -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; diff --git a/story-player-raylib/classes/event-bus.js b/story-player-raylib/classes/event-bus.js new file mode 100644 index 0000000..ed3a5ec --- /dev/null +++ b/story-player-raylib/classes/event-bus.js @@ -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(); diff --git a/story-player-raylib/classes/storage.js b/story-player-raylib/classes/storage.js new file mode 100644 index 0000000..d456e8b --- /dev/null +++ b/story-player-raylib/classes/storage.js @@ -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_'); \ No newline at end of file diff --git a/story-player-raylib/components/ParametersDialog.js b/story-player-raylib/components/ParametersDialog.js new file mode 100644 index 0000000..71ab5f2 --- /dev/null +++ b/story-player-raylib/components/ParametersDialog.js @@ -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` + + + + `; + +} + +export default ParametersDialog; \ No newline at end of file diff --git a/story-player-raylib/components/TopMenu.js b/story-player-raylib/components/TopMenu.js new file mode 100644 index 0000000..e5ff7d4 --- /dev/null +++ b/story-player-raylib/components/TopMenu.js @@ -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` +
+ ${show + ? html`
+
+ ${message} +
+
` + : html`

`} +

+ `; + +} + + +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` + +
+ <${MessageComponent} show=${error} message=${message} /> +
+ Paramètres +
+
+ `; +} +export default TopMenu; diff --git a/story-player-raylib/index.html b/story-player-raylib/index.html index 8a2774b..58a5064 100644 --- a/story-player-raylib/index.html +++ b/story-player-raylib/index.html @@ -11,29 +11,44 @@ OpenStoryTeller Web Player - + + + + + + - -
- + +
+
-
+

@@ -64,7 +79,6 @@ }; - - + \ No newline at end of file diff --git a/story-player-raylib/main.css b/story-player-raylib/main.css new file mode 100644 index 0000000..e02e653 --- /dev/null +++ b/story-player-raylib/main.css @@ -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; +} diff --git a/story-player-raylib/style.css b/story-player-raylib/style.css new file mode 100644 index 0000000..61c8762 --- /dev/null +++ b/story-player-raylib/style.css @@ -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; +} + diff --git a/story-player-raylib/vertical-menu.js b/story-player-raylib/vertical-menu.js deleted file mode 100644 index dff8029..0000000 --- a/story-player-raylib/vertical-menu.js +++ /dev/null @@ -1,27 +0,0 @@ -class VerticalMenu extends HTMLElement { - constructor() { - super(); - - // Create a wrapper nav element using Bulma - this.innerHTML = ` -

- `; - - - } - - connectedCallback() { - - } -} - -// Define the new element -customElements.define('vertical-menu', VerticalMenu);