diff --git a/README.md b/README.md index e7260d6..b2ac4f0 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,120 @@ This is in the early stages of development. Don't use this for anything important (or at all)! -## Udo's C++ Entry Points +## Overview -The aim of this project is to make a PHP-like runtime that enables server-side "scripting" using C/C++. At the core is a multi-worker FastCGI server that can be talked to from Nginx or similar front-end servers. UCE has a shared-nothing isolated architecture to serve page requests. To minimize the potential for memory leaks, UCE uses a per-request memory arena. UCE also provides a PHP-like API to ease web development. UCE aims to use minimal dependencies (at the moment, the only dependency is the Clang compiler). UCE pages are automatically recompiled and dynamically reloaded as necessary. +UCE is a PHP-inspired server-side runtime that lets you build web pages and handlers in C++ using a small `.uce` preprocessor plus a FastCGI application server. + +- `.uce` pages compile to shared objects on demand +- normal HTTP pages expose `RENDER(Request& context)` +- WebSocket pages can additionally expose `WS(Request& context)` +- sub-rendering and components pass structured data through `context.call` +- nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener +- the nginx-published application tree now lives under `site/` +- you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed +- the preprocessor has two jobs: + - allow for inline HTML within C++ and the use of templating tags inside of that HTML + - convenience directive and macro parsing so UCE files don't need a lot of boiler plate + +The abolition of boilerplate was a major design factor, resulting in a page as small as this: + +```uce +RENDER(Request& context) +{ + <>hello world +} +``` + +The runtime is still experimental. + +## Build + +Build the runtime with: + +```bash +bash scripts/build_linux.sh +``` + +The current build expects: + +- `clang++` +- `mysql_config` +- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support + +The binary is written to: + +```bash +bin/uce_fastcgi.linux.bin +``` + +## Runtime Model + +UCE pages now use explicit request handlers instead of implicit globals: + +- `RENDER(Request& context)` for normal HTTP rendering +- `WS(Request& context)` for inbound WebSocket messages + +Useful related runtime patterns: + +- `render_file(String file_name)` or `render_file(String file_name, Request& context)` to invoke another page +- `context.call` for invocation or message-local structured input +- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls +- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state + +Named component-style render handlers are also supported: + +```cpp +RENDER:BODY(Request& context) +{ + <> +

+ +} +``` + +Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry. + +## Template Output + +Inside `<> ... ` literal blocks, UCE supports three inline forms: + +- `` to emit raw C++ statements +- `` to print HTML-escaped output +- `` to print unescaped output + +Use `` by default for user-visible text. Use `` only for trusted markup or content that has already been escaped. + +## Components + +UCE includes a native component layer built on top of ordinary `.uce` files: + +- `component(name[, props[, context]])` +- `render_component(name[, props[, context]])` +- `component_exists(name)` +- `component_resolve(name)` + +Component props are passed through `context.call`. + +Component names resolve: + +1. as the exact file name you supplied +2. as that same name plus `.uce` +3. as those same two forms under `components/` + +When you want returned component markup inside a literal block, prefer: + +```cpp +<> +
+ +``` + +because `` HTML-escapes the returned markup. For direct output from C++ code, use `render_component(...)`. + +Components may expose additional named handlers with `RENDER:NAME(Request& context)`. The default handler remains the ordinary `RENDER(Request& context)`. ## WebSockets -WebSocket-enabled pages can now expose both: - -- `RENDER() { }` for normal HTTP rendering -- `WS() { }` for inbound WebSocket messages on the same `.ws.uce` page - The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code: - `ws_message()` @@ -24,1653 +127,318 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate - `ws_is_binary()` - `ws_connections([scope])` - `ws_connection_count([scope])` -- `ws_send(message[, scope])` -- `ws_broadcast(message[, scope])` -- `ws_send_to(connection_id, message)` +- `ws_send(message[, binary[, scope]])` +- `ws_send_to(connection_id, message[, binary])` - `ws_close([connection_id])` -By default, the WebSocket scope is the current page file, so `ws_send()` broadcasts to other clients connected to that same `.ws.uce` endpoint. +By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint. + +Each live WebSocket connection now owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client. `ws_message()` may contain either text or binary payload data. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type. -The current `ws_send*` helpers queue text frames. Binary send helpers are not exposed yet. +Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame. -The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS()`. +The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`. -## Service Setup +## Error Reporting -The repository now includes a systemd unit template and a management script for the FastCGI runtime: +Unhandled exceptions and recovered fatal request signals now return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`. + +The demo page `site/test/error-reporting.uce` can be used to exercise: + +- uncaught exception handling +- recovered `SIGABRT` +- recovered `SIGSEGV` + +The current error page includes: + +- request URI +- resolved script path +- high-level error summary +- signal number and name when applicable +- a native backtrace + +This recovery path currently covers normal request handling. It is not yet the universal recovery path for every runtime subsystem. + +## Docs And Tests + +The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. + +Useful entry points: + +- repo files: + - `site/doc/index.uce` + - `site/doc/singlepage.uce` + - `site/test/index.uce` +- published URLs: + - `/doc/index.uce` + - `/doc/singlepage.uce` + - `/test/index.uce` + +Representative test pages: + +- `site/test/components.uce` +- `site/test/websockets.ws.uce` +- `site/test/error-reporting.uce` +- `site/test/post-multipart.uce` +- `site/test/session.uce` + +## Deploy Behind Nginx + +The intended production shape is: + +- nginx serves static files directly +- nginx forwards `.uce` requests and ordinary `.ws.uce` page loads to the UCE FastCGI Unix socket +- nginx proxies actual WebSocket upgrade requests for `.ws.uce` endpoints to the runtime's built-in HTTP/WebSocket listener +- systemd keeps the runtime built, started, and restarted on failure + +The repository ships the pieces used for this: - `scripts/systemd/uce.service` - `scripts/systemd/manage-uce-service.sh` +- `etc/uce/settings.cfg` -Typical usage on the deployment host: +### 1. Install build and runtime dependencies + +On a Debian or Ubuntu host, start with the packages needed to build and run UCE behind nginx: + +```bash +apt update +apt install -y nginx clang mariadb-client libmariadb-dev build-essential +``` + +The exact package names may vary by distro. The important requirements are: + +- `nginx` +- `clang++` +- `mysql_config` +- normal Linux development headers for threads, sockets, `dl`, and backtrace support + +### 2. Put the repo on the server + +This README assumes the repository lives at: + +```bash +/Code/uce.openfu.com/uce +``` + +That is what the shipped `scripts/systemd/uce.service` file currently uses as its `WorkingDirectory` and build path. If you deploy somewhere else, update that unit file before enabling the service. + +### 3. Configure `/etc/uce/settings.cfg` + +The runtime reads its server settings from: + +```bash +/etc/uce/settings.cfg +``` + +The shipped example contains the important filesystem and FastCGI settings: + +```ini +BIN_DIRECTORY=/tmp/uce/work +TMP_UPLOAD_PATH=/tmp/uce/uploads +SESSION_PATH=/tmp/uce/sessions + +FCGI_SOCKET_PATH=/run/uce.sock +FCGI_PORT=9993 + +WORKER_COUNT=4 +MAX_MEMORY=16777216 +SESSION_TIME=2592000 +``` + +For nginx deployments, the most important setting is: + +- `FCGI_SOCKET_PATH=/run/uce.sock` + +That is the Unix socket nginx should use for normal `.uce` requests. + +`FCGI_PORT` is optional if nginx is talking to the Unix socket. Leave it set if you also want a TCP FastCGI listener, or remove it if you want the socket to be the only FastCGI entry point. + +If you want WebSocket support through nginx, also make sure the built-in HTTP listener is available. The runtime currently defaults `HTTP_PORT` to `8080` even if it is not present in the config file, but it is clearer to set it explicitly: + +```ini +HTTP_PORT=8080 +``` + +Recommended deployment notes: + +- keep `HTTP_PORT` bound to localhost only at the firewall or by network policy; nginx should be the public entry point +- keep `BIN_DIRECTORY`, `TMP_UPLOAD_PATH`, and `SESSION_PATH` on writable local storage +- after editing `/etc/uce/settings.cfg`, restart `uce.service` + +### 4. Install and enable the systemd service + +As root, from the repository root: ```bash scripts/systemd/manage-uce-service.sh setup +``` + +That script: + +- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service` +- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist +- reloads systemd +- enables the service at boot +- starts the runtime immediately + +Useful follow-up commands: + +```bash scripts/systemd/manage-uce-service.sh status scripts/systemd/manage-uce-service.sh restart scripts/systemd/manage-uce-service.sh logs 200 ``` -The service runs the runtime from the repository root so `COMPILER_SYS_PATH` resolves correctly and nginx can forward `.uce` requests to the Unix socket defined in `/etc/uce/settings.cfg`. +The unit currently: + +- creates `/tmp/uce/work`, `/tmp/uce/uploads`, and `/tmp/uce/sessions` +- removes any stale `/run/uce.sock` +- rebuilds the runtime before start +- runs the binary from the repo root so `COMPILER_SYS_PATH` resolves correctly + +### 5. Configure nginx for `.uce` and `.ws.uce` + +You need two nginx paths for `.ws.uce` endpoints: + +- FastCGI for ordinary `.uce` requests and plain `.ws.uce` page renders +- HTTP proxying only for actual WebSocket upgrade traffic on `.ws.uce` endpoints + +If you use WebSockets, add this `map` in the nginx `http` block: + +```nginx +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} +``` -For deployed WebSockets, nginx should proxy `.ws.uce` requests to the runtime's built-in HTTP listener instead of `fastcgi_pass`. The current OpenFU deployment already does this and keeps HTTP/WebSocket ownership on a single worker so page-scoped broadcasts stay coherent. +Then use a server block along these lines: -# API +```nginx +server { + listen 80; + server_name example.com; + root /Code/uce.openfu.com/uce/site; + + index index.uce index.html; + + location / { + try_files $uri $uri/ =404; + } -Memcache Functions -================== + location ~ \.uce$ { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param DOCUMENT_URI $uri; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_pass unix:/run/uce.sock; + } -memcache\_command ------------------ + location ~ \.ws\.uce$ { + error_page 418 = @uce_websocket; + if ($http_upgrade = "websocket") { + return 418; + } -String memcache\_command(u64 connection, String command) + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param DOCUMENT_URI $uri; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_pass unix:/run/uce.sock; + } -### Parameters + location @uce_websocket { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_pass http://127.0.0.1:8080; + } +} +``` -**connection** : connection handle +Important details: -**command** : string containing the Memcache command +- `.ws.uce` must be matched before the more general `.uce` rule +- `fastcgi_pass` should point at the same socket path as `FCGI_SOCKET_PATH` +- `proxy_pass` should point at the runtime's `HTTP_PORT` +- ordinary `GET /page.ws.uce` page renders should stay on FastCGI +- only upgrade requests for `/page.ws.uce` should go through the HTTP/WebSocket listener +- `SCRIPT_FILENAME` should resolve to the actual `.uce` file on disk +- `proxy_http_version 1.1` and the `Upgrade` / `Connection` headers are required for WebSockets -**return value** : string containing the Memcache server's response +The `location /` block above is intentionally conservative and only serves real files from `site/`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly. -### Description +### 6. Think about document root and private files -Executes a command on an open memcache connection. +Point nginx at `site/`, not the repository root. The repo still contains source, scripts, packaging files, and operational assets that are not meant to be public. -memcache\_connect ------------------ +At minimum, explicitly block internal directories that should never be served directly. For example: -u64 memcache\_connect(String host = "127.0.0.1", short port = 11211) +```nginx +location ~ ^/(src|scripts|etc|bin|work|dist|pkg)/ { + return 404; +} +``` -### Parameters +If nginx is rooted at `site/`, most of those paths will not be reachable anyway, which is the preferred setup. -**host** : optional host name of the memcache server, defaults to local address 127.0.0.1 +### 7. Reload nginx and verify the deployment -**port** : optional memcache server's port, defaults to 11211 +After writing the nginx config: -**return value** : the connection handle (or -1 if an error occurred) +```bash +nginx -t +systemctl reload nginx +``` -### Description +Then verify: -Connects to a memcache server instance. +```bash +systemctl status uce.service +curl -i http://127.0.0.1/test/index.uce +curl -i http://127.0.0.1/doc/index.uce +``` -memcache\_delete ----------------- +If WebSockets are enabled, also verify a `.ws.uce` endpoint through nginx rather than talking to the runtime directly. -bool memcache\_delete(u64 connection, String key) +### 8. Troubleshooting -### Parameters +Common failure modes: -**connection** : connection handle +- `502 Bad Gateway` + Usually means `uce.service` is down, the Unix socket path does not match, or the request crashed before sending a valid response. +- WebSocket upgrade fails + Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost. +- Requests compile but immediately crash + Check `journalctl -u uce.service` and clear stale generated artifacts under `BIN_DIRECTORY` if you have changed runtime ABI or entrypoint signatures. +- nginx serves raw source or internal files + Tighten the server root and add explicit deny rules for non-public directories. -**key** : key string +## Repo Helpers -**return value** : true if the operation was successful +- `./codesearch [rg options...]` -### Description +This is a small repo-root wrapper around `rg` that searches from the project root and skips generated/build directories such as `.git/`, `bin/`, `dist/`, and `work/`. -Deletes entry specified by the 'key'. +## Reference Notes -memcache\_get -------------- +For up-to-date usage, prefer: -String memcache\_get(u64 connection, String key, String default\_value = "") +- the live docs under `site/doc/` +- the declarations in `src/lib/compiler.h`, `src/lib/sys.h`, and `src/lib/functionlib.h` +- the example pages under `site/test/` -### Parameters +## AI Disclosure -**connection** : connection handle - -**key** : key string - -**default\_value** : optional default value - -**return value** : value that was returned by the Memcache server - -### Description - -Retrieves a value from an existing connection to a Memcache server. - -memcache\_get\_multiple ------------------------ - -StringMap memcache\_get\_multiple(u64 connection, StringList keys) - -### Parameters - -**connection** : connection handle - -**keys** : a list of strings containing the keys to be retrieved - -**return value** : a StringMap with the retrieved entries - -### Description - -Retrieves a bunch of entries all at once. - -memcache\_set -------------- - -bool memcache\_set(u64 connection, String key, String value, u64 expires\_in = 60\*60) - -### Parameters - -**connection** : connection handle - -**key** : the entry's key - -**value** : the value to be set - -**expires\_in** : optional expiration timeout, defaults to one hour - -**return value** : true if the operation was successful - -### Description - -Stores a 'value' on the Memcache server. - -MySQL Functions -=============== - -mysql\_connect --------------- - -MySQL\* mysql\_connect(String host = "localhost", String username = "root", String password = "") - -### Parameters - -**host** : host name of the MySQL server - -**username** : user name - -**password** : password - -**return value** : pointer to the MySQL connection struct - -### Description - -Establishes a connection to a MySQL server. - -mysql\_disconnect ------------------ - -void mysql\_disconnect(MySQL\* m) - -### Parameters - -**m** : pointer to an existing MySQL connection struct - -### Description - -Closes a connection to a MySQL server. - -mysql\_error ------------- - -String mysql\_error(MySQL\* m) - -### Parameters - -**m** : pointer to a MySQL connection struct - -**return value** : MySQL error message (if present, otherwise empty string) - -### Description - -Returns the last error message from a connection to a MySQL server. - -mysql\_escape -------------- - -String mysql\_escape(String raw, char quote\_char) - -### Parameters - -**raw** : the string to be escaped - -**quote\_char** : the character that should be used to wrap the string (pass NULL for no wrapping) - -**return value** : the safe version of the 'raw' string - -### Description - -Escapes a string such that it can be passed as a safe value into an SQL expression. - -mysql\_insert\_id ------------------ - -u64 mysql\_insert\_id(MySQL\* m) - -### Parameters - -**m** : pointer to an active MySQL connection - -**return value** : the last used automatic row ID - -### Description - -This retrieves the last row ID that was used for a column with an AUTO\_INCREMENT row key. - -mysql\_query ------------- - -DTree mysql\_query(MySQL\* m, String q, StringMap params) - -### Parameters - -**m** : pointer to an active MySQL connection struct - -**q** : a string containing a MySQL query - -**params** : optional, a list of query parameter keys and values - -**return value** : a list of rows returned from executing the query - -### Description - -Executes a MySQL query and returns the resulting data (if any). - -### Examples - -(tbd) - -Noise/Hash Functions -==================== - -draw\_float ------------ - -f64 draw\_float(f64 from, f64 to) - -### Parameters - -**from** : minimum value - -**to** : maximum value - -**return value** : a noise value between 'from' and 'to' - -### Description - -This function works exactly like generate\_float(), but context->random\_index is used for the 'index' value and context->random\_seed is used for the seed. After this function has been called, the context->random\_index is increased by one. At the start of every request, context->random\_seed is automatically populated with a new seed value. - -draw\_int ---------- - -u64 draw\_int(u64 from, u64 to) - -### Parameters - -**from** : minimum value - -**to** : maximum value - -**return value** : a noise value between 'from' and 'to' - -### Description - -This function works exactly like generate\_int(), but context->random\_index is used for the 'index' value and context->random\_seed is used for the seed. After this function has been called, the context->random\_index is increased by one. At the start of every request, context->random\_seed is automatically populated with a new seed value. - -gen\_noise01 ------------- - -u32 noise01(u64 index, u64 seed = 0) - -### Parameters - -**index** : index position - -**seed** : seed set (defaults to 0) - -**return value** : a noise value from 0 to 1 - -### Description - -Generates a noise value in the range from 0 to 1 for the given 'index' and 'seed' values. - -gen\_noise32 ------------- - -u32 noise32(u32 index, u32 seed = 0) - -### Parameters - -**index** : index position - -**seed** : seed set (defaults to 0) - -**return value** : a noise value given the 'index' and 'seed' values. - -### Description - -Generates a noise value for the given 'index' and 'seed' values. - -gen\_noise64 ------------- - -u32 noise64(u64 index, u64 seed = 0) - -### Parameters - -**index** : index position - -**seed** : seed set (defaults to 0) - -**return value** : a noise value given the 'index' and 'seed' values. - -### Description - -Generates a noise value for the given 'index' and 'seed' values. - -gen\_float ----------- - -f64 generate\_float(f64 from, f64 to, u64 index, u64 seed = 0) - -### Parameters - -**from** : minimum result - -**to** : maximum result - -**index** : index position to generate number from - -**seed** : seed position to generate number from (defaults to 0) - -**return value** : noise value - -### Description - -Generates a noise value between 'from' and 'to', given the 'index' and 'seed' numbers. - -gen\_int --------- - -u64 generate\_int(u64 from, u64 to, u64 index, u64 seed = 0) - -### Parameters - -**from** : minimum result - -**to** : maximum result - -**index** : index position to generate number from - -**seed** : seed position to generate number from (defaults to 0) - -**return value** : noise value - -### Description - -Generates a noise value between 'from' and 'to', given the 'index' and 'seed' numbers. - -gen\_sha1 ---------- - -String sha1(String s, bool as\_binary = false) - -### Parameters - -**s** : data to be hashed - -**as\_binary** : when set to false, returns hash in hexadecimal notation (defaults to false) - -**return value** : the resulting hash value - -### Description - -Returns the sha1 hash of 's'. - -Output Buffer Functions -======================= - -ob\_clear ---------- - -void ob\_clear() - -### Parameters - -**(none)** : - -### Description - -Discard the current output buffer. - -ob\_get -------- - -String ob\_get() - -### Parameters - -**return value** : content of the current output buffer - -### Description - -Returns the contents of the current output buffer. - -ob\_get\_clear --------------- - -String ob\_get\_clear() - -### Parameters - -**return value** : content of the current output buffer - -### Description - -Returns the contents of the current output buffer and then discards the buffer. - -ob\_start ---------- - -void ob\_start() - -### Parameters - -**(none)** : - -### Description - -Starts a new output buffer. All subsequent output will be directed into this buffer. - -Sessions -======== - -make\_session\_id ------------------ - -String make\_session\_id() - -### Parameters - -**return value** : a new session ID - -### Description - -Creates a session ID - -session\_destroy ----------------- - -void session\_destroy(String session\_name) - -### Parameters - -**session\_name** : the name of the session - -### Description - -Deletes the cookie specified by 'session\_name' and clears the data stored under the session ID. This empties the 'context->session\_id' and 'context->session' variables. - -session\_start --------------- - -String session\_start(String session\_name) - -### Parameters - -**return value** : the session ID, defaults to "uce-session" - -### Description - -Starts session or connects to existing session. This function sets a cookie with the name contained in 'session\_name' if it does not exist and fills that cookie with a new unique session ID. It then loads the session data for that session ID. Afterwards, the following fields are populated in the 'context' variable: - -context->session\_id : the current session ID - -context->session\_name : the current session cookie name - -context->session : the current session data. The session data is automatically saved after a request completes. - -Socket Functions -================ - -socket\_close -------------- - -void socket\_close(u64 sockfd) - -### Parameters - -**sockfd** : socket handle - -### Description - -Closes an existing socket connection. - -socket\_connect ---------------- - -u64 socket\_connect(String host, short port) - -### Parameters - -**host** : host name - -**port** : port number - -**return value** : the socket handle - -### Description - -Opens a socket connection to the given 'host' and 'port'. - -socket\_read ------------- - -String socket\_read(u64 sockfd, u32 max\_length = 1024\*128, u32 timeout = 1); - -### Parameters - -**sockfd** : socket handle - -**max\_length** : optional maximum data size, defaults to 128kBytes - -**timeout** : optional operation timeout, defaults to one second - -**return value** : string containing the data that was read - -### Description - -Reads data from a socket connection. - -socket\_write -------------- - -bool socket\_write(u64 sockfd, String data) - -### Parameters - -**sockfd** : socket handle - -**data** : a string containing the data to be written to the socket - -**return value** : true if the write operation was successful - -### Description - -Writes a string of 'data' to the given socket. - -String Functions -================ - -filter ------- - -StringList filter(StringList items, function f) - -vector filter(vector items, function f) - -### Parameters - -**items** : list of items to be filtered - -**f** : a function that decides which items should be in the new list - -**return value** : a new list - -### Description - -Returns a list containing the members of 'items' for which 'f' returned boolean true. - -first ------ - -String first(String... args) - -### Parameters - -**args** : a variable number of String arguments - -**return value** : first of the 'args' that was not empty. - -### Description - -Given a variable number of String parameters, the first() function returns the first of these parameters that was not empty. Leading and trailing whitespace characters are not considered, resulting in a string that contains only whitespace characters being considered empty. - -join ----- - -String join(StringList l, String delim = "\\n") - -### Parameters - -**l** : list of strings to be joined - -**delim** : delimiter (defaults to newline character) - -**return value** : a string containing items joined by 'delim' - -### Description - -Joins the items contained in 'l' into a single String. - -nibble ------- - -String nibble(String& haystack, String delim) - -### Parameters - -**haystack** : string to be nibbled at - -**delim** : delimiter - -**return value** : string before first occurrence of 'delim' - -### Description - -Returns the part of 'haystack' before the first occurrence of 'delim', removing the corresponding part from 'haystack' (including 'delim'). If the substring 'delim' does not occurr in 'haystack', the entire string is returned and 'haystack' is set to an empty string. - -split ------ - -StringList split(String str, String delim) - -### Parameters - -**str** : string to be split - -**delim** : delimiter - -**return value** : a list of strings - -### Description - -Splits 'str' into multiple strings based on the given delimiter 'delim'. - -split\_space ------------- - -StringList split\_space(String str) - -### Parameters - -**str** : string to be split - -**return value** : a list of strings - -### Description - -Splits 'str' into multiple strings along any whitespace characters (multiple whitespace characters count as one). - -split\_utf8 ------------ - -StringList split\_utf8(String str, bool compound\_characters = false) - -### Parameters - -**str** : string to be split - -**compound\_characters** : optional, if true tries to combine compound characters - -**return value** : a list of Unicode characters - -### Description - -Splits the string 'str' into its constituent Unicode code points. - -If 'compound\_characters' is true, split\_utf8 will attempt to combine compound characters based on very simple rules: - -* combine characters if they're connected by a Zero-Width Joiner (ZWJ) character - -* combine two characters if they're both a Regional Indicator Symbol Letter - -* if a character is a Variation Selector, append it to the previous character - -* in all other cases, characters remain on their own - -replace -------- - -String replace(String s, String search, String replace\_with) - -### Parameters - -**s** : the string where replacements should happen - -**search** : the string that should be searched for - -**replace\_with** : the string that should appear in places where 'search' occurs - -**return value** : a version of 's' where all instances of 'search' have been replaced with 'replace\_with' - -### Description - -Replace all occurrences of 'search' with the string defined in 'replace\_with'. - -to\_lower ---------- - -String to\_lower(String s) - -### Parameters - -**s** : the string to be converted - -**return value** : returns a version of 's' where all upper case characters have been changed into lower case - -### Description - -Returns a lower case version of the input string 's'. - -Note: this function is not yet Unicode-aware. - -to\_upper ---------- - -String to\_upper(String s) - -### Parameters - -**s** : the string to be converted - -**return value** : returns a version of 's' where all lower case characters have been changed into upper case - -### Description - -Returns a upper case version of the input string 's'. - -Note: this function is not yet Unicode-aware. - -trim ----- - -String trim(String raw) - -### Parameters - -**raw** : string to be trimmed - -**return value** : string with leading and trailing whitespace characters removed - -### Description - -Returns a string where leading an trailing whitespace characters have been trimmed off. - -File System Functions -===================== - -basename --------- - -String basename(String fn) - -### Parameters - -**fn** : raw filename - -**return value** : the file's name - -### Description - -Isolates the file name component from a path/file name. - -dirname -------- - -String dirname(String fn) - -### Parameters - -**fn** : raw filename - -**return value** : the directory's name - -### Description - -Isolates the directory name component from a path/file name. - -expand\_path ------------- - -String expand\_path(String path, String relative\_to\_path = "") - -### Parameters - -**path** : a relative path - -**relative\_to\_path** : optional, expand relative to this path (if not given, the current path is used) - -**return value** : expanded version of the 'path' - -### Description - -Converts a relative path name into an absolute path, using the current working directory as a base. - -file\_append ------------- - -void file\_append(String file\_name, ...val) - -### Parameters - -**file\_name** : file name of file that should be written to - -**...val** : one or more values that should be written into the file - -### Description - -Opens or creates a given file and appends data to it. - -file\_exists ------------- - -bool file\_exists(String path) - -### Parameters - -**path** : the path name to be checked - -**return value** : true if the file exists - -### Description - -Checks whether the file or path specified by 'path' exists. - -file\_get\_contents -------------------- - -String file\_get\_contents(String file\_name) - -### Parameters - -**file\_name** : file name of file that should be read - -**return value** : String containing the file's contents - -### Description - -Reads the file identified by 'file\_name' and returns it as a String. If the file cannot be read, this function will return an empty string. - -file\_mtime ------------ - -time\_t file\_mtime(String file\_name) - -### Parameters - -**file\_name** : name of the file - -**return value** : Unix time stamp of the file's last modification - -### Description - -Retrieves the last modification date of 'file\_name' as a Unix timestamp. - -file\_put\_contents -------------------- - -bool file\_put\_contents(String file\_name, String content) - -### Parameters - -**file\_name** : file name of file that should be written - -**content** : content that should be written - -**return value** : true if write was successful - -### Description - -Writes the String 'content' into a file identified by 'file\_name'. Any pre-existing content of the file will be overwritten. - -get\_cwd --------- - -String get\_cwd() - -### Parameters - -**return value** : the current working directory - -### Description - -Returns the current working directory. - -ls --- - -StringList ls(String path) - -### Parameters - -**path** : a filesystem path - -**return value** : list of directory entries - -### Description - -Returns a list of files and subdirectories within the given 'path'. - -mkdir ------ - -bool mkdir(String path) - -### Parameters - -**path** : the path name to be created - -**return value** : returns true if the directory was successfully created - -### Description - -Creates a directory stated by 'path' - -set\_cwd --------- - -void set\_cwd(String path) - -### Parameters - -**path** : the new working directory - -### Description - -Sets a new working directory. - -shell\_escape -------------- - -String shell\_escape(String raw) - -### Parameters - -**raw** : string that should be escaped - -**return value** : escaped version of 'raw' - -### Description - -Escapes a parameter for shell\_exec - -shell\_exec ------------ - -String shell\_exec(String cmd) - -### Parameters - -**cmd** : string that contains the shell command line to be executed - -**return value** : output of the command execution - -### Description - -Executes a Linux shell command and returns the generated output - -unlink ------- - -void unlink(String file\_name) - -### Parameters - -**file\_name** : name of the file - -### Description - -Deletes the file identified by 'file\_name'. - -Task API -======== - -kill ----- - -s64 kill(pid\_t pid, s64 sig) - -### Parameters - -**pid** : PID of the process - -**sig** : signal number - -**return value** : 0 if signal was sent, -1 otherwise - -### Description - -This is the standard POSIX kill() function, provided here for reference. - -Possible signal numbers are: SIGABND, SIGABRT, SIGALRM, SIGBUS, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGKILL, SIGPIPE, SIGPOLL, SIGPROF, SIGQUIT, SIGSEGV, SIGSYS, SIGTERM, SIGTRAP, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM, SIGXCPU, SIGXFSZ, SIGCHLD, SIGIO, SIGIOERR, SIGWINCH, SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT. - -task ----- - -pid\_t task(String key, std::function exec\_func) - -### Parameters - -**key** : string uniquely identifying the task - -**exec\_func** : function to execute - -**return value** : the process ID of the started (or still running) task - -### Description - -task() starts the 'exec\_func' in a new process and returns that process' ID. If a process with the same 'key' is already running, task will not start a new process but instead just return the PID of the process that is already running. - -task\_pid ---------- - -pid\_t task\_pid(String key) - -### Parameters - -**key** : string uniquely identifying the task - -**return value** : the process ID of the task - -### Description - -Checks whether a process with the given 'key' is running and returns its PID if it is. Returns 0 otherwise. - -Time and Date Functions -======================= - -microtime ---------- - -f64 microtime() - -### Parameters - -**return value** : current Unix timestamp - -### Description - -Returns a 64 bit float containing the current Unix timestamp with millisecond accuracy or better. - -time ----- - -u64 time() - -### Parameters - -**return value** : second-accurate current Unix timestamp - -### Description - -Returns a 64 bit integer containing the current Unix timestamp. - -date ----- - -String date(String format = "", u64 timestamp = 0) - -### Parameters - -**format** : formatting string specifying the date format - -**timestamp** : optional timestamp value, defaults to current time - -**return value** : a formatted date - -### Description - -Returns a formatted date. This is based on the Linux date() command. The formatting string supports the following sequences: - - %% a literal % - - %a locale's abbreviated weekday name (e.g., Sun) - - %A locale's full weekday name (e.g., Sunday) - - %b locale's abbreviated month name (e.g., Jan) - - %B locale's full month name (e.g., January) - - %c locale's date and time (e.g., Thu Mar 3 23:05:25 2005) - - %C century; like %Y, except omit last two digits (e.g., 20) - - %d day of month (e.g., 01) - - %D date; same as %m/%d/%y - - %e day of month, space padded; same as %\_d - - %F full date; like %+4Y-%m-%d - - %g last two digits of year of ISO week number (see %G) - - %G year of ISO week number (see %V); normally useful only - - with %V - - %h same as %b - - %H hour (00..23) - - %I hour (01..12) - - %j day of year (001..366) - - %k hour, space padded ( 0..23); same as %\_H - - %l hour, space padded ( 1..12); same as %\_I - - %m month (01..12) - - %M minute (00..59) - - %n a newline - - %N nanoseconds (000000000..999999999) - - %p locale's equivalent of either AM or PM; blank if not known - - %P like %p, but lower case - - %q quarter of year (1..4) - - %r locale's 12-hour clock time (e.g., 11:11:04 PM) - - %R 24-hour hour and minute; same as %H:%M - - %s seconds since 1970-01-01 00:00:00 UTC - - %S second (00..60) - - %t a tab - - %T time; same as %H:%M:%S - - %u day of week (1..7); 1 is Monday - - %U week number of year, with Sunday as first day of week - - (00..53) - - %V ISO week number, with Monday as first day of week (01..53) - - %w day of week (0..6); 0 is Sunday - - %W week number of year, with Monday as first day of week - - (00..53) - - %x locale's date representation (e.g., 12/31/99) - - %X locale's time representation (e.g., 23:13:48) - - %y last two digits of year (00..99) - - %Y year - - %z +hhmm numeric time zone (e.g., -0400) - - %:z +hh:mm numeric time zone (e.g., -04:00) - - %::z +hh:mm:ss numeric time zone (e.g., -04:00:00) - - %:::z numeric time zone with : to necessary precision (e.g., - - -04, +05:30) - - %Z alphabetic time zone abbreviation (e.g., EDT) - - By default, date pads numeric fields with zeroes. The following - - optional flags may follow '%': - - - (hyphen) do not pad the field - - \_ (underscore) pad with spaces - - 0 (zero) pad with zeros - - + pad with zeros, and put '+' before future years with >4 - - digits - - ^ use upper case if possible - - # use opposite case if possible - -gmdate ------- - -String gmdate(String format = "", u64 timestamp = 0) - -### Parameters - -**format** : formatting string specifying the date format - -**timestamp** : optional timestamp value, defaults to current time - -**return value** : a formatted date - -### Description - -Returns a formatted date in the GMT/UTC timezone. This is based on the Linux date() command. The formatting string supports the following sequences: - - %% a literal % - - %a locale's abbreviated weekday name (e.g., Sun) - - %A locale's full weekday name (e.g., Sunday) - - %b locale's abbreviated month name (e.g., Jan) - - %B locale's full month name (e.g., January) - - %c locale's date and time (e.g., Thu Mar 3 23:05:25 2005) - - %C century; like %Y, except omit last two digits (e.g., 20) - - %d day of month (e.g., 01) - - %D date; same as %m/%d/%y - - %e day of month, space padded; same as %\_d - - %F full date; like %+4Y-%m-%d - - %g last two digits of year of ISO week number (see %G) - - %G year of ISO week number (see %V); normally useful only - - with %V - - %h same as %b - - %H hour (00..23) - - %I hour (01..12) - - %j day of year (001..366) - - %k hour, space padded ( 0..23); same as %\_H - - %l hour, space padded ( 1..12); same as %\_I - - %m month (01..12) - - %M minute (00..59) - - %n a newline - - %N nanoseconds (000000000..999999999) - - %p locale's equivalent of either AM or PM; blank if not known - - %P like %p, but lower case - - %q quarter of year (1..4) - - %r locale's 12-hour clock time (e.g., 11:11:04 PM) - - %R 24-hour hour and minute; same as %H:%M - - %s seconds since 1970-01-01 00:00:00 UTC - - %S second (00..60) - - %t a tab - - %T time; same as %H:%M:%S - - %u day of week (1..7); 1 is Monday - - %U week number of year, with Sunday as first day of week - - (00..53) - - %V ISO week number, with Monday as first day of week (01..53) - - %w day of week (0..6); 0 is Sunday - - %W week number of year, with Monday as first day of week - - (00..53) - - %x locale's date representation (e.g., 12/31/99) - - %X locale's time representation (e.g., 23:13:48) - - %y last two digits of year (00..99) - - %Y year - - %z +hhmm numeric time zone (e.g., -0400) - - %:z +hh:mm numeric time zone (e.g., -04:00) - - %::z +hh:mm:ss numeric time zone (e.g., -04:00:00) - - %:::z numeric time zone with : to necessary precision (e.g., - - -04, +05:30) - - %Z alphabetic time zone abbreviation (e.g., EDT) - - By default, date pads numeric fields with zeroes. The following - - optional flags may follow '%': - - - (hyphen) do not pad the field - - \_ (underscore) pad with spaces - - 0 (zero) pad with zeros - - + pad with zeros, and put '+' before future years with >4 - - digits - - ^ use upper case if possible - - # use opposite case if possible - -parse\_time ------------ - -u64 parse\_time(String time\_string) - -### Parameters - -**time\_string** : a string containing a date and/or time in text form - -**return value** : the interpreted 'time\_string' as a Unix timestamp - -### Description - -Attempts to parse the given 'time\_string' into a Unix timestamp. - -URI Functions -============= - -encode\_query -------------- - -String encode\_query(StringMap map) - -### Parameters - -**q** : StringMap containing URL parameters to be encoded - -**return value** : a string with the encoded parameters - -### Description - -Encodes a StringMap containing URL parameters into a single String. - -make\_session\_id ------------------ - -String make\_session\_id() - -### Parameters - -**return value** : a new session ID - -### Description - -Creates a session ID - -parse\_query ------------- - -StringMap parse\_query(String q) - -### Parameters - -**q** : string containing URL parameters - -**return value** : a StringMap containing the parameters - -### Description - -Decodes a string of the format 'a=b&c=d' into a StringMap containing keyed entries. - -uri\_decode ------------ - -String uri\_decode(String s) - -### Parameters - -**s** : string containing URI encoded data - -**return value** : a string that contains the decoded version of 's' - -### Description - -Decodes an URI-encoded string 's'. - -uri\_encode ------------ - -String uri\_encode(String s) - -### Parameters - -**s** : string that should be encoded - -**return value** : an URI-encoded version of 's' - -### Description - -URI-encodes a string. - -Other Functions and Data Structures -=================================== - -context -------- - -Request\* context; - -### ServerState\* server - -Contains the current server state - -### StringMap params - -All FastCGI server parameters - -### StringMap get - -The current request's GET variables - -### StringMap post - -The current request's POST variables - -### StringMap cookies - -Cookies that have been transmitted by the browser - -### StringMap session - -The current session - -### String session\_id - -ID of the session cookie - -String session\_name - -Name of the session cookie - -### DTree var - -Variable user-defined data - -### std::vector uploaded\_files - -Files that have been uploaded in the current request - -### StringMap header - -Headers to be sent back to the browser - -### StringList set\_cookies; - -Cookies that should be sent back to the browser - -### u64 random\_seed - -The current request's "random" noise generator seed - -### u64 random\_index - -The current request's "random" noise generator index position - -### MemoryArena\* mem - -Contains the current request's memory arena - -### bool flags.log\_request - -Whether the request should be logged - -### Stats - -u32 stats.bytes\_written - -f64 stats.time\_init - -f64 stats.time\_start - -f64 stats.time\_end - -### invoke(String file\_name, \[DTree& call\_param\]) - -Invokes the UCE file 'file\_name' - -#load ------ - -#load "myfile.uce" - -### Parameters - -**file name** : name of an UCE file that should be included - -### Description - -Includes another UCE file - -float\_val ----------- - -f64 float\_val(String s) - -### Parameters - -**s** : string to be converted - -**return value** : a f64 containing the number (0 if no number could be identified). - -### Description - -Extracts a floating point number from a String. - -html\_escape ------------- - -String html\_escape(String s) - -### Parameters - -**s** : string to be escaped - -**return value** : an HTML-safe escaped version of 's' - -### Description - -Returns a version of the input string where the following characters have been replace by HTML entities: - -* & → & -* < → lt; -* \> → > -* " → " - -int\_val --------- - -u64 int\_val(String s, u32 base = 10) - -### Parameters - -**s** : string to be converted - -**base** : number system base (default 10) - -**return value** : a u64 containing the number (0 if no number could be identified). - -### Description - -Extracts an integer from a String. - -json\_decode ------------- - -DTree json\_decode(String s) - -### Parameters - -**s** : string containing JSON data - -**return value** : a DTree object containing the deserialized JSON data - -### Description - -Deserializes 's' into a DTree structure. - -json\_encode ------------- - -String json\_encode(DTree t) - -### Parameters - -**t** : DTree object to be serialized - -**return value** : string containing the JSON result - -### Description - -Serializes a DTree structure 't' into a String in JSON notation. - -print ------ - -void print(...val) - -### Parameters - -**...val** : one or more values that should be output - -### Description - -Appends data to the current request's output stream. - -var\_dump ---------- - -String var\_dump(StringMap t, String prefix = "", String postfix = "\\n") - -String var\_dump(StringList t, String prefix = "", String postfix = "\\n") - -String var\_dump(DTree t, String prefix = "", String postfix = "\\n") - -### Parameters - -**t** : object to be dumped into a string - -**return value** : string containing a human-friendly representation of 't' - -### Description - -Returns a string representation of 't' intended for debugging. +This project is largely human-made, with all the typical idiosyncracies of my projects clearly visible. However, OpenAI Codex was used for code review and documentation. Claude Opus was used for UI design work, and I used VS Code's git commit message generator. diff --git a/codesearch b/codesearch new file mode 100755 index 0000000..205bd4d --- /dev/null +++ b/codesearch @@ -0,0 +1,58 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" + +show_help() { + cat <<'EOF' +Usage: + ./codesearch [rg options...] + +Examples: + ./codesearch "ws_send" + ./codesearch "RENDER\\(Request& context\\)" + ./codesearch -tcpp "compiler_invoke" + ./codesearch -g '*.uce' "context.call" + +Notes: + - This is a thin wrapper around ripgrep (`rg`). + - It searches from the repository root. + - Generated/build directories are excluded by default: + `.git/`, `bin/`, `dist/`, and `work/`. +EOF +} + +if [[ $# -eq 0 ]]; then + show_help + exit 1 +fi + +case "${1:-}" in + -h|--help|help) + show_help + exit 0 + ;; +esac + +if command -v rg >/dev/null 2>&1; then + exec rg \ + --line-number \ + --column \ + --smart-case \ + --glob '!.git/**' \ + --glob '!bin/**' \ + --glob '!dist/**' \ + --glob '!work/**' \ + "$@" +fi + +pattern="$1" +shift || true + +echo "ripgrep (rg) is not installed; falling back to grep." >&2 +exec grep -RIn \ + --exclude-dir=.git \ + --exclude-dir=bin \ + --exclude-dir=dist \ + --exclude-dir=work \ + -- "$pattern" . diff --git a/doc/pages/1_WS.txt b/doc/pages/1_WS.txt deleted file mode 100644 index c9b14ee..0000000 --- a/doc/pages/1_WS.txt +++ /dev/null @@ -1,20 +0,0 @@ -:sig -WS() - -:desc -Defines the WebSocket message handler for the current `.ws.uce` page. - -The same page may expose both `RENDER()` and `WS()`. `RENDER()` serves the initial HTTP response, while `WS()` is called whenever a complete WebSocket message arrives for that page. - -UCE reassembles fragmented messages before calling `WS()`. Text and binary frames are both delivered. Use `ws_opcode()` and `ws_is_binary()` to distinguish them. - -The `call` parameter passed into `WS()` contains: - -- `message` -- `connection_id` -- `scope` -- `opcode` -- `document_uri` - -:see ->websocket diff --git a/doc/pages/draw_float.txt b/doc/pages/draw_float.txt deleted file mode 100644 index 5a1b3fd..0000000 --- a/doc/pages/draw_float.txt +++ /dev/null @@ -1,13 +0,0 @@ -:sig -f64 draw_float(f64 from, f64 to) - -:params -from : minimum value -to : maximum value -return value : a noise value between 'from' and 'to' - -:desc -This function works exactly like generate_float(), but context->random_index is used for the 'index' value and context->random_seed is used for the seed. After this function has been called, the context->random_index is increased by one. At the start of every request, context->random_seed is automatically populated with a new seed value. - -:see ->noise diff --git a/doc/pages/draw_int.txt b/doc/pages/draw_int.txt deleted file mode 100644 index 236b744..0000000 --- a/doc/pages/draw_int.txt +++ /dev/null @@ -1,13 +0,0 @@ -:sig -u64 draw_int(u64 from, u64 to) - -:params -from : minimum value -to : maximum value -return value : a noise value between 'from' and 'to' - -:desc -This function works exactly like generate_int(), but context->random_index is used for the 'index' value and context->random_seed is used for the seed. After this function has been called, the context->random_index is increased by one. At the start of every request, context->random_seed is automatically populated with a new seed value. - -:see ->noise diff --git a/doc/pages/render_file.txt b/doc/pages/render_file.txt deleted file mode 100644 index ddb7b0f..0000000 --- a/doc/pages/render_file.txt +++ /dev/null @@ -1,16 +0,0 @@ -:sig -void render_file(String file_name, DTree& call_param = null) - -:params -file_name : UCE file to load and execute -call_param : optional, call parameter - -:desc -Calls another UCE file and executes its RENDER() function. - -:Example -// call a common page template -render_file("page-template.uce"); - -:see ->ob diff --git a/doc/pages/ws_broadcast.txt b/doc/pages/ws_broadcast.txt deleted file mode 100644 index 30dc7c7..0000000 --- a/doc/pages/ws_broadcast.txt +++ /dev/null @@ -1,17 +0,0 @@ -:sig -u64 ws_broadcast(String message, String scope = "") - -:params -message : text message to send -scope : optional scope identifier, defaults to the current WebSocket page scope -return value : number of clients the message was queued for - -:desc -Queues a text WebSocket message for every client connected to the given scope and returns the number of recipients. - -If `scope` is omitted, the current page scope is used. - -This helper currently sends text frames only. - -:see ->websocket diff --git a/doc/pages/ws_send_to.txt b/doc/pages/ws_send_to.txt deleted file mode 100644 index 971bf78..0000000 --- a/doc/pages/ws_send_to.txt +++ /dev/null @@ -1,15 +0,0 @@ -:sig -bool ws_send_to(String connection_id, String message) - -:params -connection_id : ID of the target WebSocket client -message : text message to send -return value : true if the target connection exists and the message was queued - -:desc -Queues a text WebSocket message for one specific connected client. - -This helper currently sends text frames only. - -:see ->websocket diff --git a/examples/blog/LICENSE.txt b/examples/blog/LICENSE.txt deleted file mode 100644 index 294e91d..0000000 --- a/examples/blog/LICENSE.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) HTML5 Boilerplate - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/examples/blog/browserconfig.xml b/examples/blog/browserconfig.xml deleted file mode 100644 index e5a529d..0000000 --- a/examples/blog/browserconfig.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/examples/blog/config/settings.uce b/examples/blog/config/settings.uce deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blog/css/main.css b/examples/blog/css/main.css deleted file mode 100644 index 416a37e..0000000 --- a/examples/blog/css/main.css +++ /dev/null @@ -1,263 +0,0 @@ -/*! HTML5 Boilerplate v8.0.0 | MIT License | https://html5boilerplate.com/ */ - -/* main.css 2.1.0 | MIT License | https://github.com/h5bp/main.css#readme */ -/* - * What follows is the result of much research on cross-browser styling. - * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, - * Kroc Camen, and the H5BP dev community and team. - */ - -/* ========================================================================== - Base styles: opinionated defaults - ========================================================================== */ - -html { - color: #222; - font-size: 1em; - line-height: 1.4; -} - -/* - * Remove text-shadow in selection highlight: - * https://twitter.com/miketaylr/status/12228805301 - * - * Vendor-prefixed and regular ::selection selectors cannot be combined: - * https://stackoverflow.com/a/16982510/7133471 - * - * Customize the background color to match your design. - */ - -::-moz-selection { - background: #b3d4fc; - text-shadow: none; -} - -::selection { - background: #b3d4fc; - text-shadow: none; -} - -/* - * A better looking default horizontal rule - */ - -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -/* - * Remove the gap between audio, canvas, iframes, - * images, videos and the bottom of their containers: - * https://github.com/h5bp/html5-boilerplate/issues/440 - */ - -audio, -canvas, -iframe, -img, -svg, -video { - vertical-align: middle; -} - -/* - * Remove default fieldset styles. - */ - -fieldset { - border: 0; - margin: 0; - padding: 0; -} - -/* - * Allow only vertical resizing of textareas. - */ - -textarea { - resize: vertical; -} - -/* ========================================================================== - Author's custom styles - ========================================================================== */ - -/* ========================================================================== - Helper classes - ========================================================================== */ - -/* - * Hide visually and from screen readers - */ - -.hidden, -[hidden] { - display: none !important; -} - -/* - * Hide only visually, but have it available for screen readers: - * https://snook.ca/archives/html_and_css/hiding-content-for-accessibility - * - * 1. For long content, line feeds are not interpreted as spaces and small width - * causes content to wrap 1 word per line: - * https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe - */ - -.sr-only { - border: 0; - clip: rect(0, 0, 0, 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - white-space: nowrap; - width: 1px; - /* 1 */ -} - -/* - * Extends the .sr-only class to allow the element - * to be focusable when navigated to via the keyboard: - * https://www.drupal.org/node/897638 - */ - -.sr-only.focusable:active, -.sr-only.focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - white-space: inherit; - width: auto; -} - -/* - * Hide visually and from screen readers, but maintain layout - */ - -.invisible { - visibility: hidden; -} - -/* - * Clearfix: contain floats - * - * For modern browsers - * 1. The space content is one way to avoid an Opera bug when the - * `contenteditable` attribute is included anywhere else in the document. - * Otherwise it causes space to appear at the top and bottom of elements - * that receive the `clearfix` class. - * 2. The use of `table` rather than `block` is only necessary if using - * `:before` to contain the top-margins of child elements. - */ - -.clearfix::before, -.clearfix::after { - content: " "; - display: table; -} - -.clearfix::after { - clear: both; -} - -/* ========================================================================== - EXAMPLE Media Queries for Responsive Design. - These examples override the primary ('mobile first') styles. - Modify as content requires. - ========================================================================== */ - -@media only screen and (min-width: 35em) { - /* Style adjustments for viewports that meet the condition */ -} - -@media print, - (-webkit-min-device-pixel-ratio: 1.25), - (min-resolution: 1.25dppx), - (min-resolution: 120dpi) { - /* Style adjustments for high resolution devices */ -} - -/* ========================================================================== - Print styles. - Inlined to avoid the additional HTTP request: - https://www.phpied.com/delay-loading-your-print-css/ - ========================================================================== */ - -@media print { - *, - *::before, - *::after { - background: #fff !important; - color: #000 !important; - /* Black prints faster */ - box-shadow: none !important; - text-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]::after { - content: " (" attr(href) ")"; - } - - abbr[title]::after { - content: " (" attr(title) ")"; - } - - /* - * Don't show links that are fragment identifiers, - * or use the `javascript:` pseudo protocol - */ - a[href^="#"]::after, - a[href^="javascript:"]::after { - content: ""; - } - - pre { - white-space: pre-wrap !important; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - /* - * Printing Tables: - * https://web.archive.org/web/20180815150934/http://css-discuss.incutio.com/wiki/Printing_Tables - */ - thead { - display: table-header-group; - } - - tr, - img { - page-break-inside: avoid; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } -} - diff --git a/examples/blog/css/normalize.css b/examples/blog/css/normalize.css deleted file mode 100644 index 192eb9c..0000000 --- a/examples/blog/css/normalize.css +++ /dev/null @@ -1,349 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ - -/* Document - ========================================================================== */ - -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ - -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers. - */ - -body { - margin: 0; -} - -/** - * Render the `main` element consistently in IE. - */ - -main { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Remove the gray background on active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10. - */ - -img { - border-style: none; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ - -button, -[type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ - -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ - -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ - -/** - * Add the correct display in IE 10+. - */ - -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ - -[hidden] { - display: none; -} diff --git a/examples/blog/favicon.ico b/examples/blog/favicon.ico deleted file mode 100644 index be74abd..0000000 Binary files a/examples/blog/favicon.ico and /dev/null differ diff --git a/examples/blog/humans.txt b/examples/blog/humans.txt deleted file mode 100644 index 8d2330f..0000000 --- a/examples/blog/humans.txt +++ /dev/null @@ -1,15 +0,0 @@ -# humanstxt.org/ -# The humans responsible & technology colophon - -# TEAM - - -- -- - -# THANKS - - - -# TECHNOLOGY COLOPHON - - CSS3, HTML5 - Apache Server Configs, jQuery, Modernizr, Normalize.css diff --git a/examples/blog/img/.gitignore b/examples/blog/img/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blog/index.uce b/examples/blog/index.uce deleted file mode 100644 index 0acc4e8..0000000 --- a/examples/blog/index.uce +++ /dev/null @@ -1,7 +0,0 @@ -#load "config/settings.uce" -#load "lib/site.uce" - -RENDER() -{ - render_file("pages/page.html.uce"); -} diff --git a/examples/blog/js/main.js b/examples/blog/js/main.js deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blog/js/plugins.js b/examples/blog/js/plugins.js deleted file mode 100644 index feb7d19..0000000 --- a/examples/blog/js/plugins.js +++ /dev/null @@ -1,24 +0,0 @@ -// Avoid `console` errors in browsers that lack a console. -(function() { - var method; - var noop = function () {}; - var methods = [ - 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', - 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', - 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', - 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' - ]; - var length = methods.length; - var console = (window.console = window.console || {}); - - while (length--) { - method = methods[length]; - - // Only stub undefined methods. - if (!console[method]) { - console[method] = noop; - } - } -}()); - -// Place any jQuery/helper plugins in here. diff --git a/examples/blog/js/vendor/modernizr-3.11.2.min.js b/examples/blog/js/vendor/modernizr-3.11.2.min.js deleted file mode 100644 index feada51..0000000 --- a/examples/blog/js/vendor/modernizr-3.11.2.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! modernizr 3.11.2 (Custom Build) | MIT * - * https://modernizr.com/download/?-cssanimations-csscolumns-customelements-flexbox-history-picture-pointerevents-postmessage-sizes-srcset-webgl-websockets-webworkers-addtest-domprefixes-hasevent-mq-prefixedcssvalue-prefixes-setclasses-testallprops-testprop-teststyles !*/ -!function(e,t,n,r){function o(e,t){return typeof e===t}function i(e){var t=_.className,n=Modernizr._config.classPrefix||"";if(S&&(t=t.baseVal),Modernizr._config.enableJSClass){var r=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(r,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(e.length>0&&(t+=" "+n+e.join(" "+n)),S?_.className.baseVal=t:_.className=t)}function s(e,t){if("object"==typeof e)for(var n in e)k(e,n)&&s(n,e[n]);else{e=e.toLowerCase();var r=e.split("."),o=Modernizr[r[0]];if(2===r.length&&(o=o[r[1]]),void 0!==o)return Modernizr;t="function"==typeof t?t():t,1===r.length?Modernizr[r[0]]=t:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=t),i([(t&&!1!==t?"":"no-")+r.join("-")]),Modernizr._trigger(e,t)}return Modernizr}function a(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):S?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function l(){var e=n.body;return e||(e=a(S?"svg":"body"),e.fake=!0),e}function u(e,t,r,o){var i,s,u,f,c="modernizr",d=a("div"),p=l();if(parseInt(r,10))for(;r--;)u=a("div"),u.id=o?o[r]:c+(r+1),d.appendChild(u);return i=a("style"),i.type="text/css",i.id="s"+c,(p.fake?p:d).appendChild(i),p.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(n.createTextNode(e)),d.id=c,p.fake&&(p.style.background="",p.style.overflow="hidden",f=_.style.overflow,_.style.overflow="hidden",_.appendChild(p)),s=t(d,e),p.fake?(p.parentNode.removeChild(p),_.style.overflow=f,_.offsetHeight):d.parentNode.removeChild(d),!!s}function f(e,n,r){var o;if("getComputedStyle"in t){o=getComputedStyle.call(t,e,n);var i=t.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(i){var s=i.error?"error":"log";i[s].call(i,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!n&&e.currentStyle&&e.currentStyle[r];return o}function c(e,t){return!!~(""+e).indexOf(t)}function d(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function p(e,n){var o=e.length;if("CSS"in t&&"supports"in t.CSS){for(;o--;)if(t.CSS.supports(d(e[o]),n))return!0;return!1}if("CSSSupportsRule"in t){for(var i=[];o--;)i.push("("+d(e[o])+":"+n+")");return i=i.join(" or "),u("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"===f(e,null,"position")})}return r}function m(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function h(e,t,n,i){function s(){u&&(delete N.style,delete N.modElem)}if(i=!o(i,"undefined")&&i,!o(n,"undefined")){var l=p(e,n);if(!o(l,"undefined"))return l}for(var u,f,d,h,A,v=["modernizr","tspan","samp"];!N.style&&v.length;)u=!0,N.modElem=a(v.shift()),N.style=N.modElem.style;for(d=e.length,f=0;f - Hello - - return(0); -} - -RENDER() -{ - <> - - - - - - - - - - - - - - - - - - - - - - - - get["view"], "default")) + ".uce"; - if(file_exists(view_name)) - render_file(view_name); - else - print("view not found"); - ?> - - - - -} diff --git a/examples/blog/robots.txt b/examples/blog/robots.txt deleted file mode 100644 index d0e5f1b..0000000 --- a/examples/blog/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# www.robotstxt.org/ - -# Allow crawling of all content -User-agent: * -Disallow: diff --git a/examples/blog/site.webmanifest b/examples/blog/site.webmanifest deleted file mode 100644 index 222ae16..0000000 --- a/examples/blog/site.webmanifest +++ /dev/null @@ -1,12 +0,0 @@ -{ - "short_name": "", - "name": "", - "icons": [{ - "src": "icon.png", - "type": "image/png", - "sizes": "192x192" - }], - "start_url": "/?utm_source=homescreen", - "background_color": "#fafafa", - "theme_color": "#fafafa" -} diff --git a/examples/blog/tile-wide.png b/examples/blog/tile-wide.png deleted file mode 100644 index ccd739c..0000000 Binary files a/examples/blog/tile-wide.png and /dev/null differ diff --git a/examples/blog/tile.png b/examples/blog/tile.png deleted file mode 100644 index f820f61..0000000 Binary files a/examples/blog/tile.png and /dev/null differ diff --git a/scripts/setup.h.template b/scripts/setup.h.template index efacf04..bf8e77d 100644 --- a/scripts/setup.h.template +++ b/scripts/setup.h.template @@ -1,8 +1,12 @@ +#ifndef UCE_SET_CURRENT_REQUEST_DEFINED +#define UCE_SET_CURRENT_REQUEST_DEFINED + /*load_declarations*/ extern "C" void set_current_request(Request* _request) { context = _request; - signal(SIGSEGV, on_segfault); /*load_units*/ } + +#endif diff --git a/site/doc/areas/markup.txt b/site/doc/areas/markup.txt new file mode 100644 index 0000000..70bdb03 --- /dev/null +++ b/site/doc/areas/markup.txt @@ -0,0 +1,3 @@ +Markup Functions +markdown_to_ast +markdown_to_html diff --git a/doc/areas/memcache.txt b/site/doc/areas/memcache.txt similarity index 100% rename from doc/areas/memcache.txt rename to site/doc/areas/memcache.txt diff --git a/doc/areas/mysql.txt b/site/doc/areas/mysql.txt similarity index 100% rename from doc/areas/mysql.txt rename to site/doc/areas/mysql.txt diff --git a/doc/areas/noise.txt b/site/doc/areas/noise.txt similarity index 100% rename from doc/areas/noise.txt rename to site/doc/areas/noise.txt diff --git a/doc/areas/ob.txt b/site/doc/areas/ob.txt similarity index 58% rename from doc/areas/ob.txt rename to site/doc/areas/ob.txt index fea6ddc..a394486 100644 --- a/doc/areas/ob.txt +++ b/site/doc/areas/ob.txt @@ -1,10 +1,15 @@ Output / Invocation Functions +1_RENDER call_file +component +component_exists +component_resolve load ob_close ob_get ob_get_close ob_start print +render_component render_file diff --git a/doc/areas/session.txt b/site/doc/areas/session.txt similarity index 100% rename from doc/areas/session.txt rename to site/doc/areas/session.txt diff --git a/doc/areas/socket.txt b/site/doc/areas/socket.txt similarity index 100% rename from doc/areas/socket.txt rename to site/doc/areas/socket.txt diff --git a/doc/areas/string.txt b/site/doc/areas/string.txt similarity index 100% rename from doc/areas/string.txt rename to site/doc/areas/string.txt diff --git a/doc/areas/sys.txt b/site/doc/areas/sys.txt similarity index 100% rename from doc/areas/sys.txt rename to site/doc/areas/sys.txt diff --git a/doc/areas/task.txt b/site/doc/areas/task.txt similarity index 100% rename from doc/areas/task.txt rename to site/doc/areas/task.txt diff --git a/doc/areas/time.txt b/site/doc/areas/time.txt similarity index 100% rename from doc/areas/time.txt rename to site/doc/areas/time.txt diff --git a/site/doc/areas/types.txt b/site/doc/areas/types.txt new file mode 100644 index 0000000..854b9c7 --- /dev/null +++ b/site/doc/areas/types.txt @@ -0,0 +1,5 @@ +Types + +DTree +String +StringMap diff --git a/doc/areas/uri.txt b/site/doc/areas/uri.txt similarity index 100% rename from doc/areas/uri.txt rename to site/doc/areas/uri.txt diff --git a/doc/areas/websocket.txt b/site/doc/areas/websocket.txt similarity index 91% rename from doc/areas/websocket.txt rename to site/doc/areas/websocket.txt index 3ad8b88..c7470ea 100644 --- a/doc/areas/websocket.txt +++ b/site/doc/areas/websocket.txt @@ -1,6 +1,5 @@ WebSocket Functions -ws_broadcast ws_close ws_connection_count ws_connection_id diff --git a/doc/index.uce b/site/doc/index.uce similarity index 97% rename from doc/index.uce rename to site/doc/index.uce index 74d7152..3ff7c8a 100644 --- a/doc/index.uce +++ b/site/doc/index.uce @@ -19,10 +19,10 @@ void render_see_section(String name) } -RENDER() +RENDER(Request& context) { - String page = first(context->get["p"], "index"); + String page = first(context.get["p"], "index"); <> diff --git a/doc/pages/0_context.txt b/site/doc/pages/0_context.txt similarity index 71% rename from doc/pages/0_context.txt rename to site/doc/pages/0_context.txt index b414d2c..da04738 100644 --- a/doc/pages/0_context.txt +++ b/site/doc/pages/0_context.txt @@ -1,5 +1,5 @@ :sig -Request* context; +Request& context; :ServerState* server Contains the current server state @@ -22,12 +22,18 @@ The current session :String session_id ID of the session cookie -String session_name +:String session_name Name of the session cookie :DTree var Variable user-defined data +:DTree call +Invocation or message-local structured data + +:DTree connection +Broker-owned per-WebSocket-connection state. Inside `WS(Request& context)`, updates to this tree persist for the lifetime of that socket connection. + :std::vector uploaded_files Files that have been uploaded in the current request @@ -43,9 +49,6 @@ The current request's "random" noise generator seed :u64 random_index The current request's "random" noise generator index position -:MemoryArena* mem -Contains the current request's memory arena - :bool flags.log_request Whether the request should be logged @@ -55,9 +58,10 @@ Whether the request should be logged f64 stats.time_start f64 stats.time_end -:invoke(String file_name, [DTree& call_param]) -Invokes the UCE file 'file_name' - - +:render_file(String file_name, [Request& context]) +Invokes another UCE file using the current or supplied request context + +:see +>types diff --git a/site/doc/pages/1_RENDER.txt b/site/doc/pages/1_RENDER.txt new file mode 100644 index 0000000..562c296 --- /dev/null +++ b/site/doc/pages/1_RENDER.txt @@ -0,0 +1,24 @@ +:sig +RENDER(Request& context) + +:desc +Defines the main HTTP render handler for the current `.uce` page. + +When a page is requested over HTTP, the runtime loads the target file and calls its `RENDER(Request& context)` function. + +Pages may also export additional named render handlers with `RENDER:NAME(Request& context)`. + +Named render handlers are not used for the page's direct HTTP entrypoint. They are intended for component-style sub-rendering through helpers such as `component("components/card:BODY", props, context)` or `render_component("components/card:BODY", props, context)`. + +The default page entrypoint is always the plain `RENDER(Request& context)` handler. + +The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree. + +For a normal direct page request, `context.call` starts empty. + +If the page is invoked from another UCE file via `render_file(file_name, context)`, the callee receives that same `context`. + +Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. In that case `RENDER(Request& context)` serves the initial HTTP response and `WS(Request& context)` handles subsequent WebSocket messages. + +:see +>ob diff --git a/site/doc/pages/1_WS.txt b/site/doc/pages/1_WS.txt new file mode 100644 index 0000000..ce39d39 --- /dev/null +++ b/site/doc/pages/1_WS.txt @@ -0,0 +1,26 @@ +:sig +WS(Request& context) + +:desc +Defines the WebSocket message handler for the current `.ws.uce` page. + +The same page may expose both `RENDER(Request& context)` and `WS(Request& context)`. `RENDER(Request& context)` serves the initial HTTP response, while `WS(Request& context)` is called whenever a complete WebSocket message arrives for that page. + +UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. Use `context.call`, `context.connection`, `ws_opcode()`, and `ws_is_binary()` to inspect the current message. + +`context.connection` is a broker-owned `DTree` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection. + +The current message data is available in `context.call`: + +context.call["message"] : current message payload + +context.call["connection_id"] : sender connection ID + +context.call["scope"] : current endpoint scope + +context.call["opcode"] : WebSocket opcode of the current message + +context.call["document_uri"] : request URI of the current endpoint + +:see +>websocket diff --git a/site/doc/pages/1_preprocessor.txt b/site/doc/pages/1_preprocessor.txt new file mode 100644 index 0000000..b6ee0be --- /dev/null +++ b/site/doc/pages/1_preprocessor.txt @@ -0,0 +1,95 @@ +:sig +UCE source preprocessing + +:desc +UCE runs a small custom source-to-source preprocessor before Clang sees a `.uce` or `.ws.uce` file. + +The implementation lives in `src/lib/compiler.cpp`. It does not try to parse all of C++. Instead, it performs a narrow character-wise rewrite that understands UCE literal blocks, inline code islands, `#load`, and `EXPORT` harvesting, then writes a generated `.cpp` file and compiles that file into a shared object. + +:Syntax +- `<> ... ` enters literal-output mode. +- Inside a literal block, `` emits raw C++. +- Inside a literal block, `` emits `print(html_escape(expression));`. +- Inside a literal block, `` emits `print(expression);` without HTML escaping. +- `#load "other.uce"` injects another UCE unit at compile time. +- `RENDER(Request& context)` and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`. +- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata. + +:Pipeline +- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`. +- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines `set_current_request(Request*)`. +- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file. +- Each literal block is rewritten into one or more `print(R"( ... )");` calls. +- `` temporarily breaks out of literal printing, emits the enclosed C++ unchanged, then resumes literal output. +- `` becomes `print(html_escape(...));`. The runtime currently provides `html_escape()` overloads for `String`, `u64`, and `f64`. +- `` becomes `print(...);` and is intended for trusted markup or already-escaped content. +- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`. +- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file. +- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`. +- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`. + +:GeneratedFiles +- Source file: `/some/path/page.uce` +- Generated C++: `BIN_DIRECTORY/some/path/page.uce.cpp` +- Shared object: `BIN_DIRECTORY/some/path/page.uce.so` +- Export list: `BIN_DIRECTORY/some/path/page.uce.exports.txt` + +:Example +Example 1: literal output with escaped data +`RENDER(Request& context)` +`{` +` <><h1><?= context.params["DOCUMENT_URI"] ?></h1></>` +`}` + +Roughly becomes: +`print(R"(<h1>)");` +`print(html_escape(context.params["DOCUMENT_URI"]));` +`print(R"(</h1>)");` + +Example 1b: literal output with trusted unescaped markup +`RENDER(Request& context)` +`{` +` <><div class="panel"><?: component("components/card", context.call, context) ?></div></>` +`}` + +Roughly becomes: +`print(R"(<div class="panel">)");` +`print(component("components/card", context.call, context));` +`print(R"(</div>)");` + +Example 2: compile-time composition +`#load "partials/nav.uce"` +`RENDER(Request& context)` +`{` +` <><body>...</body></>` +`}` + +The loaded file is resolved relative to the current source file unless the path is already absolute. + +:Rules +- Literal mode starts only on the exact token `<>`. +- Literal mode ends only on the exact token ``. +- `#load` is recognized only when the current line starts with `#load ` at column 1. +- `EXPORT` harvesting likewise only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace. +- Relative `#load` paths are expanded against the including unit's source directory. +- `render_file()` and `call_file()` are runtime APIs; `#load` is a compile-time include/composition feature. + +:Limitations +- This pass is character-wise, not a full parser. +- Outside literal blocks it only tracks double-quoted C++ strings while deciding whether `<>` should open literal mode. +- It does not understand comments, raw string literals, templates, or general C++ token structure. +- Inside literal blocks it tracks single and double quotes while scanning a `` island so quoted `?>` text does not close the island early. +- Literal blocks are not nested. +- Because literal output is emitted as a C++ raw string literal `R"( ... )"`, literal content must not contain the exact terminator sequence `)"` or the generated C++ will break. +- `#load` depends on the target unit's generated `.cpp` existing and being compilable. If the target cannot be preprocessed or compiled correctly, the including file will fail to compile as well. + +:Debugging +- When a page is compiled, inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor. +- Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems. +- If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`. + +:see +load +render_file +call_file +0_context diff --git a/site/doc/pages/DTree.txt b/site/doc/pages/DTree.txt new file mode 100644 index 0000000..84cb1bc --- /dev/null +++ b/site/doc/pages/DTree.txt @@ -0,0 +1,27 @@ +:sig +DTree + +:desc +Dynamic tree/container type used throughout UCE for structured data. + +`DTree` can hold a `String`, `f64`, `bool`, pointer, or a nested map of child `DTree` values. + +Use `t["key"]` to access or create child entries. Use `push()` / `pop()` when treating it like an array-like container with numeric string keys. + +Common uses include: +`json_decode()` / `json_encode()` +`context.var` +`context.call` +`call_file()` return values + +Useful methods include: +`to_string()` +`to_json()` +`get_type_name()` +`set_bool()` +`remove()` +`clear()` +`each()` + +:see +>types diff --git a/site/doc/pages/String.txt b/site/doc/pages/String.txt new file mode 100644 index 0000000..73987a7 --- /dev/null +++ b/site/doc/pages/String.txt @@ -0,0 +1,16 @@ +:sig +String + +:desc +Primary string type used throughout UCE. + +`String` is an alias for `std::string`. + +It is used for request data, headers, cookie values, file contents, query strings, JSON text, and WebSocket payloads. + +Because it is backed by `std::string`, it is binary-safe and may also contain raw bytes. + +For UTF-8-aware splitting, use helpers such as `split_utf8()` instead of assuming one byte equals one character. + +:see +>types diff --git a/site/doc/pages/StringMap.txt b/site/doc/pages/StringMap.txt new file mode 100644 index 0000000..ed72705 --- /dev/null +++ b/site/doc/pages/StringMap.txt @@ -0,0 +1,22 @@ +:sig +StringMap + +:desc +Associative container mapping `String` keys to `String` values. + +`StringMap` is an alias for `std::map`. + +It is commonly used for: +`context.params` +`context.get` +`context.post` +`context.cookies` +`context.session` +`context.header` + +Because it uses `std::map`, `map["key"]` will create an empty entry when that key does not already exist. + +Related helpers such as `parse_query()` and `encode_query()` convert between query strings and `StringMap` values. + +:see +>types diff --git a/doc/pages/basename.txt b/site/doc/pages/basename.txt similarity index 100% rename from doc/pages/basename.txt rename to site/doc/pages/basename.txt diff --git a/doc/pages/call_file.txt b/site/doc/pages/call_file.txt similarity index 100% rename from doc/pages/call_file.txt rename to site/doc/pages/call_file.txt diff --git a/site/doc/pages/component.txt b/site/doc/pages/component.txt new file mode 100644 index 0000000..ff89191 --- /dev/null +++ b/site/doc/pages/component.txt @@ -0,0 +1,28 @@ +:sig +String component(String name, [DTree props], [Request& context]) + +:desc +Renders another `.uce` file as a component and returns the captured output as a `String`. + +`component()` resolves the target file relative to the current page and also tries the `components/` prefix automatically, mirroring the shorthand used by the web app starter example project. + +Component props are passed in `context.call`. + +Because `` HTML-escapes its value, embed component markup with ``, `print(component(...))`, or use `render_component(...)` for direct output. + +When `name` contains a colon, such as `components/card:BODY`, the part after the colon selects a named render handler exported from the component file through `RENDER:BODY(Request& context)`. + +The default handler is `RENDER(Request& context)`. + +Resolution order is: +exact file name +exact file name with `.uce` +the same two forms under `components/` + +Example: +`DTree props;` +`props["title"] = "Status";` +`<><?: component("workspace/panel", props, context) ?></>` + +:see +>ob diff --git a/site/doc/pages/component_exists.txt b/site/doc/pages/component_exists.txt new file mode 100644 index 0000000..677c34b --- /dev/null +++ b/site/doc/pages/component_exists.txt @@ -0,0 +1,14 @@ +:sig +bool component_exists(String name) + +:desc +Checks whether a component file can be resolved from the current page context. + +Resolution tries the exact name first and then the `components/` shorthand form. + +If `name` contains a colon, only the file portion is used for existence checks. + +This is useful when a page wants to render an optional component if it is present without hard-failing when it is missing. + +:see +>ob diff --git a/site/doc/pages/component_resolve.txt b/site/doc/pages/component_resolve.txt new file mode 100644 index 0000000..f8294fa --- /dev/null +++ b/site/doc/pages/component_resolve.txt @@ -0,0 +1,14 @@ +:sig +String component_resolve(String name) + +:desc +Resolves a component name to the concrete `.uce` file path that will be loaded. + +Resolution tries the exact file name first, then the same name with `.uce` appended, and then the same two forms under the `components/` prefix. + +If `name` contains a colon, only the file portion is used for resolution. + +This is primarily a debugging helper so you can see which concrete file a shorthand component name maps to. + +:see +>ob diff --git a/doc/pages/concat.txt b/site/doc/pages/concat.txt similarity index 100% rename from doc/pages/concat.txt rename to site/doc/pages/concat.txt diff --git a/doc/pages/date.txt b/site/doc/pages/date.txt similarity index 100% rename from doc/pages/date.txt rename to site/doc/pages/date.txt diff --git a/doc/pages/dirname.txt b/site/doc/pages/dirname.txt similarity index 100% rename from doc/pages/dirname.txt rename to site/doc/pages/dirname.txt diff --git a/site/doc/pages/draw_float.txt b/site/doc/pages/draw_float.txt new file mode 100644 index 0000000..036a8f9 --- /dev/null +++ b/site/doc/pages/draw_float.txt @@ -0,0 +1,13 @@ +:sig +f64 draw_float(f64 from, f64 to) + +:params +from : minimum value +to : maximum value +return value : a noise value between 'from' and 'to' + +:desc +This function works exactly like generate_float(), but context.random_index is used for the 'index' value and context.random_seed is used for the seed. After this function has been called, the context.random_index is increased by one. At the start of every request, context.random_seed is automatically populated with a new seed value. + +:see +>noise diff --git a/site/doc/pages/draw_int.txt b/site/doc/pages/draw_int.txt new file mode 100644 index 0000000..86dcea1 --- /dev/null +++ b/site/doc/pages/draw_int.txt @@ -0,0 +1,13 @@ +:sig +u64 draw_int(u64 from, u64 to) + +:params +from : minimum value +to : maximum value +return value : a noise value between 'from' and 'to' + +:desc +This function works exactly like generate_int(), but context.random_index is used for the 'index' value and context.random_seed is used for the seed. After this function has been called, the context.random_index is increased by one. At the start of every request, context.random_seed is automatically populated with a new seed value. + +:see +>noise diff --git a/doc/pages/encode_query.txt b/site/doc/pages/encode_query.txt similarity index 100% rename from doc/pages/encode_query.txt rename to site/doc/pages/encode_query.txt diff --git a/doc/pages/expand_path.txt b/site/doc/pages/expand_path.txt similarity index 100% rename from doc/pages/expand_path.txt rename to site/doc/pages/expand_path.txt diff --git a/doc/pages/file_append.txt b/site/doc/pages/file_append.txt similarity index 100% rename from doc/pages/file_append.txt rename to site/doc/pages/file_append.txt diff --git a/doc/pages/file_exists.txt b/site/doc/pages/file_exists.txt similarity index 100% rename from doc/pages/file_exists.txt rename to site/doc/pages/file_exists.txt diff --git a/doc/pages/file_get_contents.txt b/site/doc/pages/file_get_contents.txt similarity index 100% rename from doc/pages/file_get_contents.txt rename to site/doc/pages/file_get_contents.txt diff --git a/doc/pages/file_mtime.txt b/site/doc/pages/file_mtime.txt similarity index 100% rename from doc/pages/file_mtime.txt rename to site/doc/pages/file_mtime.txt diff --git a/doc/pages/file_put_contents.txt b/site/doc/pages/file_put_contents.txt similarity index 100% rename from doc/pages/file_put_contents.txt rename to site/doc/pages/file_put_contents.txt diff --git a/doc/pages/filter.txt b/site/doc/pages/filter.txt similarity index 100% rename from doc/pages/filter.txt rename to site/doc/pages/filter.txt diff --git a/doc/pages/first.txt b/site/doc/pages/first.txt similarity index 100% rename from doc/pages/first.txt rename to site/doc/pages/first.txt diff --git a/doc/pages/float_val.txt b/site/doc/pages/float_val.txt similarity index 100% rename from doc/pages/float_val.txt rename to site/doc/pages/float_val.txt diff --git a/doc/pages/gen_float.txt b/site/doc/pages/gen_float.txt similarity index 100% rename from doc/pages/gen_float.txt rename to site/doc/pages/gen_float.txt diff --git a/doc/pages/gen_int.txt b/site/doc/pages/gen_int.txt similarity index 100% rename from doc/pages/gen_int.txt rename to site/doc/pages/gen_int.txt diff --git a/doc/pages/gen_noise01.txt b/site/doc/pages/gen_noise01.txt similarity index 100% rename from doc/pages/gen_noise01.txt rename to site/doc/pages/gen_noise01.txt diff --git a/doc/pages/gen_noise32.txt b/site/doc/pages/gen_noise32.txt similarity index 100% rename from doc/pages/gen_noise32.txt rename to site/doc/pages/gen_noise32.txt diff --git a/doc/pages/gen_noise64.txt b/site/doc/pages/gen_noise64.txt similarity index 100% rename from doc/pages/gen_noise64.txt rename to site/doc/pages/gen_noise64.txt diff --git a/doc/pages/gen_sha1.txt b/site/doc/pages/gen_sha1.txt similarity index 100% rename from doc/pages/gen_sha1.txt rename to site/doc/pages/gen_sha1.txt diff --git a/doc/pages/get_cwd.txt b/site/doc/pages/get_cwd.txt similarity index 100% rename from doc/pages/get_cwd.txt rename to site/doc/pages/get_cwd.txt diff --git a/doc/pages/gmdate.txt b/site/doc/pages/gmdate.txt similarity index 100% rename from doc/pages/gmdate.txt rename to site/doc/pages/gmdate.txt diff --git a/doc/pages/html_escape.txt b/site/doc/pages/html_escape.txt similarity index 100% rename from doc/pages/html_escape.txt rename to site/doc/pages/html_escape.txt diff --git a/doc/pages/int_val.txt b/site/doc/pages/int_val.txt similarity index 100% rename from doc/pages/int_val.txt rename to site/doc/pages/int_val.txt diff --git a/doc/pages/join.txt b/site/doc/pages/join.txt similarity index 100% rename from doc/pages/join.txt rename to site/doc/pages/join.txt diff --git a/doc/pages/json_decode.txt b/site/doc/pages/json_decode.txt similarity index 100% rename from doc/pages/json_decode.txt rename to site/doc/pages/json_decode.txt diff --git a/doc/pages/json_encode.txt b/site/doc/pages/json_encode.txt similarity index 100% rename from doc/pages/json_encode.txt rename to site/doc/pages/json_encode.txt diff --git a/doc/pages/kill.txt b/site/doc/pages/kill.txt similarity index 100% rename from doc/pages/kill.txt rename to site/doc/pages/kill.txt diff --git a/doc/pages/load.txt b/site/doc/pages/load.txt similarity index 100% rename from doc/pages/load.txt rename to site/doc/pages/load.txt diff --git a/doc/pages/ls.txt b/site/doc/pages/ls.txt similarity index 100% rename from doc/pages/ls.txt rename to site/doc/pages/ls.txt diff --git a/doc/pages/make_session_id.txt b/site/doc/pages/make_session_id.txt similarity index 100% rename from doc/pages/make_session_id.txt rename to site/doc/pages/make_session_id.txt diff --git a/site/doc/pages/markdown_to_ast.txt b/site/doc/pages/markdown_to_ast.txt new file mode 100644 index 0000000..d772b54 --- /dev/null +++ b/site/doc/pages/markdown_to_ast.txt @@ -0,0 +1,77 @@ +:sig +DTree markdown_to_ast(String src) +DTree markdown_to_ast(String src, DTree options) + +:params +src : markdown source text +options : optional markdown options tree +return value : a `DTree` document AST + +:desc +Parses Markdown source into a structured `DTree` document tree. + +The parser targets a practical GitHub-flavored subset by default: +- ATX headings (`#`) +- setext headings +- paragraphs +- blockquotes +- ordered and unordered lists +- task list items +- fenced code blocks +- tables +- horizontal rules +- emphasis / strong / strikethrough +- links, images, autolinks, and code spans +- `:::` directive blocks for component-based extensions + +The returned AST uses `type` plus node-specific fields such as `level`, `text`, `lang`, `href`, `src`, `name`, `argument`, `attrs`, and `children`. + +Top-level documents use: +`type = "document"` +`children = [...]` + +Common block nodes: +`heading` +`paragraph` +`blockquote` +`list` +`list_item` +`code_block` +`table` +`directive` +`hr` + +Common inline nodes: +`text` +`code` +`strong` +`em` +`strike` +`link` +`image` +`raw_html` + +:Example +`DTree options = json_decode("{\"components\":{\":::warning\":\"components/markdown/warning\"}}");` +`DTree ast = markdown_to_ast(file_get_contents("README.md"), options);` +`print(json_encode(ast));` + +:Options +`options["gfm"]` +Enables GitHub-style extras such as tables, task lists, strikethrough, and bare-URL autolinks. +Defaults to true. + +`options["allow_html"]` +Allows raw HTML passthrough nodes to be captured and rendered. +Defaults to false. + +`options["components"]` +Component hook map used later by `markdown_to_html()`. +The parser preserves directive data needed by those hooks. + +:see +markdown_to_html +component +render_component +json_encode +DTree diff --git a/site/doc/pages/markdown_to_html.txt b/site/doc/pages/markdown_to_html.txt new file mode 100644 index 0000000..090b80c --- /dev/null +++ b/site/doc/pages/markdown_to_html.txt @@ -0,0 +1,100 @@ +:sig +String markdown_to_html(String src) +String markdown_to_html(String src, DTree options) + +:params +src : markdown source text +options : optional markdown options tree +return value : rendered HTML string + +:desc +Renders Markdown source into HTML and returns the generated markup as a `String`. + +`markdown_to_html()` does not write to the output stream directly. This keeps it aligned with the UCE naming convention where `render_*` names are reserved for direct-output helpers. + +Because the return value is HTML markup, embed it with ``, `print(markdown_to_html(...))`, or pass it through a component. + +By default the function aims at a practical GitHub-flavored Markdown target, including tables, task lists, fenced code blocks, autolinks, and strikethrough. + +:Example +`DTree options;` +`options["components"][":::warning"] = "components/markdown/warning";` +`options["components"]["node.code_block"] = "components/markdown/code_block";` +`String html = markdown_to_html(file_get_contents("guide.md"), options);` +`print(html);` + +:SupportedSyntax +- headings with `#` or setext underlines +- paragraphs +- ordered and unordered lists +- task lists +- blockquotes +- fenced code blocks +- horizontal rules +- tables +- inline emphasis, strong, strikethrough, code spans +- links, images, and bare `http://` / `https://` URLs +- `:::name ... :::` directive blocks + +:Options +`options["gfm"]` +Defaults to true. +Turns on GitHub-style extras such as tables, task lists, autolinks, and strikethrough. + +`options["allow_html"]` +Defaults to false. +When true, raw HTML blocks and inline tags may pass through as `raw_html` nodes instead of being escaped as plain text. + +`options["components"]` +Declares renderer extension points using normal UCE components. + +Exact directive hooks: +`options["components"][":::warning"] = "components/markdown/warning"` +This hook is selected for `:::warning ... :::` blocks. + +Generic node hooks: +`options["components"]["node.code_block"] = "components/markdown/code_block"` +`options["components"]["node.table"] = "components/markdown/table"` +`options["components"]["node.link"] = "components/markdown/link"` +`options["components"]["node.directive"] = "components/markdown/directive"` + +If both an exact directive hook and a generic `node.directive` hook exist, the exact directive hook wins. + +:ComponentProps +When a markdown hook component is called, its props arrive in `context.call`. + +Useful fields include: +`context.call["hook"]` : matched hook key such as `:::warning` or `node.code_block` +`context.call["target"]` : resolved component target name +`context.call["default_html"]` : renderer output without the hook +`context.call["children_html"]` : already-rendered child HTML +`context.call["node"]` : full AST node +`context.call["type"]` : node type +`context.call["name"]` : directive name when applicable +`context.call["argument"]` : directive remainder after the name +`context.call["text"]` : source text for nodes such as `code_block` +`context.call["lang"]` : fenced code language +`context.call["href"]` / `context.call["src"]` / `context.call["title"]` +`context.call["options"]` : full markdown options tree + +This lets a component either replace the HTML completely or wrap `default_html` / `children_html`. + +:DirectiveSchema +Directive blocks use this form: +`:::warning title="Heads up"` +`Body markdown here` +`:::` + +The parser stores: +`node["name"] = "warning"` +`node["argument"] = ...` for bare trailing text +`node["attrs"] = ...` for parsed `key=value` pairs such as `title="Heads up"` + +This makes directive components a good fit for alerts, callouts, cards, embeds, and any richer page-level markdown extension. + +:see +markdown_to_ast +component +render_component +json_decode +String diff --git a/doc/pages/memcache_command.txt b/site/doc/pages/memcache_command.txt similarity index 100% rename from doc/pages/memcache_command.txt rename to site/doc/pages/memcache_command.txt diff --git a/doc/pages/memcache_connect.txt b/site/doc/pages/memcache_connect.txt similarity index 100% rename from doc/pages/memcache_connect.txt rename to site/doc/pages/memcache_connect.txt diff --git a/doc/pages/memcache_delete.txt b/site/doc/pages/memcache_delete.txt similarity index 100% rename from doc/pages/memcache_delete.txt rename to site/doc/pages/memcache_delete.txt diff --git a/doc/pages/memcache_get.txt b/site/doc/pages/memcache_get.txt similarity index 100% rename from doc/pages/memcache_get.txt rename to site/doc/pages/memcache_get.txt diff --git a/doc/pages/memcache_get_multiple.txt b/site/doc/pages/memcache_get_multiple.txt similarity index 100% rename from doc/pages/memcache_get_multiple.txt rename to site/doc/pages/memcache_get_multiple.txt diff --git a/doc/pages/memcache_set.txt b/site/doc/pages/memcache_set.txt similarity index 100% rename from doc/pages/memcache_set.txt rename to site/doc/pages/memcache_set.txt diff --git a/doc/pages/microtime.txt b/site/doc/pages/microtime.txt similarity index 100% rename from doc/pages/microtime.txt rename to site/doc/pages/microtime.txt diff --git a/doc/pages/mkdir.txt b/site/doc/pages/mkdir.txt similarity index 100% rename from doc/pages/mkdir.txt rename to site/doc/pages/mkdir.txt diff --git a/doc/pages/mysql_connect.txt b/site/doc/pages/mysql_connect.txt similarity index 100% rename from doc/pages/mysql_connect.txt rename to site/doc/pages/mysql_connect.txt diff --git a/doc/pages/mysql_disconnect.txt b/site/doc/pages/mysql_disconnect.txt similarity index 100% rename from doc/pages/mysql_disconnect.txt rename to site/doc/pages/mysql_disconnect.txt diff --git a/doc/pages/mysql_error.txt b/site/doc/pages/mysql_error.txt similarity index 100% rename from doc/pages/mysql_error.txt rename to site/doc/pages/mysql_error.txt diff --git a/doc/pages/mysql_escape.txt b/site/doc/pages/mysql_escape.txt similarity index 100% rename from doc/pages/mysql_escape.txt rename to site/doc/pages/mysql_escape.txt diff --git a/doc/pages/mysql_insert_id.txt b/site/doc/pages/mysql_insert_id.txt similarity index 100% rename from doc/pages/mysql_insert_id.txt rename to site/doc/pages/mysql_insert_id.txt diff --git a/doc/pages/mysql_query.txt b/site/doc/pages/mysql_query.txt similarity index 100% rename from doc/pages/mysql_query.txt rename to site/doc/pages/mysql_query.txt diff --git a/doc/pages/nibble.txt b/site/doc/pages/nibble.txt similarity index 100% rename from doc/pages/nibble.txt rename to site/doc/pages/nibble.txt diff --git a/doc/pages/ob_close.txt b/site/doc/pages/ob_close.txt similarity index 100% rename from doc/pages/ob_close.txt rename to site/doc/pages/ob_close.txt diff --git a/doc/pages/ob_get.txt b/site/doc/pages/ob_get.txt similarity index 100% rename from doc/pages/ob_get.txt rename to site/doc/pages/ob_get.txt diff --git a/doc/pages/ob_get_close.txt b/site/doc/pages/ob_get_close.txt similarity index 100% rename from doc/pages/ob_get_close.txt rename to site/doc/pages/ob_get_close.txt diff --git a/doc/pages/ob_start.txt b/site/doc/pages/ob_start.txt similarity index 100% rename from doc/pages/ob_start.txt rename to site/doc/pages/ob_start.txt diff --git a/doc/pages/parse_query.txt b/site/doc/pages/parse_query.txt similarity index 100% rename from doc/pages/parse_query.txt rename to site/doc/pages/parse_query.txt diff --git a/doc/pages/parse_time.txt b/site/doc/pages/parse_time.txt similarity index 100% rename from doc/pages/parse_time.txt rename to site/doc/pages/parse_time.txt diff --git a/doc/pages/print.txt b/site/doc/pages/print.txt similarity index 100% rename from doc/pages/print.txt rename to site/doc/pages/print.txt diff --git a/site/doc/pages/render_component.txt b/site/doc/pages/render_component.txt new file mode 100644 index 0000000..34fde7d --- /dev/null +++ b/site/doc/pages/render_component.txt @@ -0,0 +1,19 @@ +:sig +void render_component(String name, [DTree props], [Request& context]) + +:desc +Renders another `.uce` file as a component and writes the result directly to the current output buffer. + +This is the direct-output counterpart to `component()`. + +Component props are passed through `context.call`, and `name:RENDERFUNC` may be used to select a named handler exported by `RENDER:RENDERFUNC(Request& context)`. + +Use `render_component()` when you want to write component output directly from C++ code instead of capturing it as a `String`. + +Example: +`DTree props;` +`props["body"] = "Hello";` +`render_component("components/card:BODY", props, context);` + +:see +>ob diff --git a/site/doc/pages/render_file.txt b/site/doc/pages/render_file.txt new file mode 100644 index 0000000..66c5fe7 --- /dev/null +++ b/site/doc/pages/render_file.txt @@ -0,0 +1,22 @@ +:sig +void render_file(String file_name) +void render_file(String file_name, Request& context) + +:params +file_name : UCE file to load and execute +context : optional request context to pass into the target page + +:desc +Calls another UCE file and executes its `RENDER(Request& context)` function. + +If `context` is omitted, the current active request context is used. + +:Example +// call a common page template +render_file("page-template.uce"); + +// explicitly pass a request context +render_file("page-template.uce", context); + +:see +>ob diff --git a/doc/pages/replace.txt b/site/doc/pages/replace.txt similarity index 100% rename from doc/pages/replace.txt rename to site/doc/pages/replace.txt diff --git a/doc/pages/session_destroy.txt b/site/doc/pages/session_destroy.txt similarity index 63% rename from doc/pages/session_destroy.txt rename to site/doc/pages/session_destroy.txt index 20c7c32..386cb26 100644 --- a/doc/pages/session_destroy.txt +++ b/site/doc/pages/session_destroy.txt @@ -5,7 +5,7 @@ void session_destroy(String session_name) session_name : the name of the session :desc -Deletes the cookie specified by 'session_name' and clears the data stored under the session ID. This empties the 'context->session_id' and 'context->session' variables. +Deletes the cookie specified by 'session_name' and clears the data stored under the session ID. This empties the 'context.session_id' and 'context.session' variables. :see >session diff --git a/doc/pages/session_start.txt b/site/doc/pages/session_start.txt similarity index 71% rename from doc/pages/session_start.txt rename to site/doc/pages/session_start.txt index da58921..c360f40 100644 --- a/doc/pages/session_start.txt +++ b/site/doc/pages/session_start.txt @@ -8,11 +8,11 @@ return value : the current session ID :desc Starts session or connects to existing session. This function sets a cookie with the name contained in 'session_name' if it does not exist and fills that cookie with a new unique session ID. It then loads the session data for that session ID. Afterwards, the following fields are populated in the 'context' variable: -context->session_id : the current session ID +context.session_id : the current session ID -context->session_name : the current session cookie name +context.session_name : the current session cookie name -context->session : the current session data. The session data is automatically saved after a request completes. +context.session : the current session data. The session data is automatically saved after a request completes. :see >session diff --git a/doc/pages/set_cwd.txt b/site/doc/pages/set_cwd.txt similarity index 100% rename from doc/pages/set_cwd.txt rename to site/doc/pages/set_cwd.txt diff --git a/doc/pages/shell_escape.txt b/site/doc/pages/shell_escape.txt similarity index 100% rename from doc/pages/shell_escape.txt rename to site/doc/pages/shell_escape.txt diff --git a/doc/pages/shell_exec.txt b/site/doc/pages/shell_exec.txt similarity index 100% rename from doc/pages/shell_exec.txt rename to site/doc/pages/shell_exec.txt diff --git a/doc/pages/socket_close.txt b/site/doc/pages/socket_close.txt similarity index 100% rename from doc/pages/socket_close.txt rename to site/doc/pages/socket_close.txt diff --git a/doc/pages/socket_connect.txt b/site/doc/pages/socket_connect.txt similarity index 100% rename from doc/pages/socket_connect.txt rename to site/doc/pages/socket_connect.txt diff --git a/doc/pages/socket_read.txt b/site/doc/pages/socket_read.txt similarity index 100% rename from doc/pages/socket_read.txt rename to site/doc/pages/socket_read.txt diff --git a/doc/pages/socket_write.txt b/site/doc/pages/socket_write.txt similarity index 100% rename from doc/pages/socket_write.txt rename to site/doc/pages/socket_write.txt diff --git a/doc/pages/split.txt b/site/doc/pages/split.txt similarity index 100% rename from doc/pages/split.txt rename to site/doc/pages/split.txt diff --git a/doc/pages/split_space.txt b/site/doc/pages/split_space.txt similarity index 100% rename from doc/pages/split_space.txt rename to site/doc/pages/split_space.txt diff --git a/doc/pages/split_utf8.txt b/site/doc/pages/split_utf8.txt similarity index 100% rename from doc/pages/split_utf8.txt rename to site/doc/pages/split_utf8.txt diff --git a/doc/pages/task.txt b/site/doc/pages/task.txt similarity index 100% rename from doc/pages/task.txt rename to site/doc/pages/task.txt diff --git a/doc/pages/task_pid.txt b/site/doc/pages/task_pid.txt similarity index 100% rename from doc/pages/task_pid.txt rename to site/doc/pages/task_pid.txt diff --git a/doc/pages/task_repeat.txt b/site/doc/pages/task_repeat.txt similarity index 100% rename from doc/pages/task_repeat.txt rename to site/doc/pages/task_repeat.txt diff --git a/doc/pages/time.txt b/site/doc/pages/time.txt similarity index 100% rename from doc/pages/time.txt rename to site/doc/pages/time.txt diff --git a/doc/pages/to_lower.txt b/site/doc/pages/to_lower.txt similarity index 100% rename from doc/pages/to_lower.txt rename to site/doc/pages/to_lower.txt diff --git a/doc/pages/to_upper.txt b/site/doc/pages/to_upper.txt similarity index 100% rename from doc/pages/to_upper.txt rename to site/doc/pages/to_upper.txt diff --git a/doc/pages/trim.txt b/site/doc/pages/trim.txt similarity index 100% rename from doc/pages/trim.txt rename to site/doc/pages/trim.txt diff --git a/doc/pages/unlink.txt b/site/doc/pages/unlink.txt similarity index 100% rename from doc/pages/unlink.txt rename to site/doc/pages/unlink.txt diff --git a/doc/pages/uri_decode.txt b/site/doc/pages/uri_decode.txt similarity index 100% rename from doc/pages/uri_decode.txt rename to site/doc/pages/uri_decode.txt diff --git a/doc/pages/uri_encode.txt b/site/doc/pages/uri_encode.txt similarity index 100% rename from doc/pages/uri_encode.txt rename to site/doc/pages/uri_encode.txt diff --git a/doc/pages/var_dump.txt b/site/doc/pages/var_dump.txt similarity index 100% rename from doc/pages/var_dump.txt rename to site/doc/pages/var_dump.txt diff --git a/doc/pages/ws_close.txt b/site/doc/pages/ws_close.txt similarity index 91% rename from doc/pages/ws_close.txt rename to site/doc/pages/ws_close.txt index deb4b7d..3158ae9 100644 --- a/doc/pages/ws_close.txt +++ b/site/doc/pages/ws_close.txt @@ -8,7 +8,7 @@ return value : true if the target connection exists and was scheduled to close :desc Queues a WebSocket close frame and closes the targeted connection. -If `connection_id` is omitted, the current connection handled by `WS()` is closed. +If `connection_id` is omitted, the current connection handled by `WS(Request& context)` is closed. :see >websocket diff --git a/doc/pages/ws_connection_count.txt b/site/doc/pages/ws_connection_count.txt similarity index 100% rename from doc/pages/ws_connection_count.txt rename to site/doc/pages/ws_connection_count.txt diff --git a/doc/pages/ws_connection_id.txt b/site/doc/pages/ws_connection_id.txt similarity index 100% rename from doc/pages/ws_connection_id.txt rename to site/doc/pages/ws_connection_id.txt diff --git a/doc/pages/ws_connections.txt b/site/doc/pages/ws_connections.txt similarity index 100% rename from doc/pages/ws_connections.txt rename to site/doc/pages/ws_connections.txt diff --git a/doc/pages/ws_is_binary.txt b/site/doc/pages/ws_is_binary.txt similarity index 65% rename from doc/pages/ws_is_binary.txt rename to site/doc/pages/ws_is_binary.txt index ca9637b..b48efad 100644 --- a/doc/pages/ws_is_binary.txt +++ b/site/doc/pages/ws_is_binary.txt @@ -5,7 +5,7 @@ bool ws_is_binary() return value : true if the current WebSocket message is binary :desc -Returns whether the message currently being handled by `WS()` arrived as a binary frame. +Returns whether the message currently being handled by `WS(Request& context)` arrived as a binary frame. If this returns `false`, the current message was delivered as a text frame. diff --git a/doc/pages/ws_message.txt b/site/doc/pages/ws_message.txt similarity index 92% rename from doc/pages/ws_message.txt rename to site/doc/pages/ws_message.txt index bdda571..aec50da 100644 --- a/doc/pages/ws_message.txt +++ b/site/doc/pages/ws_message.txt @@ -5,7 +5,7 @@ String ws_message() return value : payload of the current WebSocket message :desc -Returns the payload of the current WebSocket message being handled by `WS()`. +Returns the payload of the current WebSocket message being handled by `WS(Request& context)`. For text frames this is the decoded text payload. For binary frames this String contains the raw message bytes. diff --git a/doc/pages/ws_opcode.txt b/site/doc/pages/ws_opcode.txt similarity index 92% rename from doc/pages/ws_opcode.txt rename to site/doc/pages/ws_opcode.txt index 363d161..66d2b53 100644 --- a/doc/pages/ws_opcode.txt +++ b/site/doc/pages/ws_opcode.txt @@ -5,7 +5,7 @@ u8 ws_opcode() return value : opcode of the current WebSocket message :desc -Returns the opcode of the message currently being handled by `WS()`. +Returns the opcode of the message currently being handled by `WS(Request& context)`. Common values are: diff --git a/doc/pages/ws_scope.txt b/site/doc/pages/ws_scope.txt similarity index 69% rename from doc/pages/ws_scope.txt rename to site/doc/pages/ws_scope.txt index 4ae94cd..05676ab 100644 --- a/doc/pages/ws_scope.txt +++ b/site/doc/pages/ws_scope.txt @@ -7,7 +7,7 @@ return value : scope identifier of the current WebSocket endpoint :desc Returns the runtime's scope identifier for the current WebSocket endpoint. -This is the same default scope used by `ws_send()`, `ws_broadcast()`, `ws_connections()`, and `ws_connection_count()` when no explicit scope is supplied. +This is the same default scope used by `ws_send()`, `ws_connections()`, and `ws_connection_count()` when no explicit scope is supplied. In the current runtime implementation this scope is the page's internal endpoint identifier, typically the absolute `SCRIPT_FILENAME` of the `.ws.uce` file. diff --git a/doc/pages/ws_send.txt b/site/doc/pages/ws_send.txt similarity index 50% rename from doc/pages/ws_send.txt rename to site/doc/pages/ws_send.txt index 5b3ade9..4aaa641 100644 --- a/doc/pages/ws_send.txt +++ b/site/doc/pages/ws_send.txt @@ -1,17 +1,16 @@ :sig -bool ws_send(String message, String scope = "") +bool ws_send(String message, bool binary = false, String scope = "") :params -message : text message to send +message : message payload to send +binary : set to true to send a binary frame instead of a text frame scope : optional scope identifier, defaults to the current WebSocket page scope return value : true if the message was queued for at least one connected client :desc -Queues a text WebSocket message for every client connected to the given scope. +Queues a WebSocket message for every client connected to the given scope. If `scope` is omitted, the current page scope is used. -This helper currently sends text frames only. - :see >websocket diff --git a/site/doc/pages/ws_send_to.txt b/site/doc/pages/ws_send_to.txt new file mode 100644 index 0000000..ef0a35a --- /dev/null +++ b/site/doc/pages/ws_send_to.txt @@ -0,0 +1,14 @@ +:sig +bool ws_send_to(String connection_id, String message, bool binary = false) + +:params +connection_id : ID of the target WebSocket client +message : message payload to send +binary : set to true to send a binary frame instead of a text frame +return value : true if the target connection exists and the message was queued + +:desc +Queues a WebSocket message for one specific connected client. + +:see +>websocket diff --git a/doc/singlepage.uce b/site/doc/singlepage.uce similarity index 96% rename from doc/singlepage.uce rename to site/doc/singlepage.uce index 6e7336e..b39ef47 100644 --- a/doc/singlepage.uce +++ b/site/doc/singlepage.uce @@ -104,12 +104,12 @@ void render_see_section(String name) } -RENDER() +RENDER(Request& context) { already_shown_items = new StringMap(); - String page = first(context->get["p"], "index"); + String page = first(context.get["p"], "index"); <> diff --git a/doc/style.css b/site/doc/style.css similarity index 100% rename from doc/style.css rename to site/doc/style.css diff --git a/site/examples/uce-starter/README.md b/site/examples/uce-starter/README.md new file mode 100644 index 0000000..2c9ab0e --- /dev/null +++ b/site/examples/uce-starter/README.md @@ -0,0 +1,21 @@ +# UCE Starter + +`site/examples/uce-starter/` is a UCE-native port of the PHP `web-app-starter/`. + +It keeps the PHP starter around as reference while mirroring the same broad structure: + +- `index.uce` as the front controller +- `views/` for routed page content +- `components/` for reusable UI building blocks +- `themes/`, `js/`, and `img/` for static assets + +This port intentionally leans on UCE's component layer rather than treating components as a compatibility shim. The page shell, nav/footer chrome, theme switcher, dashboard blocks, workspace primitives, and marketing sections are all rendered through `component()`. + +The example uses query-string routing in the same style as the PHP starter: + +- `index.uce` +- `index.uce?page1` +- `index.uce?themes&theme=portal-dark` +- `index.uce?workspace/projects` + +The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state. diff --git a/site/examples/uce-starter/components/auth/oauth-client.uce b/site/examples/uce-starter/components/auth/oauth-client.uce new file mode 100644 index 0000000..2422967 --- /dev/null +++ b/site/examples/uce-starter/components/auth/oauth-client.uce @@ -0,0 +1,100 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + String title = first(context.call["title"].to_string(), "Sign in with OAuth"); + String subtitle = first(context.call["subtitle"].to_string(), "Choose your preferred authentication method"); + String callback_url = first(context.call["callback_url"].to_string(), starter_link("auth/callback", context)); + + DTree services = context.call["services"]; + if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "") + { + services["google"]["name"] = "Google"; + services["google"]["color"] = "#4285f4"; + services["google"]["icon"] = "fab fa-google"; + services["google"]["scope"] = "openid email profile"; + services["google"]["auth_url"] = "https://accounts.google.com/o/oauth2/v2/auth"; + + services["github"]["name"] = "GitHub"; + services["github"]["color"] = "#333333"; + services["github"]["icon"] = "fab fa-github"; + services["github"]["scope"] = "user:email"; + services["github"]["auth_url"] = "https://github.com/login/oauth/authorize"; + + services["discord"]["name"] = "Discord"; + services["discord"]["color"] = "#5865f2"; + services["discord"]["icon"] = "fab fa-discord"; + services["discord"]["scope"] = "identify email"; + services["discord"]["auth_url"] = "https://discord.com/api/oauth2/authorize"; + + services["twitch"]["name"] = "Twitch"; + services["twitch"]["color"] = "#9146ff"; + services["twitch"]["icon"] = "fab fa-twitch"; + services["twitch"]["scope"] = "user:read:email"; + services["twitch"]["auth_url"] = "https://id.twitch.tv/oauth2/authorize"; + } + + <> +
+
+

+ +
+
+ + + + +
+ +
+ +} diff --git a/site/examples/uce-starter/components/basic/cookie-consent.uce b/site/examples/uce-starter/components/basic/cookie-consent.uce new file mode 100644 index 0000000..174f0ac --- /dev/null +++ b/site/examples/uce-starter/components/basic/cookie-consent.uce @@ -0,0 +1,114 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + + <> + + + +} diff --git a/site/examples/uce-starter/components/data/widgets.uce b/site/examples/uce-starter/components/data/widgets.uce new file mode 100644 index 0000000..ee07030 --- /dev/null +++ b/site/examples/uce-starter/components/data/widgets.uce @@ -0,0 +1,276 @@ +#load "../../lib/app.uce" + +String data_format_bytes(String raw, bool disk = false) +{ + if(trim(raw) == "") + return("--"); + StringList units; + units.push_back("B"); + units.push_back("KB"); + units.push_back("MB"); + units.push_back("GB"); + units.push_back("TB"); + units.push_back("PB"); + f64 value = atof(raw.c_str()); + u32 unit_index = 0; + while((value >= 1024.0 || value <= -1024.0) && unit_index < units.size() - 1) + { + value /= 1024.0; + unit_index += 1; + } + u32 decimals = disk ? (unit_index >= 4 ? 2 : (unit_index >= 1 ? 1 : 0)) : (unit_index == 0 ? 0 : 1); + char buf[64]; + snprintf(buf, sizeof(buf), ("%." + std::to_string(decimals) + "f").c_str(), value); + return(String(buf) + " " + units[unit_index]); +} + +DTree data_format_value(DTree value, DTree row, DTree column) +{ + DTree result; + String format = column["format"].to_string(); + String raw = value.to_string(); + String sort_value = raw; + String display_value = raw; + if(format == "number") + { + display_value = raw; + } + else if(format == "bytes") + { + display_value = data_format_bytes(raw, false); + } + else if(format == "disk-bytes") + { + display_value = data_format_bytes(raw, true); + } + else if(format == "percent") + { + display_value = raw + "%"; + } + else if(format == "duration-ms") + { + f64 number = atof(raw.c_str()); + if(number >= 1000.0) + { + char buf[64]; + snprintf(buf, sizeof(buf), number >= 10000.0 ? "%.0f s" : "%.1f s", number / 1000.0); + display_value = buf; + } + else + { + char buf[64]; + snprintf(buf, sizeof(buf), number >= 100.0 ? "%.0f ms" : "%.1f ms", number); + display_value = buf; + } + } + else if(format == "bool") + { + bool on = raw == "1" || to_lower(raw) == "true" || to_lower(raw) == "yes"; + display_value = on ? "Yes" : "No"; + sort_value = on ? "1" : "0"; + } + result["display"] = display_value; + result["sort"] = sort_value; + return(result); +} + +RENDER:SUMMARY_METRICS(Request& context) +{ + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + + <> +
+ +
+

+

+
+ +
+ < class="dashboard-stat-card tone-"> +
+
+
+ > +
+
+ +} + +RENDER:TIMESERIES_CHART(Request& context) +{ + starter_register_js("js/u-format.js", context); + starter_register_js("js/u-timeseries-chart.js", context); + + String chart_id = first(context.call["id"].to_string(), "ts-chart-" + std::to_string((u64)time())); + String canvas_id = chart_id + "-canvas"; + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + s32 height = std::max(180, (s32)int_val(first(context.call["height"].to_string(), "320"))); + String x_axis_label = first(context.call["x_axis_label"].to_string(), "Time"); + String y_axis_left_label = first(context.call["y_axis_left_label"].to_string(), "Value"); + String y_axis_right_label = context.call["y_axis_right_label"].to_string(); + String y_axis_left_format = first(context.call["y_axis_left_format"].to_string(), "number"); + String y_axis_right_format = first(context.call["y_axis_right_format"].to_string(), "number"); + + <> +
+ +
+

+

+
+ + +
+ + +} + +RENDER:SORTABLE_TABLE(Request& context) +{ + starter_register_js("js/u-format.js", context); + starter_register_js("js/u-sortable-table.js", context); + + String table_id = first(context.call["id"].to_string(), "sortable-table-" + std::to_string((u64)time())); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + String empty_label = first(context.call["empty_label"].to_string(), "No data available"); + + DTree init_options; + init_options["storageKey"] = first(context.call["storage_key"].to_string(), "starter.sort." + table_id); + if(context.call["sort"]["column"].to_string() != "") + { + init_options["initialSort"]["column"] = context.call["sort"]["column"]; + init_options["initialSort"]["direction"] = first(context.call["sort"]["direction"].to_string(), "asc"); + } + + <> +
+ +
+

+

+
+ +
+ + + + + + + + + + + + +
>
" class="muted">
" data-sort-value="">
+
+
+ + +} + +RENDER:DATA_TABLE(Request& context) +{ + String table_id = first(context.call["id"].to_string(), "data-grid-" + std::to_string((u64)time())); + starter_register_js("js/ag-grid/ag-grid-community.min.js", context); + starter_register_css("js/ag-grid/ag-grid.css", context); + starter_register_css("js/ag-grid/ag-theme-alpine.css", context); + + DTree columns = context.call["columns"]; + if((columns.get_type_name() != "array" || columns["0"]["field"].to_string() == "") && + context.call["items"]["0"].get_type_name() == "array") + { + context.call["items"]["0"].each([&](DTree value, String key) { + DTree col; + col["field"] = key; + col["headerName"] = key; + col["sortable"].set_bool(true); + col["filter"].set_bool(true); + col["resizable"].set_bool(true); + columns.push(col); + }); + } + + DTree grid_options; + grid_options["pagination"].set_bool(true); + grid_options["paginationPageSize"] = first(context.call["options"]["paginationPageSize"].to_string(), "20"); + grid_options["suppressMenuHide"].set_bool(true); + grid_options["animateRows"].set_bool(true); + grid_options["columnDefs"] = columns; + grid_options["rowData"] = context.call["items"]; + + <> +
+
+
+ rows + +
+
+ + +
+
+
; width: 100%; border: 1px solid var(--border); border-radius: 0 0 0.5rem 0.5rem; overflow: hidden;">
+
+ + +} diff --git a/site/examples/uce-starter/components/example/marketing_blocks.uce b/site/examples/uce-starter/components/example/marketing_blocks.uce new file mode 100644 index 0000000..574e2b4 --- /dev/null +++ b/site/examples/uce-starter/components/example/marketing_blocks.uce @@ -0,0 +1,604 @@ +#load "../../lib/app.uce" + +void marketing_default_features(DTree& features) +{ + if(features.get_type_name() == "array" && features["0"]["title"].to_string() != "") + return; + DTree item; + + item["icon"] = "⚡"; + item["title"] = "Lightning Fast"; + item["description"] = "Optimized for speed and performance with modern web technologies."; + features.push(item); + item.clear(); + + item["icon"] = "🎨"; + item["title"] = "Beautiful Design"; + item["description"] = "Carefully crafted components with attention to detail and user experience."; + features.push(item); + item.clear(); + + item["icon"] = "📱"; + item["title"] = "Mobile First"; + item["description"] = "Fully responsive design that works perfectly on all devices."; + features.push(item); + item.clear(); + + item["icon"] = "🔧"; + item["title"] = "Easy to Use"; + item["description"] = "Simple and intuitive component system for rapid development."; + features.push(item); + item.clear(); + + item["icon"] = "🛡️"; + item["title"] = "Secure"; + item["description"] = "Built with security best practices and server-side rendering first."; + features.push(item); + item.clear(); + + item["icon"] = "🚀"; + item["title"] = "Scalable"; + item["description"] = "Architecture designed to grow with your application needs."; + features.push(item); +} + +RENDER:HERO_SECTION(Request& context) +{ + String title = first(context.call["title"].to_string(), "Welcome to the Present"); + String subtitle = first(context.call["subtitle"].to_string(), "Experience no-quite-modern web development with our cutting-edge framework"); + String cta_text = first(context.call["cta_text"].to_string(), "Get Started"); + String cta_link = first(context.call["cta_link"].to_string(), "#"); + + <> +
+
+

+

+
+ + Learn More +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +} + +RENDER:FEATURES_GRID(Request& context) +{ + DTree features = context.call["features"]; + marketing_default_features(features); + + <> +
+
+

Why Choose Our Framework?

+

Discover the powerful features that make development a breeze

+
+
+
+
+

+

+
+
+
+
+ + +} + +RENDER:STATS_SECTION(Request& context) +{ + DTree stats = context.call["stats"]; + if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "") + { + DTree item; + item["number"] = "99.9%"; item["label"] = "Uptime"; stats.push(item); item.clear(); + item["number"] = "500ms"; item["label"] = "Average Response"; stats.push(item); item.clear(); + item["number"] = "50K+"; item["label"] = "Active Users"; stats.push(item); item.clear(); + item["number"] = "24/7"; item["label"] = "Support"; stats.push(item); + } + + <> +
+
+
+

Trusted by Developers Worldwide

+

Join thousands of developers who have chosen our framework

+
+
+
+
+
+
+
+
+
+
+ + +} + +RENDER:BRANDS_SHOWCASE(Request& context) +{ + DTree logos = context.call["logos"]; + if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "") + { + DTree item; + for(s32 i = 1; i <= 6; i++) + { + item["name"] = "Brand " + std::to_string(i); + item["url"] = "img/cat0" + std::to_string(i) + ".jpg"; + logos.push(item); + item.clear(); + } + } + String title = first(context.call["title"].to_string(), "Trusted by Leading Companies"); + + <> +
+
+

+
+
+ " alt="" /> +
+
+
+
+ + +} + +RENDER:TESTIMONIALS(Request& context) +{ + DTree testimonials = context.call["testimonials"]; + if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "") + { + DTree item; + item["name"] = "Sarah Johnson"; item["role"] = "Senior Developer at TechCorp"; item["avatar"] = "img/cat01.jpg"; item["content"] = "This framework has revolutionized our development process."; item["rating"] = "5"; testimonials.push(item); item.clear(); + item["name"] = "Michael Chen"; item["role"] = "CTO at StartupX"; item["avatar"] = "img/cat02.jpg"; item["content"] = "We switched from our legacy system and saw immediate improvements."; item["rating"] = "5"; testimonials.push(item); item.clear(); + item["name"] = "Emily Rodriguez"; item["role"] = "Full Stack Developer"; item["avatar"] = "img/cat03.jpg"; item["content"] = "The documentation is excellent and the learning curve is gentle."; item["rating"] = "5"; testimonials.push(item); + } + + <> +
+
+
+

What Developers Say

+

Don't just take our word for it - hear from the community

+
+
+
+
+
"
+

+
+
+
+ " alt="" class="avatar"> +
+
+
+
+
+
+
+
+
+ + +} + +RENDER:PRICING_TABLE(Request& context) +{ + DTree plans = context.call["plans"]; + if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "") + { + DTree plan; + DTree feature; + + plan["name"] = "Starter"; plan["price"] = "Free"; plan["period"] = ""; plan["description"] = "Perfect for small projects and learning"; plan["cta"] = "Start Free"; feature = "Up to 3 projects"; plan["features"].push(feature); feature = "Community support"; plan["features"].push(feature); feature = "Basic components"; plan["features"].push(feature); plans.push(plan); plan.clear(); + plan["name"] = "Pro"; plan["price"] = "$24"; plan["period"] = "/mo"; plan["description"] = "For serious products and teams"; plan["cta"] = "Choose Pro"; plan["popular"].set_bool(true); feature = "Unlimited projects"; plan["features"].push(feature); feature = "Priority support"; plan["features"].push(feature); feature = "Advanced components"; plan["features"].push(feature); plans.push(plan); plan.clear(); + plan["name"] = "Enterprise"; plan["price"] = "$99"; plan["period"] = "/mo"; plan["description"] = "For larger teams and custom deployments"; plan["cta"] = "Contact Sales"; feature = "Custom integrations"; plan["features"].push(feature); feature = "Dedicated support"; plan["features"].push(feature); feature = "Architecture help"; plan["features"].push(feature); plans.push(plan); + } + + <> +
+
+
+

Simple Pricing

+

Choose the plan that fits your product stage.

+
+
+ +
+ +
+
+ + +} + +RENDER:CTA_SECTION(Request& context) +{ + String title = first(context.call["title"].to_string(), "Ready to Get Started?"); + String subtitle = first(context.call["subtitle"].to_string(), "Join thousands of developers building amazing applications with our framework."); + String cta_text = first(context.call["cta_text"].to_string(), "Start Building Now"); + String cta_link = first(context.call["cta_link"].to_string(), "#"); + String secondary_text = first(context.call["secondary_text"].to_string(), "View Documentation"); + String secondary_link = first(context.call["secondary_link"].to_string(), "#"); + + <> +
+
+
+

+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + +} diff --git a/site/examples/uce-starter/components/example/theme-switcher.uce b/site/examples/uce-starter/components/example/theme-switcher.uce new file mode 100644 index 0000000..75c3add --- /dev/null +++ b/site/examples/uce-starter/components/example/theme-switcher.uce @@ -0,0 +1,153 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + String current_theme = starter_theme_value("key", context); + String current_label = first(starter_theme_value("label", context), current_theme); + String route_path = context.var["starter"]["route"]["l_path"].to_string(); + + <> +
+ + + +
+ + + +} diff --git a/site/examples/uce-starter/components/gauges/gauges.uce b/site/examples/uce-starter/components/gauges/gauges.uce new file mode 100644 index 0000000..ff793ea --- /dev/null +++ b/site/examples/uce-starter/components/gauges/gauges.uce @@ -0,0 +1,231 @@ +#load "../../lib/app.uce" + +RENDER:PROGRESSBAR(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "progressbar-" + std::to_string((u64)time())); + String layout = first(context.call["layout"].to_string(), "horizontal"); + String style = context.call["style"].to_string(); + String item_style = context.call["item-style"].to_string(); + String value_style = context.call["value-style"].to_string(); + String bar_style = context.call["bar-style"].to_string(); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + + <> +
+ +
+

+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ +
px;"> +
+
+
+
+
+
+
+
+
+ +
+ + + + +} + +RENDER:NEEDLEGAUGE(Request& context) +{ + starter_register_css("themes/common/css/gauges.css", context); + String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time())); + String style = context.call["style"].to_string(); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + s32 size = (s32)int_val(first(context.call["size"].to_string(), "200")); + + <> +
+

+
+
+
+
+ + + + + +
+
+
+
+
+
+
+
+ + + + +} + +RENDER:ARCGAUGE(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + String gauge_id = first(context.call["id"].to_string(), "arcgauge-" + std::to_string((u64)time())); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + String style = context.call["style"].to_string(); + + <> +
+

+
+ 0.0 ? std::max(0.0, std::min(100.0, (value / max_value) * 100.0)) : 0.0; + f64 arc_length = (pct / 100.0) * 157.08; + char buf[64]; + snprintf(buf, sizeof(buf), ("%." + std::to_string(precision) + "f").c_str(), value); + ?>
+
+ +
+
+
+
+ + + + +} diff --git a/site/examples/uce-starter/components/theme/account_links.uce b/site/examples/uce-starter/components/theme/account_links.uce new file mode 100644 index 0000000..db456ec --- /dev/null +++ b/site/examples/uce-starter/components/theme/account_links.uce @@ -0,0 +1,30 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + DTree user = starter_current_user(context); + bool signed_in = user["email"].to_string() != ""; + String wrapper_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-card" : "nav-account nav-menu"; + String links_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-links" : ""; + String name_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-name" : "account-name"; + + <> +
+ + + + + + +
+ +} diff --git a/site/examples/uce-starter/components/theme/footer.uce b/site/examples/uce-starter/components/theme/footer.uce new file mode 100644 index 0000000..d7f5be3 --- /dev/null +++ b/site/examples/uce-starter/components/theme/footer.uce @@ -0,0 +1,21 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + if(context.var["starter"]["embed_mode"].to_string() != "") + return; + + String text = first(starter_theme_value("footer_text", context), starter_cfg_string("site/name", context)); + String inner_class = starter_theme_value("key", context) == "portal-light" ? "footer-inner" : ""; + + <> +
+ +

+ +

+ +
+ +} diff --git a/site/examples/uce-starter/components/theme/page_shell.uce b/site/examples/uce-starter/components/theme/page_shell.uce new file mode 100644 index 0000000..8e9d24d --- /dev/null +++ b/site/examples/uce-starter/components/theme/page_shell.uce @@ -0,0 +1,117 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + + String page_type = first(context.var["starter"]["page_type"].to_string(), "html"); + if(context.flags.status >= 300 && context.flags.status < 400) + return; + + if(page_type == "blank") + { + print(context.call["main_html"].to_string()); + return; + } + + if(page_type == "json") + { + context.header["Content-Type"] = "application/json"; + context.header["Cache-Control"] = "no-cache, no-store, must-revalidate"; + if(context.var["starter"]["json"].get_type_name() == "array") + { + print(json_encode(context.var["starter"]["json"])); + } + else + { + print(context.call["main_html"].to_string()); + } + return; + } + + String theme_key = starter_theme_value("key", context); + bool embed_mode = context.var["starter"]["embed_mode"].to_string() == "1"; + String body_class = embed_mode ? "embed-mode" : ""; + if(theme_key == "portal-dark") + body_class = "portal-theme portal-dark-theme dark-theme" + String(embed_mode ? " embed-mode" : ""); + else if(theme_key == "portal-light") + body_class = "portal-theme portal-light-theme" + String(embed_mode ? " embed-mode" : ""); + else if(theme_key == "localfirst") + body_class = "admin-page localfirst-theme dark-theme" + String(embed_mode ? " embed-mode" : ""); + else if(theme_key == "retro-gaming") + body_class = embed_mode ? "embed-mode" : ""; + + <> + + "> + + + + > +
+ + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+
+
+ + +
> + +
+ + + + + +} + +RENDER:HEAD(Request& context) +{ + starter_boot(context); + String title = first(context.var["starter"]["page_title"].to_string(), starter_cfg_string("site/default_page_title", context)); + String description = first(starter_theme_value("meta_description", context), "UCE starter example"); + String theme_color = first(starter_theme_value("theme_color", context), "#0f172a"); + String icon = starter_asset_url(starter_theme_value("path", context) + "icon.png", context); + + <> + + <?= title + " | " + starter_cfg_string("site/name", context) ?> + + + + + + + + +} diff --git a/site/examples/uce-starter/components/theme/standard_nav.uce b/site/examples/uce-starter/components/theme/standard_nav.uce new file mode 100644 index 0000000..607e13a --- /dev/null +++ b/site/examples/uce-starter/components/theme/standard_nav.uce @@ -0,0 +1,20 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + + <> + > + + + + +} diff --git a/site/examples/uce-starter/components/workspace/primitives.uce b/site/examples/uce-starter/components/workspace/primitives.uce new file mode 100644 index 0000000..64d68d1 --- /dev/null +++ b/site/examples/uce-starter/components/workspace/primitives.uce @@ -0,0 +1,122 @@ +#load "../../lib/app.uce" + +RENDER:APP_FRAME(Request& context) +{ + starter_register_css("themes/common/css/workspace.css", context); + starter_register_js("js/u-workspace-shell.js", context); + String id = context.call["id"].to_string(); + String overlay_id = context.call["overlay_id"].to_string(); + String class_name = context.call["class"].to_string(); + <> + class="ws-app"> + + class="ws-sidebar-overlay"> +
+ + +} + +RENDER:EMPTY_STATE(Request& context) +{ + <> +
"> +
">
+

+

+ +
+ +} + +RENDER:LIST_STATE(Request& context) +{ + <> +
">">
+ +} + +RENDER:MOBILE_BAR(Request& context) +{ + <> +
"> + class="ws-mobile-toggle" title="Toggle sidebar" type="button"> + "> + + +
+ +} + +RENDER:PANEL_HEADER(Request& context) +{ + <> +
"> +
+

+

+
+
+
+ +} + +RENDER:PANEL(Request& context) +{ + <> + class="ws-panel"> + + + + +} + +RENDER:SECTION_HEAD(Request& context) +{ + <> +
"> +

+
+
+ +} + +RENDER:SECTION(Request& context) +{ + <> + class="ws-section"> + + + + +} + +RENDER:SIDEBAR_SHELL(Request& context) +{ + <> + class="ws-sidebar"> + + + + +} + +RENDER:SIDEBAR_TOOLBAR(Request& context) +{ + <> +
"> + +
+ + name="" class="ws-search-input" placeholder="" autocomplete="off"> +
+
+ +} + +RENDER:STATUS_PILL(Request& context) +{ + String variant = to_lower(first(context.call["variant"].to_string(), "neutral")); + <> + "> + +} diff --git a/site/examples/uce-starter/img/cat01.jpg b/site/examples/uce-starter/img/cat01.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/uce-starter/img/cat01.jpg differ diff --git a/site/examples/uce-starter/img/cat02.jpg b/site/examples/uce-starter/img/cat02.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/uce-starter/img/cat02.jpg differ diff --git a/site/examples/uce-starter/img/cat03.jpg b/site/examples/uce-starter/img/cat03.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/uce-starter/img/cat03.jpg differ diff --git a/site/examples/uce-starter/img/cat04.jpg b/site/examples/uce-starter/img/cat04.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/uce-starter/img/cat04.jpg differ diff --git a/site/examples/uce-starter/img/cat05.jpg b/site/examples/uce-starter/img/cat05.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/uce-starter/img/cat05.jpg differ diff --git a/site/examples/uce-starter/img/cat06.jpg b/site/examples/uce-starter/img/cat06.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/uce-starter/img/cat06.jpg differ diff --git a/site/examples/uce-starter/index.uce b/site/examples/uce-starter/index.uce new file mode 100644 index 0000000..8e018d2 --- /dev/null +++ b/site/examples/uce-starter/index.uce @@ -0,0 +1,31 @@ +#load "lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + + DTree resolved = starter_resolve_view(context); + + ob_start(); + if(resolved["file"].to_string() != "") + { + if(resolved["param"].to_string() != "") + context.var["starter"]["route"]["param"] = resolved["param"]; + render_file(resolved["file"].to_string(), context); + } + else + { + starter_not_found("The requested page does not exist.", context); + <> +
+

404 Not Found

+

+
+ + } + String main_html = ob_get_close(); + + DTree shell_props; + shell_props["main_html"] = main_html; + render_component("components/theme/page_shell", shell_props, context); +} diff --git a/site/examples/uce-starter/js/ag-grid/ag-grid-community.min.js b/site/examples/uce-starter/js/ag-grid/ag-grid-community.min.js new file mode 100755 index 0000000..1a78246 --- /dev/null +++ b/site/examples/uce-starter/js/ag-grid/ag-grid-community.min.js @@ -0,0 +1,8 @@ +/** + * @ag-grid-community/all-modules - Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue + * @version v31.1.0 + * @link https://www.ag-grid.com/ + * @license MIT + */ +// @ag-grid-community/all-modules v31.1.0 +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.agGrid=t():e.agGrid=t()}(window,(function(){return function(e){var t={};function a(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,a),o.l=!0,o.exports}return a.m=e,a.c=t,a.d=function(e,t,r){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(a.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)a.d(r,o,function(t){return e[t]}.bind(null,o));return r},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=6)}([function(e,t,a){"use strict";a.r(t),a.d(t,"ColumnFactory",(function(){return it})),a.d(t,"ColumnModel",(function(){return kt})),a.d(t,"ColumnKeyCreator",(function(){return M})),a.d(t,"ColumnUtils",(function(){return At})),a.d(t,"DisplayedGroupCreator",(function(){return Ot})),a.d(t,"GroupInstanceIdCreator",(function(){return gt})),a.d(t,"GROUP_AUTO_COLUMN_ID",(function(){return ct})),a.d(t,"ComponentUtil",(function(){return Nt})),a.d(t,"AgStackComponentsRegistry",(function(){return Ut})),a.d(t,"UserComponentRegistry",(function(){return un})),a.d(t,"UserComponentFactory",(function(){return Pn})),a.d(t,"ColDefUtil",(function(){return Ln})),a.d(t,"BeanStub",(function(){return at})),a.d(t,"Context",(function(){return ie})),a.d(t,"Autowired",(function(){return de})),a.d(t,"PostConstruct",(function(){return le})),a.d(t,"PreConstruct",(function(){return ne})),a.d(t,"Optional",(function(){return ce})),a.d(t,"Bean",(function(){return ge})),a.d(t,"Qualifier",(function(){return pe})),a.d(t,"PreDestroy",(function(){return se})),a.d(t,"QuerySelector",(function(){return po})),a.d(t,"RefSelector",(function(){return ho})),a.d(t,"ExcelFactoryMode",(function(){return Nn})),a.d(t,"DragAndDropService",(function(){return Ii})),a.d(t,"DragSourceType",(function(){return Ni})),a.d(t,"DragService",(function(){return In})),a.d(t,"VirtualListDragFeature",(function(){return _n})),a.d(t,"Column",(function(){return Ee})),a.d(t,"ColumnGroup",(function(){return lt})),a.d(t,"ProvidedColumnGroup",(function(){return xe})),a.d(t,"RowNode",(function(){return Ai})),a.d(t,"RowHighlightPosition",(function(){return Bn})),a.d(t,"FilterManager",(function(){return Jn})),a.d(t,"ProvidedFilter",(function(){return ko})),a.d(t,"SimpleFilter",(function(){return Ho})),a.d(t,"ScalarFilter",(function(){return _o})),a.d(t,"NumberFilter",(function(){return ei})),a.d(t,"TextFilter",(function(){return ai})),a.d(t,"DateFilter",(function(){return Wo})),a.d(t,"TextFloatingFilter",(function(){return si})),a.d(t,"HeaderFilterCellComp",(function(){return $n})),a.d(t,"FloatingFilterMapper",(function(){return On})),a.d(t,"GridBodyComp",(function(){return $l})),a.d(t,"GridBodyCtrl",(function(){return cl})),a.d(t,"RowAnimationCssClasses",(function(){return gl})),a.d(t,"ScrollVisibleService",(function(){return ts})),a.d(t,"MouseEventService",(function(){return os})),a.d(t,"NavigationService",(function(){return ns})),a.d(t,"RowContainerComp",(function(){return us})),a.d(t,"RowContainerName",(function(){return Wl})),a.d(t,"RowContainerCtrl",(function(){return Jl})),a.d(t,"RowContainerType",(function(){return Ul})),a.d(t,"getRowContainerTypeForName",(function(){return Kl})),a.d(t,"BodyDropPivotTarget",(function(){return hs})),a.d(t,"BodyDropTarget",(function(){return bs})),a.d(t,"CssClassApplier",(function(){return Rl})),a.d(t,"HeaderRowContainerComp",(function(){return Xs})),a.d(t,"GridHeaderComp",(function(){return og})),a.d(t,"GridHeaderCtrl",(function(){return ag})),a.d(t,"HeaderRowComp",(function(){return ks})),a.d(t,"HeaderRowType",(function(){return Rs})),a.d(t,"HeaderRowCtrl",(function(){return Ks})),a.d(t,"HeaderCellCtrl",(function(){return Vs})),a.d(t,"SortIndicatorComp",(function(){return ci})),a.d(t,"HeaderFilterCellCtrl",(function(){return Ls})),a.d(t,"HeaderGroupCellCtrl",(function(){return Ws})),a.d(t,"AbstractHeaderCellCtrl",(function(){return As})),a.d(t,"HeaderRowContainerCtrl",(function(){return Qs})),a.d(t,"HorizontalResizeService",(function(){return ng})),a.d(t,"MoveColumnFeature",(function(){return fs})),a.d(t,"StandardMenuFactory",(function(){return sg})),a.d(t,"TabbedLayout",(function(){return hg})),a.d(t,"ResizeObserverService",(function(){return vg})),a.d(t,"AnimationFrameService",(function(){return wg})),a.d(t,"ExpansionService",(function(){return yg})),a.d(t,"MenuService",(function(){return Eg})),a.d(t,"LargeTextCellEditor",(function(){return wi})),a.d(t,"PopupEditorWrapper",(function(){return ss})),a.d(t,"SelectCellEditor",(function(){return Ci})),a.d(t,"TextCellEditor",(function(){return Ri})),a.d(t,"NumberCellEditor",(function(){return $i})),a.d(t,"DateCellEditor",(function(){return tn})),a.d(t,"DateStringCellEditor",(function(){return on})),a.d(t,"CheckboxCellEditor",(function(){return gn})),a.d(t,"Beans",(function(){return bl})),a.d(t,"AnimateShowChangeCellRenderer",(function(){return ki})),a.d(t,"AnimateSlideCellRenderer",(function(){return Ti})),a.d(t,"GroupCellRenderer",(function(){return ji})),a.d(t,"GroupCellRendererCtrl",(function(){return Wi})),a.d(t,"SetLeftFeature",(function(){return Os})),a.d(t,"PositionableFeature",(function(){return Ro})),a.d(t,"AutoWidthCalculator",(function(){return xg})),a.d(t,"CheckboxSelectionComponent",(function(){return Pi})),a.d(t,"CellComp",(function(){return gs})),a.d(t,"CellCtrl",(function(){return kl})),a.d(t,"RowCtrl",(function(){return Al})),a.d(t,"RowRenderer",(function(){return Ag})),a.d(t,"ValueFormatterService",(function(){return Mg})),a.d(t,"CssClassManager",(function(){return so})),a.d(t,"CheckboxCellRenderer",(function(){return ln})),a.d(t,"PinnedRowModel",(function(){return Lg})),a.d(t,"ServerSideTransactionResultStatus",(function(){return Ng})),a.d(t,"ChangedPath",(function(){return Fg})),a.d(t,"RowNodeBlock",(function(){return Ig})),a.d(t,"RowNodeBlockLoader",(function(){return _g})),a.d(t,"PaginationProxy",(function(){return qg})),a.d(t,"ClientSideRowModelSteps",(function(){return bg})),a.d(t,"StylingService",(function(){return Ug})),a.d(t,"LayoutCssClasses",(function(){return el})),a.d(t,"AgAbstractField",(function(){return Ao})),a.d(t,"AgCheckbox",(function(){return Io})),a.d(t,"AgRadioButton",(function(){return Go})),a.d(t,"AgToggleButton",(function(){return jg})),a.d(t,"AgInputTextField",(function(){return Jo})),a.d(t,"AgInputTextArea",(function(){return Kg})),a.d(t,"AgInputNumberField",(function(){return Xo})),a.d(t,"AgInputDateField",(function(){return Yg})),a.d(t,"AgInputRange",(function(){return Qg})),a.d(t,"AgRichSelect",(function(){return td})),a.d(t,"AgSelect",(function(){return Lo})),a.d(t,"AgSlider",(function(){return rd})),a.d(t,"AgGroupComponent",(function(){return id})),a.d(t,"AgMenuItemRenderer",(function(){return dn})),a.d(t,"AgMenuItemComponent",(function(){return cd})),a.d(t,"AgMenuList",(function(){return ld})),a.d(t,"AgMenuPanel",(function(){return gd})),a.d(t,"AgDialog",(function(){return md})),a.d(t,"AgPanel",(function(){return pd})),a.d(t,"Component",(function(){return uo})),a.d(t,"ManagedFocusFeature",(function(){return So})),a.d(t,"TabGuardComp",(function(){return ug})),a.d(t,"TabGuardCtrl",(function(){return cg})),a.d(t,"TabGuardClassNames",(function(){return gg})),a.d(t,"PopupComponent",(function(){return vi})),a.d(t,"PopupService",(function(){return Cd})),a.d(t,"TouchListener",(function(){return gi})),a.d(t,"VirtualList",(function(){return $g})),a.d(t,"AgAbstractLabel",(function(){return To})),a.d(t,"AgPickerField",(function(){return Oo})),a.d(t,"AgAutocomplete",(function(){return xd})),a.d(t,"CellRangeType",(function(){return pl})),a.d(t,"SelectionHandleType",(function(){return ul})),a.d(t,"AutoScrollService",(function(){return Gn})),a.d(t,"VanillaFrameworkOverrides",(function(){return zd})),a.d(t,"CellNavigationService",(function(){return Ad})),a.d(t,"AlignedGridsService",(function(){return Md})),a.d(t,"KeyCode",(function(){return Wr})),a.d(t,"VerticalDirection",(function(){return Di})),a.d(t,"HorizontalDirection",(function(){return Oi})),a.d(t,"Grid",(function(){return _u})),a.d(t,"GridCoreCreator",(function(){return qu})),a.d(t,"createGrid",(function(){return Hu})),a.d(t,"GridApi",(function(){return Un})),a.d(t,"Events",(function(){return st})),a.d(t,"FocusService",(function(){return rc})),a.d(t,"GridOptionsService",(function(){return mu})),a.d(t,"EventService",(function(){return fe})),a.d(t,"SelectableService",(function(){return kc})),a.d(t,"RowNodeSorter",(function(){return Kc})),a.d(t,"CtrlsService",(function(){return Jc})),a.d(t,"GridComp",(function(){return Xd})),a.d(t,"GridCtrl",(function(){return Qd})),a.d(t,"Logger",(function(){return Kd})),a.d(t,"LoggerFactory",(function(){return jd})),a.d(t,"SortController",(function(){return ec})),a.d(t,"TemplateService",(function(){return qd})),a.d(t,"LocaleService",(function(){return fu})),a.d(t,"_",(function(){return $r})),a.d(t,"NumberSequence",(function(){return eo})),a.d(t,"AgPromiseStatus",(function(){return to})),a.d(t,"AgPromise",(function(){return ao})),a.d(t,"Timer",(function(){return ro})),a.d(t,"ValueService",(function(){return Gd})),a.d(t,"ValueCache",(function(){return cc})),a.d(t,"ExpressionService",(function(){return _d})),a.d(t,"ValueParserService",(function(){return xu})),a.d(t,"CellPositionUtils",(function(){return Lc})),a.d(t,"RowPositionUtils",(function(){return Mc})),a.d(t,"HeaderPositionUtils",(function(){return _c})),a.d(t,"HeaderNavigationService",(function(){return eg})),a.d(t,"HeaderNavigationDirection",(function(){return Zs})),a.d(t,"DataTypeService",(function(){return Eu})),a.d(t,"PropertyKeys",(function(){return Pt})),a.d(t,"ColumnApi",(function(){return Fd})),a.d(t,"BaseComponentWrapper",(function(){return Wu})),a.d(t,"Environment",(function(){return yc})),a.d(t,"TooltipFeature",(function(){return fl})),a.d(t,"CustomTooltipFeature",(function(){return lo})),a.d(t,"DEFAULT_CHART_GROUPS",(function(){return Uu})),a.d(t,"CHART_TOOL_PANEL_ALLOW_LIST",(function(){return ju})),a.d(t,"CHART_TOOLBAR_ALLOW_LIST",(function(){return Ku})),a.d(t,"CHART_TOOL_PANEL_MENU_OPTIONS",(function(){return Yu})),a.d(t,"__FORCE_MODULE_DETECTION",(function(){return Qu})),a.d(t,"BarColumnLabelPlacement",(function(){return Ju})),a.d(t,"ModuleNames",(function(){return re})),a.d(t,"ModuleRegistry",(function(){return oe}));var r={};a.r(r),a.d(r,"makeNull",(function(){return y})),a.d(r,"exists",(function(){return S})),a.d(r,"missing",(function(){return E})),a.d(r,"missingOrEmpty",(function(){return R})),a.d(r,"toStringOrNull",(function(){return x})),a.d(r,"attrToNumber",(function(){return k})),a.d(r,"attrToBoolean",(function(){return z})),a.d(r,"attrToString",(function(){return T})),a.d(r,"jsonEquals",(function(){return A})),a.d(r,"defaultComparator",(function(){return D})),a.d(r,"values",(function(){return O}));var o={};a.r(o),a.d(o,"iterateObject",(function(){return P})),a.d(o,"cloneObject",(function(){return L})),a.d(o,"deepCloneDefinition",(function(){return N})),a.d(o,"getAllValuesInObject",(function(){return F})),a.d(o,"mergeDeep",(function(){return I})),a.d(o,"getValueUsingField",(function(){return G})),a.d(o,"removeAllReferences",(function(){return V})),a.d(o,"isNonNullObject",(function(){return H}));var i={};a.r(i),a.d(i,"doOnce",(function(){return B})),a.d(i,"warnOnce",(function(){return q})),a.d(i,"errorOnce",(function(){return W})),a.d(i,"getFunctionName",(function(){return U})),a.d(i,"isFunction",(function(){return j})),a.d(i,"executeInAWhile",(function(){return K})),a.d(i,"executeNextVMTurn",(function(){return J})),a.d(i,"executeAfter",(function(){return X})),a.d(i,"debounce",(function(){return Z})),a.d(i,"throttle",(function(){return $})),a.d(i,"waitUntil",(function(){return ee})),a.d(i,"compose",(function(){return te})),a.d(i,"noop",(function(){return ae}));var n={};a.r(n),a.d(n,"existsAndNotEmpty",(function(){return ze})),a.d(n,"last",(function(){return Te})),a.d(n,"areEqual",(function(){return Ae})),a.d(n,"shallowCompare",(function(){return De})),a.d(n,"sortNumerically",(function(){return Oe})),a.d(n,"removeRepeatsFromArray",(function(){return Me})),a.d(n,"removeFromUnorderedArray",(function(){return Pe})),a.d(n,"removeFromArray",(function(){return Le})),a.d(n,"removeAllFromUnorderedArray",(function(){return Ne})),a.d(n,"removeAllFromArray",(function(){return Fe})),a.d(n,"insertIntoArray",(function(){return Ie})),a.d(n,"insertArrayIntoArray",(function(){return Ge})),a.d(n,"moveInArray",(function(){return Ve})),a.d(n,"includes",(function(){return He})),a.d(n,"flatten",(function(){return _e})),a.d(n,"pushAll",(function(){return Be})),a.d(n,"toStrings",(function(){return qe})),a.d(n,"forEachReverse",(function(){return We}));var l={};a.r(l),a.d(l,"stopPropagationForAgGrid",(function(){return Ke})),a.d(l,"isStopPropagationForAgGrid",(function(){return Ye})),a.d(l,"isEventSupported",(function(){return Qe})),a.d(l,"getCtrlForEventTarget",(function(){return Je})),a.d(l,"isElementInEventPath",(function(){return Xe})),a.d(l,"createEventPath",(function(){return Ze})),a.d(l,"getEventPath",(function(){return $e})),a.d(l,"addSafePassiveEventListener",(function(){return et}));var s={};a.r(s),a.d(s,"utf8_encode",(function(){return mt})),a.d(s,"capitalise",(function(){return vt})),a.d(s,"escapeString",(function(){return ft})),a.d(s,"camelCaseToHumanText",(function(){return wt})),a.d(s,"camelCaseToHyphenated",(function(){return bt}));var g={};a.r(g),a.d(g,"convertToMap",(function(){return Ct})),a.d(g,"mapById",(function(){return yt})),a.d(g,"keys",(function(){return St}));var d={};a.r(d),a.d(d,"setAriaRole",(function(){return Jt})),a.d(d,"getAriaSortState",(function(){return Xt})),a.d(d,"getAriaLevel",(function(){return Zt})),a.d(d,"getAriaPosInSet",(function(){return $t})),a.d(d,"getAriaLabel",(function(){return ea})),a.d(d,"setAriaLabel",(function(){return ta})),a.d(d,"setAriaLabelledBy",(function(){return aa})),a.d(d,"setAriaDescribedBy",(function(){return ra})),a.d(d,"setAriaLive",(function(){return oa})),a.d(d,"setAriaAtomic",(function(){return ia})),a.d(d,"setAriaRelevant",(function(){return na})),a.d(d,"setAriaLevel",(function(){return la})),a.d(d,"setAriaDisabled",(function(){return sa})),a.d(d,"setAriaHidden",(function(){return ga})),a.d(d,"setAriaActiveDescendant",(function(){return da})),a.d(d,"setAriaExpanded",(function(){return ca})),a.d(d,"removeAriaExpanded",(function(){return ua})),a.d(d,"setAriaSetSize",(function(){return pa})),a.d(d,"setAriaPosInSet",(function(){return ha})),a.d(d,"setAriaMultiSelectable",(function(){return ma})),a.d(d,"setAriaRowCount",(function(){return va})),a.d(d,"setAriaRowIndex",(function(){return fa})),a.d(d,"setAriaColCount",(function(){return wa})),a.d(d,"setAriaColIndex",(function(){return ba})),a.d(d,"setAriaColSpan",(function(){return Ca})),a.d(d,"setAriaSort",(function(){return ya})),a.d(d,"removeAriaSort",(function(){return Sa})),a.d(d,"setAriaSelected",(function(){return Ea})),a.d(d,"setAriaChecked",(function(){return Ra})),a.d(d,"setAriaControls",(function(){return xa})),a.d(d,"getAriaCheckboxStateName",(function(){return ka}));var c={};a.r(c),a.d(c,"isBrowserSafari",(function(){return za})),a.d(c,"getSafariVersion",(function(){return Ta})),a.d(c,"isBrowserChrome",(function(){return Aa})),a.d(c,"isBrowserFirefox",(function(){return Da})),a.d(c,"isMacOsUserAgent",(function(){return Oa})),a.d(c,"isIOSUserAgent",(function(){return Ma})),a.d(c,"browserSupportsPreventScroll",(function(){return Pa})),a.d(c,"getTabIndex",(function(){return La})),a.d(c,"getMaxDivHeight",(function(){return Na})),a.d(c,"getBodyWidth",(function(){return Fa})),a.d(c,"getBodyHeight",(function(){return Ia})),a.d(c,"getScrollbarWidth",(function(){return Ga})),a.d(c,"isInvisibleScrollbar",(function(){return Ha}));var u={};a.r(u),a.d(u,"padStartWidthZeros",(function(){return _a})),a.d(u,"createArrayOfNumbers",(function(){return Ba})),a.d(u,"cleanNumber",(function(){return qa})),a.d(u,"decToHex",(function(){return Wa})),a.d(u,"formatNumberTwoDecimalPlacesAndCommas",(function(){return Ua})),a.d(u,"formatNumberCommas",(function(){return ja})),a.d(u,"sum",(function(){return Ka}));var p={};a.r(p),a.d(p,"serialiseDate",(function(){return Ya})),a.d(p,"dateToFormattedString",(function(){return Ja})),a.d(p,"parseDateTimeFromString",(function(){return Xa}));var h={};a.r(h),a.d(h,"radioCssClass",(function(){return $a})),a.d(h,"FOCUSABLE_SELECTOR",(function(){return er})),a.d(h,"FOCUSABLE_EXCLUDE",(function(){return tr})),a.d(h,"isFocusableFormField",(function(){return ar})),a.d(h,"setDisplayed",(function(){return rr})),a.d(h,"setVisible",(function(){return or})),a.d(h,"setDisabled",(function(){return ir})),a.d(h,"isElementChildOfClass",(function(){return nr})),a.d(h,"getElementSize",(function(){return lr})),a.d(h,"getInnerHeight",(function(){return sr})),a.d(h,"getInnerWidth",(function(){return gr})),a.d(h,"getAbsoluteHeight",(function(){return dr})),a.d(h,"getAbsoluteWidth",(function(){return cr})),a.d(h,"getElementRectWithOffset",(function(){return ur})),a.d(h,"isRtlNegativeScroll",(function(){return pr})),a.d(h,"getScrollLeft",(function(){return hr})),a.d(h,"setScrollLeft",(function(){return mr})),a.d(h,"clearElement",(function(){return vr})),a.d(h,"removeFromParent",(function(){return fr})),a.d(h,"isInDOM",(function(){return wr})),a.d(h,"isVisible",(function(){return br})),a.d(h,"loadTemplate",(function(){return Cr})),a.d(h,"ensureDomOrder",(function(){return yr})),a.d(h,"setDomChildOrder",(function(){return Sr})),a.d(h,"insertWithDomOrder",(function(){return Er})),a.d(h,"addStylesToElement",(function(){return Rr})),a.d(h,"isHorizontalScrollShowing",(function(){return xr})),a.d(h,"isVerticalScrollShowing",(function(){return kr})),a.d(h,"setElementWidth",(function(){return zr})),a.d(h,"setFixedWidth",(function(){return Tr})),a.d(h,"setElementHeight",(function(){return Ar})),a.d(h,"setFixedHeight",(function(){return Dr})),a.d(h,"formatSize",(function(){return Or})),a.d(h,"isNodeOrElement",(function(){return Mr})),a.d(h,"copyNodeList",(function(){return Pr})),a.d(h,"iterateNamedNodeMap",(function(){return Lr})),a.d(h,"addOrRemoveAttribute",(function(){return Nr})),a.d(h,"nodeListForEach",(function(){return Fr})),a.d(h,"bindCellRendererToHtmlElement",(function(){return Ir}));var m={};a.r(m),a.d(m,"fuzzyCheckStrings",(function(){return Gr})),a.d(m,"fuzzySuggestions",(function(){return Vr}));var v={};a.r(v),a.d(v,"iconNameClassMap",(function(){return _r})),a.d(v,"createIcon",(function(){return Br})),a.d(v,"createIconNoSpan",(function(){return qr}));var f={};a.r(f),a.d(f,"isEventFromPrintableCharacter",(function(){return Ur})),a.d(f,"isUserSuppressingKeyboardEvent",(function(){return jr})),a.d(f,"isUserSuppressingHeaderKeyboardEvent",(function(){return Kr})),a.d(f,"normaliseQwertyAzerty",(function(){return Yr})),a.d(f,"isDeleteKey",(function(){return Qr}));var w={};a.r(w),a.d(w,"areEventsNear",(function(){return Jr}));var b={};a.r(b),a.d(b,"sortRowNodesByOrder",(function(){return Xr}));var C={};function y(e){return null==e||""===e?null:e}function S(e,t=!1){return null!=e&&(""!==e||t)}function E(e){return!S(e)}function R(e){return null==e||0===e.length}function x(e){return null!=e&&"function"==typeof e.toString?e.toString():null}function k(e){if(void 0===e)return;if(null===e||""===e)return null;if("number"==typeof e)return isNaN(e)?void 0:e;const t=parseInt(e,10);return isNaN(t)?void 0:t}function z(e){if(void 0!==e)return null!==e&&""!==e&&("boolean"==typeof e?e:/true/i.test(e))}function T(e){if(null!=e&&""!==e)return e}function A(e,t){return(e?JSON.stringify(e):null)===(t?JSON.stringify(t):null)}function D(e,t,a=!1){const r=null==e,o=null==t;if(e&&e.toNumber&&(e=e.toNumber()),t&&t.toNumber&&(t=t.toNumber()),r&&o)return 0;if(r)return-1;if(o)return 1;function i(e,t){return e>t?1:et.push(e)),t}return Object.values(e)}a.r(C),a.d(C,"convertToSet",(function(){return Zr}));class M{constructor(){this.existingKeys={}}addExistingKeys(e){for(let t=0;t{if(t&&t.indexOf(e)>=0)return;const o=a[e],i=H(o)&&o.constructor===Object;r[e]=i?N(o):o}),r}function F(e){if(!e)return[];const t=Object;if("function"==typeof t.values)return t.values(e);const a=[];for(const t in e)e.hasOwnProperty(t)&&e.propertyIsEnumerable(t)&&a.push(e[t]);return a}function I(e,t,a=!0,r=!1){S(t)&&P(t,(t,o)=>{let i=e[t];if(i!==o){if(r){if(null==i&&null!=o){"object"==typeof o&&o.constructor===Object&&(i={},e[t]=i)}}H(o)&&H(i)&&!Array.isArray(i)?I(i,o,a,r):(a||void 0!==o)&&(e[t]=o)}})}function G(e,t,a){if(!t||!e)return;if(!a)return e[t];const r=t.split(".");let o=e;for(let e=0;e{"object"!=typeof e[a]||t.includes(a)||(e[a]=void 0)});const r=Object.getPrototypeOf(e),o={};Object.getOwnPropertyNames(r).forEach(e=>{if("function"==typeof r[e]&&!t.includes(e)){const t=()=>{console.warn((e=>`AG Grid: Grid API function ${e}() cannot be called as the grid has been destroyed.\n It is recommended to remove local references to the grid api. Alternatively, check gridApi.isDestroyed() to avoid calling methods against a destroyed grid.\n To run logic when the grid is about to be destroyed use the gridPreDestroy event. See: ${a}`)(e))};o[e]={value:t,writable:!0}}}),Object.defineProperties(e,o)}function H(e){return"object"==typeof e&&null!==e}const _={};function B(e,t){_[t]||(e(),_[t]=!0)}function q(e){B(()=>console.warn("AG Grid: "+e),e)}function W(e){B(()=>console.error("AG Grid: "+e),e)}function U(e){if(e.name)return e.name;const t=/function\s+([^\(]+)/.exec(e.toString());return t&&2===t.length?t[1].trim():null}function j(e){return!!(e&&e.constructor&&e.call&&e.apply)}function K(e){X(e,400)}const Y=[];let Q=!1;function J(e){Y.push(e),Q||(Q=!0,window.setTimeout(()=>{const e=Y.slice();Y.length=0,Q=!1,e.forEach(e=>e())},0))}function X(e,t=0){e.length>0&&window.setTimeout(()=>e.forEach(e=>e()),t)}function Z(e,t){let a;return function(...r){const o=this;window.clearTimeout(a),a=window.setTimeout((function(){e.apply(o,r)}),t)}}function $(e,t){let a=0;return function(...r){const o=(new Date).getTime();o-a{const l=(new Date).getTime()-o>a;(e()||l)&&(t(),n=!0,null!=i&&(window.clearInterval(i),i=null),l&&r&&console.warn(r))};l(),n||(i=window.setInterval(l,10))}function te(...e){return t=>e.reduce((e,t)=>t(e),t)}const ae=()=>{};var re;!function(e){e.CommunityCoreModule="@ag-grid-community/core",e.InfiniteRowModelModule="@ag-grid-community/infinite-row-model",e.ClientSideRowModelModule="@ag-grid-community/client-side-row-model",e.CsvExportModule="@ag-grid-community/csv-export",e.EnterpriseCoreModule="@ag-grid-enterprise/core",e.RowGroupingModule="@ag-grid-enterprise/row-grouping",e.ColumnsToolPanelModule="@ag-grid-enterprise/column-tool-panel",e.FiltersToolPanelModule="@ag-grid-enterprise/filter-tool-panel",e.MenuModule="@ag-grid-enterprise/menu",e.SetFilterModule="@ag-grid-enterprise/set-filter",e.MultiFilterModule="@ag-grid-enterprise/multi-filter",e.StatusBarModule="@ag-grid-enterprise/status-bar",e.SideBarModule="@ag-grid-enterprise/side-bar",e.RangeSelectionModule="@ag-grid-enterprise/range-selection",e.MasterDetailModule="@ag-grid-enterprise/master-detail",e.RichSelectModule="@ag-grid-enterprise/rich-select",e.GridChartsModule="@ag-grid-enterprise/charts",e.ViewportRowModelModule="@ag-grid-enterprise/viewport-row-model",e.ServerSideRowModelModule="@ag-grid-enterprise/server-side-row-model",e.ExcelExportModule="@ag-grid-enterprise/excel-export",e.ClipboardModule="@ag-grid-enterprise/clipboard",e.SparklinesModule="@ag-grid-enterprise/sparklines",e.AdvancedFilterModule="@ag-grid-enterprise/advanced-filter",e.AngularModule="@ag-grid-community/angular",e.ReactModule="@ag-grid-community/react",e.VueModule="@ag-grid-community/vue"}(re||(re={}));class oe{static register(e){oe.__register(e,!0,void 0)}static registerModules(e){oe.__registerModules(e,!0,void 0)}static __register(e,t,a){oe.runVersionChecks(e),void 0!==a?(oe.areGridScopedModules=!0,void 0===oe.gridModulesMap[a]&&(oe.gridModulesMap[a]={}),oe.gridModulesMap[a][e.moduleName]=e):oe.globalModulesMap[e.moduleName]=e,oe.setModuleBased(t)}static __unRegisterGridModules(e){delete oe.gridModulesMap[e]}static __registerModules(e,t,a){oe.setModuleBased(t),e&&e.forEach(e=>oe.__register(e,t,a))}static isValidModuleVersion(e){const[t,a]=e.version.split(".")||[],[r,o]=oe.currentModuleVersion.split(".")||[];return t===r&&a===o}static runVersionChecks(e){if(oe.currentModuleVersion||(oe.currentModuleVersion=e.version),e.version?oe.isValidModuleVersion(e)||console.error(`AG Grid: You are using incompatible versions of AG Grid modules. Major and minor versions should always match across modules. '${e.moduleName}' is version ${e.version} but the other modules are version ${this.currentModuleVersion}. Please update all modules to the same version.`):console.error(`AG Grid: You are using incompatible versions of AG Grid modules. Major and minor versions should always match across modules. '${e.moduleName}' is incompatible. Please update all modules to the same version.`),e.validate){const t=e.validate();if(!t.isValid){const e=t;console.error("AG Grid: "+e.message)}}}static setModuleBased(e){void 0===oe.moduleBased?oe.moduleBased=e:oe.moduleBased!==e&&B(()=>{console.warn("AG Grid: You are mixing modules (i.e. @ag-grid-community/core) and packages (ag-grid-community) - you can only use one or the other of these mechanisms."),console.warn("Please see https://www.ag-grid.com/javascript-grid/packages-modules/ for more information.")},"ModulePackageCheck")}static __setIsBundled(){oe.isBundled=!0}static __assertRegistered(e,t,a){var r;if(this.__isRegistered(e,a))return!0;const o=t+e;let i;if(oe.isBundled)i=`AG Grid: unable to use ${t} as 'ag-grid-enterprise' has not been loaded. Check you are using the Enterprise bundle:\n \n + + + diff --git a/site/examples/uce-starter/js/u-events.js b/site/examples/uce-starter/js/u-events.js new file mode 100755 index 0000000..3a0d4fe --- /dev/null +++ b/site/examples/uce-starter/js/u-events.js @@ -0,0 +1,92 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.EventEmitter = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + +class EventEmitter { + constructor() { + /** Map> */ + this._topics = new Map(); + } + + /** + * Subscribe to `topic`. If `slot_key` is provided (truthy), + * it dedupes by that key; otherwise a unique Symbol is used. + * Returns an unsubscribe fn. + */ + on(topic, handler, slot_key = null) { + let map = this._topics.get(topic); + if (!map) { + map = new Map(); + this._topics.set(topic, map); + } + // use the provided slot_key or a fresh Symbol() + const key = slot_key != null ? slot_key : Symbol(); + map.set(key, handler); + return () => this.off(topic, key); + } + + /** + * Unsubscribe by handler function or by slot_key. + */ + off(topic, handlerOrSlotKey) { + const map = this._topics.get(topic); + if (!map) return; + + // if it matches a slotKey directly, remove it + if (map.has(handlerOrSlotKey)) { + map.delete(handlerOrSlotKey); + } else { + // otherwise assume it's a function: remove all matching fns + for (const [key, fn] of map.entries()) { + if (fn === handlerOrSlotKey) { + map.delete(key); + } + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + } + + /** + * Emit to all handlers on `topic`. Handlers returning + * 'remove_handler' are auto-removed. + * Returns the number of handlers invoked. + */ + emit(topic, ...args) { + let count = 0; + const map = this._topics.get(topic); + if (!map) return count; + + for (const [key, fn] of Array.from(map.entries())) { + const res = fn(...args); + count++; + if (res === 'remove_handler') { + map.delete(key); + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + return count; + } + + clear(topic) { + if (topic) { + this._topics.delete(topic); + } + return this; + } +} + +return EventEmitter; + +})); diff --git a/site/examples/uce-starter/js/u-format.js b/site/examples/uce-starter/js/u-format.js new file mode 100644 index 0000000..a10b75c --- /dev/null +++ b/site/examples/uce-starter/js/u-format.js @@ -0,0 +1,136 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UFormat = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + + const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + + function scaleBytes(bytes) { + let value = Number(bytes || 0); + let unitIndex = 0; + while (Math.abs(value) >= 1024 && unitIndex < BYTE_UNITS.length - 1) { + value /= 1024; + unitIndex += 1; + } + return { value, unitIndex }; + } + + function formatBytes(bytes) { + if (bytes == null || bytes === '') return '--'; + const scaled = scaleBytes(bytes); + const decimals = scaled.unitIndex === 0 ? 0 : 1; + return `${scaled.value.toFixed(decimals)} ${BYTE_UNITS[scaled.unitIndex]}`; + } + + function formatDiskBytes(bytes) { + if (bytes == null || bytes === '') return '--'; + const scaled = scaleBytes(bytes); + const decimals = scaled.unitIndex >= 4 ? 2 : scaled.unitIndex >= 1 ? 1 : 0; + return `${scaled.value.toFixed(decimals)} ${BYTE_UNITS[scaled.unitIndex]}`; + } + + function formatCount(value) { + const number = Number(value); + if (!Number.isFinite(number)) return '--'; + return number.toLocaleString(); + } + + function formatDurationMs(value) { + const number = Number(value); + if (!Number.isFinite(number)) return '--'; + if (Math.abs(number) >= 1000) { + return `${(number / 1000).toFixed(number >= 10000 ? 0 : 1)} s`; + } + return `${number.toFixed(number >= 100 ? 0 : 1)} ms`; + } + + function parseUnitNumber(text) { + const normalized = String(text || '').trim().toLowerCase().replace(/,/g, ''); + if (!normalized) return null; + + const pure = normalized.match(/^([-+]?\d*\.?\d+)$/); + if (pure) { + return Number(pure[1]); + } + + const withUnit = normalized.match(/^([-+]?\d*\.?\d+)\s*([a-z%][a-z0-9\/_-]*)$/); + if (!withUnit) { + return null; + } + + const value = Number(withUnit[1]); + let unit = withUnit[2]; + if (!Number.isFinite(value)) { + return null; + } + + if (unit.endsWith('/s')) { + unit = unit.slice(0, -2); + } + + const bytes = { + b: 1, + kb: 1024, + kib: 1024, + mb: 1024 ** 2, + mib: 1024 ** 2, + gb: 1024 ** 3, + gib: 1024 ** 3, + tb: 1024 ** 4, + tib: 1024 ** 4, + pb: 1024 ** 5, + pib: 1024 ** 5, + }; + + if (Object.prototype.hasOwnProperty.call(bytes, unit)) { + return value * bytes[unit]; + } + + const durations = { + ms: 0.001, + s: 1, + sec: 1, + secs: 1, + second: 1, + seconds: 1, + m: 60, + min: 60, + mins: 60, + minute: 60, + minutes: 60, + h: 3600, + hr: 3600, + hrs: 3600, + hour: 3600, + hours: 3600, + d: 86400, + day: 86400, + days: 86400, + }; + + if (Object.prototype.hasOwnProperty.call(durations, unit)) { + return value * durations[unit]; + } + + if (unit === '%') { + return value; + } + + return null; + } + + return { + formatBytes, + formatCount, + formatDiskBytes, + formatDurationMs, + parseUnitNumber, + scaleBytes, + }; +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-howler-demo.html b/site/examples/uce-starter/js/u-howler-demo.html new file mode 100755 index 0000000..7d86387 --- /dev/null +++ b/site/examples/uce-starter/js/u-howler-demo.html @@ -0,0 +1,1488 @@ + + + + + + U-Howler.js Demo + + + +
+

U-Howler.js Demo

+ +
+
+

Basic Controls

+
+
+
+
+
+
+
+ + + + +
+ +
+ + + 440 Hz +
+ +
+ + + 1.0 +
+
+ +
+
+ + +
+ +
+
+ +
+

Global Controls

+
+ + +
+ +
+ + + 1.0 +
+
+
+ +
+

API Overview

+ +
+

Howler (Global)

+
volume(vol?) → Howler|number
+
mute(muted?) → Howler
+
stop() → Howler
+
unload() → Howler
+
codecs(ext) → boolean
+
environment(env) → Howler
+
+ +
+

Howl Constructor

+
new Howl(options)
+
+ Options: src, volume, rate, loop, mute, preload, autoplay, sprite, format, html5, pool, xhr, spatial, on* +
+
+ +
+

Core Methods

+
load() → Promise<Howl>
+
play(sprite?) → number|Promise
+
pause(id?) → Howl
+
stop(id?) → Howl
+
mute(muted?, id?) → Howl|boolean
+
volume(vol?, id?) → Howl|number
+
fade(from, to, duration, id?) → Howl
+
rate(rate?, id?) → Howl|number
+
seek(seek?, id?) → Howl|number
+
+ +
+

Examples

+
+// Promise support
+const howl = new Howl({src: 'audio.mp3'});
+await howl.load();
+const id = howl.play();
+
+// Sprite usage
+const soundFx = new Howl({
+  src: 'sounds.mp3',
+  sprite: {
+    jump: [0, 500],
+    shoot: [600, 300]
+  }
+});
+soundFx.play('jump');
+
+
+
+
+ +
+
+

Audio Sprites

+
+

Audio Sprites Demo

+
+
+
+
+
+ +
+ + +
+ +
+ + + + + + + + +
+ +
+
+ +
+

Audio Sprites API

+ +
+

Sprite Methods

+
play(spriteName) → number
+
+ Play a specific sprite by name from the sprite collection. +
+
+ +
+

Sprite Configuration

+
+ Sprite Format: { name: [start_ms, duration_ms] }
+ • start_ms: Starting position in milliseconds
+ • duration_ms: Length of the sprite in milliseconds
+ • Sprites can be tightly packed or have gaps between them
+
+
+const spriteMap = {
+  jump: [0, 500],       // 0ms - 500ms
+  coin: [500, 300],     // 500ms - 800ms
+  laser: [800, 400],    // 800ms - 1200ms
+  explosion: [1200, 800] // 1200ms - 2000ms
+};
+
+const gameAudio = new Howl({
+  src: 'game-sounds.wav',
+  sprite: spriteMap
+});
+
+gameAudio.play('jump');
+
+
+
+
+ +
+
+

Audio Effects

+
+

Filters

+
+ + + + + + +
+
+ + +
+
+ +
+
+

Rate Control

+
+ + + 1.0x +
+ +
+ +
+

Fade Effects

+
+ + +
+
+
+
+ +
+

Effects API

+ +
+

Effect Methods

+
addEffect(type, options?) → AudioNode
+
effectsArray<AudioNode>
+
updateEffectsChain() → Howl
+
enableEQ(bands?, callback?, boost?) → Howl
+
disableEQ() → Howl
+
+ +
+

Audio Effects

+
+ addEffect(type, options) - Add audio effect to the sound
+ • Returns the Web Audio node for direct manipulation
+ • Types: 'lowpass', 'highpass', 'bandpass', 'notch', 'delay', 'reverb', 'gain', 'distortion'
+ • Options vary by type (frequency, Q, delay, gain, etc.)

+ effects - Getter for effects array (for direct manipulation)
+ updateEffectsChain() - Rebuild audio chain after effects manipulation +
+
+// Add effects and manipulate them
+const filter = howl.addEffect('lowpass', {
+  frequency: 1000, Q: 1
+});
+
+// Direct effects array manipulation
+howl.effects.splice(1, 1); // Remove delay effect
+howl.effects.reverse(); // Reorder effects
+howl.updateEffectsChain(); // Apply changes
+
+
+ +
+

Frequency Analysis

+
+ enableEQ(bands, callback, boost) - Enable real-time frequency analysis
+ • bands: Number of frequency bands (default: 16)
+ • callback: Function called with frequency data array
+ • boost: Gain multiplier for visualization (default: 4.0)

+ disableEQ() - Disable frequency analysis
+
+
+
+
+ +
+
+

3D Spatial Audio

+
+
+
+
+
+
+
+ 🟢 Green is you! + Drag the dots to move them around the 3D space! + 🏛️ Cathedral environment provides realistic reverb. +
+
+ + +
+
+
+ +
+

Spatial API

+ +
+

3D Spatial Audio

+
+ spatial - Object containing 3D audio properties
+ • Properties: position{x,y,z}, orientation{x,y,z}, velocity{x,y,z}
+ • distance{ref, max, rolloff, model}, cone{inner, outer, outerGain}
+ • occlusion, obstruction, panningModel

+ Howler.listener - Global listener (camera/player) object
+ Howler.environment(env) - Set reverb environment
+ • Accepts preset strings: 'room', 'hall', 'cathedral'
+ • Or custom objects: { size, decay, dampening }
+ • Pass null to disable environment reverb +
+
+ +
+

Environment Presets

+
+ Built-in Reverb Environments: 'room', 'hall', 'cathedral', 'cave', 'underwater'

+ + Custom Environment Parameters:
+ • size: 'small' | 'medium' | 'large' - Room size
+ • decay: 0.1-20.0 - Reverb tail length in seconds
+ • dampening: 0.0-1.0 - High frequency absorption (0=bright, 1=muffled) +
+
+ +
+

Spatial Examples

+
+const spatial = new Howl({
+  src: 'footsteps.mp3',
+  spatial: {
+    position: [10, 0, -5],
+    distance: { ref: 2, max: 100 },
+    cone: { inner: 45, outer: 90, outerGain: 0.2 }
+  }
+});
+
+Howler.listener.position = { x: 0, y: 1.8, z: 0 };
+Howler.listener.update();
+
+spatial.spatial.position = { x: enemy.x, y: enemy.y, z: enemy.z };
+spatial.spatial.update();
+
+// Built-in presets
+Howler.environment('room');      // Small room reverb
+Howler.environment('hall');      // Medium hall reverb
+Howler.environment('cathedral'); // Large cathedral reverb
+
+// Custom environment
+Howler.environment({
+  size: 'medium',
+  decay: 3.0,
+  dampening: 0.4
+});
+
+Howler.environment(null); // Disable environment
+
+
+
+
+ +
+
+

Library Info

+
+
Loading library information...
+ +

Supported Codecs:

+
+
+
+ +
+

Events & Utilities

+ +
+

Event Listeners

+
on(event, fn, id?) → Howl
+
off(event?, fn?, id?) → Howl
+
once(event, fn, id?) → Howl
+
+ +
+

Utility Methods

+
playing(id?) → boolean
+
duration(id?) → number
+
state() → string
+
unload() → null
+
+ +
+

Events

+
load() - Audio loaded successfully
+
loaderror(id, error) - Failed to load audio
+
play(id) - Sound started playing
+
pause(id) - Sound paused
+
stop(id) - Sound stopped
+
end(id) - Sound finished playing
+
fade(id) - Fade effect completed
+
volume(id) - Volume changed
+
rate(id) - Playback rate changed
+
seek(id) - Playback position changed
+
mute(id) - Mute state changed
+
unlock() - Audio context unlocked (user interaction)
+
playerror(id, error) - Playback error occurred
+
+
+
+
+ + + + + diff --git a/site/examples/uce-starter/js/u-howler.js b/site/examples/uce-starter/js/u-howler.js new file mode 100755 index 0000000..d322751 --- /dev/null +++ b/site/examples/uce-starter/js/u-howler.js @@ -0,0 +1,1162 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + var exports_obj = factory(); + module.exports = exports_obj; + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + var exports_obj = factory(); + root.Howler = exports_obj.Howler; + root.Howl = exports_obj.Howl; + } +}(typeof self !== 'undefined' ? self : this, function () { + +/** + * based on Howler.js - (c) 2013-2020, James Simpson of GoldFire Studios + * Mostly compatible API + */ + +class HowlerGlobal { + constructor() { + this._counter = 1000; + this._howls = []; + this._volume = 1; + this._muted = false; + this._html5Pool = []; + this._codecs = {}; + this.ctx = null; + this.masterGain = null; + this.usingWebAudio = true; + this.autoUnlock = true; + this._audioUnlocked = false; + this._environment = null; + this._setup(); + } + + volume(vol) { + if (vol === undefined) return this._volume; + this._volume = Math.max(0, Math.min(1, vol)); + + if (!this._muted) { + if (this.usingWebAudio && this.masterGain) { + this.masterGain.gain.setValueAtTime(this._volume, this.ctx.currentTime); + } + this._howls.forEach(howl => howl._updateVolume()); + } + return this; + } + + mute(muted = true) { + this._muted = muted; + let vol = muted ? 0 : this._volume; + + if (this.usingWebAudio && this.masterGain) { + this.masterGain.gain.setValueAtTime(vol, this.ctx.currentTime); + } + this._howls.forEach(howl => howl._updateVolume()); + return this; + } + + stop() { + this._howls.forEach(howl => howl.stop()); + return this; + } + + unload() { + this._howls.slice().forEach(howl => howl.unload()); + if (this.ctx?.close) { + this.ctx.close(); + this.ctx = null; + this._setupAudioContext(); + } + return this; + } + + codecs(ext) { + return this._codecs[ext?.replace(/^x-/, '')]; + } + + _setup() { + this._setupAudioContext(); + this._setupCodecs(); + if (this.autoUnlock) this._setupUnlock(); + } + + _setupAudioContext() { + if (!this.usingWebAudio) return; + + try { + this.ctx = new (window.AudioContext || window.webkitAudioContext)(); + this.masterGain = this.ctx.createGain(); + this.masterGain.connect(this.ctx.destination); + this.masterGain.gain.setValueAtTime(this._muted ? 0 : this._volume, this.ctx.currentTime); + + this.listener = { + position: { x: 0, y: 0, z: 0 }, + orientation: { + forward: { x: 0, y: 0, z: -1 }, + up: { x: 0, y: 1, z: 0 } + }, + velocity: { x: 0, y: 0, z: 0 }, + update: () => this._updateListener() + }; + } catch (e) { + this.usingWebAudio = false; + } + } + + _setupCodecs() { + if (typeof Audio === 'undefined') return; + + let audio = new Audio(); + let test = (mime) => !!audio.canPlayType(mime).replace(/^no$/, ''); + + this._codecs = { + mp3: test('audio/mpeg;'), + opus: test('audio/ogg; codecs="opus"'), + ogg: test('audio/ogg; codecs="vorbis"'), + wav: test('audio/wav; codecs="1"') || test('audio/wav'), + aac: test('audio/aac;'), + m4a: test('audio/x-m4a;') || test('audio/m4a;') || test('audio/aac;'), + mp4: test('audio/x-mp4;') || test('audio/mp4;') || test('audio/aac;'), + webm: test('audio/webm; codecs="vorbis"'), + flac: test('audio/x-flac;') || test('audio/flac;') + }; + } + + _setupUnlock() { + if (this._audioUnlocked || !this.ctx) return; + + let unlock = () => { + if (this._audioUnlocked) return; + + let buffer = this.ctx.createBuffer(1, 1, 22050); + let source = this.ctx.createBufferSource(); + source.buffer = buffer; + source.connect(this.ctx.destination); + source.start(0); + + if (this.ctx.resume) this.ctx.resume(); + + source.onended = () => { + this._audioUnlocked = true; + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('click', unlock, true); + this._howls.forEach(howl => howl._emit('unlock')); + }; + }; + + document.addEventListener('touchstart', unlock, true); + document.addEventListener('click', unlock, true); + } + + _getHtml5Audio() { + return this._html5Pool.pop() || new Audio(); + } + + _releaseHtml5Audio(audio) { + if (audio._unlocked && this._html5Pool.length < 10) { + this._html5Pool.push(audio); + } + } + + environment(env) { + if(!env) + return this._environment; + if (typeof env === 'string') { + let presets = { + room: { size: 'small', decay: 1.5, dampening: 0.8 }, + hall: { size: 'medium', decay: 2.5, dampening: 0.6 }, + cathedral: { size: 'large', decay: 8.0, dampening: 0.1 }, + cave: { size: 'large', decay: 6.0, dampening: 0.2 }, + underwater: { size: 'medium', decay: 3.0, dampening: 0.9 } + }; + this._environment = presets[env] || null; + } else { + this._environment = env; + } + this._howls.forEach(howl => howl._updateEnvironment?.()); + return this; + } + + _updateListener() { + if (!this.ctx?.listener) return; + + let { position, orientation, velocity } = this.listener; + let l = this.ctx.listener; + + if (l.positionX) { + l.positionX.setValueAtTime(position.x, this.ctx.currentTime); + l.positionY.setValueAtTime(position.y, this.ctx.currentTime); + l.positionZ.setValueAtTime(position.z, this.ctx.currentTime); + l.forwardX.setValueAtTime(orientation.forward.x, this.ctx.currentTime); + l.forwardY.setValueAtTime(orientation.forward.y, this.ctx.currentTime); + l.forwardZ.setValueAtTime(orientation.forward.z, this.ctx.currentTime); + l.upX.setValueAtTime(orientation.up.x, this.ctx.currentTime); + l.upY.setValueAtTime(orientation.up.y, this.ctx.currentTime); + l.upZ.setValueAtTime(orientation.up.z, this.ctx.currentTime); + } else { + l.setPosition(position.x, position.y, position.z); + l.setOrientation( + orientation.forward.x, orientation.forward.y, orientation.forward.z, + orientation.up.x, orientation.up.y, orientation.up.z + ); + } + + if (l.setVelocity) { + l.setVelocity(velocity.x, velocity.y, velocity.z); + } + } +} + +class Howl { + constructor(options = {}) { + if(options.url) options.src = options.url; // backwards compat + if(options.urls) options.src = options.urls; // backwards compat + if (!options.src?.length) throw new Error('src required'); + + Object.assign(this, { + _src: Array.isArray(options.src) ? options.src : [options.src], + _volume: options.volume ?? 1, + _rate: options.rate ?? 1, + _loop: options.loop ?? false, + _muted: options.mute ?? false, + _preload: options.preload ?? true, + _autoplay: options.autoplay ?? false, + _sprite: options.sprite ?? {}, + _format: options.format ? (Array.isArray(options.format) ? options.format : [options.format]) : null, + _html5: options.html5 ?? false, + _pool: options.pool ?? 5, + _xhr: { method: 'GET', ...options.xhr }, + _duration: 0, + _state: 'unloaded', + _sounds: [], + _queue: [], + _listeners: {}, + _webAudio: Howler.usingWebAudio && !options.html5, + + _analyzer: null, + _frequencyCallback: null, + _frequencyData: null, + _frequencyBands: options.frequencyBands || 16, + _frequencyBoost: 4.0, + _frequencyEnabled: false, + + _effects: [] + }); + + if (options.spatial) { + this.spatial = { + enabled: true, + position: { x: 0, y: 0, z: 0 }, + orientation: { x: 0, y: 0, z: -1 }, + velocity: { x: 0, y: 0, z: 0 }, + distance: { + ref: 1, + max: 10000, + rolloff: 1, + model: 'inverse' + }, + cone: { + inner: 360, + outer: 360, + outerGain: 0 + }, + occlusion: 0, + obstruction: 0, + panningModel: 'HRTF', + update: () => this._updateSpatial() + }; + + if (Array.isArray(options.spatial.position)) { + let [x, y, z] = options.spatial.position; + Object.assign(this.spatial.position, { x, y, z }); + } + if (options.spatial.distance) { + Object.assign(this.spatial.distance, options.spatial.distance); + } + if (options.spatial.cone) { + Object.assign(this.spatial.cone, options.spatial.cone); + } + if (options.spatial.panningModel) { + this.spatial.panningModel = options.spatial.panningModel; + } + } + + ['load', 'loaderror', 'play', 'pause', 'stop', 'end', 'fade', 'volume', 'rate', 'seek', 'mute', 'unlock'].forEach(event => { + if (options[`on${event}`]) this.on(event, options[`on${event}`]); + }); + + Howler._howls.push(this); + + if (this._autoplay) this._queue.push({ event: 'play', action: () => this.play() }); + if (this._preload) this.load(); + } + + load() { + if (this._state !== 'unloaded') return Promise.resolve(this); + + this._state = 'loading'; + + return new Promise((resolve, reject) => { + let url = this._selectSource(); + if (!url) { + let error = 'No compatible source found'; + this._emit('loaderror', null, error); + return reject(new Error(error)); + } + + this._src = url; + + if (this._webAudio) { + this._loadWebAudio(url).then(resolve).catch(reject); + } else { + this._loadHtml5(url).then(resolve).catch(reject); + } + }); + } + + play(sprite) { + if (typeof sprite === 'number') { + let sound = this._sounds.find(s => s._id === sprite); + return sound ? this._playSound(sound) : null; + } + + if (this._state !== 'loaded') { + if (this._state === 'loading') { + return new Promise(resolve => { + this._queue.push({ event: 'play', action: () => resolve(this.play(sprite)) }); + }); + } + return this.load().then(() => this.play(sprite)); + } + + let sound = this._getSound(); + sound._sprite = sprite || '__default'; + return this._playSound(sound); + } + + pause(id) { + this._getSounds(id).forEach(sound => { + if (!sound._paused) { + sound._paused = true; + if (sound._node) { + if (this._webAudio && sound._source) { + sound._source.stop(); + this._cleanupSound(sound); + } else { + sound._node.pause(); + } + } + this._emit('pause', sound._id); + } + }); + return this; + } + + stop(id) { + this._getSounds(id).forEach(sound => { + sound._paused = true; + sound._ended = true; + if (sound._node) { + if (this._webAudio) { + if (sound._source) { + sound._source.stop(); + } + } else { + sound._node.pause(); + sound._node.currentTime = 0; + } + this._cleanupSound(sound); + } + this._emit('stop', sound._id); + }); + return this; + } + + mute(muted, id) { + if (muted === undefined) return this._muted; + + if (id === undefined) this._muted = muted; + + this._getSounds(id).forEach(sound => { + sound._muted = muted; + this._updateSoundVolume(sound); + this._emit('mute', sound._id); + }); + return this; + } + + volume(vol, id) { + if (vol === undefined) { + let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0]; + return sound ? sound._volume : this._volume; + } + + vol = Math.max(0, Math.min(1, vol)); + if (id === undefined) this._volume = vol; + + this._getSounds(id).forEach(sound => { + sound._volume = vol; + this._updateSoundVolume(sound); + this._emit('volume', sound._id); + }); + return this; + } + + fade(from, to, duration, id) { + this._getSounds(id).forEach(sound => { + sound._volume = from; + this._updateSoundVolume(sound); + + let startTime = Date.now(); + let fadeStep = () => { + let elapsed = Date.now() - startTime; + let progress = Math.min(elapsed / duration, 1); + let currentVol = from + (to - from) * progress; + + sound._volume = currentVol; + this._updateSoundVolume(sound); + + if (progress < 1) { + requestAnimationFrame(fadeStep); + } else { + this._emit('fade', sound._id); + } + }; + + requestAnimationFrame(fadeStep); + }); + return this; + } + + rate(rate, id) { + if (rate === undefined) { + let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0]; + return sound ? sound._rate : this._rate; + } + + if (id === undefined) this._rate = rate; + + this._getSounds(id).forEach(sound => { + sound._rate = rate; + if (sound._node) { + if (this._webAudio && sound._source) { + sound._source.playbackRate.setValueAtTime(rate, Howler.ctx.currentTime); + } else { + sound._node.playbackRate = rate; + } + } + this._emit('rate', sound._id); + }); + return this; + } + + seek(seek, id) { + if (seek === undefined) { + let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0]; + if (!sound) return 0; + + if (this._webAudio) { + let elapsed = sound._playStart ? Howler.ctx.currentTime - sound._playStart : 0; + return sound._seek + elapsed * sound._rate; + } + return sound._node?.currentTime || 0; + } + + this._getSounds(id).forEach(sound => { + let wasPlaying = !sound._paused; + if (wasPlaying) this.pause(sound._id); + + sound._seek = seek; + if (!this._webAudio && sound._node) { + sound._node.currentTime = seek; + } + + if (wasPlaying) this.play(sound._id); + this._emit('seek', sound._id); + }); + return this; + } + + playing(id) { + if (id) { + let sound = this._sounds.find(s => s._id === id); + return sound ? !sound._paused : false; + } + return this._sounds.some(s => !s._paused); + } + + duration(id) { + if (id) { + let sound = this._sounds.find(s => s._id === id); + if (sound && this._sprite[sound._sprite]) { + return this._sprite[sound._sprite][1] / 1000; + } + } + return this._duration; + } + + state() { + return this._state; + } + + unload() { + this.stop(); + this._stopFrequencyAnalysis(); + Howler._howls.splice(Howler._howls.indexOf(this), 1); + this._sounds = []; + this._state = 'unloaded'; + return null; + } + + enableEQ(bands = 16, callback, boost = 4.0) { + if (!this._webAudio) { + console.warn('Frequency analysis requires Web Audio API'); + return this; + } + + this._frequencyCallback = callback; + this._frequencyBands = bands; + this._frequencyBoost = boost; + this._frequencyEnabled = true; + + if (!this._analyzer) { + this._analyzer = Howler.ctx.createAnalyser(); + this._analyzer.fftSize = 2048; + this._analyzer.smoothingTimeConstant = 0.3; + this._frequencyData = new Uint8Array(this._analyzer.frequencyBinCount); + this._analyzer.connect(Howler.masterGain); + } + + this._rebuildEffectChain(); + this._startFrequencyAnalysis(); + return this; + } + + disableEQ() { + this._frequencyEnabled = false; + this._stopFrequencyAnalysis(); + this._rebuildEffectChain(); + return this; + } + + getFrequencyData() { + if (!this._analyzer || !this._frequencyEnabled) return null; + + this._analyzer.getByteFrequencyData(this._frequencyData); + + let bands = new Array(this._frequencyBands); + let nyquist = Howler.ctx.sampleRate / 2; + let dataLength = this._frequencyData.length; + + let minFreq = 20; + let maxFreq = nyquist; + let logMin = Math.log(minFreq); + let logMax = Math.log(maxFreq); + let logRange = logMax - logMin; + + for (let i = 0; i < this._frequencyBands; i++) { + // Logarithmic frequency distribution + let logStart = logMin + (i / this._frequencyBands) * logRange; + let logEnd = logMin + ((i + 1) / this._frequencyBands) * logRange; + + let startFreq = Math.exp(logStart); + let endFreq = Math.exp(logEnd); + + let startBin = Math.floor((startFreq / nyquist) * dataLength); + let endBin = Math.ceil((endFreq / nyquist) * dataLength); + + let sum = 0; + let count = 0; + for (let j = startBin; j < Math.min(endBin, dataLength); j++) { + sum += this._frequencyData[j]; + count++; + } + + bands[i] = count > 0 ? (sum / count / 255) : 0; // Normalize to 0-1 + } + + for (let i = 0; i < bands.length; i++) { + bands[i] = Math.min(1.0, bands[i] * this._frequencyBoost); + } + + return bands; + } + + _startFrequencyAnalysis() { + if (!this._frequencyEnabled || this._frequencyAnimationId) return; + + let analyze = () => { + if (!this._frequencyEnabled) return; + + let frequencyData = this.getFrequencyData(); + if (frequencyData && this._frequencyCallback) { + this._frequencyCallback(frequencyData); + } + + this._frequencyAnimationId = requestAnimationFrame(analyze); + }; + + analyze(); + } + + _stopFrequencyAnalysis() { + if (this._frequencyAnimationId) { + cancelAnimationFrame(this._frequencyAnimationId); + this._frequencyAnimationId = null; + } + } + + addEffect(type, options = {}) { + if (!this._webAudio) { + console.warn('Audio effects require Web Audio API'); + return null; + } + + let effect; + + switch (type.toLowerCase()) { + case 'lowpass': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'lowpass'; + effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime); + break; + + case 'highpass': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'highpass'; + effect.frequency.setValueAtTime(options.frequency || 300, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime); + break; + + case 'bandpass': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'bandpass'; + effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime); + break; + + case 'notch': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'notch'; + effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 30, Howler.ctx.currentTime); + break; + + case 'delay': + effect = Howler.ctx.createDelay(options.maxDelay || 1); + effect.delayTime.setValueAtTime(options.delay || 0.3, Howler.ctx.currentTime); + break; + + case 'reverb': + effect = Howler.ctx.createConvolver(); + let length = Howler.ctx.sampleRate * (options.duration || 2); + let impulse = Howler.ctx.createBuffer(2, length, Howler.ctx.sampleRate); + let decay = options.decay || 2; + + for (let channel = 0; channel < 2; channel++) { + let channelData = impulse.getChannelData(channel); + for (let i = 0; i < length; i++) { + channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay); + } + } + effect.buffer = impulse; + break; + + case 'gain': + effect = Howler.ctx.createGain(); + effect.gain.setValueAtTime(options.gain || 1, Howler.ctx.currentTime); + break; + + case 'distortion': + effect = Howler.ctx.createWaveShaper(); + let samples = 44100; + let curve = new Float32Array(samples); + let amount = options.amount || 50; + for (let i = 0; i < samples; i++) { + let x = (i * 2) / samples - 1; + curve[i] = ((3 + amount) * x * 20 * Math.PI / 180) / (Math.PI + amount * Math.abs(x)); + } + effect.curve = curve; + effect.oversample = options.oversample || '4x'; + break; + + default: + console.warn(`Unknown effect type: ${type}`); + return null; + } + + if (effect) { + this._effects.push(effect); + this._rebuildEffectChain(); + } + + return effect; + } + + get effects() { + return this._effects; + } + + updateEffectsChain() { + this._rebuildEffectChain(); + return this; + } + + _rebuildEffectChain() { + this._sounds.forEach(sound => { + if (sound._node && this._webAudio) { + let startNode = sound._panner || sound._node; + this._connectSoundToChain(sound, startNode); + } + }); + } + + _connectSoundToChain(sound, startNode = null) { + if (!sound._node || !this._webAudio) return; + + let sourceNode = startNode || sound._node; + sourceNode.disconnect(); + + let currentNode = sourceNode; + + this._effects.forEach(effect => { + currentNode.connect(effect); + currentNode = effect; + }); + + if (this._environmentReverb && sound._panner) { + this._ensureEnvironmentSend(sound); + currentNode.connect(sound._environmentSend); + sound._environmentSend.connect(this._environmentReverb); + } + + if (this._analyzer && this._frequencyEnabled) { + currentNode.connect(this._analyzer); + sound._analyzerConnected = true; + } else { + currentNode.connect(Howler.masterGain); + sound._analyzerConnected = false; + } + } + + _ensureEnvironmentSend(sound) { + if (!sound._environmentSend) { + sound._environmentSend = Howler.ctx.createGain(); + sound._environmentSend.gain.setValueAtTime(0.7, Howler.ctx.currentTime); + } + } + + on(event, fn, id) { + (this._listeners[event] = this._listeners[event] || []).push({ fn, id }); + return this; + } + + off(event, fn, id) { + let listeners = this._listeners[event]; + if (!listeners) return this; + + if (!fn && !id) { + this._listeners[event] = []; + } else { + this._listeners[event] = listeners.filter(l => + (fn && l.fn !== fn) || (id && l.id !== id) + ); + } + return this; + } + + once(event, fn, id) { + let onceFn = (...args) => { + fn(...args); + this.off(event, onceFn, id); + }; + return this.on(event, onceFn, id); + } + + _selectSource() { + for (let src of this._src) { + let ext = this._format?.[this._src.indexOf(src)] || + src.match(/\.([^.?]+)(\?|$)/)?.[1] || + src.match(/^data:audio\/([^;,]+)/)?.[1]; + + if (ext && Howler.codecs(ext)) return src; + } + return null; + } + + async _loadWebAudio(url) { + try { + let arrayBuffer; + + if (url.startsWith('data:')) { + let data = atob(url.split(',')[1]); + arrayBuffer = new Uint8Array(data.length); + for (let i = 0; i < data.length; i++) { + arrayBuffer[i] = data.charCodeAt(i); + } + arrayBuffer = arrayBuffer.buffer; + } else { + let response = await fetch(url, this._xhr); + arrayBuffer = await response.arrayBuffer(); + } + + let audioBuffer = await Howler.ctx.decodeAudioData(arrayBuffer); + this._buffer = audioBuffer; + this._duration = audioBuffer.duration; + this._finishLoad(); + return this; + } catch (error) { + this._emit('loaderror', null, error.message); + throw error; + } + } + + async _loadHtml5(url) { + return new Promise((resolve, reject) => { + let audio = Howler._getHtml5Audio(); + + let onLoad = () => { + this._duration = Math.ceil(audio.duration * 10) / 10; + this._finishLoad(); + audio.removeEventListener('canplaythrough', onLoad); + audio.removeEventListener('error', onError); + resolve(this); + }; + + let onError = () => { + let error = `Failed to load: ${url}`; + audio.removeEventListener('canplaythrough', onLoad); + audio.removeEventListener('error', onError); + this._emit('loaderror', null, error); + reject(new Error(error)); + }; + + audio.addEventListener('canplaythrough', onLoad); + audio.addEventListener('error', onError); + audio.src = url; + audio.load(); + }); + } + + _finishLoad() { + if (!Object.keys(this._sprite).length) { + this._sprite = { __default: [0, this._duration * 1000] }; + } + + this._state = 'loaded'; + this._emit('load'); + this._processQueue(); + } + + _processQueue() { + if (this._queue.length) { + let { action } = this._queue.shift(); + action(); + this._processQueue(); + } + } + + _getSound() { + let sound = this._sounds.find(s => s._ended); + + if (!sound) { + if (this._sounds.length >= this._pool) { + return this._sounds.find(s => s._paused) || this._sounds[0]; + } + sound = this._createSound(); + } + + return this._resetSound(sound); + } + + _createSound() { + let sound = { + _id: ++Howler._counter, + _volume: this._volume, + _rate: this._rate, + _seek: 0, + _paused: true, + _ended: true, + _muted: this._muted, + _sprite: '__default' + }; + + this._sounds.push(sound); + this._createSoundNode(sound); + return sound; + } + + _resetSound(sound) { + Object.assign(sound, { + _volume: this._volume, + _rate: this._rate, + _seek: 0, + _paused: true, + _ended: true, + _muted: this._muted, + _sprite: '__default' + }); + return sound; + } + + _createSoundNode(sound) { + if (this._webAudio) { + sound._node = Howler.ctx.createGain(); + + if (this.spatial?.enabled) { + sound._panner = Howler.ctx.createPanner(); + // Set the panning model (this doesn't change dynamically) + sound._panner.panningModel = this.spatial.panningModel; + sound._node.connect(sound._panner); + this._updateSoundSpatial(sound); + + if (Howler._environment) { + this._updateEnvironment(); + } + + this._connectSoundToChain(sound, sound._panner); + } else { + this._connectSoundToChain(sound); + } + } else { + sound._node = Howler._getHtml5Audio(); + sound._node.src = this._src; + } + } + + _playSound(sound) { + if (!sound || this._state !== 'loaded') return null; + + let sprite = this._sprite[sound._sprite]; + if (!sprite) return null; + + let [start, duration] = sprite; + let seek = Math.max(0, sound._seek || start / 1000); + + sound._paused = false; + sound._ended = false; + + if (this._webAudio) { + this._playWebAudio(sound, seek, duration / 1000); + } else { + this._playHtml5(sound, seek); + } + + this._emit('play', sound._id); + return sound._id; + } + + _playWebAudio(sound, seek, duration) { + this._cleanupSound(sound); + + sound._source = Howler.ctx.createBufferSource(); + sound._source.buffer = this._buffer; + sound._source.connect(sound._node); + + sound._source.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime); + sound._playStart = Howler.ctx.currentTime; + + if (sound._loop) { + sound._source.loop = true; + sound._source.start(0, seek); + } else { + // Fix: duration is the sprite duration, not total audio duration + sound._source.start(0, seek, duration); + sound._source.onended = () => this._onSoundEnd(sound); + } + + this._updateSoundVolume(sound); + } + + _playHtml5(sound, seek) { + let { _node: node } = sound; + node.currentTime = seek; + node.volume = this._getEffectiveVolume(sound); + node.playbackRate = sound._rate; + node.muted = sound._muted || this._muted || Howler._muted; + + let promise = node.play(); + if (promise) { + promise.catch(() => this._emit('playerror', sound._id, 'Playback failed')); + } + + if (!sound._loop) { + node.onended = () => this._onSoundEnd(sound); + } + } + + _onSoundEnd(sound) { + sound._paused = true; + sound._ended = true; + this._emit('end', sound._id); + + if (sound._loop) { + sound._ended = false; + this._playSound(sound); + } + } + + _updateSoundVolume(sound) { + if (!sound._node) return; + + let volume = this._getEffectiveVolume(sound); + + if (this._webAudio) { + sound._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + } else { + sound._node.volume = volume; + } + } + + _getEffectiveVolume(sound) { + if (sound._muted || this._muted || Howler._muted) return 0; + return sound._volume * this._volume * Howler._volume; + } + + _updateVolume() { + this._sounds.forEach(sound => this._updateSoundVolume(sound)); + } + + _cleanupSound(sound) { + if (sound._source) { + sound._source.disconnect(); + sound._source = null; + } + if (sound._occlusionFilter) { + sound._occlusionFilter.disconnect(); + sound._occlusionFilter = null; + } + } + + _getSounds(id) { + return id ? this._sounds.filter(s => s._id === id) : this._sounds; + } + + _emit(event, id, data) { + let listeners = this._listeners[event]; + if (!listeners) return; + + listeners.forEach(({ fn, id: listenerId }) => { + if (!listenerId || listenerId === id) { + setTimeout(() => fn(id, data), 0); + } + }); + } + + _updateSpatial() { + if (!this.spatial?.enabled) return; + + this._sounds.forEach(sound => { + if (sound._panner) { + this._updateSoundSpatial(sound); + } + }); + } + + _updateSoundSpatial(sound) { + if (!sound._panner || !this.spatial?.enabled) return; + + let { position, orientation, velocity, distance, cone, occlusion, obstruction } = this.spatial; + let p = sound._panner; + + if (p.positionX) { + p.positionX.setValueAtTime(position.x, Howler.ctx.currentTime); + p.positionY.setValueAtTime(position.y, Howler.ctx.currentTime); + p.positionZ.setValueAtTime(position.z, Howler.ctx.currentTime); + p.orientationX.setValueAtTime(orientation.x, Howler.ctx.currentTime); + p.orientationY.setValueAtTime(orientation.y, Howler.ctx.currentTime); + p.orientationZ.setValueAtTime(orientation.z, Howler.ctx.currentTime); + } else { + p.setPosition(position.x, position.y, position.z); + p.setOrientation(orientation.x, orientation.y, orientation.z); + } + + if (p.setVelocity && velocity) { + p.setVelocity(velocity.x, velocity.y, velocity.z); + } + + p.refDistance = distance.ref; + p.maxDistance = distance.max; + p.rolloffFactor = distance.rolloff; + p.distanceModel = distance.model; + + p.coneInnerAngle = cone.inner; + p.coneOuterAngle = cone.outer; + p.coneOuterGain = cone.outerGain; + + this._applyOcclusion(sound, occlusion || 0, obstruction || 0); + } + + _applyOcclusion(sound, occlusion, obstruction) { + if (!sound._occlusionFilter && (occlusion > 0 || obstruction > 0)) { + sound._occlusionFilter = Howler.ctx.createBiquadFilter(); + sound._occlusionFilter.type = 'lowpass'; + sound._node.disconnect(); + sound._node.connect(sound._occlusionFilter); + sound._occlusionFilter.connect(sound._panner || Howler.masterGain); + } + + if (sound._occlusionFilter) { + let freq = 20000 * (1 - Math.max(occlusion, obstruction) * 0.9); + let gain = 1 - occlusion * 0.8; + sound._occlusionFilter.frequency.setValueAtTime(freq, Howler.ctx.currentTime); + sound._node.gain.setValueAtTime(gain * this._getEffectiveVolume(sound), Howler.ctx.currentTime); + } + } + + _updateEnvironment() { + if (!Howler._environment || !this._webAudio) return; + + if (!this._environmentReverb) { + this._environmentReverb = Howler.ctx.createConvolver(); + let { size, decay, dampening } = Howler._environment; + let length = Howler.ctx.sampleRate * (size === 'large' ? 6 : size === 'medium' ? 3 : 1.5); + let impulse = Howler.ctx.createBuffer(2, length, Howler.ctx.sampleRate); + + for (let channel = 0; channel < 2; channel++) { + let channelData = impulse.getChannelData(channel); + for (let i = 0; i < length; i++) { + let progress = i / length; + let sample = 0; + + if (i < Howler.ctx.sampleRate * 0.2) { + for (let j = 0; j < 8; j++) { + let delay = (j + 1) * 0.02 * Howler.ctx.sampleRate; + if (Math.abs(i - delay) < 100) { + sample += (Math.random() * 2 - 1) * 0.3 * Math.exp(-j * 0.5); + } + } + } + + let envelope = Math.pow(1 - progress, decay); + let reverb = (Math.random() * 2 - 1) * envelope; + sample += reverb * (1 - dampening + Math.random() * dampening * 0.3); + + if (size === 'large') { + let lowFreq = Math.sin(i * 0.01) * envelope * 0.2; + sample += lowFreq; + } + + channelData[i] = Math.tanh(sample * 0.8); + } + } + this._environmentReverb.buffer = impulse; + + this._environmentGain = Howler.ctx.createGain(); + this._environmentGain.gain.setValueAtTime(0.8, Howler.ctx.currentTime); + this._environmentReverb.connect(this._environmentGain); + this._environmentGain.connect(Howler.masterGain); + } + + this._rebuildEffectChain(); + } +} + +let Howler = new HowlerGlobal(); + +return { Howler, Howl }; + +})); diff --git a/site/examples/uce-starter/js/u-macrobars-demo.html b/site/examples/uce-starter/js/u-macrobars-demo.html new file mode 100755 index 0000000..63dc607 --- /dev/null +++ b/site/examples/uce-starter/js/u-macrobars-demo.html @@ -0,0 +1,977 @@ + + + + + + U-Macrobars.js Demo + + + +
+

U-Macrobars.js Demo

+ +
+
+

Basic Field Output

+
+
{{name}} is {{age}} years old and works as {{job or "unemployed"}}
+
+
+ + +
+
+ +
+

Number Formatting

+
Price: ${{%price}} | Large Number: {{~bigNumber}}
+
+
+ + +
+
+ +
+

Default Values & Safety

+
{{username or "Guest"}} | {{profile.bio or "No bio available"}}
+
+
+ + +
+
+
+ +
+

Field Output API

+ +
+

Basic Syntax

+
{{field}}Safe HTML output
+
{{{field}}}Raw HTML output
+
{{:variable}}Direct variable
+
{{field or "default"}}With fallback
+
+ +
+

Number Formatting

+
{{%number}}2 decimal places
+
{{~number}}Rounded (1k, 1M)
+
+ +
+

Examples

+
+// Basic field output
+const template = Macrobars.compile('{{name}}');
+const result = template({name: 'John'});
+
+// With defaults
+const withDefault = '{{title or "Untitled"}}';
+
+// Number formatting
+const price = '${{%cost}}'; // $12.34
+const count = '{{~views}}';  // 1.2k
+
+
+
+
+ +
+
+

Control Structures

+
+

Conditionals

+
{{#if isLoggedIn}} +Welcome back, {{username}}! +{{#else}} +Please log in to continue. +{{/if}}
+
+
+ + +
+
+ +
+

Loops & Iteration

+
{{#each items}} +• {{number}}. {{name}} - ${{price}} +{{/each}}
+
+
+ + +
+
+ +
+

Equality & Lookup

+
{{#eq status "active"}}User is active{{/eq}} +{{lookup user "permissions"}}
+
+
+ + +
+
+
+ +
+

Control Flow API

+ +
+

Conditionals

+
{{#if condition}}...{{/if}}
+
{{#else}}Alternative branch
+
+ Conditional rendering based on truthy values. Supports nested conditions. +
+
+ +
+

Loops

+
{{#each items}}...{{/each}}
+
{{#each items as item}}...{{/each}}
+
+ • Standard: data becomes current item
+ • Named: item becomes current item, data preserved +
+
+ +
+

Comparison & Lookup

+
{{#eq val1 val2}}...{{/eq}}
+
{{eq val1 val2}}"true" or ""
+
{{#lookup obj key}}...{{/lookup}}
+
{{lookup obj key}}value or ""
+
+ +
+

Examples

+
+// Conditionals
+'{{#if user.isAdmin}}Admin Panel{{/if}}'
+
+// Named loops
+'{{#each products as product}}'
+  '{{product.name}} - {{data.storeName}}'
+'{{/each}}'
+
+// Equality & lookup
+'{{#eq user.role "admin"}}Secret{{/eq}}'
+'{{lookup config "theme"}}'
+
+
+
+
+ +
+
+

Advanced Features

+
+

Event Binding

+
<button {{@click="handleClick"}}>Click Count: {{clickCount}}</button>
+
+
+ + +
+
+ +
+

Code Blocks

+
<script>var computed = data.value * 2;</script> +Result: {{:computed}}
+
+
+ + +
+
+ +
+

Components

+
{{#component userCard}}
+
+
+ + +
+
+
+ +
+

Advanced API

+ +
+

Event Binding

+
{{@event="handler"}}DOM attribute
+
template.renderTo(container, data)
+
+ Events are automatically bound when using renderTo(). Handler can reference data properties or global functions. +
+
+ +
+

Code Execution

+
<script>...</script>
+
<defer>...</defer>
+
<? code ?>
+
<?= expression ?>
+
+ +
+

Components & Compilation

+
Macrobars.compile(template, options)
+
Macrobars.createComponents(definitions)
+
+ +
+

Examples

+
+// Event binding with renderTo
+const template = Macrobars.compile(
+  '<button {{@click="increment"}}>{{count}}</button>'
+);
+template.renderTo('#container', {
+  count: 0,
+  increment: function() { this.count++; }
+});
+
+// Components
+const components = Macrobars.createComponents({
+  userCard: '<div>{{name}} - {{email}}</div>'
+});
+
+
+
+
+ +
+
+

Template Editor

+
+ + + + + + + +
+ + + + +
+ +
Click "Render Template"
+ +
+

Template Information

+
Ready.
+
+ +
+
+ +
+

Compilation & Debugging

+ +
+

Compilation Options

+
decimals: numberNumber precision
+
strict: booleanStrict mode
+
components: objectReusable templates
+
+ +
+

Debugging Properties

+
template.tokensParsed tokens
+
template.gensourceGenerated JS
+
template.event_bindingsEvent data
+
+ +
+

Utility Functions

+
Macrobars.safe_out(text, default)
+
Macrobars.num_out(number, decimals)
+
Macrobars.num_out_round(number)
+
+ +
+

Example Templates

+
+// Complete example
+const template = Macrobars.compile(`
+<div class="user-card">
+  <h3>{{name or "Unknown User"}}</h3>
+  {{#if profile.verified}}✅ Verified{{/if}}
+  <p>Balance: ${{%balance}}</p>
+  {{#each achievements}}
+    <span class="badge">{{data}}</span>
+  {{/each}}
+</div>
+`, { decimals: 2 });
+
+const result = template(userData);
+
+
+
+
+
+ + + + + diff --git a/site/examples/uce-starter/js/u-macrobars.js b/site/examples/uce-starter/js/u-macrobars.js new file mode 100755 index 0000000..b09b46d --- /dev/null +++ b/site/examples/uce-starter/js/u-macrobars.js @@ -0,0 +1,411 @@ +(function (root, factory) { // I really hate this convoluted bullshit + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.Macrobars = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + + if(!String.prototype.replaceAll) String.prototype.replaceAll = function(search, replacement) { + var target = this; + return target.split(search).join(replacement); + }; + + var safe_out = (s, defaultValue = '') => { + if(typeof s == 'undefined' || s === null || s === false || s === '') s = defaultValue; + return((''+s).replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')); + }; + + var num_out = (n, decimals = 2) => { + if(typeof n == 'undefined' || n === false) n = 0; + if(decimals == 0) return(Math.round(n)); + return(n.toFixed(decimals)); + } + + var num_out_round = (n) => { + if(typeof n == 'undefined' || n === false || n === null) n = 0; + + if(typeof n === 'string') { + n = parseFloat(n); + if(isNaN(n)) return '0'; + } + + if(Math.abs(n) >= 1e15) { + return n.toExponential(2); + } + + if(Math.abs(n) > 0 && Math.abs(n) < 0.001) { + return n.toExponential(2); + } + + if(Math.abs(n) >= 1e12) { + return (n/1e12).toFixed(1) + 'T'; + } else if(Math.abs(n) >= 1e9) { + return (n/1e9).toFixed(1) + 'B'; + } else if(Math.abs(n) >= 1e6) { + return (n/1e6).toFixed(1) + 'M'; + } else if(Math.abs(n) >= 1e3) { + return (n/1e3).toFixed(1) + 'k'; + } else if(Math.abs(n) >= 1) { + return Math.round(n); + } else if(Math.abs(n) >= 0.01) { + return n.toFixed(2); + } else if(Math.abs(n) >= 0.001) { + return n.toFixed(3); + } else if(n === 0) { + return 0; + } + + return n.toString(); + } + + var debug_out = (ee) => { + var es = ee.stack.split("\n"); + var loc = es[1]; + var p0 = loc.indexOf(''); + if(p0 != -1) { + loc = loc.substr(p0+(''.length+1)); + loc = loc.substr(0, loc.length-1); + } + return('Macrobars '+es[0]+' ('+loc+')'); + } + + var signposts = [ + { start : '', type : 'code' }, + { start : '', end : '', type : 'defer' }, + + { start : '', type : 'field' }, + { start : '', type : 'var_out' }, + { start : '', type : 'code' }, + { start : '', type : 'code' }, + + { start : '{{:', end : '}}', type : 'var_out' }, + { start : '{{#eq ', end : '}}', type : 'eq_block_start' }, + { start : '{{#lookup ', end : '}}', type : 'lookup_block_start' }, + { start : '{{eq ', end : '}}', type : 'eq_inline' }, + { start : '{{lookup ', end : '}}', type : 'lookup_inline' }, + { start : '{{#default ', end : '}}', type : 'default_empty' }, + { start : '{{#if ', end : '}}', type : 'if_start' }, + { start : '{{#else}}', end : '', type : 'else' }, + { start : '{{#component ', end : '}}', type : 'component' }, + { start : '{{#each ', end : '}}', type : 'each_start' }, + { start : '{{/each}}', end : '', type : 'each_end' }, + { start : '{{/if}}', end : '', type : 'if_end' }, + { start : '{{/eq}}', end : '', type : 'eq_block_end' }, + { start : '{{/lookup}}', end : '', type : 'lookup_block_end' }, + { start : '{{/', end : '}}', type : 'block_end' }, + { start : '{{@', end : '}}', type : 'event_bind' }, + { start : '{{{', end : '}}}', type : 'field_unsafe' }, + { start : '{{%', end : '}}', type : 'field_number' }, + { start : '{{~', end : '}}', type : 'field_number_round' }, + { start : '{{', end : '}}', type : 'field' }, + ]; + + if(!each) function each(o, f) { + if(!o) + return; + if(o.forEach) { + o.forEach(f); + } else { + for(var prop in o) if(o.hasOwnProperty(prop)) { + f(o[prop], prop); + } + } + } + + var data_prefix = (identifier) => { + if(identifier.substr(0, 1) == ':') + return(identifier.substr(1)); + else + return('data.'+identifier); + } + + var parse_field_with_default = (fieldText) => { + var orMatch = fieldText.match(/^(.+?)\s+or\s+(['"])(.*?)\2$/); + if (orMatch) { + return { + field: orMatch[1].trim(), + default: orMatch[3] + }; + } + return { + field: fieldText.trim(), + default: null + }; + } + + var compile = function(text, options = {}) { + + var crash_counter = 0; + var tokens = []; + var gensource = []; + var event_bindings = []; + var components = options.components || {}; + var condition_stack = []; + + var emit = { + text : (token) => { + gensource.push('output += '+JSON.stringify(token.text)+';'); + }, + code : (token) => { + gensource.push(token.text); + }, + defer : (token) => { + gensource.push('output += "";'); + }, + var_out : (token) => { + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out('+(token.text)+', '+defaultVal+');'); + }, + field : (token) => { + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out('+data_prefix(token.text)+', '+defaultVal+');'); + }, + field_unsafe : (token) => { + if (token.default) { + gensource.push('output += ('+data_prefix(token.text)+' || '+JSON.stringify(token.default)+');'); + } else { + gensource.push('output += ('+data_prefix(token.text)+' || default_empty_field);'); + } + }, + field_number : (token) => { + gensource.push('output += num_out('+data_prefix(token.text)+', this.decimals);'); + }, + field_number_round : (token) => { + gensource.push('output += num_out_round('+data_prefix(token.text)+');'); + }, + each_start : (token) => { + var parts = token.text.split(' as '); + var collection = parts[0].trim(); + var itemName = parts.length > 1 ? parts[1].trim() : 'data'; + + gensource.push('each('+data_prefix(collection)+', ('+itemName+', index) => {'); + + }, + if_start : (token) => { + condition_stack.push('if'); + gensource.push('if ('+data_prefix(token.text)+') {'); + }, + else : (token) => { + gensource.push('} else {'); + }, + if_end : (token) => { + condition_stack.pop(); + gensource.push('} /* if end */'); + }, + each_end : (token) => { + gensource.push('}); /* each end */'); + }, + block_end : (token) => { + var block_type = condition_stack.pop(); + gensource.push('}); /* '+(block_type || 'each')+' end */'); + }, + component : (token) => { + var comp_name = token.text.trim(); + if (components[comp_name]) { + gensource.push('output += components['+JSON.stringify(comp_name)+'](data);'); + } else { + gensource.push('output += "[Component '+comp_name+' not found]";'); + } + }, + event_bind : (token) => { + // Parse event binding: @click="functionName" or @click="data.handler" + var parts = token.text.split('='); + if (parts.length === 2) { + var event_type = parts[0].trim(); + var handler_ref = parts[1].trim().replace(/"/g, ''); + var binding_id = 'mb_bind_' + event_bindings.length; + event_bindings.push({ + id: binding_id, + event: event_type, + handler: handler_ref + }); + gensource.push('output += " data-mb-bind=\\"'+binding_id+'\\"";'); + } + }, + eq_block_start : (token) => { + // Parse "value1 value2" for equality comparison block + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var val1 = parts[0].startsWith('"') || parts[0].startsWith("'") ? parts[0] : data_prefix(parts[0]); + var val2 = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + condition_stack.push('eq'); + gensource.push('if ('+val1+' == '+val2+') {'); + } + }, + lookup_block_start : (token) => { + // Parse "object key" for dynamic property lookup block (checks if property exists and is truthy) + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var obj = data_prefix(parts[0]); + var key = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + condition_stack.push('lookup'); + gensource.push('if ('+obj+' && '+obj+'['+key+']) {'); + } + }, + eq_inline : (token) => { + // Parse "value1 value2" for equality comparison - outputs result directly + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var val1 = parts[0].startsWith('"') || parts[0].startsWith("'") ? parts[0] : data_prefix(parts[0]); + var val2 = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out(('+val1+' == '+val2+') ? "true" : "", '+defaultVal+');'); + } + }, + lookup_inline : (token) => { + // Parse "object key" for dynamic property lookup - outputs result directly + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var obj = data_prefix(parts[0]); + var key = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out(('+obj+' && '+obj+'['+key+']) || "", '+defaultVal+');'); + } + }, + eq_block_end : (token) => { + condition_stack.pop(); + gensource.push('} /* eq end */'); + }, + lookup_block_end : (token) => { + condition_stack.pop(); + gensource.push('} /* lookup end */'); + }, + default_empty : (token) => { + gensource.push('default_empty_field = '+JSON.stringify(token.text)+';'); + }, + } + + while(text != '' && crash_counter < 100) { + + crash_counter+=1; + var p0 = -1; + var sp_found = false; + signposts.forEach((sp) => { + var ps = text.indexOf(sp.start); + if(ps != -1 && (p0 == -1 || p0 > ps)) { + sp_found = sp; + p0 = ps; + } + }); + + if(sp_found) + { + tokens.push({ type : 'text', text : text.substr(0, p0) }); + text = text.substr(p0 + sp_found.start.length); + var p1 = text.indexOf(sp_found.end); + if(p1 == -1) p1 = text.length; + + var tokenText = text.substr(0, p1); + var token = { type : sp_found.type, text : tokenText }; + + if (sp_found.type === 'field' || sp_found.type === 'var_out' || + sp_found.type === 'field_unsafe' || sp_found.type === 'field_number' || + sp_found.type === 'field_number_round') { + var parsed = parse_field_with_default(tokenText); + token.text = parsed.field; + token.default = parsed.default; + } + + tokens.push(token); + text = text.substr(p1+sp_found.end.length); + } + else + { + tokens.push({ type : 'text', text : text }); + text = ''; + } + + } + + if(typeof options.decimals == 'undefined') + options.decimals = 2; + this.decimals = options.decimals; + + gensource.push('(data) => { '); + if(options.strict) + gensource.push('"use strict";'); + gensource.push(' try {'); + gensource.push(' if(!data) data = {}; var data_root = data;'); + gensource.push(' var output = ""; var default_empty_field = "";'); + gensource.push(' var echo = (s) => { output += s; };'); + tokens.forEach((tok) => { + emit[tok.type](tok); + }); + gensource.push(' } catch (ee) { console.error(ee); output += debug_out(ee); }'); + gensource.push(' return(output);'); + gensource.push('}'); + + var f = {}; + try { + f = eval('('+gensource.join("\n")+')'); + } catch(ce) { + console.error('Macrobars compilation error:', ce); + console.error('Template source:', text.substring(0, 200) + (text.length > 200 ? '...' : '')); + console.error('Generated JavaScript:', gensource.join("\n")); + } + + f.tokens = tokens; + f.gensource = gensource; + f.event_bindings = event_bindings; + f.options = options; + f.components = components; + + f.renderTo = function(container_or_query, data) { + var container; + if (typeof container_or_query === 'string') { + container = document.querySelector(container_or_query); + if (!container) { + return null; + } + } else { + container = container_or_query; + } + + try { + var html = f(data); + container.innerHTML = html; + event_bindings.forEach(binding => { + var elements = container.querySelectorAll('[data-mb-bind="' + binding.id + '"]'); + elements.forEach(element => { + var handler = data[binding.handler] || eval(binding.handler); + if (typeof handler === 'function') { + element.addEventListener(binding.event, handler); + } + }); + }); + } catch(renderError) { + console.error('Macrobars renderTo error:', renderError); + console.error('Data passed to template:', data); + throw renderError; + } + + return container; + }; + + return(f); + } + + return({ + + compile : compile, + + createComponents : function(definitions) { + var components = {}; + each(definitions, (template, name) => { + components[name] = compile(template); + }); + return components; + }, + + num_out : num_out, + safe_out : safe_out, + num_out_round : num_out_round, + debug_out : debug_out, + each : each, + + }); + +})); diff --git a/site/examples/uce-starter/js/u-macrobars.md b/site/examples/uce-starter/js/u-macrobars.md new file mode 100755 index 0000000..191aedb --- /dev/null +++ b/site/examples/uce-starter/js/u-macrobars.md @@ -0,0 +1,137 @@ +# macrobars.js + +A lightweight JavaScript templating engine with PHP-style syntax and Handlebars-inspired features. + +## Basic Usage + +```javascript +const template = Macrobars.compile('

{{title}}

{{content}}

'); +const html = template({title: 'Hello', content: 'World'}); +``` + +## Field Output + +```javascript +{{field}} // Safe HTML output (escaped) +{{{field}}} // Unsafe HTML output (raw) +{{field or "default"}} // With default value +{{:variable}} // Direct variable (no data. prefix) +``` + +## Numbers + +```javascript +{{%number}} // Formatted number (2 decimals) +{{~number}} // Rounded number (1k, 1M format) +``` + +## Control Structures + +### Conditionals +```javascript +{{#if condition}} + Content when true +{{#else}} + Content when false +{{/if}} +``` + +### Equality Blocks +```javascript +{{#eq value1 value2}} + Content when equal +{{/eq}} + +{{eq value1 value2}} // Inline: outputs "true" or "" +``` + +### Loops +```javascript +{{#each items}} + {{name}} - {{index}} +{{/each}} + +{{#each items as item}} + {{item.name}} +{{/each}} +``` + +### Property Lookup +```javascript +{{#lookup object key}} + {{value}} exists +{{/lookup}} + +{{lookup object key}} // Inline: outputs value or "" +``` + +## Code Blocks + +```javascript + + + + // Deferred JavaScript execution + + + // PHP-style output + // PHP-style code +``` + +## Event Binding + +```javascript + +``` + +Events are automatically bound when using `renderTo()`. + +## Components + +```javascript +{{#component myComponent}} // Render registered component +``` + +## Compilation Options + +```javascript +const template = Macrobars.compile(templateString, { + decimals: 2, // Number formatting precision + strict: true, // Use strict mode + components: {...} // Reusable components +}); +``` + +## DOM Integration + +```javascript +template.renderTo('#container', data); // Render to element +template.renderTo(document.getElementById('x'), data); +``` + +## Utility Functions + +```javascript +Macrobars.safe_out(text, default) // HTML escape +Macrobars.num_out(number, decimals) // Format number +Macrobars.num_out_round(number) // Round with k/M suffix +``` + +## Template Debugging + +Access compiled template information: +```javascript +template.tokens // Parsed tokens +template.gensource // Generated JavaScript +template.event_bindings // Event binding data +``` + +## Default Values + +```javascript +{{#default "N/A"}} // Set default for empty fields +{{field}} // Will show "N/A" if empty +``` + diff --git a/site/examples/uce-starter/js/u-popup-menu.css b/site/examples/uce-starter/js/u-popup-menu.css new file mode 100644 index 0000000..fb30ca3 --- /dev/null +++ b/site/examples/uce-starter/js/u-popup-menu.css @@ -0,0 +1,34 @@ +:root { + --vp-size-popup-min-width: 160px; +} + +.vp-popup-menu { + position: absolute; + background: var(--vp-color-black); + border: 1px solid var(--color-border); + color: var(--vp-color-white); + min-width: var(--vp-size-popup-min-width); + z-index: var(--vp-z-popup); + border-radius: var(--vp-space-l); + overflow: hidden; +} + +.vp-popup-item { + padding: var(--vp-space-l); + cursor: pointer; + white-space: nowrap; +} + +.vp-popup-item:hover { + color: var(--color-highlight); +} + +.vp-popup-item.focused { + background: var(--color-highlight); + color: var(--vp-color-black); +} + +.vp-popup-empty { + padding: var(--vp-space-l); + color: var(--vp-color-gray); +} \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-popup-menu.js b/site/examples/uce-starter/js/u-popup-menu.js new file mode 100644 index 0000000..e30498b --- /dev/null +++ b/site/examples/uce-starter/js/u-popup-menu.js @@ -0,0 +1,84 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.showPopupMenu = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + let showPopupMenu = (x, y, items, prop = {}) => { + document.querySelectorAll('.vp-popup-menu').forEach(menu => { + if(menu.parentNode) menu.parentNode.removeChild(menu); + }); + + let container = prop.container || document.body; + let menu = $('
').css({ + position: 'absolute', + left: (x|0) + 'px', + top: (y|0) + 'px' + })[0]; + + let elems = []; + if(!items || !items.length) { + $(menu).append(`
${prop.emptyText || 'No items'}
`); + } else { + items.forEach((it, idx) => { + let label = typeof it === 'string' ? it : (it.label || it.screen || it.name || JSON.stringify(it)); + let $item = $(`
${label}
`) + .attr('data-idx', idx) + .on('click', ev => { ev.stopPropagation(); try { (prop.onSelect || (()=>{}))(it); } catch(e){ console.error(e); } removeMenu(); }) + .on('keydown', e => { if(e.key === 'Enter') { e.preventDefault(); $item[0].click(); } }); + $(menu).append($item[0]); + elems.push({el: $item[0], data: it}); + }); + } + + let focusedIndex = elems.length ? 0 : -1; + let focusAt = i => { + elems.forEach((it, idx) => { $(it.el).removeClass('focused'); if(idx === i) { $(it.el).addClass('focused'); try{ it.el.focus(); }catch(e){} } }); + focusedIndex = i; + }; + + let removeMenu = () => { + if(menu.parentNode) menu.parentNode.removeChild(menu); + $(document).off('mousedown', onDoc).off('keydown', onKey); + $(window).off('resize', onResize); + }; + let onDoc = ev => { if(!menu.contains(ev.target)) removeMenu(); }; + let onResize = () => reposition(); + let onKey = ev => { + if(!elems.length) { if(ev.key === 'Escape') removeMenu(); return; } + if(ev.key === 'Escape') { ev.preventDefault(); removeMenu(); return; } + if(ev.key === 'ArrowDown') { ev.preventDefault(); focusAt((focusedIndex + 1) % elems.length); return; } + if(ev.key === 'ArrowUp') { ev.preventDefault(); focusAt((focusedIndex - 1 + elems.length) % elems.length); return; } + if(ev.key === 'Home') { ev.preventDefault(); focusAt(0); return; } + if(ev.key === 'End') { ev.preventDefault(); focusAt(elems.length - 1); return; } + if(ev.key === 'Enter') { ev.preventDefault(); if(focusedIndex >= 0) elems[focusedIndex].el.click(); } + }; + + container.appendChild(menu); + let reposition = () => { + let mRect = menu.getBoundingClientRect(); + let winW = window.innerWidth, winH = window.innerHeight; + let left = parseInt($(menu).css('left'),10) || 0, top = parseInt($(menu).css('top'),10) || 0; + if(mRect.right > winW) left = Math.max(4, winW - Math.ceil(mRect.width) - 8); + if(mRect.bottom > winH) top = Math.max(4, winH - Math.ceil(mRect.height) - 8); + if(left < 4) left = 4; if(top < 4) top = 4; + $(menu).css({left: left + 'px', top: top + 'px'}); + }; + + $(document).on('mousedown', onDoc).on('keydown', onKey); + $(window).on('resize', onResize); + if(elems.length) { focusAt(0); } + setTimeout(reposition, 0); + return menu; + }; + + return showPopupMenu; +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-query-demo.html b/site/examples/uce-starter/js/u-query-demo.html new file mode 100755 index 0000000..ce48d39 --- /dev/null +++ b/site/examples/uce-starter/js/u-query-demo.html @@ -0,0 +1,976 @@ + + + + + + U-Query.js Demo + + + +
+

U-Query.js Demo

+ +
+
+

Element Selection & DOM Manipulation

+
+

DOM Queries

+
+ Select elements to see them highlighted +
+
Test Element 1
+
Test Element 2
+
Test Element 3 (special)
+ +
+ + + + +
+ +
+
+ +
+

DOM Manipulation

+
Original content
+ +
+ + + + +
+ +
+
+ +
+

CSS & Classes

+
CSS Target Element
+ +
+ + + + +
+ +
+
+
+ +
+

Selection API

+ +
+

Core Selectors

+
$(selector) → NodeList
+
$('.class') → NodeList
+
$('#id') → NodeList
+
$('tag') → NodeList
+
+ +
+

DOM Manipulation

+
html(content?) → NodeList|string
+
text(content?) → NodeList|string
+
append(content) → NodeList
+
prepend(content) → NodeList
+
remove() → NodeList
+
+ +
+

CSS & Classes

+
css(styles) → NodeList
+
addClass(className) → NodeList
+
removeClass(className) → NodeList
+
toggleClass(className) → NodeList
+
+ +
+

Examples

+
+// Element selection
+const elements = $('.my-class');
+const byId = $('#my-id');
+
+$('.target').html('<b>New content</b>');
+$('.target').text('Plain text');
+$('.container').append('<p>Added</p>');
+
+$('.element').css({
+  'color': 'red',
+  'background': 'blue'
+});
+$('.element').addClass('active');
+
+
+
+
+ +
+
+

Event Handling & Visibility

+
+

Event System

+ + +
+ + + +
+ +
+
+ +
+

Show/Hide/Toggle

+
Toggle me!
+ +
+ + + + +
+ +
+
+ +
+

Method Chaining

+
+
Chain Target 1
+
Chain Target 2
+
Chain Target 3
+
+ +
+ + +
+ +
+
+
+ +
+

Events & Visibility API

+ +
+

Event Methods

+
on(event, handler) → NodeList
+
off(event, handler?) → NodeList
+
trigger(event, data?) → NodeList
+
once(event, handler) → NodeList
+
+ +
+

Visibility Control

+
show() → NodeList
+
hide() → NodeList
+
toggle() → NodeList
+
fadeIn(duration?) → NodeList
+
fadeOut(duration?) → NodeList
+
+ +
+

Method Chaining

+
+ All u-query methods return the NodeList, enabling method chaining. +
+
+$('.button').on('click', function() {
+  console.log('Clicked!');
+});
+
+$('.element')
+  .addClass('active')
+  .css({'color': 'red'})
+  .show()
+  .on('click', handler);
+
+$('.modal').fadeIn(300);
+$('.tooltip').fadeOut(200);
+
+
+
+
+ +
+
+

AJAX & Global Events

+
+

AJAX Utilities

+
+
+ AJAX Request Status +
+ +
+ + + +
+ +
+
+ +
+

Global Event System

+ + +
+ + + +
+ +
+
+ +
+

Utility Functions

+
Utility Element 1
+
Utility Element 2
+
Utility Element 3
+ +
+ + + +
+ +
+
+
+ +
+

AJAX & Utilities API

+ +
+

AJAX Methods

+
$.get(url, callback) → Promise
+
$.post(url, data, callback) → Promise
+
$.ajax(options) → Promise
+
$.json(url) → Promise
+
+ +
+

Global Event System

+
$.on(event, handler) → $
+
$.off(event, handler?) → $
+
$.emit(event, data?) → number
+
$.trigger(event, data?) → number
+
+ +
+

Utility Functions

+
$.each(selector, callback) → $
+
$.ready(callback) → $
+
$.extend(target, ...sources) → object
+
$.map(selector, callback) → Array
+
+ +
+

Examples

+
+const data = await $.get('/api/users');
+$.post('/api/users', {name: 'John'})
+  .then(response => console.log(response));
+
+$.on('user-login', user => {
+  console.log('User logged in:', user);
+});
+$.emit('user-login', {id: 123});
+
+$.each('.item', (el, i) => {
+  el.textContent = `Item ${i}`;
+});
+
+$.ready(() => {
+  console.log('DOM ready!');
+});
+
+
+
+
+ +
+
+

Browser Info

+
+
Loading library information...
+ +

This browser supports

+
+ +
+ + +
+
+
+ +
+

Advanced Features

+ +
+

Advanced Selectors

+
$(':visible') → NodeList
+
$(':hidden') → NodeList
+
$(':checked') → NodeList
+
$(':first') → NodeList
+
$(':last') → NodeList
+
+ +
+

Data Attributes

+
data(key, value?) → NodeList|any
+
attr(name, value?) → NodeList|string
+
prop(name, value?) → NodeList|any
+
val(value?) → NodeList|string
+
+ +
+

Traversal Methods

+
parent() → NodeList
+
children() → NodeList
+
siblings() → NodeList
+
find(selector) → NodeList
+
closest(selector) → NodeList
+
+ +
+

DOM Diff Updates

+
+ u-query can make use of morphdom.js for efficient DOM diff updates. +
+
$.options.alwaysDoDifferentialUpdate = true
+
+ Global flag for morphdom. +
+
+ +
+
+
+ + + + + diff --git a/site/examples/uce-starter/js/u-query.js b/site/examples/uce-starter/js/u-query.js new file mode 100755 index 0000000..907e8ba --- /dev/null +++ b/site/examples/uce-starter/js/u-query.js @@ -0,0 +1,589 @@ +(function (root, factory) { + // this is the stupid UMD pattern, but eh we lost that battle a long time ago + if (typeof exports === 'object' && typeof module !== 'undefined') { + var exports_obj = factory(); + module.exports = exports_obj.$; + // Also export individual components + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + exports[key] = exports_obj[key]; + } + } + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + var exports_obj = factory(); + root.$ = exports_obj.$; + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + root[key] = exports_obj[key]; + } + } + } +}(typeof self !== 'undefined' ? self : this, function () { + +// a collection of DOM utility functions that I thought were cool in jQuery + +class EventEmitter { + constructor() { + /** Map> */ + this._topics = new Map(); + } + + /** + * Subscribe to `topic`. If `slot_key` is provided (truthy), + * it dedupes by that key; otherwise a unique Symbol is used. + * Returns an unsubscribe fn. + */ + on(topic, handler, slot_key = null) { + let map = this._topics.get(topic); + if (!map) { + map = new Map(); + this._topics.set(topic, map); + } + // use the provided slot_key or a fresh Symbol() + const key = slot_key != null ? slot_key : Symbol(); + map.set(key, handler); + return () => this.off(topic, key); + } + + /** + * Unsubscribe by handler function or by slot_key. + */ + off(topic, handlerOrSlotKey) { + const map = this._topics.get(topic); + if (!map) return; + + // if it matches a slotKey directly, remove it + if (map.has(handlerOrSlotKey)) { + map.delete(handlerOrSlotKey); + } else { + // otherwise assume it's a function: remove all matching fns + for (const [key, fn] of map.entries()) { + if (fn === handlerOrSlotKey) { + map.delete(key); + } + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + } + + /** + * Emit to all handlers on `topic`. Handlers returning + * 'remove_handler' are auto-removed. + * Returns the number of handlers invoked. + */ + emit(topic, ...args) { + let count = 0; + const map = this._topics.get(topic); + if (!map) return count; + + for (const [key, fn] of Array.from(map.entries())) { + const res = fn(...args); + count++; + if (res === 'remove_handler') { + map.delete(key); + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + return count; + } +} + +class QueryWrapper extends Array { + constructor(elements) { + super(); + if (elements) { + this.push(...(Array.isArray(elements) ? elements : [elements])); + } + } +} + +$ = function(selector_or_element) { + let elements; + if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) { + elements = [selector_or_element]; + } else if (typeof selector_or_element === 'string') { + // Check if string looks like HTML (starts with < and ends with >) + if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') { + // Create element from HTML string + let temp = document.createElement('div'); + temp.innerHTML = selector_or_element.trim(); + elements = Array.from(temp.children); + } else { + // Treat as CSS selector + elements = Array.from(document.querySelectorAll(selector_or_element)); + } + } else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) { + elements = Array.from(selector_or_element); + } else { + elements = [selector_or_element]; + } + return new QueryWrapper(elements); +}; + +$.options = { + alwaysDoDifferentialUpdate: true, +}; + +$.events = new EventEmitter(); +$.on = $.events.on.bind($.events); +$.off = $.events.off.bind($.events); +$.emit = $.events.emit.bind($.events); + +$.each = function(selector, callback) { + let elements; + if (typeof selector === 'string') { + elements = document.querySelectorAll(selector); + } else if (selector instanceof QueryWrapper) { + elements = selector; + } else { + elements = selector; + } + for (let i = 0; i < elements.length; i++) { + callback(elements[i], i); + } +} + +$.post = function(url, data, callback) { + return $.ajax({ + method: 'POST', + url: url, + data: data, + success: callback, + error: function(xhr) { + console.error('POST request failed:', xhr); + } + }); +} + +$.get = function(url, callback) { + return $.ajax({ + method: 'GET', + url: url, + success: callback, + error: function(xhr) { + console.error('GET request failed:', xhr); + } + }); +} + +$.ajax = function(options) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open(options.method || 'GET', options.url, true); + + // Set default headers + if (options.method === 'POST' || options.data) { + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + } + + // Set custom headers + if (options.headers) { + for (var key in options.headers) { + xhr.setRequestHeader(key, options.headers[key]); + } + } + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + var response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + // If JSON parsing fails, use raw response + response = xhr.responseText; + } + + if (xhr.status >= 200 && xhr.status < 300) { + if (options.success) options.success(response); + resolve(response); + } else { + if (options.error) options.error(xhr); + reject(xhr); + } + } + }; + + var data = null; + if (options.data) { + data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data); + } + + xhr.send(data); + }); +} + +$.ready = function(callback) { + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } +} + +// Helper function to execute scripts in HTML content +function executeScripts(htmlContent) { + // Create a temporary container to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // Find all script tags and disable them temporarily + const scripts = tempDiv.querySelectorAll('script'); + const scriptData = []; + + scripts.forEach((script) => { + // Store script data for later execution + scriptData.push({ + content: script.textContent || script.innerText || '', + src: script.src, + type: script.type, + nonce: script.nonce, + async: script.async, + defer: script.defer + }); + + // Disable the script by changing its type + script.type = 'text/disabled-script'; + }); + + // Return the modified HTML and script data + return { + html: tempDiv.innerHTML, + scripts: scriptData + }; +} + +// Helper function to execute a single script +function executeScript(scriptInfo) { + if (scriptInfo.src) { + // External script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + if (scriptInfo.async) newScript.async = scriptInfo.async; + if (scriptInfo.defer) newScript.defer = scriptInfo.defer; + newScript.src = scriptInfo.src; + + document.head.appendChild(newScript); + } else if (scriptInfo.content.trim()) { + // Inline script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + newScript.text = scriptInfo.content; + + // Execute by inserting and immediately removing + document.head.appendChild(newScript); + document.head.removeChild(newScript); + } +} + +// Add all the jQuery-like methods to QueryWrapper +QueryWrapper.prototype.parent = function() { + const parents = Array.from(this).map(el => el.parentNode).filter(el => el); + return new QueryWrapper(parents); +} + +QueryWrapper.prototype.load = function(url, opt = {}) { + return new Promise((resolve, reject) => { + const ajaxOptions = { + method: opt.method || (opt.postData ? 'POST' : 'GET'), + url: url, + success: (response) => { + if(opt.replace) + this.replaceWith(response, opt); + else + this.html(response, opt); + if (opt.onLoad) opt.onLoad(response, null); + resolve(this); + }, + error: (xhr) => { + if (opt.onError) opt.onError(xhr); + reject(new Error('Failed to load: ' + url)); + } + }; + + if (opt.postData) { + ajaxOptions.data = opt.postData; + } + + $.ajax(ajaxOptions); + }); +} + +QueryWrapper.prototype.html = function(opt_content = false, prop = {}) { + if (opt_content === false) { + return Array.from(this).map(el => el.innerHTML).join(''); + } + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + const temp = document.createElement('div'); + temp.innerHTML = opt_content; + this.forEach(el => { + morphdom(el, temp, { childrenOnly: true }); + }); + } else { + this.forEach(el => { + const result = executeScripts(opt_content); + el.innerHTML = result.html; + result.scripts.forEach(executeScript); + }); + } + return this; +} + +QueryWrapper.prototype.text = function(opt_content = false) { + if (opt_content === false) { + return Array.from(this).map(el => el.textContent).join(''); + } + this.forEach(el => { + el.textContent = opt_content; + }); + return this; +} + +QueryWrapper.prototype.replaceWith = function(content, prop = {}) { + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + this.forEach(el => { + morphdom(el, content); + }); + } else { + if(typeof content === 'string') { + this.forEach(el => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content; + el.parentNode.replaceChild(tempDiv.firstChild, el); + }); + } else if(content instanceof Element) { + this.forEach(el => { + el.parentNode.replaceChild(content.cloneNode(true), el); + }); + } + } + return this; +} + +QueryWrapper.prototype.query = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.find = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.on = function(event, handler) { + this.forEach(function(el) { + el.addEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.off = function(event, handler) { + this.forEach(function(el) { + el.removeEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.hide = function() { + return this.css({ display: 'none' }); +} + +QueryWrapper.prototype.show = function() { + return this.css({ display: '' }); +} + +QueryWrapper.prototype.toggle = function() { + this.forEach(function(el) { + el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none'; + }); + return this; +} + +QueryWrapper.prototype.attr = function(name, value) { + if (value === undefined) { + return this.length > 0 ? this[0].getAttribute(name) : null; + } + this.forEach(function(el) { + el.setAttribute(name, value); + }); + return this; +} + +QueryWrapper.prototype.addClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.add(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.removeClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.remove(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.toggleClass = function(classNameOrList, force) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + if (typeof force !== 'undefined') { + el.classList.toggle(className, force); + } else { + el.classList.toggle(className); + } + }); + }); + }); + return this; +} + +QueryWrapper.prototype.empty = function() { + this.forEach(function(el) { + el.innerHTML = ''; + }); + return this; +} + +QueryWrapper.prototype.css = function(styles, optOrValue) { + // If no arguments or first argument is a string (getter mode) + if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) { + if (this.length === 0) return undefined; + var el = this[0]; + if (typeof styles === 'string') { + // Get computed style for a specific property + return window.getComputedStyle(el)[styles]; + } else { + // Return computed styles object (not commonly used) + return window.getComputedStyle(el); + } + } + + // Setter mode + this.forEach(function(el) { + if (typeof styles === 'string' && arguments.length >= 2) { + // Single property setter: .css('prop', 'value') + el.style[styles] = optOrValue; + } else if (typeof styles === 'object') { + // Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'}) + for (var key in styles) { + el.style[key] = styles[key]; + } + } + }); + return this; +} + +QueryWrapper.prototype.each = function(callback) { + this.forEach(function(el, index) { + callback(el, index); + }); + return this; +} + +QueryWrapper.prototype.append = function(child_or_html) { + this.forEach(function(el) { + if (typeof child_or_html === 'string' && el.insertAdjacentHTML) { + if (child_or_html.includes(' max) return max; + return v; +} + +function pick_entry_from_range(array, value) { + if (!array) return {}; + let result = {}; + for (const [pv] of Object.entries(array)) { + if (value >= pv.from && value <= pv.to) result = pv; + } + return result; +} + +return { + $: $, + EventEmitter: EventEmitter, + QueryWrapper: QueryWrapper, + first: first, + clamp: clamp, + pick_entry_from_range: pick_entry_from_range +}; + +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-sortable-table.js b/site/examples/uce-starter/js/u-sortable-table.js new file mode 100644 index 0000000..e9fee4f --- /dev/null +++ b/site/examples/uce-starter/js/u-sortable-table.js @@ -0,0 +1,168 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.USortableTable = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getTable(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target.nodeType === 1 ? target : null; + } + + function getStorageKey(table, options) { + if (options && options.storageKey) return options.storageKey; + if (table && table.id) return `u-sortable-table.${table.id}`; + return null; + } + + function loadSort(table, options) { + const storageKey = getStorageKey(table, options); + if (!storageKey || !globalScope.localStorage) return null; + try { + const raw = globalScope.localStorage.getItem(storageKey); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (!parsed || !Number.isInteger(parsed.column) || (parsed.direction !== 'asc' && parsed.direction !== 'desc')) { + return null; + } + return parsed; + } catch (error) { + return null; + } + } + + function saveSort(table, column, direction, options) { + const storageKey = getStorageKey(table, options); + if (!storageKey || !globalScope.localStorage) return; + try { + globalScope.localStorage.setItem(storageKey, JSON.stringify({ column, direction })); + } catch (error) { + // Ignore storage errors. + } + } + + function parseSortValue(text) { + const normalized = String(text || '').trim(); + if (!normalized) return { type: 'text', value: '' }; + + if (globalScope.UFormat && typeof globalScope.UFormat.parseUnitNumber === 'function') { + const unitValue = globalScope.UFormat.parseUnitNumber(normalized); + if (unitValue != null) { + return { type: 'number', value: unitValue }; + } + } + + const timestamp = Date.parse(normalized); + if (!Number.isNaN(timestamp)) { + return { type: 'number', value: timestamp }; + } + + return { type: 'text', value: normalized.toLowerCase() }; + } + + function setHeaderState(table, columnIndex, direction) { + Array.from(table.querySelectorAll('thead th')).forEach((header, index) => { + header.classList.remove('sorted-asc', 'sorted-desc'); + header.setAttribute('aria-sort', 'none'); + if (index === columnIndex) { + header.classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc'); + header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending'); + } + }); + } + + function sortTable(table, columnIndex, direction, options, persist) { + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.children.length > 0 && !row.hasAttribute('data-empty-row')); + if (!rows.length) return; + + rows.sort((rowA, rowB) => { + const cellA = rowA.children[columnIndex]; + const cellB = rowB.children[columnIndex]; + const rawA = cellA ? (cellA.getAttribute('data-sort-value') || cellA.textContent) : ''; + const rawB = cellB ? (cellB.getAttribute('data-sort-value') || cellB.textContent) : ''; + const valueA = parseSortValue(rawA); + const valueB = parseSortValue(rawB); + + let comparison = 0; + if (valueA.type === 'number' && valueB.type === 'number') { + comparison = valueA.value - valueB.value; + } else { + comparison = String(valueA.value).localeCompare(String(valueB.value), undefined, { + numeric: true, + sensitivity: 'base', + }); + } + return direction === 'asc' ? comparison : -comparison; + }); + + rows.forEach((row) => tbody.appendChild(row)); + setHeaderState(table, columnIndex, direction); + if (persist !== false) { + saveSort(table, columnIndex, direction, options); + } + } + + function getInitialSort(table, options) { + const saved = loadSort(table, options); + if (saved) return saved; + if (options && options.initialSort) { + return { + column: Number(options.initialSort.column || 0), + direction: options.initialSort.direction === 'desc' ? 'desc' : 'asc', + }; + } + return null; + } + + function attachHeader(header, table, columnIndex, options) { + if (header.getAttribute('data-sortable') === 'false') return; + header.classList.add('sortable'); + header.tabIndex = 0; + header.addEventListener('click', function () { + const current = loadSort(table, options); + const nextDirection = current && current.column === columnIndex && current.direction === 'asc' ? 'desc' : 'asc'; + sortTable(table, columnIndex, nextDirection, options, true); + }); + header.addEventListener('keydown', function (event) { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + header.click(); + }); + } + + function init(target, options = {}) { + const table = getTable(target); + if (!table || table.dataset.sortableTableInit === '1') return table; + + Array.from(table.querySelectorAll('thead th')).forEach((header, index) => attachHeader(header, table, index, options)); + table.dataset.sortableTableInit = '1'; + + const initialSort = getInitialSort(table, options); + if (initialSort) { + sortTable(table, initialSort.column, initialSort.direction, options, false); + } + + return table; + } + + function initAll(selector = '.u-sortable-table') { + Array.from(document.querySelectorAll(selector)).forEach((table) => init(table)); + } + + return { + init, + initAll, + parseSortValue, + sortTable, + }; +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-tiling-viewport.css b/site/examples/uce-starter/js/u-tiling-viewport.css new file mode 100644 index 0000000..63976c7 --- /dev/null +++ b/site/examples/uce-starter/js/u-tiling-viewport.css @@ -0,0 +1,165 @@ +:root { + --vp-color-primary: rgba(255, 125, 55, 1); + --vp-color-primary-focus: rgba(255, 120, 60, 0.4); + --vp-background: rgba(0,0,0,0.2); + + --vp-color-black: #000; + --vp-color-gray: #666; + --vp-color-white: #fff; + --vp-space-s: 4px; + --vp-space-l: 6px; + + --vp-size-icon: 12px; + --vp-size-nav-height: 48px; + + --vp-opacity: 0.4; + --vp-opacity-active: 0.9; + + --vp-z-pane: 2; + --vp-z-toolbar: 3; + --vp-z-popup: 9999; + --vp-duration: 0.25s; + --vp-easing: ease; +} + +#fabric { + position: absolute; + top: var(--vp-size-nav-height); + left: 0; right: 0; bottom: 0; + display: flex; + overflow: hidden; +} + +.vp-split { + flex: 1 1 auto; + display: flex; + position: relative; + overflow: hidden; +} + +.vp-split.row { flex-direction: row; } +.vp-split.col { flex-direction: column; } + +.viewport-pane { + flex: 1 1 0; + position: relative; + border: var(--vp-space-s) solid var(--color-border); + background: var(--vp-background); + padding: var(--vp-space-l); + overflow: auto; + font-family: console; + transition: + border-color var(--vp-duration) var(--vp-easing), + box-shadow var(--vp-duration) var(--vp-easing), + background var(--vp-duration) var(--vp-easing); +} + +.vp-split > .viewport-pane { + transition: + flex var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); +} + +.vp-no-transition, +.vp-no-transition * { + transition: none !important; +} + +.viewport-pane:before { + content: attr(data-title); + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + opacity: var(--vp-opacity-medium); + pointer-events: none; +} + +.viewport-pane.focused { + border-color: var(--vp-color-primary-focus); + z-index: var(--vp-z-pane); +} + +.viewport-pane.focused:before { + opacity: var(--vp-opacity-active); + color: var(--color-highlight); +} + +.vp-divider { + background: var(--color-border); + opacity: var(--vp-opacity); + position: relative; + flex: 0 0 auto; +} + +.vp-divider.row { + width: var(--vp-space-l); + cursor: col-resize; +} + +.vp-divider.col { + height: var(--vp-space-l); + cursor: row-resize; +} + +.vp-divider:after { + content:''; + position: absolute; + inset: 0; + transition: + background var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); + background: var(--color-border); +} + +.vp-divider.row:after { + background: var(--color-border); +} + +.vp-divider:hover:after { + background: var(--color-highlight); + opacity: var(--vp-opacity); +} + +.vp-divider.dragging:after { + background: var(--color-highlight); + opacity: var(--vp-opacity-active); +} + +.vp-help-overlay { + position: absolute; + top: var(--vp-space-s); + left: var(--vp-space-s); + opacity: var(--vp-opacity); + pointer-events: none; +} + +.vp-pane-toolbar { + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + display: flex; + gap: var(--vp-space-s); + z-index: var(--vp-z-toolbar); +} + +.vp-pane-toolbar button { + padding: 0 var(--vp-space-l); + border: 1px solid var(--color-border); + background: var(--vp-background); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.vp-pane-toolbar button:hover { + background: var(--color-highlight); + color: var(--vp-color-black); + text-shadow: none; +} + +.vp-icon { + width: var(--vp-size-icon); + height: var(--vp-size-icon); + display: block; +} \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-tiling-viewport.js b/site/examples/uce-starter/js/u-tiling-viewport.js new file mode 100644 index 0000000..3762033 --- /dev/null +++ b/site/examples/uce-starter/js/u-tiling-viewport.js @@ -0,0 +1,388 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.TilingViewportManager = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + // Note: This module expects showPopupMenu to be available globally + // or you may need to adjust the dependency list above to include it + + let TilingViewportManager = function(container, options = {}) { + + let idCounter = 1, rootNode = null, focusedPane = null, containerEl = null; + let nextId = () => 'vp_' + (idCounter++); + let paneMeta = new Map(); + let pendingViewLoads = []; + let defaultoptions = { + minPaneSize: 100, + keyboard: true, + showHelp: true, + closeTransitionTimeout: 400, + toolbarButtons: ['split-v','split-h','views','close'], + standardViewList: [], + cssVars: { '--vp-duration': '.22s', '--vp-easing': 'ease' } + }; + options = Object.assign({}, defaultoptions, options); + + let createPane = (content, props = {}) => { + let id = nextId(); + let $el = $('
').attr('data-id', id); + if(props.title) $el.attr('data-title', props.title); + $el.html(content || `
Use Alt+<H V O X ↑ ← → ↓>
`) + .on('mousedown', () => focusPaneByEl($el[0])); + attachPaneToolbar($el[0]); + paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props}); + if(props.view?.screen) pendingViewLoads.push({id, view: props.view}); + return {type: 'pane', id, el: $el[0]}; + }; + + let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); }; + let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); }; + let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id'))); + let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r); + let getNodeById = id => findNodeById(rootNode, id) || null; + let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r); + let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc); + let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); }; + let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); }; + + let split = (direction, newPaneProps = {}) => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canSplit === false) return; + + let paneRect = focusedPane.el.getBoundingClientRect(); + let availableSpace = direction === 'row' ? paneRect.width : paneRect.height; + let requiredSpace = 2 * options.minPaneSize; + if(availableSpace < requiredSpace) { + //console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`); + return; + } + + let parentPath = findParentPath(rootNode, focusedPane.id); + let newPane = createPane(undefined, newPaneProps); + let animatedSplit = null, newIndex = -1; + if(!parentPath?.length) { + rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + animatedSplit = rootNode; newIndex = 1; + } else { + let parent = parentPath.at(-1); + if(parent.type === 'split' && parent.direction === direction) { + let idx = parent.children.indexOf(focusedPane); + parent.children.splice(idx+1, 0, newPane); + parent.sizes = parent.sizes || parent.children.map(()=>1); + parent.sizes.splice(idx+1, 0, 0); + renormalizeSizes(parent); + animatedSplit = parent; newIndex = idx+1; + } else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) { + parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0]; + animatedSplit = parent; newIndex = parent.children.length-1; + } else { + let idx = parent.children.indexOf(focusedPane); + let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + parent.children.splice(idx, 1, created); + animatedSplit = created; newIndex = 1; + } + } + render(); + if(animatedSplit && newIndex >= 0) { + requestAnimationFrame(() => requestAnimationFrame(() => { + animatedSplit.sizes = animatedSplit.children.map(()=>1); + renormalizeSizes(animatedSplit); + animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`)); + })); + } + focusPane(newPane); return newPane; + }; + + let splitVertical = () => split('row'); + let splitHorizontal = () => split('col'); + + let closeFocused = () => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canClose === false) return; + if(rootNode === focusedPane) { buildInitial(); return; } + if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id); + if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id); + let parentPath = findParentPath(rootNode, focusedPane.id); + if(!parentPath?.length) return; + let parent = parentPath.at(-1); + if(parent.type !== 'split') return; + let idx = parent.children.indexOf(focusedPane); + + let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice(); + let oldSize = oldSizes[idx] || 0; + let remaining = 1 - oldSize; + let newSizes = oldSizes.slice(); + if(remaining > 0) { + for(let i=0;i ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`)); + if(focusedPane.el) { + focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease'; + focusedPane.el.style.opacity = '0'; + } + let done = false; + let cleanup = () => { + if(done) return; done = true; + parent.children.splice(idx,1); + parent.sizes?.splice(idx,1); + paneMeta.delete(focusedPane.id); + if(parent.children.length === 1) { + let only = parent.children[0]; + parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only); + } else if(parent.sizes) renormalizeSizes(parent); + render(); focusPane(paneList(rootNode)[0]); + }; + let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } }; + if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd); + setTimeout(() => cleanup(), 350); + }; + + let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; }; + let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); }; + let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); }; + + let serialize = (node=rootNode) => { + let ser = n => !n ? null : n.type==='pane' ? + {type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} : + {type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)}; + return {focus: focusedPane?.id, layout: ser(node)}; + }; + + let restore = data => { + if(!data) return; + let wrapper = data.layout ? data : {layout:data}; + paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0; + let idNums = []; + let build = l => l && l.type==='pane' ? + (() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view}); + if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); } + let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); } + return pane; })() : + l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null; + rootNode = build(wrapper.layout) || createPane(); + if(idNums.length) idCounter = Math.max(...idNums) + 1; + render(); + pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0; + focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]); + }; + + let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); }; + + let renderNode = node => { + if(node.type==='pane') return node.el; + node.el = node.el || $('
')[0]; + $(node.el).empty().addClass(`vp-split ${node.direction}`); + if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); } + node.children.forEach((ch,i) => { + let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl); + if(i < node.children.length-1) { let div = $('
').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); } + }); + return node.el; + }; + + let setupDivider = (div, splitNode, leftIdx) => { + $(div).on('mousedown', e => { + e.preventDefault(); $(div).addClass('dragging'); + $(containerEl).addClass('vp-no-transition'); + let isRow = splitNode.direction === 'row'; + let startPos = isRow ? e.clientX : e.clientY; + let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect()); + let prop = isRow ? 'width' : 'height'; + let startPixels = childRects.map(r => r[prop]); + let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]]; + let totalPixels = startPixels.reduce((a,b)=>a+b,0); + let onMove = ev => { + let curPos = isRow ? ev.clientX : ev.clientY; + let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos]; + if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize]; + if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize]; + startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx; + splitNode.sizes = startPixels.map(p => p / totalPixels); + splitNode.children.forEach((ch,i)=> { + if(ch.el) { + let flexValue = `${splitNode.sizes[i]} 1 0`; + $(ch.el).css('flex', flexValue); + } + }); + }; + let onUp = () => { + $(div).removeClass('dragging'); + $(containerEl).removeClass('vp-no-transition'); + $(document).off('mousemove', onMove).off('mouseup', onUp); + }; + $(document).on('mousemove', onMove).on('mouseup', onUp); + }); + $(div).on('dblclick', () => { + $(containerEl).addClass('vp-no-transition'); + let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len); + splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`)); + setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0); + }); + }; + + let attachPaneToolbar = el => { + let bar = $('
').addClass('vp-pane-toolbar').html(` + + + + `)[0]; + el.appendChild(bar); + $(bar).on('mousedown', ev => ev.stopPropagation()); + $(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el); + let button = ev.target.closest('button[data-act]'); + let act = button ? button.getAttribute('data-act') : null; + if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused(); + else if(act==='views') { + // compute button position + let rect = button.getBoundingClientRect(); + let items = options.standardViewList.slice(); + showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => { + // support string or object {screen} + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(el.dataset.id, view); + }, container: document.body, emptyText: 'No views available' }); + } + }); + refreshToolbarState(el.dataset.id); + }; + + let refreshToolbarState = paneId => { + let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; + if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return; + let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false]; + ['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } }); + let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); } + }; + + let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props); + if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; } + if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload; + paneMeta.set(paneId, meta); refreshToolbarState(paneId); }; + let getPaneProps = paneId => paneMeta.get(paneId); + + let focusDirection = (dx, dy) => { + if(!focusedPane) return; + let panes = Array.from(containerEl.querySelectorAll('.viewport-pane')); + let cur = focusedPane.el.getBoundingClientRect(); + let candidates = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); + if(dx) { + if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null; + let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top)); + let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.height - overlap)}; + } + if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null; + let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left)); + let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.width - overlap)}; + }).filter(Boolean); + if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el); + let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2]; + let fallback = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2]; + let [vx, vy] = [px-cx, py-cy]; + if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null; + return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))}; + }).filter(Boolean); + if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el); + }; + + let injectHelpOverlay = () => { + if(!options.showHelp) return; + if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``})); + }; + + let getPaneEl = id => findNodeById(rootNode, id)?.el || null; + + let openViewsPopupForPane = (paneId) => { + let node = findNodeById(rootNode, paneId || focusedPane?.id); + if(!node || !node.el) return; + let rect = node.el.getBoundingClientRect(); + let x = Math.max(8, rect.right - 80); + let y = rect.top + 24; + let items = options.standardViewList.slice(); + showPopupMenu(x, y, items, { onSelect: it => { + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(node.id, view); + }, container: document.body, emptyText: 'No views available' }); + }; + + let loadView = (paneId, view) => { + let node = findNodeById(rootNode, paneId); + if(!node || !node.el) return; // nothing to load into + let meta = paneMeta.get(paneId); + try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); } + if(meta) meta.view = view; + node.el.innerHTML = `
Loading ${view.screen||'...'}...
`; + $(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})}); + }; + + let handleKeys = e => { + if(!e.altKey || !options.keyboard) return; + let actions = { + 'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(), + 'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(), + 'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(), + 'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(), + 'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext() + }; + if(actions[e.code] || actions[e.key]) { + (actions[e.code] || actions[e.key])(); + e.preventDefault(); + } + }; + + containerEl = typeof container === 'string' ? document.querySelector(container) : container; + if(!containerEl) throw new Error('TilingViewportManager: container not found'); + if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k])); + buildInitial(); + if(options.keyboard) document.addEventListener('keydown', handleKeys); + + this.splitVertical = splitVertical; + this.splitHorizontal = splitHorizontal; + this.closeFocused = closeFocused; + this.focusNext = focusNext; + this.focusPrev = focusPrev; + this.serialize = serialize; + this.restore = restore; + this.setPaneProps = setPaneProps; + this.getPaneProps = getPaneProps; + this.options = options; + this.loadView = loadView; + this.getPaneEl = getPaneEl; + this.getNodeById = getNodeById; + + Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) }); + Object.defineProperty(this, 'focused', { get: () => focusedPane?.id }); + }; + + return TilingViewportManager; +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-timeseries-chart.js b/site/examples/uce-starter/js/u-timeseries-chart.js new file mode 100644 index 0000000..b19f3eb --- /dev/null +++ b/site/examples/uce-starter/js/u-timeseries-chart.js @@ -0,0 +1,354 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UTimeSeriesChart = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + class TimeSeriesChart { + constructor(canvasOrId, options = {}) { + this.canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId; + if (!this.canvas || !this.canvas.getContext) { + throw new Error('UTimeSeriesChart requires a canvas element'); + } + + this.ctx = this.canvas.getContext('2d'); + const theme = this.resolveThemeColors(); + this.options = Object.assign({ + xAxisLabel: 'Time', + yAxisLeftLabel: 'Value', + yAxisRightLabel: '', + yAxisLeftFormat: 'number', + yAxisRightFormat: 'number', + padding: { top: 18, right: 62, bottom: 34, left: 58 }, + gridColor: theme.gridColor, + axisColor: theme.axisColor, + textColor: theme.textColor, + legendBackground: theme.legendBackground, + legendTextColor: theme.legendTextColor, + tooltipBackground: theme.tooltipBackground, + tooltipBorder: theme.tooltipBorder, + tooltipTitleColor: theme.tooltipTitleColor, + tooltipTextColor: theme.tooltipTextColor, + hoverLineColor: 'rgba(255,255,255,0.35)', + }, options); + + this.series = []; + this.xLabels = []; + this.hoverIndex = null; + this.hoverX = 0; + this.width = 0; + this.height = 0; + + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + this.onResize = this.onResize.bind(this); + + this.canvas.addEventListener('mousemove', this.onMouseMove); + this.canvas.addEventListener('mouseleave', this.onMouseLeave); + globalScope.addEventListener('resize', this.onResize); + if (typeof ResizeObserver !== 'undefined') { + this.resizeObserver = new ResizeObserver(this.onResize); + this.resizeObserver.observe(this.canvas); + } + + this.resize(); + } + + resolveThemeColors() { + const styles = globalScope.getComputedStyle ? globalScope.getComputedStyle(document.documentElement) : null; + const pick = function (name, fallback) { + if (!styles) return fallback; + const value = styles.getPropertyValue(name).trim(); + return value || fallback; + }; + return { + gridColor: pick('--border', 'rgba(148, 163, 184, 0.20)'), + axisColor: pick('--border-hover', pick('--border', 'rgba(148, 163, 184, 0.42)')), + textColor: pick('--text-secondary', '#64748b'), + legendBackground: pick('--surface', 'rgba(15, 23, 42, 0.72)'), + legendTextColor: pick('--text-primary', '#0f172a'), + tooltipBackground: pick('--surface', '#0f172a'), + tooltipBorder: pick('--border', 'rgba(148, 163, 184, 0.30)'), + tooltipTitleColor: pick('--primary', '#3b82f6'), + tooltipTextColor: pick('--text-primary', '#0f172a'), + }; + } + + destroy() { + this.canvas.removeEventListener('mousemove', this.onMouseMove); + this.canvas.removeEventListener('mouseleave', this.onMouseLeave); + globalScope.removeEventListener('resize', this.onResize); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + } + + onResize() { + this.resize(); + } + + onMouseLeave() { + this.hoverIndex = null; + this.draw(); + } + + resize() { + const rect = this.canvas.getBoundingClientRect(); + const cssWidth = Math.max(280, Math.round(rect.width || this.canvas.width || 640)); + const cssHeight = Math.max(180, Math.round(rect.height || this.canvas.height || 320)); + const pixelRatio = globalScope.devicePixelRatio || 1; + + this.width = cssWidth; + this.height = cssHeight; + this.canvas.width = Math.round(cssWidth * pixelRatio); + this.canvas.height = Math.round(cssHeight * pixelRatio); + this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + this.draw(); + } + + setData(series, xLabels = []) { + this.series = (Array.isArray(series) ? series : []).map((item) => ({ + key: item.key || '', + label: item.label || 'Series', + color: item.color || '#60a5fa', + axis: item.axis === 'right' ? 'right' : 'left', + unit: item.unit || '', + format: item.format || '', + maxHint: Number(item.maxHint || 0), + decimals: Number.isInteger(item.decimals) ? item.decimals : 2, + values: Array.isArray(item.values) ? item.values.map((value) => Number(value || 0)) : [], + })); + this.xLabels = Array.isArray(xLabels) ? xLabels : []; + this.draw(); + } + + getPlotRect() { + const padding = this.options.padding; + return { + x: padding.left, + y: padding.top, + w: this.width - padding.left - padding.right, + h: this.height - padding.top - padding.bottom, + }; + } + + getMaxForAxis(axis) { + const relevant = this.series.filter((item) => item.axis === axis); + if (!relevant.length) return 1; + let max = 1; + relevant.forEach((item) => { + const localMax = Math.max(...(item.values.length ? item.values : [0]), item.maxHint || 0, 1); + if (localMax > max) max = localMax; + }); + return max; + } + + xForIndex(index, pointCount, plot) { + if (pointCount <= 1) return plot.x; + return plot.x + (index / (pointCount - 1)) * plot.w; + } + + yForValue(value, axisMax, plot) { + const clamped = Math.max(0, Number(value || 0)); + return plot.y + plot.h - ((clamped / axisMax) * plot.h); + } + + formatValue(value, format, unit, decimals) { + const number = Number(value || 0); + if (globalScope.UFormat) { + if (format === 'bytes' && globalScope.UFormat.formatBytes) return globalScope.UFormat.formatBytes(number); + if (format === 'disk-bytes' && globalScope.UFormat.formatDiskBytes) return globalScope.UFormat.formatDiskBytes(number); + if (format === 'count' && globalScope.UFormat.formatCount) return globalScope.UFormat.formatCount(number); + if (format === 'duration-ms' && globalScope.UFormat.formatDurationMs) return globalScope.UFormat.formatDurationMs(number); + } + return `${number.toFixed(decimals)}${unit || ''}`; + } + + drawGridAndAxes(plot, maxLeft, maxRight) { + const ctx = this.ctx; + ctx.strokeStyle = this.options.gridColor; + ctx.lineWidth = 1; + + for (let index = 0; index <= 4; index += 1) { + const y = plot.y + (plot.h / 4) * index; + ctx.beginPath(); + ctx.moveTo(plot.x, y); + ctx.lineTo(plot.x + plot.w, y); + ctx.stroke(); + + const leftValue = maxLeft - ((maxLeft / 4) * index); + const leftTick = this.formatValue(leftValue, this.options.yAxisLeftFormat, '', 1); + ctx.fillStyle = this.options.textColor; + ctx.font = '11px Inter, sans-serif'; + const leftWidth = ctx.measureText(leftTick).width; + ctx.fillText(leftTick, Math.max(4, plot.x - leftWidth - 8), y + 4); + + if (this.options.yAxisRightLabel) { + const rightValue = maxRight - ((maxRight / 4) * index); + const rightTick = this.formatValue(rightValue, this.options.yAxisRightFormat, '', 1); + ctx.fillText(rightTick, plot.x + plot.w + 8, y + 4); + } + } + + ctx.strokeStyle = this.options.axisColor; + ctx.beginPath(); + ctx.moveTo(plot.x, plot.y); + ctx.lineTo(plot.x, plot.y + plot.h); + ctx.lineTo(plot.x + plot.w, plot.y + plot.h); + ctx.stroke(); + + ctx.fillStyle = this.options.textColor; + ctx.font = '12px Inter, sans-serif'; + ctx.save(); + ctx.translate(14, plot.y + (plot.h / 2)); + ctx.rotate(-Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.yAxisLeftLabel, 0, 0); + ctx.restore(); + + if (this.options.yAxisRightLabel) { + ctx.save(); + ctx.translate(this.width - 14, plot.y + (plot.h / 2)); + ctx.rotate(Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.yAxisRightLabel, 0, 0); + ctx.restore(); + } + + const xLabel = this.options.xAxisLabel; + const xLabelWidth = ctx.measureText(xLabel).width; + ctx.fillText(xLabel, plot.x + plot.w - xLabelWidth, this.height - 10); + } + + drawSeries(plot, maxLeft, maxRight) { + const ctx = this.ctx; + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) return; + + this.series.forEach((item) => { + if (!item.values.length) return; + const axisMax = item.axis === 'right' ? maxRight : maxLeft; + ctx.beginPath(); + item.values.forEach((value, index) => { + const x = this.xForIndex(index, item.values.length, plot); + const y = this.yForValue(value, axisMax, plot); + if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); + }); + ctx.strokeStyle = item.color; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + if (this.series.length > 1) { + ctx.fillStyle = this.options.legendBackground; + ctx.fillRect(plot.x + 4, plot.y + 4, Math.min(plot.w - 8, 320), 24); + let offsetX = plot.x + 12; + this.series.forEach((item) => { + ctx.fillStyle = item.color; + ctx.fillRect(offsetX, plot.y + 13, 14, 3); + offsetX += 20; + ctx.fillStyle = this.options.legendTextColor; + ctx.font = '11px Inter, sans-serif'; + ctx.fillText(item.label, offsetX, plot.y + 17); + offsetX += ctx.measureText(item.label).width + 18; + }); + } + } + + drawHover(plot, maxLeft, maxRight) { + if (this.hoverIndex == null) return; + const ctx = this.ctx; + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) return; + + const index = Math.max(0, Math.min(pointCount - 1, this.hoverIndex)); + const x = this.xForIndex(index, pointCount, plot); + + ctx.strokeStyle = this.options.hoverLineColor; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, plot.y); + ctx.lineTo(x, plot.y + plot.h); + ctx.stroke(); + + const lines = []; + lines.push(this.xLabels[index] || `Sample ${index + 1}`); + + this.series.forEach((item) => { + if (index >= item.values.length) return; + const axisMax = item.axis === 'right' ? maxRight : maxLeft; + const value = item.values[index] ?? 0; + const y = this.yForValue(value, axisMax, plot); + ctx.fillStyle = item.color; + ctx.beginPath(); + ctx.arc(x, y, 3.5, 0, Math.PI * 2); + ctx.fill(); + lines.push(`${item.label}: ${this.formatValue(value, item.format || (item.axis === 'right' ? this.options.yAxisRightFormat : this.options.yAxisLeftFormat), item.unit || '', item.decimals)}`); + }); + + ctx.font = '11px Inter, sans-serif'; + const padding = 8; + const lineHeight = 14; + const tooltipWidth = Math.max(...lines.map((line) => ctx.measureText(line).width)) + padding * 2; + const tooltipHeight = lines.length * lineHeight + padding * 2 - 2; + let tooltipX = this.hoverX + 12; + let tooltipY = plot.y + 8; + + if (tooltipX + tooltipWidth > this.width - 4) tooltipX = this.hoverX - tooltipWidth - 12; + if (tooltipX < 4) tooltipX = 4; + if (tooltipY + tooltipHeight > this.height - 4) tooltipY = this.height - tooltipHeight - 4; + + ctx.fillStyle = this.options.tooltipBackground; + ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight); + ctx.strokeStyle = this.options.tooltipBorder; + ctx.strokeRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight); + + lines.forEach((line, lineIndex) => { + ctx.fillStyle = lineIndex === 0 ? this.options.tooltipTitleColor : this.options.tooltipTextColor; + ctx.fillText(line, tooltipX + padding, tooltipY + padding + 10 + lineIndex * lineHeight); + }); + } + + onMouseMove(event) { + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + this.hoverX = x; + + const plot = this.getPlotRect(); + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) { + this.hoverIndex = null; + this.draw(); + return; + } + + const relative = Math.max(0, Math.min(plot.w, x - plot.x)); + this.hoverIndex = pointCount <= 1 ? 0 : Math.round((relative / plot.w) * (pointCount - 1)); + this.draw(); + } + + draw() { + const ctx = this.ctx; + ctx.clearRect(0, 0, this.width, this.height); + + const plot = this.getPlotRect(); + if (plot.w <= 0 || plot.h <= 0) return; + const maxLeft = this.getMaxForAxis('left'); + const maxRight = this.getMaxForAxis('right'); + + this.drawGridAndAxes(plot, maxLeft, maxRight); + this.drawSeries(plot, maxLeft, maxRight); + this.drawHover(plot, maxLeft, maxRight); + } + } + + return TimeSeriesChart; +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-workspace-shell.js b/site/examples/uce-starter/js/u-workspace-shell.js new file mode 100644 index 0000000..90fbd31 --- /dev/null +++ b/site/examples/uce-starter/js/u-workspace-shell.js @@ -0,0 +1,68 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UWorkspaceShell = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getElement(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target && target.nodeType === 1 ? target : null; + } + + function bindShell(options) { + const sidebar = getElement(options.sidebarId || options.sidebar); + const overlay = getElement(options.overlayId || options.overlay); + const toggle = getElement(options.toggleButtonId || options.toggle); + const closeOnNav = options.closeOnNav !== false; + + if (!sidebar || !overlay) return null; + + function setOpen(open) { + sidebar.classList.toggle('is-open', !!open); + overlay.classList.toggle('is-open', !!open); + } + + function toggleOpen() { + setOpen(!sidebar.classList.contains('is-open')); + } + + if (toggle) { + toggle.addEventListener('click', toggleOpen); + } + + overlay.addEventListener('click', function () { + setOpen(false); + }); + + if (closeOnNav) { + sidebar.querySelectorAll('a').forEach(function (link) { + link.addEventListener('click', function () { + setOpen(false); + }); + }); + } + + globalScope.addEventListener('resize', function () { + if (globalScope.innerWidth > 860) { + setOpen(false); + } + }); + + return { + open: function () { setOpen(true); }, + close: function () { setOpen(false); }, + toggle: toggleOpen, + }; + } + + return { + init: bindShell, + }; +})); \ No newline at end of file diff --git a/site/examples/uce-starter/js/u-wsconnection.js b/site/examples/uce-starter/js/u-wsconnection.js new file mode 100755 index 0000000..9c3b40f --- /dev/null +++ b/site/examples/uce-starter/js/u-wsconnection.js @@ -0,0 +1,111 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.Connection = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + +var Connection = { + + auto_reconnect : false, + established : false, + + update_indicator : (status) => { + var status_colors = { + 'offline' : 'red', + 'online' : 'lightgreen', + 'error' : 'DarkOrange', + }; + $('#connection-status').text(status).css('color', status_colors[status] || 'gray'); + }, + + debug : true, + auto_reconnect : true, + cmd_waiting_rels : {}, + + server_url : '', + + init : () => { + new EventSystem(Connection); + }, + + start : (url = null) => { + + if(url) Connection.server_url = url; + + Connection.update_indicator('offline'); + if(Connection.socket) Connection.socket.close(); + Connection.socket = new WebSocket(Connection.server_url); + Connection.socket.onmessage = function(rawmsg) { + var msg = JSON.parse(rawmsg.data); + if(Connection.debug) console.log('CONNECTION MSG', msg); + if(msg.type) Connection.trigger(msg.type, msg); + Connection.trigger('message', msg); + } + Connection.socket.onclose = function() { + Connection.update_indicator('offline'); + if(Connection.debug) console.log('CONNECTION CLOSED'); + Connection.established = false; + Connection.trigger('close', {}); + } + Connection.socket.onerror = function(error) { + Connection.update_indicator('error'); + console.error('CONNECTION', error); + Connection.established = false; + } + Connection.socket.onopen = function() { + Connection.update_indicator('online'); + if(Connection.debug) console.log('CONNECTION ESTABLISHED'); + Connection.established = true; + Connection.trigger('open', {}); + } + setTimeout(Connection.reconnect, 2000); + + }, + + deauth : () => { + Game.session = {}; + }, + + reconnect : () => { + if(!Connection.established && Connection.auto_reconnect) + Connection.start(); + else + setTimeout(Connection.reconnect, 2000); + }, + + queue : [], + + dequeue : () => { + var dq = Connection.queue; + Connection.queue = []; + dq.forEach(function(fm) { + Connection.send(fm); + }); + }, + + send : (msg) => { + if(Connection.established) { + if(typeof msg == 'function') + msg(); + else + Connection.socket.send(JSON.stringify(msg)); + } else { + Connection.queue.push(msg); + } + }, + + close : () => { + Connection.auto_reconnect = false; + Connection.socket.close(); + }, + +} + +return Connection; + +})); + diff --git a/site/examples/uce-starter/lib/app.uce b/site/examples/uce-starter/lib/app.uce new file mode 100644 index 0000000..dfd8c57 --- /dev/null +++ b/site/examples/uce-starter/lib/app.uce @@ -0,0 +1,564 @@ +String starter_dirname(String path) +{ + if(path == "") + return(""); + auto parts = split(path, "/"); + if(parts.size() <= 1) + return(""); + parts.pop_back(); + String result = join(parts, "/"); + if(result == "") + return("/"); + return(result); +} + +String starter_fs_join(String root, String relative) +{ + if(relative == "") + return(root); + if(relative[0] == '/') + return(relative); + if(root == "" || root == "/") + return(root + relative); + if(root[root.length() - 1] == '/') + return(root + relative); + return(root + "/" + relative); +} + +String starter_safe_name(String raw) +{ + String result = ""; + for(auto c : raw) + { + if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) + result.append(1, c); + else + result.append(1, '_'); + } + return(result); +} + +String starter_json_string(String raw) +{ + DTree value; + value = raw; + return(json_encode(value)); +} + +String starter_fs_root(Request& context) +{ + String root = context.var["starter"]["fs_root"].to_string(); + if(root == "") + root = get_cwd(); + return(root); +} + +String starter_script_url(Request& context) +{ + String url = context.var["starter"]["script_url"].to_string(); + if(url == "") + url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]); + return(url); +} + +String starter_base_url(Request& context) +{ + String base = context.var["starter"]["base_url"].to_string(); + if(base == "") + { + base = starter_dirname(starter_script_url(context)); + if(base == "") + base = "/"; + if(base[base.length() - 1] != '/') + base.append(1, '/'); + } + return(base); +} + +String starter_fs_path(String relative, Request& context) +{ + return(starter_fs_join(starter_fs_root(context), relative)); +} + +DTree starter_cfg(String path, Request& context) +{ + DTree result = context.var["starter"]["config"]; + for(auto segment : split(path, "/")) + { + if(segment == "") + continue; + result = result[segment]; + } + return(result); +} + +String starter_cfg_string(String path, Request& context) +{ + return(starter_cfg(path, context).to_string()); +} + +String starter_link(String path, Request& context); +String starter_link(String path, StringMap params, Request& context); + +String starter_theme_value(String key, Request& context) +{ + return(starter_cfg("theme/" + key, context).to_string()); +} + +String starter_first_route_segment(Request& context) +{ + String query = context.params["QUERY_STRING"]; + for(auto part : split(query, "&")) + { + if(part == "") + continue; + if(part.find("=") == String::npos) + return(uri_decode(part)); + } + return(""); +} + +DTree starter_make_route(Request& context) +{ + DTree route; + String path = starter_first_route_segment(context); + path = trim(path); + while(path.length() > 0 && path[0] == '/') + path = path.substr(1); + while(path.length() > 0 && path[path.length() - 1] == '/') + path = path.substr(0, path.length() - 1); + if(path == "") + path = "index"; + route["l_path"] = path; + route["page"] = nibble(path, "/"); + if(route["page"].to_string() == "") + route["page"] = "index"; + return(route); +} + +DTree starter_resolve_view(Request& context, String base_dir = "views") +{ + DTree result; + DTree route = context.var["starter"]["route"]; + String lpath = first(route["l_path"].to_string(), "index"); + String base = trim(base_dir); + if(base != "" && base[base.length() - 1] == '/') + base = base.substr(0, base.length() - 1); + + String exact = base + "/" + lpath + ".uce"; + if(file_exists(exact)) + { + result["file"] = exact; + return(result); + } + + String dir_index = base + "/" + lpath + "/index.uce"; + if(file_exists(dir_index)) + { + result["file"] = dir_index; + return(result); + } + + auto parts = split(lpath, "/"); + if(parts.size() > 1) + { + String param = parts.back(); + parts.pop_back(); + String parent = join(parts, "/"); + String parent_index = base + "/" + parent + "/index.uce"; + if(file_exists(parent_index)) + { + result["file"] = parent_index; + result["param"] = param; + return(result); + } + } + + return(result); +} + +DTree starter_default_config() +{ + DTree config; + + config["site"]["name"] = "UCE Starter"; + config["site"]["default_page_title"] = "Home"; + config["site"]["timezone"] = "UTC"; + + config["users"]["enable_signup"].set_bool(true); + config["filebase"]["path"] = "/tmp/uce-starter-data"; + + config["theme"]["key"] = "portal-dark"; + config["theme"]["options"]["light"]["label"] = "Starter Light"; + config["theme"]["options"]["light"]["path"] = "themes/light/"; + config["theme"]["options"]["light"]["mode"] = "light"; + config["theme"]["options"]["light"]["description"] = "Original starter light theme kept for backward compatibility."; + config["theme"]["options"]["light"]["footer_text"] = "UCE Starter running with the Starter Light theme."; + config["theme"]["options"]["light"]["meta_description"] = "Starter Light theme for the UCE starter"; + config["theme"]["options"]["light"]["theme_color"] = "#3b82f6"; + + config["theme"]["options"]["dark"]["label"] = "Starter Dark"; + config["theme"]["options"]["dark"]["path"] = "themes/dark/"; + config["theme"]["options"]["dark"]["mode"] = "dark"; + config["theme"]["options"]["dark"]["description"] = "Original starter dark theme kept for backward compatibility."; + config["theme"]["options"]["dark"]["footer_text"] = "UCE Starter running with the Starter Dark theme."; + config["theme"]["options"]["dark"]["meta_description"] = "Starter Dark theme for the UCE starter"; + config["theme"]["options"]["dark"]["theme_color"] = "#0f172a"; + + config["theme"]["options"]["portal-light"]["label"] = "AI Portal Light"; + config["theme"]["options"]["portal-light"]["path"] = "themes/portal-light/"; + config["theme"]["options"]["portal-light"]["mode"] = "light"; + config["theme"]["options"]["portal-light"]["description"] = "Dense, corporate portal layout in a light palette."; + config["theme"]["options"]["portal-light"]["footer_text"] = "UCE Starter running with the AI Portal Light starter theme."; + config["theme"]["options"]["portal-light"]["meta_description"] = "AI Portal Light theme for the UCE starter"; + config["theme"]["options"]["portal-light"]["theme_color"] = "#0f68ad"; + + config["theme"]["options"]["portal-dark"]["label"] = "AI Portal Dark"; + config["theme"]["options"]["portal-dark"]["path"] = "themes/portal-dark/"; + config["theme"]["options"]["portal-dark"]["mode"] = "dark"; + config["theme"]["options"]["portal-dark"]["description"] = "Glassy dark portal shell and the current default starter theme."; + config["theme"]["options"]["portal-dark"]["footer_text"] = "UCE Starter running with the AI Portal Dark starter theme."; + config["theme"]["options"]["portal-dark"]["meta_description"] = "AI Portal Dark theme for the UCE starter"; + config["theme"]["options"]["portal-dark"]["theme_color"] = "#0f172a"; + + config["theme"]["options"]["localfirst"]["label"] = "Local First"; + config["theme"]["options"]["localfirst"]["path"] = "themes/localfirst/"; + config["theme"]["options"]["localfirst"]["mode"] = "dark"; + config["theme"]["options"]["localfirst"]["description"] = "llm2-derived admin shell with sidebar chrome."; + config["theme"]["options"]["localfirst"]["footer_text"] = "UCE Starter running with the Local First starter theme."; + config["theme"]["options"]["localfirst"]["meta_description"] = "Local First admin-shell theme for the UCE starter"; + config["theme"]["options"]["localfirst"]["theme_color"] = "#020913"; + + config["theme"]["options"]["retro-gaming"]["label"] = "Retro Gaming"; + config["theme"]["options"]["retro-gaming"]["path"] = "themes/retro-gaming/"; + config["theme"]["options"]["retro-gaming"]["mode"] = "dark"; + config["theme"]["options"]["retro-gaming"]["description"] = "CRT scanlines, pixel font, neon glow."; + config["theme"]["options"]["retro-gaming"]["footer_text"] = "INSERT COIN // UCE Starter running the Retro Gaming theme."; + config["theme"]["options"]["retro-gaming"]["meta_description"] = "Retro Gaming pixel theme for the UCE starter"; + config["theme"]["options"]["retro-gaming"]["theme_color"] = "#0a0a1a"; + + config["menu"][""]["title"] = "Home"; + config["menu"][""]["hidden"].set_bool(true); + config["menu"]["page1"]["title"] = "Components"; + config["menu"]["features"]["title"] = "Features"; + config["menu"]["page2"]["title"] = "Ajaxy"; + config["menu"]["gauges"]["title"] = "Gauges"; + config["menu"]["dashboard"]["title"] = "Dashboard"; + config["menu"]["workspace"]["title"] = "Workspace"; + config["menu"]["themes"]["title"] = "Themes"; + config["menu"]["auth/demo"]["title"] = "Auth"; + + return(config); +} + +void starter_set_status(Request& context, s32 code, String reason) +{ + context.response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason; + context.flags.status = code; +} + +void starter_register_css(String path, Request& context) +{ + context.var["starter"]["assets"]["css"][path] = path; +} + +void starter_register_js(String path, Request& context) +{ + context.var["starter"]["assets"]["js"][path] = path; +} + +String starter_asset_url(String path, Request& context) +{ + String url = starter_base_url(context) + path; + String fs_path = starter_fs_path(path, context); + if(file_exists(fs_path)) + url += "?v=" + std::to_string((u64)file_mtime(fs_path)); + return(url); +} + +void starter_render_registered_css(Request& context) +{ + context.var["starter"]["assets"]["css"].each([&](DTree item, String key) { + String path = item.to_string(); + if(path != "") + { + <> + } + }); +} + +void starter_render_registered_js(Request& context) +{ + context.var["starter"]["assets"]["js"].each([&](DTree item, String key) { + String path = item.to_string(); + if(path != "") + { + <> + } + }); +} + +String starter_link(String path, Request& context) +{ + StringMap params; + return(starter_link(path, params, context)); +} + +String starter_link(String path, StringMap params, Request& context) +{ + String query = ""; + path = trim(path); + if(path != "") + query = uri_encode(path); + String extra = encode_query(params); + if(query != "" && extra != "") + query += "&" + extra; + else if(query == "") + query = extra; + String url = starter_script_url(context); + if(query != "") + url += "?" + query; + return(url); +} + +String starter_filebase_root(Request& context) +{ + String root = starter_cfg_string("filebase/path", context); + if(root == "") + root = "/tmp/uce-starter-data"; + return(root); +} + +String starter_hash_id(String raw) +{ + raw = to_lower(trim(raw)); + return(gen_sha1("uce-starter:" + raw).substr(0, 12)); +} + +String starter_user_dir(String email, Request& context) +{ + String bucket = starter_hash_id(email); + return(starter_filebase_root(context) + "/users/" + bucket.substr(0, 2) + "/" + bucket); +} + +String starter_user_file(String email, Request& context) +{ + return(starter_user_dir(email, context) + "/account.json"); +} + +String starter_password_hash(String password, String salt) +{ + String hash = "uce-starter:" + password + ":" + salt; + for(s32 i = 0; i < 2048; i++) + hash = gen_sha1(hash + ":" + std::to_string(i)); + return(hash); +} + +DTree starter_read_json_file(String file_name) +{ + DTree result; + if(!file_exists(file_name)) + return(result); + String raw = trim(file_get_contents(file_name)); + if(raw == "") + return(result); + return(json_decode(raw)); +} + +bool starter_write_json_file(String file_name, DTree data) +{ + mkdir(starter_dirname(file_name)); + return(file_put_contents(file_name, json_encode(data))); +} + +DTree starter_user_load(String email, Request& context) +{ + DTree user = starter_read_json_file(starter_user_file(email, context)); + if(user.get_type_name() != "array") + return(DTree()); + user["id"] = to_lower(trim(email)); + return(user); +} + +DTree starter_user_create(String email, String password, Request& context) +{ + DTree result; + email = to_lower(trim(email)); + password = trim(password); + result["message"] = ""; + result["result"].set_bool(false); + + if(email == "" || password == "") + { + result["message"] = "email_and_password_required"; + return(result); + } + if(email.find("@") == String::npos) + { + result["message"] = "invalid_email"; + return(result); + } + + DTree existing = starter_user_load(email, context); + if(existing.get_type_name() == "array" && existing["email"].to_string() != "") + { + result["message"] = "user_exists"; + return(result); + } + + String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(microtime() * 1000000.0)) + ":" + make_session_id()).substr(0, 24); + DTree user; + user["email"] = email; + user["salt"] = salt; + user["password_hash"] = starter_password_hash(password, salt); + user["created"] = std::to_string((u64)time()); + DTree role; + role = "user"; + user["roles"].push(role); + + starter_write_json_file(starter_user_file(email, context), user); + user["id"] = email; + result["result"].set_bool(true); + result["id"] = email; + result["profile"] = user; + return(result); +} + +DTree starter_user_sign_in(String email, String password, Request& context) +{ + DTree result; + result["result"].set_bool(false); + email = to_lower(trim(email)); + password = trim(password); + + DTree user = starter_user_load(email, context); + if(user.get_type_name() != "array" || user["email"].to_string() == "") + { + result["message"] = "no_such_user"; + return(result); + } + if(user["password_hash"].to_string() == "") + { + result["message"] = "no_password_set"; + return(result); + } + + String expected = starter_password_hash(password, user["salt"].to_string()); + if(expected != user["password_hash"].to_string()) + { + result["message"] = "invalid_password"; + return(result); + } + + session_start(); + context.session["starter_user_id"] = email; + context.var["starter"]["current_user"] = user; + result["result"].set_bool(true); + result["profile"] = user; + return(result); +} + +bool starter_is_signed_in(Request& context) +{ + if(context.var["starter"]["current_user"]["email"].to_string() != "") + return(true); + String user_id = context.session["starter_user_id"]; + if(user_id == "") + return(false); + DTree user = starter_user_load(user_id, context); + if(user["email"].to_string() == "") + { + context.session.erase("starter_user_id"); + return(false); + } + context.var["starter"]["current_user"] = user; + return(true); +} + +DTree starter_current_user(Request& context) +{ + if(starter_is_signed_in(context)) + return(context.var["starter"]["current_user"]); + return(DTree()); +} + +void starter_user_logout(Request& context) +{ + context.session.erase("starter_user_id"); + context.var["starter"]["current_user"].clear(); +} + +void starter_redirect(String path, Request& context) +{ + starter_set_status(context, 302, "Found"); + context.header["Location"] = starter_link(path, context); +} + +void starter_not_found(String message, Request& context) +{ + starter_set_status(context, 404, "Not Found"); + context.var["starter"]["error"] = message; + context.var["starter"]["page_title"] = "404 Not Found"; +} + +String starter_menu_href(String menu_key, DTree menu_item, Request& context) +{ + if(menu_item["external"].to_string() != "") + return("/" + menu_key); + return(starter_link(menu_key, context)); +} + +String starter_html_class(Request& context) +{ + String classes = "no-js"; + if(starter_theme_value("mode", context) == "dark") + classes += " dark-theme"; + return(classes); +} + +void starter_boot(Request& context) +{ + if(context.var["starter"]["booted"].to_string() == "1") + return; + + context.var["starter"]["booted"] = "1"; + context.var["starter"]["fs_root"] = get_cwd(); + context.var["starter"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]); + + String base_url = starter_dirname(context.var["starter"]["script_url"].to_string()); + if(base_url == "") + base_url = "/"; + if(base_url[base_url.length() - 1] != '/') + base_url.append(1, '/'); + context.var["starter"]["base_url"] = base_url; + + DTree config = starter_default_config(); + String requested_theme = first(context.get["theme"], context.cookies["starter_theme"], config["theme"]["key"].to_string()); + if(config["theme"]["options"][requested_theme].to_string() == "" && config["theme"]["options"][requested_theme].get_type_name() != "array") + requested_theme = config["theme"]["key"].to_string(); + + config["theme"]["options"][requested_theme].each([&](DTree item, String key) { + config["theme"][key] = item; + }); + config["theme"]["key"] = requested_theme; + context.var["starter"]["config"] = config; + context.var["starter"]["route"] = starter_make_route(context); + context.var["starter"]["page_type"] = "html"; + context.var["starter"]["page_title"] = first(config["menu"][context.var["starter"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home"); + context.var["starter"]["embed_mode"].set_bool(context.get["embed"] != ""); + + if(context.get["theme"] != "" && context.get["theme"] == requested_theme) + { + set_cookie("starter_theme", requested_theme, time() + (86400 * 180)); + context.cookies["starter_theme"] = requested_theme; + } + + session_start(); + starter_is_signed_in(context); + + starter_register_css(starter_theme_value("path", context) + "css/style.css", context); + starter_register_css("themes/common/fontawesome/css/all.min.css", context); + starter_register_js("js/u-query.js", context); + starter_register_js("js/morphdom.js", context); + starter_register_js("js/site.js", context); +} diff --git a/site/examples/uce-starter/themes/common/css/gauges.css b/site/examples/uce-starter/themes/common/css/gauges.css new file mode 100644 index 0000000..1c465da --- /dev/null +++ b/site/examples/uce-starter/themes/common/css/gauges.css @@ -0,0 +1,348 @@ +.gauge-demo-row { + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: stretch; + margin-bottom: 1.5rem; +} + +.gauge-control-panel { + flex: 1 1 18rem; + min-width: 16rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-control-panel h4 { + margin: 0 0 0.75rem; + color: var(--text-primary); +} + +.gauge-slider-group { + display: grid; + gap: 0.35rem; + margin-bottom: 0.9rem; +} + +.gauge-slider-group label { + color: var(--text-secondary); + font-weight: 600; + font-size: 0.92rem; +} + +.gauge-slider-group input[type="range"] { + width: 100%; + accent-color: var(--primary); +} + +.gauge-set, +.arcgauge-set { + flex: 1 1 24rem; + min-width: 20rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-set-header, +.arcgauge-set-header { + margin-bottom: 1rem; +} + +.gauge-set-header h3, +.arcgauge-set-header h3 { + margin: 0 0 0.3rem; + color: var(--text-primary); +} + +.gauge-set-header p, +.arcgauge-set-header p { + margin: 0; + color: var(--text-secondary); +} + +.gauge-card, +.arcgauge-card { + display: flex; + flex-direction: column; + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-elevated, var(--surface)); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: relative; + overflow: hidden; +} + +.gauge-card::before, +.arcgauge-card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.5; +} + +.gauge-metric-label { + color: var(--text-secondary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.gauge-metric-value { + color: var(--text-primary); + font-size: 1.1rem; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); + letter-spacing: -0.03em; +} + +.gauge-metric-meta { + color: var(--text-muted); + font-size: 0.84rem; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.progressbar-set-horizontal, +.progressbar-set-vertical { + align-self: stretch; +} + +.progressbar-grid-horizontal { + display: grid; + gap: 0.85rem; +} + +.progressbar-grid-vertical { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + align-items: stretch; +} + +.progressbar-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.7rem; +} + +.progressbar-card-horizontal { + gap: 0.7rem; +} + +.progressbar-card-vertical { + align-items: center; + justify-content: flex-end; + text-align: center; + gap: 0.7rem; +} + +.progressbar-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.progressbar-background { + position: relative; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent 30%); + background: color-mix(in srgb, var(--bg-color) 72%, var(--surface) 28%); + display: flex; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); +} + +.progressbar-background-horizontal { + height: 0.9rem; + border-radius: 999px; +} + +.progressbar-background-vertical { + height: var(--progressbar-height, 240px); + width: 100%; + max-width: 4rem; + margin: 0 auto; + border-radius: 1.2rem; + justify-content: flex-end; + padding: 0.35rem; + flex-direction: column; +} + + +.progressbar-bar { + transition: all 0.35s ease; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12), 0 0 24px color-mix(in srgb, currentColor 28%, transparent 72%); +} + +.progressbar-background-horizontal .progressbar-bar { + height: 100%; + border-radius: 999px; +} + +.progressbar-background-vertical .progressbar-bar { + width: 100%; + border-radius: 0.95rem; +} + +.progressbar-marker { + position: absolute; + z-index: 10; + opacity: 0.72; + background: var(--text-color, var(--text-primary, #333)); + box-shadow: 0 0 0 1px rgba(255,255,255,0.16); +} + +.progressbar-marker:hover { + opacity: 1; +} + +.progressbar-background-horizontal .progressbar-marker { + height: 100%; + top: 0; + width: 3px; + transform: translateX(-50%); +} + +.progressbar-background-vertical .progressbar-marker { + width: 100%; + left: 0; + height: 3px; + transform: translateY(50%); +} + +.needlegauge-svg .needle { + transition: all 0.3s ease; +} + +.progressbar-meta { + margin-top: 0.1rem; +} + +.needlegauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); +} + +.needlegauge-card { + align-items: center; + text-align: center; + gap: 0.55rem; +} + +.needlegauge-label-head { + align-self: flex-start; +} + +.needlegauge-visual { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.needlegauge-svg { + max-width: 100%; + overflow: visible; +} + + +.needlegauge-svg .ticks line { + opacity: 0.8; +} + +.needlegauge-svg .ticks text { + font-family: var(--font-mono, ui-monospace, monospace); + fill: var(--text-muted); +} + +.needlegauge-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.arcgauge-label { + margin-bottom: 0.35rem; +} + +.arcgauge-svg { + width: 130px; + height: 72px; + margin: 0.1rem 0 0.35rem; + overflow: visible; +} + +.arcgauge-track { + stroke: color-mix(in srgb, var(--border) 70%, transparent 30%); +} + +.arcgauge-arc-dyn { + transition: stroke-dasharray 0.6s ease, stroke 0.6s ease; +} + +.arcgauge-watermark { + stroke-width: 1.5; + stroke-linecap: round; +} + +.arcgauge-watermark-lo { + stroke: var(--primary); +} + +.arcgauge-watermark-hi { + stroke: var(--accent); +} + +.arcgauge-value { + fill: var(--text-primary); + font-size: 17px; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.arcgauge-caption { + fill: var(--text-muted); + font-size: 7px; + letter-spacing: 1.5px; + font-family: var(--font-sans, system-ui, sans-serif); +} + +.arcgauge-meta { + width: 100%; + text-align: center; +} + +.arcgauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); +} + +.arcgauge-card { + align-items: center; + text-align: center; +} + +@media (max-width: 768px) { + .gauge-demo-row { + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/css/workspace.css b/site/examples/uce-starter/themes/common/css/workspace.css new file mode 100644 index 0000000..b0575cb --- /dev/null +++ b/site/examples/uce-starter/themes/common/css/workspace.css @@ -0,0 +1,522 @@ +.ws-app { + --ws-sidebar-width: 280px; + --ws-frame-bg: var(--surface); + --ws-sidebar-bg: color-mix(in srgb, var(--bg-secondary) 80%, var(--surface) 20%); + --ws-main-bg: var(--surface); + --ws-surface-alt: color-mix(in srgb, var(--bg-secondary) 72%, var(--surface) 28%); + --ws-shadow: var(--shadow-md); + display: flex; + width: 100%; + min-height: min(76vh, 900px); + overflow: hidden; + position: relative; + background: var(--ws-frame-bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--ws-shadow); +} + +.ws-sidebar { + width: var(--ws-sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + min-height: 0; + background: var(--ws-sidebar-bg); + border-right: 1px solid var(--border); + position: relative; + z-index: 2; +} + +.ws-main { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--ws-main-bg); + padding: 0; +} + +.ws-sidebar-top, +.ws-panel-header, +.ws-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.ws-sidebar-top { + padding: 0.9rem; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + flex-direction: column; + align-items: stretch; + background: color-mix(in srgb, var(--surface) 78%, var(--bg-secondary) 22%); +} + +.ws-search-wrap { + position: relative; +} + +.ws-search-icon { + position: absolute; + left: 0.7rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 0.8rem; + pointer-events: none; +} + +.ws-search-input { + width: 100%; + min-height: 36px; + padding: 0 0.8rem 0 2rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + font-size: 0.9rem; + color: var(--text-primary); +} + +.ws-search-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent 80%); +} + +.ws-panel-title-group, +.ws-header-actions { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.ws-panel-title-group { + flex-direction: column; + align-items: flex-start; +} + +.ws-panel-title-group h2, +.ws-section-head h3, +.ws-empty-state h2 { + margin: 0; +} + +.ws-subtitle { + margin: 0; + color: var(--text-secondary); + font-size: 0.92rem; +} + +.ws-sidebar-overlay { + display: none; +} + +.ws-mobile-bar { + display: none; + align-items: center; + gap: 0.7rem; + padding: 0.9rem 1rem; + border-bottom: 1px solid var(--border); + background: color-mix(in srgb, var(--surface) 92%, var(--bg-secondary) 8%); + position: sticky; + top: 0; + z-index: 1; +} + +.ws-mobile-title { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); +} + +.ws-empty-state { + min-height: 320px; + display: grid; + place-items: center; + align-content: center; + gap: 0.75rem; + text-align: center; + padding: 2rem; + max-width: 40rem; + margin: auto; +} + +.ws-empty-state p { + max-width: 32rem; + color: var(--text-secondary); + margin: 0; +} + +.ws-empty-icon { + width: 64px; + height: 64px; + display: grid; + place-items: center; + border-radius: var(--radius); + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: #fff; + font-size: 24px; + box-shadow: var(--shadow-sm); +} + +.ws-panel { + min-height: 0; + display: flex; + flex-direction: column; + padding: 1.1rem 1.25rem; + gap: 1rem; +} + +.ws-section { + display: flex; + flex-direction: column; + gap: 0.8rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--ws-surface-alt); +} + +.ws-section + .ws-section { + margin-top: 0.25rem; +} + +.ws-icon-btn { + min-width: 34px; + height: 34px; + padding: 0 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + flex-shrink: 0; + background: var(--bg-secondary); + color: var(--text-secondary); + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease, border-color 0.2s ease; + text-decoration: none; +} + +.ws-icon-btn:hover { + background: var(--surface-hover); + border-color: var(--border-hover); + color: var(--text-primary); + text-decoration: none; +} + +.ws-icon-btn:active { + transform: translateY(1px); +} + +.ws-primary-btn, +.ws-sidebar-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + width: 100%; + min-height: 38px; + padding: 0.55rem 0.85rem; + border: none; + border-radius: var(--radius-sm); + background: var(--primary); + color: #fff; + font-size: 0.92rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; +} + +.ws-primary-btn:hover, +.ws-sidebar-action-btn:hover { + background: var(--primary-dark); + text-decoration: none; + color: #fff; +} + +.ws-primary-btn:active, +.ws-sidebar-action-btn:active { + transform: translateY(1px); +} + +.ws-list-state { + padding: 1rem; + color: var(--text-secondary); + font-size: 0.86rem; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + border-top: 1px solid var(--border); +} + +.ws-nav-group-label { + padding: 0.7rem 0.95rem 0.35rem; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + user-select: none; +} + +.ws-nav-list { + display: flex; + flex-direction: column; + padding: 0.3rem 0.4rem 0.9rem; + gap: 0.2rem; + overflow: auto; + min-height: 0; + flex: 1; +} + +.ws-nav-item { + display: block; + padding: 0 0.2rem; + text-decoration: none; + color: inherit; +} + +.ws-nav-item-inner { + display: flex; + align-items: center; + gap: 0.55rem; + width: 100%; + padding: 0.58rem 0.65rem; + border-radius: var(--radius-sm); + min-width: 0; + transition: background 0.2s ease, border-color 0.2s ease, padding-left 0.2s ease; + border-left: 3px solid transparent; +} + +.ws-nav-item:hover .ws-nav-item-inner { + background: color-mix(in srgb, var(--surface-hover) 85%, transparent 15%); +} + +.ws-nav-item.active .ws-nav-item-inner, +.ws-nav-item.is-active .ws-nav-item-inner { + background: color-mix(in srgb, var(--primary) 12%, transparent 88%); + border-left-color: var(--primary); + padding-left: calc(0.65rem - 3px); +} + +.ws-nav-icon { + flex-shrink: 0; + width: 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; +} + +.ws-nav-item.active .ws-nav-icon, +.ws-nav-item.is-active .ws-nav-icon { + color: var(--primary); +} + +.ws-nav-item-text { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.45rem; + overflow: hidden; +} + +.ws-nav-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.9rem; + color: var(--text-primary); + line-height: 1.3; +} + +.ws-nav-item.active .ws-nav-title, +.ws-nav-item.is-active .ws-nav-title { + font-weight: 700; + color: var(--primary-dark); +} + +.ws-nav-meta { + flex-shrink: 0; + font-size: 0.72rem; + color: var(--text-muted); + transition: opacity 0.2s ease; +} + +.ws-status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0.2rem 0.6rem; + border-radius: 999px; + border: 1px solid transparent; + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.ws-status-pill-neutral { + background: color-mix(in srgb, var(--text-muted) 16%, transparent 84%); + border-color: color-mix(in srgb, var(--text-muted) 28%, transparent 72%); + color: var(--text-secondary); +} + +.ws-status-pill-success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +.ws-status-pill-warn { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + +.ws-status-pill-danger { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); +} + +.ws-status-pill-info { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); +} + +.ws-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.85rem; +} + +.ws-stat-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.ws-stat-card-label { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + color: var(--text-secondary); +} + +.ws-stat-card-value { + font-size: 1.45rem; + font-weight: 700; + line-height: 1.1; + font-variant-numeric: tabular-nums; +} + +.ws-stat-card-meta, +.ws-section-copy, +.ws-inline-note, +.ws-detail-copy { + margin: 0; + color: var(--text-secondary); +} + +.ws-detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.85rem; +} + +.ws-detail-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--bg-secondary) 85%, var(--surface) 15%); + min-height: 100%; +} + +.ws-detail-card h4 { + margin: 0 0 0.35rem; + font-size: 0.95rem; +} + +.ws-detail-card p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.ws-demo-sidebar-copy { + padding: 0 1rem 1rem; + font-size: 0.84rem; + color: var(--text-secondary); +} + +.ws-demo-sidebar-copy p { + margin: 0; +} + +@media (max-width: 860px) { + .ws-app { + display: block; + min-height: 72vh; + } + + .ws-sidebar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: min(88vw, max(var(--ws-sidebar-width), 320px)); + transform: translateX(-100%); + transition: transform 160ms ease; + box-shadow: var(--shadow-lg); + z-index: 10; + } + + .ws-sidebar.is-open { + transform: translateX(0); + } + + .ws-sidebar-overlay.is-open { + display: block; + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.38); + z-index: 5; + } + + .ws-mobile-bar { + display: flex; + } + + .ws-panel { + padding: 1rem; + } + + .ws-stat-grid, + .ws-detail-grid { + grid-template-columns: 1fr; + } + + .ws-section-head, + .ws-panel-header { + align-items: flex-start; + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/fontawesome/LICENSE.txt b/site/examples/uce-starter/themes/common/fontawesome/LICENSE.txt new file mode 100755 index 0000000..f31bef9 --- /dev/null +++ b/site/examples/uce-starter/themes/common/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/site/examples/uce-starter/themes/common/fontawesome/css/all.min.css b/site/examples/uce-starter/themes/common/fontawesome/css/all.min.css new file mode 100755 index 0000000..0f922d8 --- /dev/null +++ b/site/examples/uce-starter/themes/common/fontawesome/css/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/fontawesome/css/fontawesome.min.css b/site/examples/uce-starter/themes/common/fontawesome/css/fontawesome.min.css new file mode 100755 index 0000000..ab52c55 --- /dev/null +++ b/site/examples/uce-starter/themes/common/fontawesome/css/fontawesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot new file mode 100755 index 0000000..30a2784 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg new file mode 100755 index 0000000..47bb690 --- /dev/null +++ b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg @@ -0,0 +1,3451 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf new file mode 100755 index 0000000..a7ab4d4 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff new file mode 100755 index 0000000..ec5b613 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100755 index 0000000..df11cea Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot new file mode 100755 index 0000000..b5440c9 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg new file mode 100755 index 0000000..5ec81a8 --- /dev/null +++ b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf new file mode 100755 index 0000000..87693a8 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff new file mode 100755 index 0000000..917bf73 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100755 index 0000000..0f10115 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot new file mode 100755 index 0000000..305fc64 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg new file mode 100755 index 0000000..eb47f6a --- /dev/null +++ b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,4649 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:21 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf new file mode 100755 index 0000000..8fb4d53 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff new file mode 100755 index 0000000..69f4474 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff differ diff --git a/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100755 index 0000000..20e4ce2 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-bold-italic.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-bold-italic.ttf new file mode 100755 index 0000000..049965e Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-bold-italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-bold.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-bold.ttf new file mode 100755 index 0000000..3c92e2e Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-bold.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-italic.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-italic.ttf new file mode 100755 index 0000000..01ec3c5 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf new file mode 100755 index 0000000..ec2a812 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf new file mode 100755 index 0000000..fb26114 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-italic.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-italic.ttf new file mode 100755 index 0000000..af20348 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf new file mode 100755 index 0000000..aaac636 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/b612/b612-regular.ttf b/site/examples/uce-starter/themes/common/fonts/b612/b612-regular.ttf new file mode 100755 index 0000000..c685a23 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/b612/b612-regular.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf new file mode 100755 index 0000000..4c0ee56 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf new file mode 100755 index 0000000..fa51576 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf new file mode 100755 index 0000000..3cb9269 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf new file mode 100755 index 0000000..010ed45 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf new file mode 100755 index 0000000..8550ed9 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf new file mode 100755 index 0000000..2e93072 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf new file mode 100755 index 0000000..592caec Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf new file mode 100755 index 0000000..1f42361 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf new file mode 100755 index 0000000..ff1769f Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf new file mode 100755 index 0000000..dcd2c27 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf new file mode 100755 index 0000000..18da8c6 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf new file mode 100755 index 0000000..0af4e38 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf new file mode 100755 index 0000000..09cc053 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf new file mode 100755 index 0000000..e25a540 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/press-start-2p/OFL.txt b/site/examples/uce-starter/themes/common/fonts/press-start-2p/OFL.txt new file mode 100644 index 0000000..22796df --- /dev/null +++ b/site/examples/uce-starter/themes/common/fonts/press-start-2p/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf b/site/examples/uce-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Black.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Black.ttf new file mode 100755 index 0000000..689fe5c Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Black.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100755 index 0000000..0b4e0ee Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Bold.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Bold.ttf new file mode 100755 index 0000000..d3f01ad Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Bold.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100755 index 0000000..41cc1e7 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Italic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Italic.ttf new file mode 100755 index 0000000..6a1cee5 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Light.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Light.ttf new file mode 100755 index 0000000..219063a Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Light.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf new file mode 100755 index 0000000..0e81e87 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Medium.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Medium.ttf new file mode 100755 index 0000000..1a7f3b0 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Medium.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100755 index 0000000..0030295 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Regular.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Regular.ttf new file mode 100755 index 0000000..2c97eea Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Regular.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Thin.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Thin.ttf new file mode 100755 index 0000000..b74a4fd Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-Thin.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100755 index 0000000..dd0ddb8 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf new file mode 100755 index 0000000..fc28868 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf new file mode 100755 index 0000000..e1a648f Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf new file mode 100755 index 0000000..97ff9f1 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf new file mode 100755 index 0000000..2dae31e Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf new file mode 100755 index 0000000..da108d3 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf differ diff --git a/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf new file mode 100755 index 0000000..c2304c1 Binary files /dev/null and b/site/examples/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf differ diff --git a/site/examples/uce-starter/themes/common/page.blank.php b/site/examples/uce-starter/themes/common/page.blank.php new file mode 100755 index 0000000..92858e3 --- /dev/null +++ b/site/examples/uce-starter/themes/common/page.blank.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/examples/uce-starter/themes/common/page.json.php b/site/examples/uce-starter/themes/common/page.json.php new file mode 100755 index 0000000..89fe5e2 --- /dev/null +++ b/site/examples/uce-starter/themes/common/page.json.php @@ -0,0 +1,12 @@ + a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +nav.nav-scrolled { + background: rgba(30, 41, 59, 0.95) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} + +h1::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: var(--bg-gradient); + border-radius: 3px; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px var(--success-bg); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px var(--error-bg); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #8b5cf6; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: var(--bg-color); +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/examples/blog/icon.png b/site/examples/uce-starter/themes/dark/icon.png old mode 100644 new mode 100755 similarity index 100% rename from examples/blog/icon.png rename to site/examples/uce-starter/themes/dark/icon.png diff --git a/site/examples/uce-starter/themes/dark/page.html.php b/site/examples/uce-starter/themes/dark/page.html.php new file mode 100755 index 0000000..f3b3b1f --- /dev/null +++ b/site/examples/uce-starter/themes/dark/page.html.php @@ -0,0 +1,21 @@ + + + + + +> + + false, + 'account_wrapper_class' => 'nav-account nav-menu', + )); ?> +
> + +
+ $embed_mode)); ?> + + + diff --git a/site/examples/uce-starter/themes/light/css/style.css b/site/examples/uce-starter/themes/light/css/style.css new file mode 100755 index 0000000..b872b44 --- /dev/null +++ b/site/examples/uce-starter/themes/light/css/style.css @@ -0,0 +1,540 @@ +:root { + /* Light theme color palette */ + --bg-color: #f8fafc; + --bg-secondary: #ffffff; + --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --surface: #ffffff; + --surface-elevated: #ffffff; + --surface-hover: #f1f5f9; + --primary: #3b82f6; + --primary-dark: #1d4ed8; + --primary-light: #93c5fd; + --secondary: #8b5cf6; + --accent: #06b6d4; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --border: #e2e8f0; + --border-hover: #cbd5e1; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + + /* Light theme specific variables */ + --glass-bg: rgba(255, 255, 255, 0.95); + --glass-border: rgba(0, 0, 0, 0.1); + --overlay: rgba(0, 0, 0, 0.3); + --success: #10b981; + --success-bg: #dcfce7; + --success-border: #16a34a; + --success-text: #065f46; + --warning: #f59e0b; + --warning-bg: #fef3c7; + --warning-border: #d97706; + --warning-text: #92400e; + --error: #ef4444; + --error-bg: #fee2e2; + --error-border: #dc2626; + --error-text: #991b1b; + --info: #3b82f6; + --info-bg: #dbeafe; + --info-border: #2563eb; + --info-text: #1e40af; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* Links */ +a { + color: var(--primary); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--primary-dark); + text-decoration: underline; +} + +/* Modern Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +nav > a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + margin-right: 0.5rem; + font-size: 0.95rem; +} + +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} + +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary-dark); +} + +/* Navigation scroll state */ +nav.nav-scrolled { + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +/* Main Content Area */ +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2.5rem; + background: var(--surface); + padding: 1.75rem 2rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--border); + position: relative; + overflow: hidden; +} + +h1::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--bg-gradient); +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #7c3aed; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/examples/uce-starter/themes/light/icon.png b/site/examples/uce-starter/themes/light/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/examples/uce-starter/themes/light/icon.png differ diff --git a/site/examples/uce-starter/themes/light/img/favicon.png b/site/examples/uce-starter/themes/light/img/favicon.png new file mode 100755 index 0000000..6653d41 Binary files /dev/null and b/site/examples/uce-starter/themes/light/img/favicon.png differ diff --git a/site/examples/uce-starter/themes/light/page.html.php b/site/examples/uce-starter/themes/light/page.html.php new file mode 100755 index 0000000..1df8f11 --- /dev/null +++ b/site/examples/uce-starter/themes/light/page.html.php @@ -0,0 +1,19 @@ + + + + + +> + + false)); ?> +
> + +
+ $embed_mode)); ?> + + + + diff --git a/site/examples/uce-starter/themes/localfirst/css/style.css b/site/examples/uce-starter/themes/localfirst/css/style.css new file mode 100644 index 0000000..088ce45 --- /dev/null +++ b/site/examples/uce-starter/themes/localfirst/css/style.css @@ -0,0 +1,382 @@ +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.28); } + 50% { opacity: 0.6; box-shadow: 0 0 4px var(--accent), 0 0 8px rgba(255,107,26,0.16); } +} + +:root { + --brand-cyan: #00b7f9; + --brand-cyan-soft: #58d2ff; + --brand-cyan-bright: #b6eeff; + --brand-cyan-deep: #0088bb; + --brand-orange: #ff6b1a; + --brand-orange-soft: #ff9f6a; + --brand-orange-bright: #ffd0b3; + --bg-color: #020913; + --bg-secondary: #07131f; + --surface: rgba(7, 19, 31, 0.9); + --surface-elevated: rgba(10, 28, 43, 0.96); + --surface-hover: rgba(0, 183, 249, 0.08); + --card: rgba(7, 19, 31, 0.84); + --text-primary: var(--brand-cyan-soft); + --text-secondary: rgba(139, 222, 255, 0.82); + --text-muted: rgba(101, 161, 188, 0.78); + --primary: var(--brand-cyan); + --primary-light: rgba(0, 183, 249, 0.18); + --primary-dark: var(--brand-cyan-deep); + --secondary: var(--brand-cyan-soft); + --accent: var(--brand-orange); + --success: #34d399; + --success-bg: rgba(52, 211, 153, 0.1); + --success-border: rgba(52, 211, 153, 0.3); + --success-text: #7ef0bb; + --warning: var(--brand-orange); + --warning-bg: rgba(255, 107, 26, 0.12); + --warning-border: rgba(255, 107, 26, 0.32); + --warning-text: #ffc29f; + --error: #fb7185; + --error-bg: rgba(251, 113, 133, 0.12); + --error-border: rgba(251, 113, 133, 0.3); + --error-text: #fecdd3; + --info: var(--brand-cyan); + --info-bg: rgba(0, 183, 249, 0.1); + --info-border: rgba(0, 183, 249, 0.28); + --info-text: var(--brand-cyan-bright); + --border: rgba(67, 149, 182, 0.22); + --border-hover: rgba(98, 205, 246, 0.36); + --shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.18); + --shadow-md: 0 14px 32px rgba(0, 0, 0, 0.24); + --shadow-lg: 0 18px 38px rgba(0, 0, 0, 0.28); + --shadow-xl: 0 22px 48px rgba(0, 0, 0, 0.34); + --radius: 10px; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 20px; + --font-sans: 'B612', 'Segoe UI', sans-serif; + --font-mono: 'B612 Mono', ui-monospace, monospace; +} + +* { box-sizing: border-box; margin: 0; } +html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +body { + font-family: var(--font-sans); + font-size: 14px; + background: var(--bg-color); + color: var(--text-primary); + min-height: 100vh; +} +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 10% 0%, rgba(0,183,249,0.12) 0%, transparent 60%), + radial-gradient(ellipse 60% 50% at 90% 100%, rgba(255,107,26,0.08) 0%, transparent 60%); + pointer-events: none; + z-index: 0; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +body.admin-page { overflow: hidden; height: 100vh; } +.admin-shell { display: flex; height: 100vh; overflow: hidden; position: relative; z-index: 1; } +.admin-nav { + width: 210px; + flex-shrink: 0; + background: + linear-gradient(180deg, rgba(5, 16, 26, 0.98), rgba(2, 10, 18, 0.98)), + linear-gradient(180deg, rgba(0, 174, 240, 0.06), rgba(255, 107, 26, 0.04)); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0 0 16px; +} +.admin-nav-header { + border-bottom: 1px solid var(--border); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 9px; + padding: 10px 10px 12px; +} +.admin-nav-title { display: block; width: 100%; } +.admin-nav-logo-img { display: block; width: 100%; height: auto; } +.admin-nav-item { + display: block; + padding: 9px 16px 9px 14px; + text-decoration: none; + color: var(--text-secondary); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; + border-left: 3px solid transparent; +} +.admin-nav-item:hover { + color: var(--brand-cyan-bright); + background: linear-gradient(90deg, rgba(0, 174, 240, 0.12), rgba(255, 107, 26, 0.03)); + text-decoration: none; +} +.admin-nav-item.active { + color: var(--accent); + border-left-color: var(--accent); + background: linear-gradient(90deg, rgba(255, 107, 26, 0.16), rgba(255, 107, 26, 0.04)); + box-shadow: inset 0 0 0 1px rgba(255, 107, 26, 0.08); +} + +.admin-main { + flex: 1; + min-width: 0; + position: relative; + display: flex; + flex-direction: column; +} + +.admin-toolbar { + position: fixed; + top: 16px; + right: 20px; + z-index: 40; + display: flex; + justify-content: flex-end; + pointer-events: none; +} + +.admin-account-card { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(7, 19, 31, 0.78); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} +.admin-account-name { + color: var(--brand-cyan-bright); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} +.admin-account-links { + display: inline-flex; + gap: 10px; + align-items: center; +} +.admin-account-links a { + color: var(--accent); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} + +.admin-content { + flex: 1; + overflow-y: auto; + padding: 16px 20px 28px; + position: relative; + min-width: 0; +} + +h1 { + font-size: 18px; + font-weight: 600; + letter-spacing: 1.4px; + text-transform: uppercase; + color: var(--brand-cyan-bright); + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 14px; +} +h1::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background: var(--accent); + border-radius: 2px; + box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.24); + animation: pulse-glow 2.5s ease-in-out infinite; +} +h2, h3, h4, h5, h6 { + color: var(--brand-cyan-bright); + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} +p { margin: 0 0 12px; color: var(--text-secondary); } + +.card, +.block, +.dashboard-panel, +#content > div:not(.ws-app):not(.demo-section) { + background: var(--card); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + position: relative; + transition: border-color 0.25s ease, box-shadow 0.25s ease; + margin-bottom: 14px; +} +.card::before, +.block::before, +.dashboard-panel::before, +#content > div:not(.ws-app):not(.demo-section)::before { + content: ''; + position: absolute; + top: 0; + left: 12px; + right: 12px; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.55; +} +.card:hover, +.block:hover, +.dashboard-panel:hover, +#content > div:not(.ws-app):not(.demo-section):hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-md); +} + +form { max-width: 760px; } +form > div { display: grid; gap: 6px; margin-bottom: 12px; } +label { + color: var(--text-muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} +input, select, textarea { + width: 100%; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(3, 7, 18, 0.55); + color: var(--brand-cyan-bright); + padding: 10px 12px; + font: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--border-hover); + box-shadow: 0 0 0 3px rgba(0, 183, 249, 0.12); +} +button, .btn, input[type="submit"] { + border: 1px solid var(--border); + background: rgba(255, 107, 26, 0.12); + color: var(--brand-orange-bright); + border-radius: var(--radius); + padding: 10px 14px; + font: inherit; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: rgba(255, 107, 26, 0.18); + border-color: rgba(255, 107, 26, 0.42); + text-decoration: none; +} +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: var(--radius); + padding: 10px 12px; + border: 1px solid var(--border); + background: rgba(0, 183, 249, 0.08); + color: var(--text-secondary); + margin-bottom: 12px; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: rgba(3, 7, 18, 0.42); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + background: rgba(0, 183, 249, 0.08); +} +tr:last-child td { border-bottom: none; } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 12px; + background: rgba(3, 7, 18, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +@media (max-width: 980px) { + .admin-shell { flex-direction: column; height: auto; min-height: 100vh; } + .admin-nav { + width: 100%; + flex-direction: row; + overflow-x: auto; + padding-bottom: 0; + } + .admin-nav-header { min-width: 180px; margin-bottom: 0; border-bottom: 0; border-right: 1px solid var(--border); } + .admin-nav-item { white-space: nowrap; border-left: 0; border-bottom: 3px solid transparent; } + .admin-nav-item.active { border-bottom-color: var(--accent); border-left-color: transparent; } + .admin-toolbar { position: static; justify-content: flex-start; margin: 16px 20px 0; } + .admin-content { padding-top: 12px; } +} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/localfirst/icon.png b/site/examples/uce-starter/themes/localfirst/icon.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/examples/uce-starter/themes/localfirst/icon.png differ diff --git a/site/examples/uce-starter/themes/localfirst/img/local_first_logo.png b/site/examples/uce-starter/themes/localfirst/img/local_first_logo.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/examples/uce-starter/themes/localfirst/img/local_first_logo.png differ diff --git a/site/examples/uce-starter/themes/localfirst/page.html.php b/site/examples/uce-starter/themes/localfirst/page.html.php new file mode 100644 index 0000000..6c9db4d --- /dev/null +++ b/site/examples/uce-starter/themes/localfirst/page.html.php @@ -0,0 +1,38 @@ + + + + + + + false)); ?> +
+ +
+
+ 'admin-account-card', + 'links_wrapper_class' => 'admin-account-links', + 'name_class' => 'admin-account-name', + )); ?> +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/site/examples/uce-starter/themes/portal-dark/css/style.css b/site/examples/uce-starter/themes/portal-dark/css/style.css new file mode 100644 index 0000000..90001c3 --- /dev/null +++ b/site/examples/uce-starter/themes/portal-dark/css/style.css @@ -0,0 +1,313 @@ +:root { + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --surface: #1e293b; + --surface-elevated: #334155; + --surface-hover: #475569; + --primary: #60a5fa; + --primary-dark: #3b82f6; + --primary-light: #93c5fd; + --secondary: #a78bfa; + --accent: #22d3ee; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --border: #334155; + --border-hover: #475569; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + --glass-bg: rgba(30, 41, 59, 0.8); + --success: #10b981; + --success-bg: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.3); + --success-text: #10b981; + --warning: #f59e0b; + --warning-bg: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.3); + --warning-text: #f59e0b; + --error: #ef4444; + --error-bg: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.3); + --error-text: #ef4444; + --info: #3b82f6; + --info-bg: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.3); + --info-text: #3b82f6; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'SFMono-Regular', Consolas, monospace; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; } +body { + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +a { color: var(--primary); text-decoration: none; transition: all 0.2s ease; } +a:hover { color: var(--primary-light); text-decoration: underline; } + +::selection { + background: rgba(96, 165, 250, 0.3); + color: var(--text-primary); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 50% at 20% -10%, rgba(96, 165, 250, 0.05) 0%, transparent 60%), + radial-gradient(ellipse 60% 40% at 85% 110%, rgba(34, 211, 238, 0.04) 0%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} +nav::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.3; +} + +.nav-menu { display: flex; align-items: center; gap: 0.25rem; } +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding: 1rem 1.5rem; + margin: 0.5rem 0.25rem; +} +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary); +} +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + font-size: 0.95rem; + margin-right: 0.5rem; +} +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid var(--border); +} +.nav-account .nav-avatar { width: 30px; height: 30px; } +.profile-avatar { width: 96px; height: 96px; } + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 3px; +} +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } + +#content > div:not(.ws-app), .block, .card, .dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover, .dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +form { max-width: 600px; margin: 0 auto; } +form > div { display: flex; flex-direction: column; margin-bottom: 1.5rem; gap: 0.5rem; } +label { font-weight: 500; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; } +input, select, textarea { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); + text-decoration: none; +} + +.banner { + border-radius: var(--radius); + padding: 0.8rem 1rem; + border: 1px solid var(--border); + margin-bottom: 1rem; + background: rgba(255,255,255,0.03); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--border); + background: rgba(255,255,255,0.02); + border-radius: var(--radius); + overflow: hidden; +} +th, td { padding: 0.8rem 0.9rem; border-bottom: 1px solid var(--border); text-align: left; } +th { background: var(--surface-elevated); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(255,255,255,0.02); } +code, pre { font-family: var(--font-mono); } +pre { + padding: 1rem; + background: rgba(15, 23, 42, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + padding: 1.25rem 1rem; + color: var(--text-muted); + font-size: 0.875rem; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-secondary); + outline: none; + border: none; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} + +@media (max-width: 860px) { + nav { flex-wrap: wrap; padding-bottom: 0.6rem; } + .nav-account { width: 100%; margin-left: 0; justify-content: flex-start; } + #content { margin-top: 7.5rem; padding: 1rem 0.75rem; } + #content > div:not(.ws-app), .block, .card, .dashboard-panel { padding: 1.25rem; margin-bottom: 1rem; } + h1 { font-size: 2rem; padding: 1.25rem; } +} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/portal-dark/icon.png b/site/examples/uce-starter/themes/portal-dark/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/examples/uce-starter/themes/portal-dark/icon.png differ diff --git a/site/examples/uce-starter/themes/portal-dark/page.html.php b/site/examples/uce-starter/themes/portal-dark/page.html.php new file mode 100644 index 0000000..ae86edd --- /dev/null +++ b/site/examples/uce-starter/themes/portal-dark/page.html.php @@ -0,0 +1,18 @@ + + + + + + + + 'nav-account nav-menu')); ?> +
> + +
+ $embed_mode)); ?> + + + \ No newline at end of file diff --git a/site/examples/uce-starter/themes/portal-light/css/style.css b/site/examples/uce-starter/themes/portal-light/css/style.css new file mode 100644 index 0000000..141b15f --- /dev/null +++ b/site/examples/uce-starter/themes/portal-light/css/style.css @@ -0,0 +1,392 @@ +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-italic.ttf") format("truetype"); + font-style: italic; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +:root { + --kg-blue: #0f68ad; + --kg-blue-dark: #0b4f85; + --kg-blue-light: #2d86ca; + --kg-bg: #e9e9ea; + --kg-surface: #ffffff; + --kg-border: rgba(0, 0, 0, 0.2); + --kg-text: #121417; + --kg-muted: #6c737b; + --kg-orange: #f08a00; + --kg-success: #149b2e; + --kg-danger: #e11818; + --topbar-height: 56px; + --bg-color: var(--kg-bg); + --bg-secondary: #ffffff; + --surface: var(--kg-surface); + --surface-elevated: #ffffff; + --surface-hover: #e8eef3; + --primary: var(--kg-blue); + --primary-dark: var(--kg-blue-dark); + --primary-light: var(--kg-blue-light); + --secondary: #596572; + --accent: var(--kg-orange); + --text-primary: var(--kg-text); + --text-secondary: #4b5560; + --text-muted: var(--kg-muted); + --border: var(--kg-border); + --border-hover: rgba(15, 104, 173, 0.3); + --success: var(--kg-success); + --success-bg: #e7f5ea; + --success-border: #86c88f; + --success-text: #0f6e22; + --warning: #c97a14; + --warning-bg: #fff4e4; + --warning-border: #f0c07b; + --warning-text: #92550a; + --error: var(--kg-danger); + --error-bg: #ffe7e7; + --error-border: #ea9999; + --error-text: #b80f0f; + --info: var(--primary); + --info-bg: #e8f2ff; + --info-border: #b9d4f3; + --info-text: #0b4f85; + --shadow-sm: 0 1px 2px rgba(18, 20, 23, 0.06); + --shadow-md: 0 8px 22px rgba(18, 20, 23, 0.06); + --shadow-lg: 0 16px 34px rgba(18, 20, 23, 0.1); + --shadow-xl: 0 20px 40px rgba(18, 20, 23, 0.14); + --radius: 8px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --font-sans: "B612", "Segoe UI", sans-serif; + --font-mono: "B612 Mono", "SFMono-Regular", Consolas, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body { min-height: 100%; } +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.4; + color: var(--text-primary); + background: var(--bg-color); +} +a { color: var(--primary-dark); text-decoration: none; } +a:hover { text-decoration: underline; } + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--topbar-height); + display: flex; + align-items: stretch; + background: var(--primary); + border-bottom: 1px solid var(--primary-dark); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); + z-index: 60; +} + +.nav-menu { + display: flex; + align-items: stretch; + overflow-x: auto; + white-space: nowrap; + flex: 1; +} + +.nav-menu > a { + display: inline-flex; + align-items: center; + padding: 0 0.9rem; + color: #fff; + border-right: 1px solid rgba(255, 255, 255, 0.16); + font-size: 14px; + font-weight: 700; + text-decoration: none; + background: transparent; +} + +.nav-menu > a:first-child { + font-size: 18px; + min-width: 250px; + background: rgba(0, 0, 0, 0.08); +} + +.nav-menu > a:hover, +.nav-menu > a:focus { + background: var(--primary-dark); + text-decoration: none; +} + +.nav-account { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0 0.7rem; + border-left: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.12); + flex-shrink: 0; +} + +.nav-account .account-name { + color: #d8f8df; + font-size: 14px; + font-weight: 700; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid rgba(255, 255, 255, 0.4); +} + +.nav-account .nav-avatar { + width: 30px; + height: 30px; +} + +.profile-avatar { + width: 96px; + height: 96px; + border-color: var(--border); +} + +.nav-account a { + color: #fff; + font-size: 14px; + font-weight: 700; + padding: 0.22rem 0.5rem; + border-radius: 3px; +} + +.nav-account a:hover { + background: rgba(255, 255, 255, 0.16); + text-decoration: none; +} + +#content { + margin-top: var(--topbar-height); + padding: 1rem; + min-height: calc(100vh - var(--topbar-height)); +} + +#content > div:not(.ws-app), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: 1.25rem; + margin-bottom: 1rem; + transition: box-shadow 0.2s ease, border-color 0.2s ease; +} + +#content > div:not(.ws-app):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: rgba(15, 104, 173, 0.2); +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + color: #252b31; + margin-bottom: 0.7rem; +} +h1 { font-size: 22px; padding-bottom: 0.5rem; border-bottom: 2px solid #d2d3d7; position: relative; } +h1::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 50px; + height: 2px; + background: var(--primary); + border-radius: 2px; +} +h2 { font-size: 18px; } +h3 { font-size: 16px; } +h4, h5, h6 { font-size: 14px; } + +p { margin: 0.4rem 0 0.85rem; } + +form { max-width: 780px; } +form > div { margin-bottom: 0.8rem; } +label { + display: block; + font-weight: 700; + font-size: 14px; + color: #3f464f; + margin-bottom: 0.3rem; +} +input, select, textarea { + width: 100%; + min-height: 36px; + padding: 0 0.7rem; + border: 1px solid #a8aaaf; + border-radius: 4px; + background: #fff; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} +textarea { min-height: 100px; padding: 0.5rem 0.7rem; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-light); + box-shadow: 0 0 0 1px var(--primary-light); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 0.9rem; + border: 1px solid #7f8084; + border-radius: 4px; + background: #f7f7f7; + color: #1a1e23; + font-size: 14px; + font-weight: 700; + cursor: pointer; + font-family: inherit; +} +button:hover, .btn:hover, input[type="submit"]:hover { background: #ececef; text-decoration: none; } +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: 4px; + padding: 0.65rem 0.8rem; + border: 1px solid #b4b5ba; + margin-bottom: 0.8rem; + background: #fafafa; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 0.7rem 0.8rem; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { background: #e7e7e8; font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 0, 0, 0.02); } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.9rem; + background: #fff; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + background: #ffffff; + color: var(--text-muted); + padding: 1rem 1rem 1.5rem; + font-size: 13px; +} +.footer-inner { max-width: 1200px; margin: 0 auto; } + +input[type="range"] { + height: 6px; + padding: 0; + min-height: auto; + border: none; + background: #d4d5d9; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +::selection { + background: rgba(15, 104, 173, 0.2); + color: var(--text-primary); +} + +@media (max-width: 860px) { + nav { + height: auto; + flex-direction: column; + } + .nav-menu { + flex-wrap: wrap; + } + .nav-menu > a:first-child { + min-width: 0; + } + .nav-account { + justify-content: flex-start; + padding: 0.6rem 0.7rem; + } + #content { + margin-top: 104px; + padding: 0.75rem; + } +} \ No newline at end of file diff --git a/site/examples/uce-starter/themes/portal-light/icon.png b/site/examples/uce-starter/themes/portal-light/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/examples/uce-starter/themes/portal-light/icon.png differ diff --git a/site/examples/uce-starter/themes/portal-light/page.html.php b/site/examples/uce-starter/themes/portal-light/page.html.php new file mode 100644 index 0000000..0382b0f --- /dev/null +++ b/site/examples/uce-starter/themes/portal-light/page.html.php @@ -0,0 +1,18 @@ + + + + + + + + +
> + +
+ $embed_mode, 'inner_class' => 'footer-inner')); ?> + + + \ No newline at end of file diff --git a/site/examples/uce-starter/themes/retro-gaming/css/style.css b/site/examples/uce-starter/themes/retro-gaming/css/style.css new file mode 100644 index 0000000..f187fa6 --- /dev/null +++ b/site/examples/uce-starter/themes/retro-gaming/css/style.css @@ -0,0 +1,523 @@ +/* ──────────────────────────────────────────────────────── + RETRO GAMING THEME + CRT scanlines · pixel font · neon glow · 8-bit palette + ──────────────────────────────────────────────────────── */ + +@font-face { + font-family: 'Press Start 2P'; + src: url('../../common/fonts/press-start-2p/press-start-2p-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} + +/* ── tokens ─────────────────────────────────────────── */ +:root { + /* background */ + --bg-color: #0a0a1a; + --bg-secondary: #10102a; + --surface: #12122e; + --surface-elevated: #1a1a3e; + --surface-hover: #22224e; + + /* neon palette */ + --primary: #00ff88; + --primary-dark: #00cc6a; + --primary-light: #66ffbb; + --secondary: #ff00ff; + --accent: #ffff00; + --text-primary: #e0e0ff; + --text-secondary: #a0a0cc; + --text-muted: #6868a0; + + /* borders, shadows */ + --border: #2a2a5a; + --border-hover: #3a3a7a; + --shadow-sm: 0 2px 0 #000; + --shadow-md: 0 4px 0 #000, 0 0 12px rgba(0, 255, 136, 0.08); + --shadow-lg: 0 6px 0 #000, 0 0 24px rgba(0, 255, 136, 0.12); + --shadow-xl: 0 8px 0 #000, 0 0 40px rgba(0, 255, 136, 0.16); + + /* semantic */ + --success: #00ff88; + --success-bg: rgba(0, 255, 136, 0.1); + --success-border: rgba(0, 255, 136, 0.3); + --success-text: #00ff88; + --warning: #ffff00; + --warning-bg: rgba(255, 255, 0, 0.1); + --warning-border: rgba(255, 255, 0, 0.3); + --warning-text: #ffff00; + --error: #ff3366; + --error-bg: rgba(255, 51, 102, 0.1); + --error-border: rgba(255, 51, 102, 0.3); + --error-text: #ff3366; + --info: #00ccff; + --info-bg: rgba(0, 204, 255, 0.1); + --info-border: rgba(0, 204, 255, 0.3); + --info-text: #00ccff; + + /* geometry */ + --radius: 0px; + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 2px; + --radius-xl: 4px; + + /* typography */ + --font-sans: 'Press Start 2P', monospace; + --font-mono: 'Press Start 2P', monospace; +} + +/* ── reset ──────────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; image-rendering: pixelated; } + +body { + font-family: var(--font-sans); + font-size: 10px; + line-height: 2; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: none; + -moz-osx-font-smoothing: unset; +} + +/* ── CRT scanline overlay ───────────────────────────── */ +body::before { + content: ''; + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.15) 2px, + rgba(0, 0, 0, 0.15) 4px + ); + pointer-events: none; + z-index: 9000; +} + +/* subtle vignette */ +body::after { + content: ''; + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.5) 100%); + pointer-events: none; + z-index: 8999; +} + +::selection { + background: var(--primary); + color: var(--bg-color); +} + +/* ── links ──────────────────────────────────────────── */ +a { + color: var(--primary); + text-decoration: none; + transition: color 0.1s step-end; +} +a:hover { + color: var(--accent); + text-decoration: none; + text-shadow: 0 0 6px var(--accent); +} + +/* ── navigation ─────────────────────────────────────── */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--bg-secondary); + border-bottom: 3px solid var(--primary); + padding: 0 0.5rem; + display: flex; + align-items: center; + box-shadow: 0 3px 0 #000, 0 6px 20px rgba(0, 255, 136, 0.15); +} + +.nav-menu { + display: flex; + align-items: center; + gap: 0; + flex: 1; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; +} +.nav-menu::-webkit-scrollbar { display: none; } + +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.75rem 0.8rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 8px; + letter-spacing: 0.5px; + text-transform: uppercase; + border-right: 1px solid var(--border); + transition: background 0.1s step-end, color 0.1s step-end; +} + +.nav-menu > a:first-child { + color: var(--primary); + text-shadow: 0 0 8px rgba(0, 255, 136, 0.5); + font-size: 10px; + padding: 0.75rem 1rem; + border-right: 2px solid var(--primary); +} + +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--accent); + text-shadow: 0 0 6px var(--accent); + text-decoration: none; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0; + flex: 0 0 auto; + overflow: visible; +} +.nav-account a { + border-right: none; + border-left: 1px solid var(--border); +} + +.nav-account .account-name { + color: var(--primary); + font-size: 8px; + padding: 0 0.5rem; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 0; + border: 2px solid var(--primary); + image-rendering: pixelated; +} +.nav-account .nav-avatar { width: 24px; height: 24px; } +.profile-avatar { width: 64px; height: 64px; } + +/* ── main content ───────────────────────────────────── */ +#content { + margin-top: 3.5rem; + min-height: calc(100vh - 7rem); + padding: 1.5rem 1rem; + max-width: 1100px; + margin-left: auto; + margin-right: auto; + position: relative; + z-index: 1; +} + +/* ── typography ─────────────────────────────────────── */ +h1, h2, h3, h4, h5, h6 { + font-weight: 400; + line-height: 2; + margin-bottom: 0.75rem; + color: var(--primary); + text-shadow: 0 0 10px rgba(0, 255, 136, 0.35); + text-transform: uppercase; +} + +h1 { + font-size: 16px; + padding: 0.75rem 0; + border-bottom: 3px double var(--primary); + margin-bottom: 1.25rem; + position: relative; +} +h1::before { + content: '►'; + margin-right: 0.5rem; + animation: blink-cursor 1s step-end infinite; +} +h2 { font-size: 12px; color: var(--info-text); text-shadow: 0 0 8px rgba(0, 204, 255, 0.3); } +h3 { font-size: 10px; color: var(--secondary); text-shadow: 0 0 8px rgba(255, 0, 255, 0.3); } +h4, h5, h6 { font-size: 10px; } + +p { margin: 0 0 0.75rem; } + +@keyframes blink-cursor { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* ── cards / blocks ─────────────────────────────────── */ +#content > div:not(.ws-app):not(.demo-section), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 2px solid var(--border); + padding: 1rem; + margin-bottom: 1.25rem; + box-shadow: var(--shadow-md); + position: relative; + transition: border-color 0.1s step-end; +} + +#content > div:not(.ws-app):not(.demo-section)::before, +.block::before, +.card::before, +.dashboard-panel::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, var(--primary), var(--secondary), var(--accent)); +} + +#content > div:not(.ws-app):not(.demo-section):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-lg); +} + +/* ── forms ──────────────────────────────────────────── */ +form { max-width: 640px; } +form > div { display: grid; gap: 0.35rem; margin-bottom: 1rem; } + +label { + color: var(--accent); + font-size: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +input, select, textarea { + width: 100%; + padding: 0.6rem 0.7rem; + border: 2px solid var(--border); + background: var(--bg-color); + color: var(--primary); + font-family: var(--font-mono); + font-size: 10px; + caret-color: var(--primary); +} +textarea { min-height: 100px; } + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary), 0 0 12px rgba(0, 255, 136, 0.2); +} + +input::placeholder, textarea::placeholder { + color: var(--text-muted); + opacity: 1; +} + +/* ── buttons ────────────────────────────────────────── */ +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1.2rem; + border: 2px solid var(--primary); + background: transparent; + color: var(--primary); + font-family: var(--font-sans); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.1s step-end; + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--primary); +} + +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary); + color: var(--bg-color); + text-shadow: none; + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--primary); + transform: translate(2px, 2px); + text-decoration: none; +} + +button:active, .btn:active, input[type="submit"]:active { + box-shadow: none; + transform: translate(4px, 4px); +} + +.btn-secondary { + border-color: var(--secondary); + color: var(--secondary); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--secondary); +} +.btn-secondary:hover { + background: var(--secondary); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--secondary); +} + +.btn-outline { + border-color: var(--accent); + color: var(--accent); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--accent); +} +.btn-outline:hover { + background: var(--accent); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--accent); +} + +/* ── range slider ───────────────────────────────────── */ +input[type="range"] { + height: 8px; + padding: 0; + border: 2px solid var(--border); + background: var(--bg-color); + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} +input[type="range"]::-moz-range-thumb { + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + border-radius: 0; + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} + +/* ── banners ────────────────────────────────────────── */ +.banner { + padding: 0.6rem 0.8rem; + border: 2px solid var(--border); + margin-bottom: 0.75rem; + background: var(--surface); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +/* ── tables ─────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border: 2px solid var(--border); +} +th, td { + padding: 0.5rem 0.65rem; + border-bottom: 1px solid var(--border); + text-align: left; + font-size: 9px; +} +th { + background: var(--surface-elevated); + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid var(--accent); +} +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 255, 136, 0.04); } + +/* ── code ───────────────────────────────────────────── */ +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.75rem; + background: var(--bg-color); + border: 2px solid var(--border); + overflow: auto; + color: var(--primary); +} + +/* ── footer ─────────────────────────────────────────── */ +footer { + border-top: 3px solid var(--primary); + padding: 1rem; + color: var(--text-muted); + font-size: 8px; + background: var(--bg-secondary); + box-shadow: 0 -3px 0 #000; + position: relative; + z-index: 1; +} +footer::before { + content: '// '; + color: var(--text-muted); +} + +/* ── pixel decoration classes (for page template) ──── */ +.retro-stars { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(1px 1px at 10% 15%, var(--primary), transparent), + radial-gradient(1px 1px at 30% 45%, var(--accent), transparent), + radial-gradient(1px 1px at 55% 20%, var(--secondary), transparent), + radial-gradient(1px 1px at 70% 65%, var(--info-text), transparent), + radial-gradient(1px 1px at 85% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 15% 75%, var(--accent), transparent), + radial-gradient(1px 1px at 45% 85%, var(--secondary), transparent), + radial-gradient(1px 1px at 90% 80%, var(--primary), transparent), + radial-gradient(1px 1px at 25% 55%, var(--info-text), transparent), + radial-gradient(1px 1px at 60% 50%, var(--accent), transparent), + radial-gradient(1px 1px at 5% 40%, var(--error-text), transparent), + radial-gradient(1px 1px at 75% 10%, var(--accent), transparent), + radial-gradient(1px 1px at 40% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 95% 55%, var(--secondary), transparent), + radial-gradient(1px 1px at 50% 70%, var(--primary), transparent), + radial-gradient(1px 1px at 20% 90%, var(--info-text), transparent); + animation: twinkle 4s ease-in-out infinite alternate; +} +@keyframes twinkle { + 0% { opacity: 0.4; } + 100% { opacity: 0.8; } +} + +/* ── responsive ─────────────────────────────────────── */ +@media (max-width: 860px) { + nav { flex-wrap: wrap; } + .nav-menu { overflow-x: auto; white-space: nowrap; } + .nav-account { + width: 100%; + margin-left: 0; + border-top: 1px solid var(--border); + justify-content: flex-start; + } + #content { margin-top: 5rem; padding: 1rem 0.75rem; } + h1 { font-size: 12px; } + h2 { font-size: 10px; } +} + +/* ── focus accessibility ────────────────────────────── */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* ── print ──────────────────────────────────────────── */ +@media print { + nav, footer, .retro-stars { display: none; } + body::before, body::after { display: none; } + #content { margin-top: 0; } + * { background: white !important; color: black !important; box-shadow: none !important; text-shadow: none !important; } +} diff --git a/site/examples/uce-starter/themes/retro-gaming/icon.png b/site/examples/uce-starter/themes/retro-gaming/icon.png new file mode 100644 index 0000000..12dfe2c Binary files /dev/null and b/site/examples/uce-starter/themes/retro-gaming/icon.png differ diff --git a/site/examples/uce-starter/themes/retro-gaming/page.html.php b/site/examples/uce-starter/themes/retro-gaming/page.html.php new file mode 100644 index 0000000..acf2c11 --- /dev/null +++ b/site/examples/uce-starter/themes/retro-gaming/page.html.php @@ -0,0 +1,22 @@ + + + + + +> +
+ + false, + 'account_wrapper_class' => 'nav-account nav-menu', + )); ?> +
> + +
+ $embed_mode)); ?> + + + diff --git a/site/examples/uce-starter/views/account/login.uce b/site/examples/uce-starter/views/account/login.uce new file mode 100644 index 0000000..9e23d04 --- /dev/null +++ b/site/examples/uce-starter/views/account/login.uce @@ -0,0 +1,27 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Login"; + DTree result; + if(context.params["REQUEST_METHOD"] == "POST") + { + result = starter_user_sign_in(context.post["email"], context.post["password"], context); + if(result["result"].to_string() != "") + { + starter_redirect("account/profile", context); + return; + } + } + <> +

Login

+ +
+
+
+ +
+

">Register

+ +} diff --git a/site/examples/uce-starter/views/account/logout.uce b/site/examples/uce-starter/views/account/logout.uce new file mode 100644 index 0000000..74c81e8 --- /dev/null +++ b/site/examples/uce-starter/views/account/logout.uce @@ -0,0 +1,8 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + starter_user_logout(context); + starter_redirect("account/login", context); +} diff --git a/site/examples/uce-starter/views/account/profile.uce b/site/examples/uce-starter/views/account/profile.uce new file mode 100644 index 0000000..322520c --- /dev/null +++ b/site/examples/uce-starter/views/account/profile.uce @@ -0,0 +1,26 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + if(!starter_is_signed_in(context)) + { + starter_redirect("account/login", context); + return; + } + context.var["starter"]["page_title"] = "Profile"; + DTree user = starter_current_user(context); + String roles = ""; + user["roles"].each([&](DTree role, String key) { + if(roles != "") + roles += ", "; + roles += role.to_string(); + }); + <> +

Profile

+

Email:

+

Roles:

+

Created:

+

">Logout

+ +} diff --git a/site/examples/uce-starter/views/account/register.uce b/site/examples/uce-starter/views/account/register.uce new file mode 100644 index 0000000..b33f05c --- /dev/null +++ b/site/examples/uce-starter/views/account/register.uce @@ -0,0 +1,21 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Register"; + DTree result; + if(context.params["REQUEST_METHOD"] == "POST") + result = starter_user_create(context.post["email"], context.post["password"], context); + + <> +

Register

+ + +
+
+
+ +
+ +} diff --git a/site/examples/uce-starter/views/auth/callback.uce b/site/examples/uce-starter/views/auth/callback.uce new file mode 100644 index 0000000..5775764 --- /dev/null +++ b/site/examples/uce-starter/views/auth/callback.uce @@ -0,0 +1,50 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "OAuth Callback"; + String code = context.get["code"]; + String state = context.get["state"]; + String error = context.get["error"]; + String error_description = context.get["error_description"]; + + <> +
+
+ + + +
+
+

Authentication Successful

+

OAuth callback received successfully! Demo mode is active until real provider credentials are configured.

+
+

Service:

+

Code: ...

+

State:

+
+ +
+ +
+
+

Invalid Callback

+

Missing required OAuth parameters.

+
+ +
+
+ +} diff --git a/site/examples/uce-starter/views/auth/demo.uce b/site/examples/uce-starter/views/auth/demo.uce new file mode 100644 index 0000000..c632df3 --- /dev/null +++ b/site/examples/uce-starter/views/auth/demo.uce @@ -0,0 +1,40 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Auth"; + starter_register_css("views/marketing.css", context); + + DTree props; + props["title"] = "Sign In to Your Account"; + props["subtitle"] = "Choose your preferred authentication method to continue"; + props["google_client_id"] = "YOUR_GOOGLE_CLIENT_ID"; + props["github_client_id"] = "YOUR_GITHUB_CLIENT_ID"; + props["discord_client_id"] = "YOUR_DISCORD_CLIENT_ID"; + props["twitch_client_id"] = "YOUR_TWITCH_CLIENT_ID"; + props["callback_url"] = starter_link("auth/callback", context); + + <> +

Authentication Demo

+
+

OAuth Authentication

+

This component provides secure OAuth authentication with popular identity providers.

+ +
+
+

Current Session Status

+ +
+

Logged In

+

User ID:

+
+ +
+

Not Logged In

+

Use the OAuth component above to sign in with Google, GitHub, Discord, or Twitch.

+
+ +
+ +} diff --git a/site/examples/uce-starter/views/auth/store-oauth-session.uce b/site/examples/uce-starter/views/auth/store-oauth-session.uce new file mode 100644 index 0000000..f80f7c6 --- /dev/null +++ b/site/examples/uce-starter/views/auth/store-oauth-session.uce @@ -0,0 +1,21 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_type"] = "json"; + + DTree body = json_decode(context.in); + if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "") + { + context.session["oauth_service"] = body["oauth_service"].to_string(); + context.session["oauth_state"] = body["oauth_state"].to_string(); + context.var["starter"]["json"]["status"] = "success"; + } + else + { + starter_set_status(context, 400, "Bad Request"); + context.var["starter"]["json"]["status"] = "error"; + context.var["starter"]["json"]["message"] = "Invalid input"; + } +} diff --git a/site/examples/uce-starter/views/dashboard.css b/site/examples/uce-starter/views/dashboard.css new file mode 100644 index 0000000..ce90e84 --- /dev/null +++ b/site/examples/uce-starter/views/dashboard.css @@ -0,0 +1,205 @@ +.dashboard-intro p, +.dashboard-panel-header p, +.dashboard-note { + color: var(--text-secondary); + max-width: 72ch; +} + +.dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + margin-bottom: 2rem; + overflow: hidden; + padding: 1.5rem; +} + +.dashboard-panel-header { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-bottom: 1.25rem; +} + +.dashboard-panel-header h2 { + font-size: 1.4rem; + margin-bottom: 0; +} + +.dashboard-stat-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.dashboard-stat-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + color: inherit; + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1rem; + text-decoration: none; + transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease; + position: relative; + overflow: hidden; +} + +.dashboard-stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + text-decoration: none; +} + +.dashboard-stat-card::before { + content: ''; + position: absolute; + inset: 0 auto 0 0; + width: 4px; + background: var(--primary); +} + +.dashboard-stat-card.tone-success::before { + background: var(--success); +} + +.dashboard-stat-card.tone-warning::before { + background: var(--warning); +} + +.dashboard-stat-card.tone-danger::before { + background: var(--error); +} + +.dashboard-stat-card.tone-info::before { + background: var(--info); +} + +.dashboard-stat-label { + color: var(--text-secondary); + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.dashboard-stat-value { + font-size: clamp(1.6rem, 2vw, 2.1rem); + font-variant-numeric: tabular-nums; + font-weight: 700; + line-height: 1.1; +} + +.dashboard-stat-meta { + color: var(--text-secondary); + font-size: 0.92rem; +} + +.dashboard-chart-canvas { + background: + radial-gradient(circle at top left, rgba(255, 255, 255, 0.05), transparent 45%), + linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(15, 23, 42, 0.08)); + border: 1px solid var(--border); + border-radius: var(--radius); + display: block; + width: 100%; + min-height: 180px; +} + +.dashboard-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.u-sortable-table { + border-collapse: collapse; + font-size: 0.95rem; + min-width: 720px; + width: 100%; +} + +.u-sortable-table thead th { + background: var(--surface-elevated); + border-bottom: 1px solid var(--border); + color: var(--text-primary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.05em; + padding: 0.85rem 1rem; + position: relative; + text-transform: uppercase; + white-space: nowrap; +} + +.u-sortable-table thead th.sortable { + cursor: pointer; + padding-right: 2rem; + user-select: none; +} + +.u-sortable-table thead th.sortable::after { + content: ':'; + color: var(--text-muted); + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + font-size: 0.8rem; +} + +.u-sortable-table thead th.sorted-asc::after { + content: '^'; + color: var(--primary); +} + +.u-sortable-table thead th.sorted-desc::after { + content: 'v'; + color: var(--primary); +} + +.u-sortable-table tbody tr:nth-child(odd) { + background: rgba(148, 163, 184, 0.05); +} + +.u-sortable-table tbody tr:hover { + background: rgba(96, 165, 250, 0.08); +} + +.u-sortable-table td { + border-bottom: 1px solid var(--border); + padding: 0.85rem 1rem; + vertical-align: top; + font-variant-numeric: tabular-nums; +} + +.u-sortable-table tbody tr:last-child td { + border-bottom: none; +} + +.u-sortable-table .align-right { + text-align: right; +} + +.u-sortable-table .align-center { + text-align: center; +} + +.u-sortable-table .muted { + color: var(--text-secondary); + text-align: center; +} + +@media (max-width: 768px) { + .dashboard-panel { + padding: 1rem; + } + + .u-sortable-table { + min-width: 600px; + } +} \ No newline at end of file diff --git a/site/examples/uce-starter/views/dashboard.uce b/site/examples/uce-starter/views/dashboard.uce new file mode 100644 index 0000000..14c71b6 --- /dev/null +++ b/site/examples/uce-starter/views/dashboard.uce @@ -0,0 +1,70 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Dashboard"; + starter_register_css("views/dashboard.css", context); + + DTree props; + DTree item; + item["label"] = "24h Requests"; item["value"] = "18,420"; item["meta"] = "+12.8% vs yesterday"; item["tone"] = "info"; props["items"].push(item); item.clear(); + item["label"] = "Median Latency"; item["value"] = "182 ms"; item["meta"] = "stable over last 7 samples"; item["tone"] = "success"; props["items"].push(item); item.clear(); + item["label"] = "Resident Memory"; item["value"] = "2.4 GB"; item["meta"] = "combined across workers"; item["tone"] = "warning"; props["items"].push(item); item.clear(); + item["label"] = "Healthy Services"; item["value"] = "3 / 4"; item["meta"] = "one degraded background worker"; item["tone"] = "danger"; props["items"].push(item); + props["title"] = "Starter-Friendly Overview Cards"; + props["subtitle"] = "Small summary tiles work well across admin pages, internal tools, and SSR dashboards."; + + <> +

Dashboard Primitives

+

This page is the first backport slice from the LocalAI dashboard frontend. It keeps the generic parts that fit the starter itself.

+ + print(component("../components/data/widgets:SUMMARY_METRICS", props, context)); + + props.clear(); + DTree series; + DTree values; + DTree entry; + entry["key"] = "requests"; entry["label"] = "Requests"; entry["color"] = "#60a5fa"; entry["axis"] = "left"; entry["format"] = "count"; + values = "240"; entry["values"].push(values); values = "268"; entry["values"].push(values); values = "294"; entry["values"].push(values); values = "322"; entry["values"].push(values); values = "301"; entry["values"].push(values); values = "356"; entry["values"].push(values); values = "388"; entry["values"].push(values); + props["series"].push(entry); entry.clear(); + entry["key"] = "latency"; entry["label"] = "Latency"; entry["color"] = "#f59e0b"; entry["axis"] = "right"; entry["format"] = "duration-ms"; + values = "182"; entry["values"].push(values); values = "176"; entry["values"].push(values); values = "191"; entry["values"].push(values); values = "204"; entry["values"].push(values); values = "188"; entry["values"].push(values); values = "166"; entry["values"].push(values); values = "159"; entry["values"].push(values); + props["series"].push(entry); + values = "08:00"; props["x_labels"].push(values); values = "09:00"; props["x_labels"].push(values); values = "10:00"; props["x_labels"].push(values); values = "11:00"; props["x_labels"].push(values); values = "12:00"; props["x_labels"].push(values); values = "13:00"; props["x_labels"].push(values); values = "14:00"; props["x_labels"].push(values); + props["id"] = "dashboard-demo-traffic"; + props["title"] = "Requests vs Latency"; + props["subtitle"] = "Same generic chart primitive can track throughput, job backlog, token volume, or queue time."; + props["height"] = "320"; + props["x_axis_label"] = "Last 7 Hours"; + props["y_axis_left_label"] = "Requests"; + props["y_axis_right_label"] = "Latency"; + props["y_axis_left_format"] = "count"; + props["y_axis_right_format"] = "duration-ms"; + print(component("../components/data/widgets:TIMESERIES_CHART", props, context)); + + props.clear(); + props["id"] = "dashboard-service-table"; + props["title"] = "Service Snapshot"; + props["subtitle"] = "Vanilla HTML table enhancement for cases where ag-Grid is overkill."; + props["storage_key"] = "starter.dashboard.services"; + props["sort"]["column"] = "2"; + props["sort"]["direction"] = "desc"; + DTree col; + col["key"] = "service"; col["label"] = "Service"; props["columns"].push(col); col.clear(); + col["key"] = "uptime"; col["label"] = "Uptime"; props["columns"].push(col); col.clear(); + col["key"] = "requests"; col["label"] = "Requests"; col["align"] = "right"; col["format"] = "number"; props["columns"].push(col); col.clear(); + col["key"] = "memory"; col["label"] = "Memory"; col["align"] = "right"; col["format"] = "bytes"; props["columns"].push(col); col.clear(); + col["key"] = "p95_latency"; col["label"] = "P95 Latency"; col["align"] = "right"; col["format"] = "duration-ms"; props["columns"].push(col); col.clear(); + col["key"] = "healthy"; col["label"] = "Healthy"; col["align"] = "center"; col["format"] = "bool"; props["columns"].push(col); + DTree row; + row["service"] = "router"; row["uptime"] = "12 days"; row["requests"] = "142890"; row["memory"] = "402653184"; row["p95_latency"] = "148"; row["healthy"] = "true"; props["rows"].push(row); row.clear(); + row["service"] = "queue-worker"; row["uptime"] = "8 days"; row["requests"] = "98214"; row["memory"] = "654311424"; row["p95_latency"] = "231"; row["healthy"] = "true"; props["rows"].push(row); row.clear(); + row["service"] = "vector-index"; row["uptime"] = "5 days"; row["requests"] = "44892"; row["memory"] = "1241513984"; row["p95_latency"] = "312"; row["healthy"] = "true"; props["rows"].push(row); row.clear(); + row["service"] = "sandbox"; row["uptime"] = "19 hours"; row["requests"] = "12810"; row["memory"] = "295698432"; row["p95_latency"] = "418"; row["healthy"] = "false"; props["rows"].push(row); + print(component("../components/data/widgets:SORTABLE_TABLE", props, context)); + + <> +

What Was Backported

The charting logic and data-formatting ideas come directly from the more complex dashboard frontend, but the starter version is stripped down to generic building blocks.

+ +} diff --git a/site/examples/uce-starter/views/features.uce b/site/examples/uce-starter/views/features.uce new file mode 100644 index 0000000..3455fee --- /dev/null +++ b/site/examples/uce-starter/views/features.uce @@ -0,0 +1,13 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Features"; + starter_register_css("views/marketing.css", context); + <> +

Features

+

This page exists in the UCE port so the starter menu resolves cleanly as a full route, while still reusing the same marketing components as the home page.

+ + print(component("../components/example/marketing_blocks:FEATURES_GRID", context)); +} diff --git a/site/examples/uce-starter/views/gauges.uce b/site/examples/uce-starter/views/gauges.uce new file mode 100644 index 0000000..7b7e656 --- /dev/null +++ b/site/examples/uce-starter/views/gauges.uce @@ -0,0 +1,90 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Gauges"; + starter_register_css("themes/common/css/gauges.css", context); + + DTree props; + DTree item; + + <> +

Gauge Components Demo

+
+ + + props["id"] = "horizontal_demo"; + props["title"] = "Horizontal Progress Bar"; + props["subtitle"] = "The original bar gauges, restyled to share the same card and token language as the new arc gauges."; + props["layout"] = "horizontal"; + props["style"] = "flex:1 1 24rem"; + props["listen"].set_bool(true); + item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item; item.clear(); + item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; item.clear(); + item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item; + print(component("../components/gauges/gauges:PROGRESSBAR", props, context)); + + props.clear(); + props["id"] = "vertical_demo"; + props["title"] = "Vertical Progress Bar"; + props["subtitle"] = "Same abstraction, but stacked as compact KPI cards."; + props["layout"] = "vertical"; + props["style"] = "flex:1 1 24rem"; + props["height"] = "350"; + props["listen"].set_bool(true); + item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; props["items"]["cpu"] = item; + item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "RAM"; props["items"]["memory"] = item; + item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "I/O"; props["items"]["disk"] = item; + print(component("../components/gauges/gauges:PROGRESSBAR", props, context)); + + <> +
+
+

Event Binding

+
+
+
+
+
+
+
+ + + props.clear(); + props["id"] = "needle_demo"; + props["title"] = "Needle Gauge"; + props["subtitle"] = "The original analog gauge now uses the same elevated panels and theme-token palette as the arc gauges."; + props["style"] = "flex:1 1 24rem"; + props["listen"].set_bool(true); + item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item; + item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; + item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item; + print(component("../components/gauges/gauges:NEEDLEGAUGE", props, context)); + + <> +
+
+ + + props.clear(); + props["id"] = "arc_demo"; + props["title"] = "SVG Arc Gauges"; + props["subtitle"] = "Backported from the llm2 overview as reusable KPI-style gauges."; + props["style"] = "flex: 2 1 36rem"; + props["listen"].set_bool(true); + item.clear(); item["label"] = "System Load"; item["value"] = "1.8"; item["max"] = "8"; item["precision"] = "1"; item["caption"] = "LOAD 1M"; item["meta"] = "4 cores available"; props["items"]["load"] = item; + item.clear(); item["label"] = "Memory"; item["value"] = "62"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = "%"; item["caption"] = "MEMORY"; item["meta"] = "9.9 / 16 GB"; props["items"]["memory_arc"] = item; + item.clear(); item["label"] = "Network"; item["value"] = "18"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = " MB/s"; item["caption"] = "THROUGHPUT"; item["meta"] = "Inbound + outbound"; props["items"]["network"] = item; + print(component("../components/gauges/gauges:ARCGAUGE", props, context)); + + <> +
+

Arc Gauge Controls

+
+
+
+
+
+ +} diff --git a/site/examples/uce-starter/views/index.uce b/site/examples/uce-starter/views/index.uce new file mode 100644 index 0000000..83f7b4e --- /dev/null +++ b/site/examples/uce-starter/views/index.uce @@ -0,0 +1,128 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Home"; + starter_register_css("views/marketing.css", context); + + DTree props; + + props["title"] = "Stunning Apps"; + props["subtitle"] = "Experience the power of super bloated PHP development with our gigantic and truly unwieldy component-based framework. Seriously though, this page only serves as an example repository of different styles and blocks."; + props["cta_text"] = "Get Started Free(mium)"; + props["cta_link"] = "#features"; + print(component("../components/example/marketing_blocks:HERO_SECTION", props, context)); + print(component("../components/example/marketing_blocks:FEATURES_GRID", context)); + + props.clear(); + DTree stat; + stat["number"] = "10K+"; stat["label"] = "Happy Vibe Coders"; props["stats"].push(stat); stat.clear(); + stat["number"] = "<1s"; stat["label"] = "Page Load Time"; props["stats"].push(stat); stat.clear(); + stat["number"] = "99.9%"; stat["label"] = "Uptime"; props["stats"].push(stat); stat.clear(); + stat["number"] = "24/7"; stat["label"] = "Support"; props["stats"].push(stat); + print(component("../components/example/marketing_blocks:STATS_SECTION", props, context)); + print(component("../components/example/marketing_blocks:BRANDS_SHOWCASE", context)); + print(component("../components/example/marketing_blocks:TESTIMONIALS", context)); + print(component("../components/example/marketing_blocks:PRICING_TABLE", context)); + + <> +
+
+

Demo

+

Try our unbelievable components in action

+
+
+

Modern Forms

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

Button Variations

+
+ + + + +
+
+ + + +
+
+
+
+
+
+

Theme Families

+

Compare the starter’s built-in theme families and jump to the live gallery.

+
+
+

+

+ ">Preview Theme +
+
+

Starter Themes

+

The original light and dark starter skins remain available in the same gallery for side-by-side checks.

+ ">Open Gallery +
+
+
+ + + props.clear(); + props["title"] = "Ready to Transform Your Development?"; + props["subtitle"] = "Join thousands of developers who have already modernized their workflow with our framework."; + props["cta_text"] = "Start Your Project"; + props["secondary_text"] = "View GitHub"; + print(component("../components/example/marketing_blocks:CTA_SECTION", props, context)); + + <> +
+

Component Development Guidelines

+
+
🏷️Use semantic HTML5 elements
+
🎨Implement CSS custom properties for theming
+
📱Add responsive design with mobile-first approach
+
Include accessibility attributes
+
Use progressive enhancement for JavaScript features
+
+
+ + + props.clear(); + DTree row; + row["name"] = "John Doe"; row["age"] = "25"; row["gender"] = "male"; row["department"] = "Engineering"; row["salary"] = "75000"; row["active"] = "true"; props["items"].push(row); row.clear(); + row["name"] = "Jane Smith"; row["age"] = "30"; row["gender"] = "female"; row["department"] = "Design"; row["salary"] = "82000"; row["active"] = "true"; props["items"].push(row); row.clear(); + row["name"] = "Bob Johnson"; row["age"] = "35"; row["gender"] = "male"; row["department"] = "Marketing"; row["salary"] = "68000"; row["active"] = "false"; props["items"].push(row); row.clear(); + row["name"] = "Alice Brown"; row["age"] = "40"; row["gender"] = "female"; row["department"] = "Engineering"; row["salary"] = "95000"; row["active"] = "true"; props["items"].push(row); + props["height"] = "350px"; + <> +
+

Simple Data Table (ag-Grid)

+

Basic data table with auto-generated columns, sorting, and filtering:

+ +
+ +} diff --git a/site/examples/uce-starter/views/marketing.css b/site/examples/uce-starter/views/marketing.css new file mode 100755 index 0000000..77c5028 --- /dev/null +++ b/site/examples/uce-starter/views/marketing.css @@ -0,0 +1,233 @@ + +/* Shared base styles */ +.mono-text, +.code-block { + font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; +} + +/* Code block container with terminal styling */ +.code-block-container { + background: linear-gradient(135deg, var(--surface) 0%, var(--surface-elevated) 100%); + padding: 2rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border); + margin: 1rem 0; + position: relative; + overflow: hidden; +} + +/* Terminal header bar */ +.terminal-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; +} + +.terminal-header.primary-gradient { + background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%); +} + +.terminal-header.success-gradient { + background: linear-gradient(90deg, var(--success) 0%, var(--primary) 50%, var(--secondary) 100%); +} + +.terminal-header.warning-gradient { + background: linear-gradient(90deg, var(--warning) 0%, var(--accent) 50%, var(--primary) 100%); +} + +/* Terminal window controls */ +.terminal-controls { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.window-dot { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 8px; +} + +.window-dot.red { background: #ff5f56; } +.window-dot.yellow { background: #ffbd2e; } +.window-dot.green { + background: #27ca3f; + margin-right: 1rem; +} + +/* Monospace text styling */ +.mono-text { + font-size: 0.875rem; + color: var(--text-muted); +} + +/* Code block styling */ +.code-block { + margin: 0; + font-size: 0.9rem; + line-height: 1.6; + color: var(--text-primary); + background: none; +} + +/* Card components - using shared base styles */ +.component-card, +.success-card, +.enhancement-card { + padding: 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); +} + +.component-card { + background: var(--surface-elevated); +} + +.success-card { + background: var(--success-bg); + border-color: var(--success-border); +} + +.enhancement-card { + color: var(--bg-color); + text-align: center; + border: none; +} + +/* Grid layouts - shared base properties */ +.components-grid, +.theme-grid, +.enhancements-grid, +.guidelines-grid { + display: grid; + gap: 1rem; +} + +.components-grid { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + margin: 1rem 0; +} + +.theme-grid { + grid-template-columns: 1fr 1fr; + margin: 1rem 0; +} + +.enhancements-grid { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.guidelines-grid { + gap: 0.5rem; +} + +/* Guideline items */ +.guideline-item { + display: flex; + align-items: center; + background: var(--surface-elevated); + padding: 1rem; + border-radius: var(--radius); + border-left: 4px solid; +} + +.guideline-icon { + font-size: 1.5rem; + margin-right: 1rem; +} + +/* Demo section styling */ +.demo-section { + padding: 4rem 0; + background: var(--bg-color); +} + +.demo-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + text-align: center; +} + +.demo-container h2 { + font-size: 2.5rem; + margin-bottom: 1rem; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.demo-container > p { + font-size: 1.25rem; + color: var(--text-secondary); + margin-bottom: 3rem; +} + +.demo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 2rem; + margin-top: 3rem; +} + +.demo-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + padding: 2.5rem; + box-shadow: var(--shadow-md); + text-align: left; +} + +.demo-card h3 { + margin-bottom: 2rem; + color: var(--text-primary); + text-align: center; +} + +.demo-form { + max-width: none; +} + +/* Flex layouts */ +.button-showcase, +.notification-demo { + display: flex; + gap: 1rem; +} + +.button-showcase { + flex-wrap: wrap; + margin-bottom: 2rem; + justify-content: center; +} + +.notification-demo { + flex-direction: column; +} + +/* Responsive design lol */ +@media (max-width: 768px) { + .demo-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .demo-card { + padding: 2rem 1.5rem; + } + + .button-showcase { + flex-direction: column; + align-items: center; + } + + .button-showcase .btn { + width: 100%; + max-width: 250px; + } +} \ No newline at end of file diff --git a/site/examples/uce-starter/views/page1.uce b/site/examples/uce-starter/views/page1.uce new file mode 100644 index 0000000..32bc283 --- /dev/null +++ b/site/examples/uce-starter/views/page1.uce @@ -0,0 +1,59 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Components"; + starter_register_css("views/marketing.css", context); + + <> +

Component System

+
+

Component Declaration

+
+
+
+
+
+
+ marketing_blocks.uce +
+
RENDER:HERO_SECTION(Request& context)
+{
+    String title = first(context.call["title"].to_string(), "Welcome");
+    <>
+        <div class="hero-section">...</div>
+    </>
+}
+
+
+ +
+

Example Components

+
+

marketing_blocks:HERO_SECTION

Landing page hero with CTA buttons

+

marketing_blocks:FEATURES_GRID

Feature showcase grid

+

widgets:DATA_TABLE

ag-Grid table wrapper

+

widgets:SORTABLE_TABLE

Lightweight sortable HTML table

+

primitives:APP_FRAME

Workspace shell wrapper

+

gauges:ARCGAUGE

SVG KPI gauge component

+
+
+ +
+

Usage Examples

+
+
+
+
+
+
+ views/index.uce +
+
DTree props;
+props["title"] = "Stunning Apps";
+<><?: component("example/marketing_blocks:HERO_SECTION", props, context) ?></>
+
+
+ +} diff --git a/site/examples/uce-starter/views/page2-section1.uce b/site/examples/uce-starter/views/page2-section1.uce new file mode 100644 index 0000000..c520b13 --- /dev/null +++ b/site/examples/uce-starter/views/page2-section1.uce @@ -0,0 +1,11 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_type"] = "blank"; + <> + - Page 2 Section 1 loaded +
UCE starter AJAX fragment response
+ +} diff --git a/site/examples/uce-starter/views/page2.uce b/site/examples/uce-starter/views/page2.uce new file mode 100644 index 0000000..ebdafbb --- /dev/null +++ b/site/examples/uce-starter/views/page2.uce @@ -0,0 +1,16 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Ajaxy"; + <> +

Ajax Demo

+
+

This is the content of Page 2. You can add more information here.

+

Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.

+
+ +
+ +} diff --git a/site/examples/uce-starter/views/theme-preview.uce b/site/examples/uce-starter/views/theme-preview.uce new file mode 100644 index 0000000..7689033 --- /dev/null +++ b/site/examples/uce-starter/views/theme-preview.uce @@ -0,0 +1,69 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Theme Preview"; + starter_register_css("views/themes.css", context); + String current_theme_key = starter_theme_value("key", context); + String theme_label = first(starter_theme_value("label", context), current_theme_key); + String theme_mode = starter_theme_value("mode", context); + StringMap theme_params; + theme_params["theme"] = current_theme_key; + + <> +
+
+ Starter Theme Preview +

+

This route renders the same neutral content inside each theme so downstream projects can compare layout, typography, color tokens, and chrome.

+ +
+
+
+

Tokens at a Glance

+
+
Current Theme
+
Mode
+
Preview Routetheme-preview
+
+
+ Primary + Secondary + Accent + Surface +
+
+
+

System Message States

+ + + +
+
+

Form Controls

+
+
+
+
+
+
+
+
+

Dense Content Block

+ + + + + + + +
AreaExpectationStatus
NavigationShould remain readable when menus get long.Verified
CardsShould keep spacing and hierarchy on both desktop and mobile.Verified
EmbedsShould render cleanly inside the gallery iframe.Verified
+
+
+
+ +} diff --git a/site/examples/uce-starter/views/themes.css b/site/examples/uce-starter/views/themes.css new file mode 100644 index 0000000..aea3292 --- /dev/null +++ b/site/examples/uce-starter/views/themes.css @@ -0,0 +1,180 @@ +.theme-gallery-shell, +.theme-preview-shell { + display: grid; + gap: 1.5rem; +} + +.theme-gallery-kicker, +.theme-preview-kicker { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); + margin-bottom: 0.75rem; +} + +.theme-gallery-hero h1, +.theme-preview-hero h1 { + margin-bottom: 0.75rem; +} + +.theme-gallery-hero p, +.theme-preview-hero p { + max-width: 72ch; + color: var(--text-secondary); +} + +.theme-gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 1.25rem; +} + +.theme-gallery-card, +.theme-preview-card, +.theme-preview-hero { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg, var(--radius)); + padding: 1.25rem; + box-shadow: var(--shadow-sm); +} + +.theme-gallery-card.is-active { + border-color: var(--primary); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 35%, transparent 65%), var(--shadow-md); +} + +.theme-gallery-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.theme-gallery-head p { + margin: 0.4rem 0 0; + color: var(--text-secondary); +} + +.theme-gallery-badge { + display: inline-flex; + align-items: center; + padding: 0.3rem 0.65rem; + border-radius: 999px; + background: color-mix(in srgb, var(--primary) 15%, transparent 85%); + color: var(--primary); + font-size: 0.78rem; + font-weight: 700; + white-space: nowrap; +} + +.theme-gallery-actions, +.theme-preview-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.theme-gallery-frame-wrap { + border: 1px solid var(--border); + border-radius: calc(var(--radius-lg, var(--radius)) - 4px); + overflow: hidden; + background: var(--bg-secondary); +} + +.theme-gallery-frame-wrap iframe { + display: block; + width: 100%; + height: 430px; + border: 0; + background: #fff; +} + +.theme-preview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +.theme-preview-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.theme-preview-stat { + display: grid; + gap: 0.2rem; + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-elevated, var(--bg-secondary)); +} + +.theme-preview-stat span { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-muted); +} + +.theme-preview-swatches { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; +} + +.theme-preview-swatches span { + display: flex; + align-items: end; + min-height: 92px; + padding: 0.75rem; + border-radius: var(--radius); + color: #fff; + font-weight: 700; + box-shadow: inset 0 -20px 30px rgba(0, 0, 0, 0.12); +} + +.theme-preview-form { + max-width: none; + display: grid; + gap: 0.85rem; +} + +.embed-mode nav, +.embed-mode footer, +.embed-mode #theme-switcher, +.embed-mode .admin-toolbar { + display: none !important; +} + +.embed-mode #content, +.embed-mode .admin-content { + margin-top: 0 !important; + min-height: 100vh !important; + padding-top: 1rem !important; +} + +@media (max-width: 720px) { + .theme-gallery-grid, + .theme-preview-grid { + grid-template-columns: 1fr; + } + + .theme-gallery-head { + flex-direction: column; + } + + .theme-gallery-frame-wrap iframe { + height: 360px; + } +} \ No newline at end of file diff --git a/site/examples/uce-starter/views/themes.uce b/site/examples/uce-starter/views/themes.uce new file mode 100644 index 0000000..ad4594c --- /dev/null +++ b/site/examples/uce-starter/views/themes.uce @@ -0,0 +1,46 @@ +#load "../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Themes"; + starter_register_css("views/themes.css", context); + String current_theme = starter_theme_value("key", context); + + <> + + +} diff --git a/site/examples/uce-starter/views/workspace/index.uce b/site/examples/uce-starter/views/workspace/index.uce new file mode 100644 index 0000000..620bf31 --- /dev/null +++ b/site/examples/uce-starter/views/workspace/index.uce @@ -0,0 +1,134 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Workspace"; + String section = first(context.var["starter"]["route"]["param"].to_string(), "overview"); + if(section != "overview" && section != "projects" && section != "activity") + section = "overview"; + + String title = section == "projects" ? "Projects queue" : (section == "activity" ? "Recent activity" : "Workspace overview"); + String subtitle = section == "projects" ? "Nested path fallback lets one controller serve multiple panes cleanly." : (section == "activity" ? "Sidebar-first tools often need a live or recent-events pane." : "A generic app-shell pattern for tools, admin consoles, and internal products."); + String status_label = section == "projects" ? "3 active" : (section == "activity" ? "Monitoring" : "Ready"); + String status_variant = section == "projects" ? "info" : (section == "activity" ? "warn" : "success"); + String description = section == "projects" ? "This page is served from views/workspace/index.uce while the route remains workspace/projects." : (section == "activity" ? "The workspace shell is a better fit than the marketing-style home page when the app is navigation-heavy and stateful." : "This slice comes from the portal app: a reusable workspace shell with sidebar navigation, compact mobile controls, and semantic panel primitives."); + + String sidebar_body = ""; + ob_start(); + <> +
Workspace areas
+ +

This sidebar and mobile shell pattern was extracted from the AI portal app and normalized against starter theme tokens.

+ + sidebar_body = ob_get_close(); + DTree sidebar_note; + sidebar_note["icon_class"] = "fas fa-circle-info"; + sidebar_note["text"] = "Use workspace/overview, workspace/projects, or workspace/activity"; + sidebar_body += component("../../components/workspace/primitives:LIST_STATE", sidebar_note, context); + + DTree toolbar_props; + toolbar_props["action_html"] = "Open shell"; + toolbar_props["search_input_id"] = "workspace-demo-search"; + toolbar_props["search_input_name"] = "workspace_demo_search"; + toolbar_props["search_placeholder"] = "Search workspace sections"; + String sidebar_top = component("../../components/workspace/primitives:SIDEBAR_TOOLBAR", toolbar_props, context); + + DTree sidebar_props; + sidebar_props["id"] = "workspace-demo-sidebar"; + sidebar_props["top_html"] = sidebar_top; + sidebar_props["body_html"] = sidebar_body; + String sidebar = component("../../components/workspace/primitives:SIDEBAR_SHELL", sidebar_props, context); + + DTree mobile_props; + mobile_props["button_id"] = "workspace-demo-toggle"; + mobile_props["title"] = "Starter Workspace"; + String mobile_bar = component("../../components/workspace/primitives:MOBILE_BAR", mobile_props, context); + + DTree pill_props; + pill_props["label"] = status_label; + pill_props["variant"] = status_variant; + String status_html = component("../../components/workspace/primitives:STATUS_PILL", pill_props, context); + + DTree header_props; + header_props["title"] = title; + header_props["subtitle"] = subtitle; + header_props["actions_html"] = status_html; + String header = component("../../components/workspace/primitives:PANEL_HEADER", header_props, context); + + String stats_html = ""; + ob_start(); + <> +
+
Sidebar pattern
Responsive
Overlay on mobile
+
Routing mode
Nested
Parent-path fallback
+
Source app
uh-ai
Portal shell
+
+ + stats_html = ob_get_close(); + + String detail_html = ""; + ob_start(); + <> +
+

Shell layout

Sidebar plus main panel, responsive overlay behavior, and a compact mobile header.

+

Semantic primitives

Panels, section heads, status pills, list states, and empty-state placeholders.

+

Starter-friendly

Backported against the starter theme variables instead of portal-specific branding tokens.

+
+ + detail_html = ob_get_close(); + + DTree section_head; + section_head["title"] = "Why this belongs in the starter"; + String why_head = component("../../components/workspace/primitives:SECTION_HEAD", section_head, context); + DTree section_props; + section_props["header_html"] = why_head; + section_props["body_html"] = "

" + html_escape(description) + "

" + stats_html; + String main_body = component("../../components/workspace/primitives:SECTION", section_props, context); + + section_head.clear(); + section_head["title"] = "Starter demo content"; + String demo_head = component("../../components/workspace/primitives:SECTION_HEAD", section_head, context); + section_props.clear(); + section_props["header_html"] = demo_head; + section_props["body_html"] = detail_html + "

Try visiting " + html_escape(starter_link("workspace/projects", context)) + " or " + html_escape(starter_link("workspace/activity", context)) + " to see the nested route fallback in action.

"; + main_body += component("../../components/workspace/primitives:SECTION", section_props, context); + + if(section == "activity") + { + DTree empty_props; + empty_props["icon_class"] = "fas fa-clock-rotate-left"; + empty_props["title"] = "No live stream wired yet"; + empty_props["text"] = "The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one."; + empty_props["action_html"] = "Open dashboard demo"; + main_body += component("../../components/workspace/primitives:EMPTY_STATE", empty_props, context); + } + + DTree panel_props; + panel_props["header_html"] = header; + panel_props["body_html"] = main_body; + String main = mobile_bar + component("../../components/workspace/primitives:PANEL", panel_props, context); + + DTree app_props; + app_props["id"] = "workspace-demo-shell"; + app_props["overlay_id"] = "workspace-demo-overlay"; + app_props["sidebar_html"] = sidebar; + app_props["main_html"] = main; + print(component("../../components/workspace/primitives:APP_FRAME", app_props, context)); + <> + + +} diff --git a/site/examples/web-app-starter/.ai/instructions.md b/site/examples/web-app-starter/.ai/instructions.md new file mode 100755 index 0000000..facb2e0 --- /dev/null +++ b/site/examples/web-app-starter/.ai/instructions.md @@ -0,0 +1,134 @@ +# Web App Starter - AI Assistant Instructions + +## Project Architecture +**Custom PHP framework** with server-side rendering focus, component-based UI, and clean URL routing. + +### Core Flow +1. `index.php` → `URL::MakeRoute()` → `views/{page}.php` → theme template +2. All requests flow through `index.php` (single entry point) +3. Views render into `URL::$fragments['main']`, then wrapped by theme + +### Key Directories +- `lib/` - Core classes (URL, DB, Log, Profiler, components) +- `views/` - Page templates (maps to URL routes) +- `components/` - Reusable UI components +- `config/settings.php` - Main configuration +- `themes/` - Page layout templates +- `js/` - Client-side code (uquery.js, morphdom.js, etc.) + +## Core Classes & Functions + +### Configuration +```php +cfg('key/path') // Get config value +cfg('url/pretty') // true=clean URLs, false=query strings +``` + +### URL Routing +```php +URL::MakeRoute() // Parse URL into URL::$route +URL::Link($path, $params) // Generate URLs (respects pretty URL setting) +URL::$route['page'] // Target view (default: 'index') +URL::$route['l-path'] // Full URL path +URL::$fragments['main'] // Main content area +``` + +### Components +**Files return arrays with render functions:** +```php + function($prop) { return '
...
'; }, + 'about' => 'Component description' +]; +``` + +**Usage:** +```php +component('path/to/component', ['prop' => 'value']) +component('path/to/component:method', $props) // Specific render method +component_declare('name', $definition) // Inline declaration +``` + +### Database +```php +DB::Query($sql, $params) // Execute any SQL query +DB::Get($sql, $params) // Get multiple rows as array +DB::GetRowWithQuery($sql, $params) // Get single row +DB::GetRow($table, $keyvalue) // Get row by primary key +DB::GetRow($table, $id, $keyname) // Get row by specific key +DB::GetRowsMatch($table, $criteria) // Get rows matching criteria +DB::Insert($table, $data) // Insert new row, returns ID +DB::Commit($table, $data) // Insert or update (REPLACE) +DB::Update($table, $where, $data) // Update existing rows +DB::RemoveRow($table, $keyvalue) // Delete row by key +``` + +### Logging +```php +Log::debug($module, $text) // log/debug.YYYY-mm.log +Log::text($module, $text) // log/log.YYYY-mm.log +Log::audit($module, $text) // System journal +``` + +### Profiler +```php +Profiler::log($text, $indent_level) // Runtime profiling +Profiler::$log // Access logged data +``` + +## Coding Conventions + +### File Structure +- Views: `views/{pagename}.php` +- Components: `components/{category}/{name}.php` +- Classes: `lib/{name}.class.php` +- Utilities: `lib/{name}.php` + +### JS and PHP Coding Style +- Use ` function($prop) { + return "
{$prop['content']}
"; + }, + 'render:variant' => function($prop) { /* alternative render */ }, + 'about' => 'Component description' +]; +``` + +### URL Handling +- Use `URL::Link()` for all internal links +- Views auto-map to URLs (`views/users.php` → `/users`) + +## Built-in Libraries +- **uquery.js** - jQuery-like DOM manipulation +- **morphdom.js** - DOM diffing +- **macrobars.js** - JavaScript templating + +## Common Patterns + +### New Page +1. Create `views/{pagename}.php` +2. Set URL::$page_type to 'json' or 'blank' if page is AJAX +3. Content (auto-wrapped by theme page variant controlled by URL::$page_type) + +### New Component +1. Create `components/{category}/{name}.php` +2. Return array with `render` function +3. Use `component('components/{category}/{name}', $props)` + +### Error Handling +- Logging: Use `Log::debug()` for development, `Log::audit()` for security +- Profiling: Use `Profiler::log()` for performance analysis + +## Development Notes +- Server-side rendering preferred over SPA approach +- Component system encourages reusability +- Single entry point simplifies routing/middleware diff --git a/site/examples/web-app-starter/.gitignore b/site/examples/web-app-starter/.gitignore new file mode 100755 index 0000000..f499afe --- /dev/null +++ b/site/examples/web-app-starter/.gitignore @@ -0,0 +1,66 @@ +# Logs +logs +*.log +.fuse* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +log/ +private/ +public/ +config/ + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next diff --git a/site/examples/web-app-starter/README.md b/site/examples/web-app-starter/README.md new file mode 100755 index 0000000..1eb29e7 --- /dev/null +++ b/site/examples/web-app-starter/README.md @@ -0,0 +1,208 @@ +# Web App Starter + +Simple web app starter package, expects to run in a domain's root directory but will run everywhere as long as cfg('url/pretty') is set to false. + +Don't be afraid of server-side rendering! But we have JS options too. + +## Nginx Configuration + +### Pretty URLs Support (Optional) +For pretty URLs (`cfg('url/pretty') = true`), add this configuration to enable clean URLs like `/users/profile` instead of `/?users/profile`: + +```nginx +location / { + try_files $uri $uri/ /index.php?$args; +} + +# the rest of the owl +location ~ ^/(config|lib|private|\.git)/ { + deny all; + return 404; +} +location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; +} +``` + +**Note**: Without pretty URL configuration, set `cfg('url/pretty') = false` in your config/settings.php and URLs will use query string format: `/?page¶m=value`. You can generate URLs with `URL::Link()` which will automatically use the appropriate format if you want to keep your options open (as to whether to use pretty URLs or not). + +## Routing + +`URL::MakeRoute()` creates `URL::$route` data. For example, a request to `http://example.com/some/url/path` +would look like this: + +``` +Array +( + [page] => index + [l-path] => some/url/path +) +``` + +The `page` key refers to the view should be rendered. By default, this is just `'index'`, leading to +`'views/index.php'` getting invoked. If the URL's path element starts with a colon, the entire path +is used as the `page` value. + +The `l-path` key refers to the actual URL path as given by the browser. For example, the `'views/index.php'` +view uses this to invoke the correct sub-view. + +The starter also supports two additional patterns backported from downstream apps: + +- directory index routing: `/workspace` can resolve to `views/workspace/index.php` +- parent-path fallback: `/workspace/activity` can resolve to `views/workspace/index.php` with `URL::$route['param'] === 'activity'` + +## Components + +Components are reusable PHP-backed view fragments. A component file returns an array with one or more render functions plus optional metadata. + +The default starter convention is to keep reusable components in `components/`. The loader supports explicit paths like `components/example/theme-switcher` and shorthand names like `example/theme-switcher`, which resolve under `components/`. + +```php + function($prop) { + return '
' . $prop['content'] . '
'; + }, + + 'about' => 'Description of what this component does', + +]; +``` + +### Using Components + +```php + +``` + +**Using a Specific Render Method Explicitly:** +```php + 'Status']) ?> +``` + +The older `component-name:method` shorthand is still supported for backwards compatibility, but `component_call()` or an explicit `render_call` prop is the preferred API because it is clearer and easier to grep. + +## Dashboard Primitives + +The starter now includes a small set of dashboard-focused building blocks backported from a more complex production frontend: + +- `js/u-format.js` for human-readable byte, count, and duration formatting plus unit-aware parsing +- `js/u-sortable-table.js` for lightweight sortable HTML tables with persisted sort state +- `js/u-timeseries-chart.js` for multi-series canvas charts without pulling in a charting framework +- `components/data/summary-metrics.php`, `components/data/sortable-table.php`, and `components/data/timeseries-chart.php` +- `views/dashboard.php` as a working end-to-end example + +## Workspace Primitives + +The starter also includes a second backport slice from the `uh-ai` portal app: + +- `components/workspace/*` for app shells, sidebars, panel headers, status pills, empty states, and compact utility buttons +- `themes/common/css/workspace.css` for the shared workspace shell styling +- `js/u-workspace-shell.js` for simple responsive sidebar toggling +- `views/workspace/index.php` as a nested-route demo for shell-style products + +## Theme Families + +The starter now includes named theme families derived from the `uh-ai` portal app and the `uh-llm2` admin shell: + +- `portal-light` for the B612-based corporate portal look +- `portal-dark` for the glassy dark portal shell and the current default starter theme +- `localfirst` for the cyan/orange llm2-style admin shell + +Theme choice is resolved server-side from `config/settings.php`, with a validated theme key stored in the `starter_theme` cookie. Theme metadata such as descriptions, footer text, and browser theme colors now live in the same config entry as the theme path, so the gallery, page shell, and docs all read from the same source of truth. + +To compare themes side by side, use `/?themes`. The gallery embeds a shared `/?theme-preview` route under every available theme so you can evaluate shell chrome, content density, and form styling with the same fixture. + +The page shell is now split between theme CSS/assets and shared helper functions in `lib/theme_helpers.php`, which keeps the four top-nav themes aligned while still allowing `localfirst` to provide its own admin-shell layout. + +The homepage demo form now includes proper `id` and `name` attributes so the starter does not emit the earlier label/field accessibility warnings on the landing page. + +## Gauge Primitives + +The gauges demo now contains two gauge families from `uh-llm2`: + +- `components/gauges/progressbar.php` and `components/gauges/needlegauge.php` for the original bar and needle gauges +- `components/gauges/arcgauge.php` for the llm2-style SVG KPI arc gauges with optional watermark tracking + +Abstract gauge classes live in `themes/common/css/gauges.css`, so the components stay theme-aware through CSS variables instead of shipping a competing visual system. + +## Testing + +The starter now includes a no-dependency smoke suite under `tests/`. + +Run it with: + +```bash +php tests/smoke.php +``` + +It covers route resolution, link generation, component lookup, shorthand component rendering, and centralized theme metadata. + +### Inline Components + +You can also declare components directly in code: + +```php +component_declare('my-button', [ + 'render' => function($prop) { + return ""; + } +]); +``` + +## Logging + +This package includes a very basic Log class. + +```php +Log::debug($module, $text) /* writes to log/debug.[Y]-[m].log */ +Log::text($module, $text) /* writes to log/log.[Y]-[m].log */ +Log::audit($module, $text) /* writes to system journal */ +``` + +`$module` should be a short string identifying the module or context of the log line. + +`$text` text to be logged. + +## Profiler + +A basic profiling fixture. + +```php +Profiler::log($text, $indent_level = 0) +``` + +`$text` name/text describing the checkpoint to be profiled. + +Profiler logs are not committed to disk. Log data for the current request is stored in +`Profiler::$log`. + +## Batteries included +``` +- lib/ulib.php - convenience functions +- lib/profiler.class.php - profiling +- lib/log.class.php - logging +- lib/url.class.php - URL routing +- lib/components.php - component rendering +- lib/odt.class.php - ODT document generator +- lib/db.class.php - database access + +- js/morphdom.js - DOM diffing +- js/uquery.js - DOM manipulation (my attempt at keeping the good parts of jQuery) +- js/site.js - site-wide JS, as a starting point +- js/macrobars.js - JS templating + +- js/ag-grid - ag-Grid +``` +## License (MIT Open Source) + +Copyright 2018-2025 udo@openfu.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/site/examples/web-app-starter/codesearch b/site/examples/web-app-starter/codesearch new file mode 100755 index 0000000..3d03d31 --- /dev/null +++ b/site/examples/web-app-starter/codesearch @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +csensitive=false +patterns=() + +# Parse flags and patterns +while [[ $# -gt 0 ]]; do + case "$1" in + -c|--case-sensitive) + csensitive=true + shift + ;; + -h|--help) + echo "Usage: $0 [-c|--case-sensitive] [...]" >&2 + exit 1 + ;; + -* ) + echo "Unknown option: $1" >&2 + echo "Usage: $0 [-c|--case-sensitive] [...]" >&2 + exit 1 + ;; + * ) + patterns+=("$1") + shift + ;; + esac +done + +# Ensure at least one pattern is provided +if (( ${#patterns[@]} == 0 )); then + echo "Usage: $0 [-c|--case-sensitive] [...]" >&2 + exit 1 +fi + +# Build common grep options +grep_opts=( -nR -H --color=tty ) +if ! $csensitive; then + grep_opts+=( -i ) +fi + +# Build -e arguments for each pattern +pattern_opts=() +for pat in "${patterns[@]}"; do + pattern_opts+=( -e "$pat" ) +done + +# Define search paths +declare -a php_sea=(".") +declare -a css_sea=(".") +declare -a js_sea=(".") + +# Search PHP files +for vs in "${php_sea[@]}"; do + grep "${grep_opts[@]}" --include="*.php" "${pattern_opts[@]}" "$vs" +done + +# Search CSS files (exclude fontawesome) +for vs in "${css_sea[@]}"; do + grep "${grep_opts[@]}" --include="*.css" "${pattern_opts[@]}" "$vs" \ + | grep -iv "fontawesome" +done + +# Search JS files +for vs in "${js_sea[@]}"; do + grep "${grep_opts[@]}" --include="*.js" "${pattern_opts[@]}" "$vs" +done diff --git a/site/examples/web-app-starter/components/auth/oauth-client.php b/site/examples/web-app-starter/components/auth/oauth-client.php new file mode 100644 index 0000000..a9e086b --- /dev/null +++ b/site/examples/web-app-starter/components/auth/oauth-client.php @@ -0,0 +1,283 @@ + function($prop) { + $services = $prop['services'] ?? [ + 'google' => [ + 'name' => 'Google', + 'color' => '#4285f4', + 'icon' => 'fab fa-google', + 'scope' => 'openid email profile', + 'auth_url' => 'https://accounts.google.com/oauth/authorize', + 'token_url' => 'https://oauth2.googleapis.com/token', + 'user_info_url' => 'https://www.googleapis.com/oauth2/v2/userinfo' + ], + 'github' => [ + 'name' => 'GitHub', + 'color' => '#333333', + 'icon' => 'fab fa-github', + 'scope' => 'user:email', + 'auth_url' => 'https://github.com/login/oauth/authorize', + 'token_url' => 'https://github.com/login/oauth/access_token', + 'user_info_url' => 'https://api.github.com/user' + ], + 'discord' => [ + 'name' => 'Discord', + 'color' => '#5865f2', + 'icon' => 'fab fa-discord', + 'scope' => 'identify email', + 'auth_url' => 'https://discord.com/api/oauth2/authorize', + 'token_url' => 'https://discord.com/api/oauth2/token', + 'user_info_url' => 'https://discord.com/api/users/@me' + ], + 'twitch' => [ + 'name' => 'Twitch', + 'color' => '#9146ff', + 'icon' => 'fab fa-twitch', + 'scope' => 'user:read:email', + 'auth_url' => 'https://id.twitch.tv/oauth2/authorize', + 'token_url' => 'https://id.twitch.tv/oauth2/token', + 'user_info_url' => 'https://api.twitch.tv/helix/users' + ] + ]; + $title = $prop['title'] ?? 'Sign in with OAuth'; + $subtitle = $prop['subtitle'] ?? 'Choose your preferred authentication method'; + $callback_url = $prop['callback_url'] ?? URL::Link('auth/callback'); + + ?> +
+
+

+ +
+ + $service): ?> +
+ + + + + +
+ + + +
+ + + + + 'OAuth authentication component with support for Google and other providers. Handles the complete OAuth flow including state verification and callback processing.' +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/basic/cookie-consent.php b/site/examples/web-app-starter/components/basic/cookie-consent.php new file mode 100755 index 0000000..b6794b0 --- /dev/null +++ b/site/examples/web-app-starter/components/basic/cookie-consent.php @@ -0,0 +1,245 @@ + function($prop) { + ?> + + + + + + + 'A GDPR-compliant cookie consent banner that appears at the bottom of the page with accept/reject options' +]; diff --git a/site/examples/web-app-starter/components/data/sortable-table.php b/site/examples/web-app-starter/components/data/sortable-table.php new file mode 100644 index 0000000..dab39d7 --- /dev/null +++ b/site/examples/web-app-starter/components/data/sortable-table.php @@ -0,0 +1,130 @@ += 1024 && $unitIndex < count($units) - 1) { + $value /= 1024; + $unitIndex += 1; + } + $decimals = $disk ? ($unitIndex >= 4 ? 2 : ($unitIndex >= 1 ? 1 : 0)) : ($unitIndex === 0 ? 0 : 1); + return number_format($value, $decimals) . ' ' . $units[$unitIndex]; + } +} + +if (!function_exists('sortable_table_format_value')) { + function sortable_table_format_value($value, $row, $column) + { + $format = $column['format'] ?? null; + if (is_callable($format)) { + return [$format($value, $row, $column), $value]; + } + + switch ($format) { + case 'number': + return [number_format((float)$value), (float)$value]; + case 'bytes': + return [sortable_table_format_bytes($value, false), (float)$value]; + case 'disk-bytes': + return [sortable_table_format_bytes($value, true), (float)$value]; + case 'percent': + return [number_format((float)$value, 1) . '%', (float)$value]; + case 'duration-ms': + $number = (float)$value; + if ($number >= 1000) { + return [number_format($number / 1000, $number >= 10000 ? 0 : 1) . ' s', $number]; + } + return [number_format($number, $number >= 100 ? 0 : 1) . ' ms', $number]; + case 'bool': + return [$value ? 'Yes' : 'No', $value ? 1 : 0]; + default: + if (is_bool($value)) { + return [$value ? 'Yes' : 'No', $value ? 1 : 0]; + } + return [(string)$value, is_scalar($value) ? $value : '']; + } + } +} + +return [ + 'render' => function($prop) { + include_js('js/u-format.js'); + include_js('js/u-sortable-table.js'); + + $tableId = (string)($prop['id'] ?? ('sortable-table-' . uniqid())); + $title = (string)($prop['title'] ?? ''); + $subtitle = (string)($prop['subtitle'] ?? ''); + $columns = is_array($prop['columns'] ?? null) ? $prop['columns'] : []; + $rows = is_array($prop['rows'] ?? null) ? $prop['rows'] : []; + $emptyLabel = (string)($prop['empty_label'] ?? 'No data available'); + $storageKey = (string)($prop['storage_key'] ?? ('starter.sort.' . $tableId)); + $sort = is_array($prop['sort'] ?? null) ? $prop['sort'] : []; + $initOptions = ['storageKey' => $storageKey]; + if (isset($sort['column'])) { + $initOptions['initialSort'] = [ + 'column' => (int)$sort['column'], + 'direction' => (($sort['direction'] ?? 'asc') === 'desc') ? 'desc' : 'asc', + ]; + } + $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; + ob_start(); + ?> +
+ +
+

+

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
>
+
+
+ + 'Lightweight sortable HTML table with remembered sort state and human-readable data formatting', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/data/summary-metrics.php b/site/examples/web-app-starter/components/data/summary-metrics.php new file mode 100644 index 0000000..b7fe0c0 --- /dev/null +++ b/site/examples/web-app-starter/components/data/summary-metrics.php @@ -0,0 +1,37 @@ + function($prop) { + $items = is_array($prop['items'] ?? null) ? $prop['items'] : []; + $title = (string)($prop['title'] ?? ''); + $subtitle = (string)($prop['subtitle'] ?? ''); + ob_start(); + ?> +
+ +
+

+

+
+ +
+ + + < class="dashboard-stat-card tone-"> +
+
+ +
+ + > + +
+
+ 'Responsive metric cards extracted from dashboard-style pages and adapted for generic starter overviews', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/data/table.php b/site/examples/web-app-starter/components/data/table.php new file mode 100755 index 0000000..da597b7 --- /dev/null +++ b/site/examples/web-app-starter/components/data/table.php @@ -0,0 +1,297 @@ + function($prop) { + // Generate unique ID for this table instance + $tableId = $prop['id'] ?? 'data-grid-' . uniqid(); + $data = $prop['items'] ?? []; + $columns = $prop['columns'] ?? null; + $options = $prop['options'] ?? []; + + // Auto-generate columns if not provided + if (!$columns && !empty($data)) { + $columns = []; + $firstItem = reset($data); + if (is_array($firstItem)) { + foreach (array_keys($firstItem) as $key) { + $columns[] = [ + 'field' => $key, + 'headerName' => ucfirst(str_replace('_', ' ', $key)), + 'sortable' => true, + 'filter' => true, + 'resizable' => true + ]; + } + } + } + + // Default grid options (Community edition compatible) + $defaultOptions = [ + 'pagination' => true, + 'paginationPageSize' => 20, + 'suppressMenuHide' => true, + 'animateRows' => true, + 'defaultColDef' => [ + 'sortable' => true, + 'filter' => true, + 'resizable' => true, + 'minWidth' => 100 + ] + ]; + + $gridOptions = array_merge($defaultOptions, $options); + $gridOptions['columnDefs'] = $columns; + $gridOptions['rowData'] = $data; + + ?> + +
+
+
+ + + + + + + rows + + + +
+ + + + + +
+
+
+ + +
+
+ +
+
+ + + + + + 'A powerful data grid component powered by ag-Grid with sorting, filtering, pagination, and export capabilities' +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/data/timeseries-chart.php b/site/examples/web-app-starter/components/data/timeseries-chart.php new file mode 100644 index 0000000..ededc4a --- /dev/null +++ b/site/examples/web-app-starter/components/data/timeseries-chart.php @@ -0,0 +1,44 @@ + function($prop) { + include_js('js/u-format.js'); + include_js('js/u-timeseries-chart.js'); + + $chartId = (string)($prop['id'] ?? ('ts-chart-' . uniqid())); + $canvasId = $chartId . '-canvas'; + $title = (string)($prop['title'] ?? ''); + $subtitle = (string)($prop['subtitle'] ?? ''); + $height = max(180, (int)($prop['height'] ?? 320)); + $series = array_values(is_array($prop['series'] ?? null) ? $prop['series'] : []); + $xLabels = array_values(is_array($prop['x_labels'] ?? null) ? $prop['x_labels'] : []); + $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; + ob_start(); + ?> +
+ +
+

+

+
+ + +
+ + 'Canvas-based multi-series time-series chart component backported from a production dashboard', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/example/brands-showcase.php b/site/examples/web-app-starter/components/example/brands-showcase.php new file mode 100755 index 0000000..5f210d3 --- /dev/null +++ b/site/examples/web-app-starter/components/example/brands-showcase.php @@ -0,0 +1,111 @@ + function($prop) { + $title = $prop['title'] ?? 'Trusted by Leading Companies'; + $logos = $prop['logos'] ?? [ + ['name' => 'TechCorp', 'url' => 'img/cat01.jpg'], + ['name' => 'StartupX', 'url' => 'img/cat01.jpg'], + ['name' => 'DevStudio', 'url' => 'img/cat01.jpg'], + ['name' => 'WebFlow', 'url' => 'img/cat01.jpg'], + ['name' => 'CodeLab', 'url' => 'img/cat01.jpg'], + ['name' => 'AppCraft', 'url' => 'img/cat01.jpg'] + ]; + + ?> +
+
+

+
+ $logo): ?> +
+ <?= safe($logo['name']) ?> +
+ +
+
+
+ + + 'A brand/logo showcase section with hover effects and animations' +]; ?> diff --git a/site/examples/web-app-starter/components/example/cta-section.php b/site/examples/web-app-starter/components/example/cta-section.php new file mode 100755 index 0000000..4587b7d --- /dev/null +++ b/site/examples/web-app-starter/components/example/cta-section.php @@ -0,0 +1,224 @@ + function($prop) { + $title = $prop['title'] ?? 'Ready to Get Started?'; + $subtitle = $prop['subtitle'] ?? 'Join thousands of developers building amazing applications with our framework.'; + $cta_text = $prop['cta_text'] ?? 'Start Building Now'; + $cta_link = $prop['cta_link'] ?? '#'; + $secondary_text = $prop['secondary_text'] ?? 'View Documentation'; + $secondary_link = $prop['secondary_link'] ?? '#'; + + ?> +
+
+
+

+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + 'A compelling call-to-action section with animated floating shapes and gradient background' +]; ?> diff --git a/site/examples/web-app-starter/components/example/features-grid.php b/site/examples/web-app-starter/components/example/features-grid.php new file mode 100755 index 0000000..a96095f --- /dev/null +++ b/site/examples/web-app-starter/components/example/features-grid.php @@ -0,0 +1,203 @@ + function($prop) { + $features = $prop['features'] ?? [ + [ + 'icon' => '⚡', + 'title' => 'Lightning Fast', + 'description' => 'Optimized for speed and performance with modern web technologies.' + ], + [ + 'icon' => '🎨', + 'title' => 'Beautiful Design', + 'description' => 'Carefully crafted components with attention to detail and user experience.' + ], + [ + 'icon' => '📱', + 'title' => 'Mobile First', + 'description' => 'Fully responsive design that works perfectly on all devices.' + ], + [ + 'icon' => '🔧', + 'title' => 'Easy to Use', + 'description' => 'Simple and intuitive component system for rapid development.' + ], + [ + 'icon' => '🛡️', + 'title' => 'Secure', + 'description' => 'Built with security best practices and modern PHP standards.' + ], + [ + 'icon' => '🚀', + 'title' => 'Scalable', + 'description' => 'Architecture designed to grow with your application needs.' + ] + ]; + + ?> +
+
+

Why Choose Our Framework?

+

Discover the powerful features that make development a breeze

+
+
+ $feature): ?> +
+
+ +
+

+

+
+
+ +
+
+ + + 'A modern features grid with hover animations and gradient effects' +]; ?> diff --git a/site/examples/web-app-starter/components/example/hero-section.php b/site/examples/web-app-starter/components/example/hero-section.php new file mode 100755 index 0000000..fada8dc --- /dev/null +++ b/site/examples/web-app-starter/components/example/hero-section.php @@ -0,0 +1,264 @@ + function($prop) { + $title = $prop['title'] ?? 'Welcome to the Present'; + $subtitle = $prop['subtitle'] ?? 'Experience no-quite-modern web development with our cutting-edge framework'; + $cta_text = $prop['cta_text'] ?? 'Get Started'; + $cta_link = $prop['cta_link'] ?? '#'; + + ?> +
+
+

+

+
+ + Learn More +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + 'A modern hero section with animated floating elements and gradient background' +]; ?> diff --git a/site/examples/web-app-starter/components/example/pricing-table.php b/site/examples/web-app-starter/components/example/pricing-table.php new file mode 100755 index 0000000..d2d6248 --- /dev/null +++ b/site/examples/web-app-starter/components/example/pricing-table.php @@ -0,0 +1,310 @@ + function($prop) { + $plans = $prop['plans'] ?? [ + [ + 'name' => 'Starter', + 'price' => 'Free', + 'period' => '', + 'description' => 'Perfect for small projects and learning', + 'features' => [ + 'Up to 3 projects', + 'Community support', + 'Core components', + 'Basic documentation' + ], + 'cta' => 'Get Started', + 'popular' => false + ], + [ + 'name' => 'Professional', + 'price' => '$29', + 'period' => '/month', + 'description' => 'Ideal for growing businesses and teams', + 'features' => [ + 'Unlimited projects', + 'Advanced components', + 'Priority support', + 'Team collaboration', + 'Custom themes', + 'Analytics dashboard' + ], + 'cta' => 'Start Free Trial', + 'popular' => true + ], + [ + 'name' => 'Enterprise', + 'price' => '$99', + 'period' => '/month', + 'description' => 'For large organizations with advanced needs', + 'features' => [ + 'Everything in Professional', + 'Custom integrations', + 'Dedicated support', + 'SLA guarantee', + 'Advanced security', + 'White-label options' + ], + 'cta' => 'Contact Sales', + 'popular' => false + ] + ]; + + ?> +
+
+
+

Choose Your Plan

+

Start building great applications today with our flexible pricing options

+
+
+ $plan): ?> +
+ + + + +
+

+
+ + + + +
+

+
+ +
+
    + +
  • + + +
  • + +
+
+ + +
+ +
+ + +
+
+ + + 'A modern pricing table with popular plan highlighting and smooth animations' +]; ?> diff --git a/site/examples/web-app-starter/components/example/stats-section.php b/site/examples/web-app-starter/components/example/stats-section.php new file mode 100755 index 0000000..baff19e --- /dev/null +++ b/site/examples/web-app-starter/components/example/stats-section.php @@ -0,0 +1,179 @@ + function($prop) { + $stats = $prop['stats'] ?? [ + ['number' => '99.9%', 'label' => 'Uptime'], + ['number' => '500ms', 'label' => 'Average Response'], + ['number' => '50K+', 'label' => 'Active Users'], + ['number' => '24/7', 'label' => 'Support'] + ]; + + ?> +
+
+
+

Trusted by Developers Worldwide

+

Join thousands of developers who have chosen our framework

+
+
+ $stat): ?> +
+
+
+
+
+
+
+ +
+
+
+ + + 'An animated statistics section with gradient backgrounds and progress bars' +]; ?> diff --git a/site/examples/web-app-starter/components/example/testimonials.php b/site/examples/web-app-starter/components/example/testimonials.php new file mode 100755 index 0000000..6663248 --- /dev/null +++ b/site/examples/web-app-starter/components/example/testimonials.php @@ -0,0 +1,214 @@ + function($prop) { + $testimonials = $prop['testimonials'] ?? [ + [ + 'name' => 'Sarah Johnson', + 'role' => 'Senior Developer at TechCorp', + 'avatar' => 'img/cat01.jpg', + 'content' => 'This framework has revolutionized our development process. The component system is intuitive and the performance is outstanding.', + 'rating' => 5 + ], + [ + 'name' => 'Michael Chen', + 'role' => 'CTO at StartupX', + 'avatar' => 'img/cat01.jpg', + 'content' => 'We switched from our legacy system to this framework and saw immediate improvements in both development speed and code quality.', + 'rating' => 5 + ], + [ + 'name' => 'Emily Rodriguez', + 'role' => 'Full Stack Developer', + 'avatar' => 'img/cat01.jpg', + 'content' => 'The documentation is excellent and the learning curve is gentle. Perfect for both beginners and experienced developers.', + 'rating' => 5 + ] + ]; + + ?> +
+
+
+

What Developers Say

+

Don't just take our word for it - hear from the community

+
+
+ $testimonial): ?> +
+
+
"
+

+
+ + + +
+
+
+ <?= safe($testimonial['name']) ?> +
+
+
+
+
+
+ +
+
+
+ + + 'A testimonials section with user photos, ratings, and smooth animations' +]; ?> diff --git a/site/examples/web-app-starter/components/example/theme-switcher.php b/site/examples/web-app-starter/components/example/theme-switcher.php new file mode 100755 index 0000000..f44254c --- /dev/null +++ b/site/examples/web-app-starter/components/example/theme-switcher.php @@ -0,0 +1,153 @@ + function($prop) { + $themes = cfg('theme/options'); + $currentTheme = (string)cfg('theme/key'); + $currentLabel = (string)first(cfg('theme/label'), $themes[$currentTheme]['label'] ?? $currentTheme); + $routePath = (string)(URL::$route['l-path'] ?? ''); + $buildThemeLink = function($themeKey) use($routePath) { + return URL::Link($routePath, ['theme' => $themeKey]); + }; + ?> +
+ + + +
+ + + 'A floating theme picker that switches between all configured starter theme families' +]; diff --git a/site/examples/web-app-starter/components/gauges/arcgauge.php b/site/examples/web-app-starter/components/gauges/arcgauge.php new file mode 100644 index 0000000..18002ba --- /dev/null +++ b/site/examples/web-app-starter/components/gauges/arcgauge.php @@ -0,0 +1,105 @@ + function($prop) { + $prop['id'] = !empty($prop['id']) ? $prop['id'] : 'arcgauge-'.uniqid(); + $prop['scale'] = $prop['scale'] ?? array(); + $prop['items'] = $prop['items'] ?? array(); + ?> +
+ +
+

+

+
+ +
+ $item) { + $item = array_merge($prop['scale'], $item); + $value = (float)first($item['value'], 0); + $max = (float)first($item['max'], 100); + $precision = isset($item['precision']) ? (int)$item['precision'] : 1; + $pct = $max > 0 ? clamp(($value / $max) * 100, 0, 100) : 0; + $arc_length = round(($pct / 100) * 157.08, 1); + $display_value = number_format($value, $precision).safe((string)($item['unit'] ?? '')); + $resolved_color = '#10b981'; + if(isset($item['color'])) + { + if(is_array($item['color'])) + { + $color_entry = pick_entry_from_range($item['color'], $value); + $resolved_color = first($color_entry['color'] ?? false, $resolved_color); + } + else + { + $resolved_color = (string)$item['color']; + } + } + else if($pct >= 85) + { + $resolved_color = 'var(--error, #ef4444)'; + } + else if($pct >= 60) + { + $resolved_color = 'var(--warning, #f59e0b)'; + } + else + { + $resolved_color = 'var(--success, #10b981)'; + } + ?> +
+
+ +
+
+ +
+
+ = entry.from && value <= entry.to) return entry; + } + return null; + }, + + /** + * Creates an SVG element with namespace + */ + createSVGElement: function(tagName, attributes = {}) { + const element = document.createElementNS('http://www.w3.org/2000/svg', tagName); + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + return element; + }, + + /** + * Gets CSS custom property value from computed styles + */ + getCSSVar: function(varName) { + return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); + }, + + resolveColor: function(colorSpec, value, pct) { + if (Array.isArray(colorSpec)) { + const match = this.pickEntryFromRange(colorSpec, value); + if (match && match.color) return match.color; + } + if (typeof colorSpec === 'string' && colorSpec !== '') return colorSpec; + if (pct < 60) return this.getCSSVar('--success') || '#10b981'; + if (pct < 85) return this.getCSSVar('--warning') || '#f59e0b'; + return this.getCSSVar('--error') || '#ef4444'; + }, + + formatValue: function(value, precision, suffix) { + const numericValue = Number(value); + const normalizedPrecision = Number.isFinite(precision) ? precision : 1; + if (!Number.isFinite(numericValue)) return '--'; + return numericValue.toFixed(normalizedPrecision) + (suffix || ''); + }, + + gaugeArcPoint: function(pct, radius = 50) { + const angle = Math.PI - (pct / 100) * Math.PI; + return { + x: 60 + radius * Math.cos(angle), + y: 60 - radius * Math.sin(angle), + }; + }, + + updateWatermark: function(prefix, pct) { + const now = Date.now(); + this._watermarks = this._watermarks || {}; + let watermark = this._watermarks[prefix]; + const resetWindow = 10 * 60 * 1000; + if (!watermark || (now - watermark.resetTs) > resetWindow) { + watermark = { lo: pct, hi: pct, resetTs: now }; + this._watermarks[prefix] = watermark; + } else { + if (pct < watermark.lo) watermark.lo = pct; + if (pct > watermark.hi) watermark.hi = pct; + } + return watermark; + }, + + renderWatermarkTick: function(lineId, pct) { + const line = document.getElementById(lineId); + if (!line) return; + if (pct == null) { + line.setAttribute('opacity', '0'); + return; + } + const outer = this.gaugeArcPoint(pct, 53); + const inner = this.gaugeArcPoint(pct, 43); + line.setAttribute('x1', outer.x.toFixed(1)); + line.setAttribute('y1', outer.y.toFixed(1)); + line.setAttribute('x2', inner.x.toFixed(1)); + line.setAttribute('y2', inner.y.toFixed(1)); + line.setAttribute('opacity', '0.7'); + }, + + updateArcGauge: function(options) { + const value = Number(options.value); + const max = Number(options.max || 100); + const normalizedValue = Number.isFinite(value) ? value : 0; + const pct = this.clampValue(max === 0 ? 0 : (normalizedValue / max) * 100, 0, 100); + const arcLength = (pct / 100) * 157.08; + const arc = document.getElementById(options.arcId); + const text = document.getElementById(options.textId); + const meta = options.metaId ? document.getElementById(options.metaId) : null; + if (arc) { + arc.setAttribute('stroke-dasharray', arcLength.toFixed(1) + ' 157.08'); + arc.setAttribute('stroke', this.resolveColor(options.color, normalizedValue, pct)); + } + if (text) { + text.textContent = this.formatValue(normalizedValue, options.precision, options.suffix); + } + if (meta && options.meta != null) { + meta.textContent = options.meta; + } + if (options.watermarkPrefix) { + const watermark = this.updateWatermark(options.watermarkPrefix, pct); + this.renderWatermarkTick(options.watermarkPrefix + 'WmLo', watermark.lo); + this.renderWatermarkTick(options.watermarkPrefix + 'WmHi', watermark.hi); + } + } + +}); \ No newline at end of file diff --git a/site/examples/web-app-starter/components/gauges/needlegauge.php b/site/examples/web-app-starter/components/gauges/needlegauge.php new file mode 100755 index 0000000..80ad178 --- /dev/null +++ b/site/examples/web-app-starter/components/gauges/needlegauge.php @@ -0,0 +1,173 @@ + function($prop) { + $prop['id'] = !empty($prop['id']) ? $prop['id'] : 'needlegauge-' . uniqid(); + $prop['style'] = (string)($prop['style'] ?? ''); + $prop['items'] = (array)($prop['items'] ?? array()); + $prop['scale'] = (array)($prop['scale'] ?? array()); + $prop['size'] = first($prop['size'] ?? false, 200); + $prop['subtitle'] = (string)($prop['subtitle'] ?? ''); + $prop['scale']['angle_start'] = first($prop['scale']['angle_start'], -pi()); + $prop['scale']['angle_end'] = first($prop['scale']['angle_end'], 0); + $prop['img_height'] = first($prop['img_height'] ?? false, + $prop['size'] * max(cos(first($prop['scale']['angle_start'], -pi())), cos(first($prop['scale']['angle_end'], 0)))); + ?> +
+ +
+

+

+
+ +
+ $item) + { + $item = array_merge($prop['scale'], $item); + $item['min'] = first($item['min'] ?? false, 0); + $item['max'] = first($item['max'] ?? false, 100); + $item['tooltip'] = (string)($item['tooltip'] ?? ''); + $item['unit'] = (string)($item['unit'] ?? ''); + $item['color'] = $item['color'] ?? false; + $item['label'] = (string)first($item['label'] ?? false, ucfirst((string)$item_id)); + $vrange = ($item['max'] - $item['min']); + $pct_value = clamp(($item['value'] - $item['min']) / $vrange, 0, 1); + $needle_angle = -pi() + $item['angle_start'] + ($pct_value * ($item['angle_end'] - $item['angle_start'])); + $needle_color = first($item['color'] ?? false, '#888888'); + if(is_array($item['color'])) { + $color_entry = pick_entry_from_range($item['color'], $item['value']); + $needle_color = first($color_entry['color'] ?? false, '#888888'); + } + $tick_interval = first($item['ticks-every'], $vrange / 20); + $label_interval = first($item['value-labels-every'], $vrange / 4); + $tick_number = 0; + $ticks_html = ''; + $min_angle = first($item['angle_start']); + $max_angle = first($item['angle_end']); + $tick_color = first($item['tick-color'], '#888888'); + + for($v = $item['min']; $v <= $item['max']; $v += $tick_interval) + { + $tick_number++; + $angle = $min_angle + (($v-$item['min'])/$vrange) * ($max_angle - $min_angle); + + $x1 = $prop['size']/2 + cos($angle) * $prop['size']*0.38; + $y1 = $prop['size']/2 + sin($angle) * $prop['size']*0.38; + $x2 = $prop['size']/2 + cos($angle) * $prop['size']*0.41; + $y2 = $prop['size']/2 + sin($angle) * $prop['size']*0.41; + + $ticks_html .= ''; + + } + + for($v = $item['min']; $v <= $item['max']; $v += $label_interval) + { + $tick_number++; + $angle = $min_angle + (($v-$item['min'])/$vrange) * ($max_angle - $min_angle); + + $x1 = $prop['size']/2 + cos($angle) * $prop['size']*0.35; + $y1 = $prop['size']/2 + sin($angle) * $prop['size']*0.35; + $x2 = $prop['size']/2 + cos($angle) * $prop['size']*0.41; + $y2 = $prop['size']/2 + sin($angle) * $prop['size']*0.41; + + $ticks_html .= ''; + + $lx = $prop['size']/2 + cos($angle) * $prop['size']*0.45; + $ly = $prop['size']/2 + sin($angle) * $prop['size']*0.45; + $ticks_html .= ''.($v == 0 ? '0' : $v).''; + } + + ?> +
+
+ +
+ + + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+ +
+
+ + +window.ProgressbarComponents = window.ProgressbarComponents || { + + start_listen : function(prop) { + $.events.on('value-broadcast', function(data) { + if(prop.items[data.name]) { + // update value text + let bar = Object.assign({}, prop.scale, prop.items[data.name]); + $('#' + prop.id + '-' + data.name + '-value').text(data.value + (bar.unit || '')); + // update bar width/height + let vrange = (bar.max || 100) - (bar.min || 0); + let pct_value = GaugeComponents.clampValue((data.value - (bar.min || 0)) / vrange * 100, 0, 100); + if(Array.isArray(bar.color)) { + let colorMatch = GaugeComponents.pickEntryFromRange(bar.color, Number(data.value)); + if(colorMatch && colorMatch.color) + $('#' + prop.id + '-' + data.name + '-bar').css('background', colorMatch.color); + } + if(prop.layout === 'horizontal') { + $('#' + prop.id + '-' + data.name + '-bar').css('width', pct_value + '%'); + } else { + $('#' + prop.id + '-' + data.name + '-bar').css('height', pct_value + '%'); + } + } + }); + }, + +} + + function($prop) { + $prop['id'] = !empty($prop['id']) ? $prop['id'] : 'progressbar-'.uniqid(); + $prop['style'] = (string)($prop['style'] ?? ''); + $prop['item-style'] = (string)($prop['item-style'] ?? ''); + $prop['label-style'] = (string)($prop['label-style'] ?? ''); + $prop['value-style'] = (string)($prop['value-style'] ?? ''); + $prop['bar-style'] = (string)($prop['bar-style'] ?? ''); + $prop['items'] = (array)($prop['items'] ?? array()); + $default_palette = [ + 'var(--success, #10b981)', + 'var(--primary, #60a5fa)', + 'var(--accent, #22d3ee)', + 'var(--warning, #f59e0b)', + ]; + $auto_color_counter = 0; + if(empty($prop['scale'])) $prop['scale'] = []; + $layout = first($prop['layout'] ?? false, 'horizontal'); + ?> +
+ +
+

+

+
+ + +
+ $bar) + { + $bar = array_merge($prop['scale'], $bar); + $bar['style'] = (string)($bar['style'] ?? ''); + $bar['tooltip'] = (string)($bar['tooltip'] ?? ''); + $bar['before'] = $bar['before'] ?? ''; + $bar['after'] = $bar['after'] ?? ''; + $bar['unit'] = (string)($bar['unit'] ?? ''); + $bar['color'] = $bar['color'] ?? false; + $vrange = (first($bar['max'], 100) - first($bar['min'], 0)); + $bar['pct-value'] = clamp(($bar['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100); + if(is_array($bar['color'])) + { + $color_entry = pick_entry_from_range($bar['color'], $bar['value']); + $bar['color'] = $color_entry['color'] ?? false; + } + if(!$bar['color']) + $bar['color'] = first($prop['bar-color'] ?? false, $default_palette[$auto_color_counter++ % sizeof($default_palette)]); + ?> +
+ +
+
+ +
+
+ +
+
+
+
+
+ $marker) { + $marker_pct = clamp(($marker['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100); + ?> +
+
+ +
+
+ +
+ +
+ +
+ 0) + { + foreach($prop['items'] as $bar_id => $bar) + { + $bar = array_merge($prop['scale'], $bar); + $bar['style'] = (string)($bar['style'] ?? ''); + $bar['tooltip'] = (string)($bar['tooltip'] ?? ''); + $bar['before'] = $bar['before'] ?? ''; + $bar['after'] = $bar['after'] ?? ''; + $bar['unit'] = (string)($bar['unit'] ?? ''); + $bar['color'] = $bar['color'] ?? false; + $vrange = (first($bar['max'], 100) - first($bar['min'], 0)); + $bar['pct-value'] = clamp(($bar['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100); + if(is_array($bar['color'])) + { + $color_entry = pick_entry_from_range($bar['color'], $bar['value']); + $bar['color'] = $color_entry['color'] ?? false; + } + if(!$bar['color']) + $bar['color'] = first($prop['bar-color'] ?? false, $default_palette[$auto_color_counter++ % sizeof($default_palette)]); + ?> +
+ +
+ +
+ +
+
+
+ $marker) { + $marker_pct = clamp(($marker['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100); + ?> +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+ function($prop) { + include_css('themes/common/css/workspace.css'); + include_js('js/u-workspace-shell.js'); + + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $sidebar = (string)($prop['sidebar_html'] ?? ''); + $main = (string)($prop['main_html'] ?? ''); + $overlayId = trim((string)($prop['overlay_id'] ?? '')); + ob_start(); + ?> + class="ws-app"> + + class="ws-sidebar-overlay">
+
+ +
+
+ 'Generic workspace app shell with sidebar, overlay, and main content area', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/empty-state.php b/site/examples/web-app-starter/components/workspace/empty-state.php new file mode 100644 index 0000000..e1a94a2 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/empty-state.php @@ -0,0 +1,22 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $icon = trim((string)($prop['icon_class'] ?? 'fas fa-layer-group')); + $title = (string)($prop['title'] ?? ''); + $text = (string)($prop['text'] ?? ''); + $actionHtml = (string)($prop['action_html'] ?? ''); + ob_start(); + ?> + class="ws-empty-state"> +
+

+

+ +
+ 'Centered empty-state block for shell panels and placeholder screens', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/icon-button.php b/site/examples/web-app-starter/components/workspace/icon-button.php new file mode 100644 index 0000000..b638395 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/icon-button.php @@ -0,0 +1,21 @@ + function($prop) { + $tag = trim((string)($prop['tag'] ?? 'button')); + if (!in_array($tag, ['button', 'span', 'a'], true)) $tag = 'button'; + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $title = trim((string)($prop['title'] ?? '')); + $icon = trim((string)($prop['icon_class'] ?? '')); + $text = (string)($prop['text'] ?? ''); + $attrs = trim((string)($prop['attrs'] ?? '')); + $href = trim((string)($prop['href'] ?? '')); + $type = trim((string)($prop['type'] ?? 'button')); + ob_start(); + ?> + < class="ws-icon-btn">> + 'Small utility icon button for shell toolbars and compact actions', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/list-state.php b/site/examples/web-app-starter/components/workspace/list-state.php new file mode 100644 index 0000000..cf3fc8d --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/list-state.php @@ -0,0 +1,15 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $icon = trim((string)($prop['icon_class'] ?? '')); + $text = (string)($prop['text'] ?? ''); + ob_start(); + ?> + class="ws-list-state">
+ 'Compact sidebar/list placeholder state with optional icon', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/mobile-bar.php b/site/examples/web-app-starter/components/workspace/mobile-bar.php new file mode 100644 index 0000000..992405a --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/mobile-bar.php @@ -0,0 +1,24 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $buttonId = trim((string)($prop['button_id'] ?? '')); + $buttonClass = trim((string)($prop['button_class'] ?? '')); + $titleId = trim((string)($prop['title_id'] ?? '')); + $titleClass = trim((string)($prop['title_class'] ?? '')); + $title = (string)($prop['title'] ?? ''); + $icon = trim((string)($prop['icon_class'] ?? 'fas fa-bars')); + ob_start(); + ?> + class="ws-mobile-bar"> + class="ws-mobile-toggle" title="Toggle sidebar" type="button"> + + + class="ws-mobile-title"> +
+ 'Compact mobile header bar for workspace layouts with a sidebar toggle', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/panel-header.php b/site/examples/web-app-starter/components/workspace/panel-header.php new file mode 100644 index 0000000..d8eb2f3 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/panel-header.php @@ -0,0 +1,25 @@ + function($prop) { + $class = trim((string)($prop['class'] ?? '')); + $title = (string)($prop['title'] ?? ''); + $subtitle = (string)($prop['subtitle'] ?? ''); + $titleId = trim((string)($prop['title_id'] ?? '')); + $subtitleId = trim((string)($prop['subtitle_id'] ?? '')); + $actionsHtml = (string)($prop['actions_html'] ?? ''); + ob_start(); + ?> +
+
+ > + + class="ws-subtitle">

+ +
+
+
+ 'Panel heading with title, optional subtitle, and actions slot', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/panel.php b/site/examples/web-app-starter/components/workspace/panel.php new file mode 100644 index 0000000..2f27de2 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/panel.php @@ -0,0 +1,19 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $attrs = trim((string)($prop['attrs'] ?? '')); + $header = (string)($prop['header_html'] ?? ''); + $body = (string)($prop['body_html'] ?? ''); + ob_start(); + ?> + class="ws-panel"> + + + + 'Flexible workspace panel container with separate header and body slots', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/section-head.php b/site/examples/web-app-starter/components/workspace/section-head.php new file mode 100644 index 0000000..c377509 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/section-head.php @@ -0,0 +1,17 @@ + function($prop) { + $class = trim((string)($prop['class'] ?? '')); + $title = (string)($prop['title'] ?? ''); + $actionsHtml = (string)($prop['actions_html'] ?? ''); + ob_start(); + ?> +
+

+
+
+ 'Compact section heading for grouped workspace content', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/section.php b/site/examples/web-app-starter/components/workspace/section.php new file mode 100644 index 0000000..611f41e --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/section.php @@ -0,0 +1,19 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $attrs = trim((string)($prop['attrs'] ?? '')); + $header = (string)($prop['header_html'] ?? ''); + $body = (string)($prop['body_html'] ?? ''); + ob_start(); + ?> + class="ws-section"> + + + + 'Stacked workspace section wrapper for grouped content blocks', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/sidebar-shell.php b/site/examples/web-app-starter/components/workspace/sidebar-shell.php new file mode 100644 index 0000000..8720e4f --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/sidebar-shell.php @@ -0,0 +1,18 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $top = (string)($prop['top_html'] ?? ''); + $body = (string)($prop['body_html'] ?? ''); + ob_start(); + ?> + class="ws-sidebar"> + + + + 'Generic workspace sidebar wrapper with separate top and body slots', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/sidebar-toolbar.php b/site/examples/web-app-starter/components/workspace/sidebar-toolbar.php new file mode 100644 index 0000000..946ec26 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/sidebar-toolbar.php @@ -0,0 +1,26 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $actionHtml = (string)($prop['action_html'] ?? ''); + $searchId = trim((string)($prop['search_id'] ?? '')); + $searchClass = trim((string)($prop['search_class'] ?? '')); + $searchInputId = trim((string)($prop['search_input_id'] ?? '')); + $searchInputName = trim((string)($prop['search_input_name'] ?? 'search')); + $searchInputClass = trim((string)($prop['search_input_class'] ?? '')); + $searchPlaceholder = (string)($prop['search_placeholder'] ?? 'Search...'); + ob_start(); + ?> + class="ws-sidebar-top"> + + class="ws-search-wrap"> + + name="" class="ws-search-input" placeholder="" autocomplete="off"> + + + 'Sidebar toolbar with optional action area and compact search field', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/components/workspace/status-pill.php b/site/examples/web-app-starter/components/workspace/status-pill.php new file mode 100644 index 0000000..90f0f35 --- /dev/null +++ b/site/examples/web-app-starter/components/workspace/status-pill.php @@ -0,0 +1,17 @@ + function($prop) { + $id = trim((string)($prop['id'] ?? '')); + $class = trim((string)($prop['class'] ?? '')); + $label = trim((string)($prop['label'] ?? $prop['text'] ?? '')); + $variant = preg_replace('/[^a-z0-9_-]/i', '', strtolower(trim((string)($prop['variant'] ?? 'neutral')))); + if ($variant === '') $variant = 'neutral'; + $title = trim((string)($prop['title'] ?? '')); + ob_start(); + ?> + class="ws-status-pill ws-status-pill-"> + 'Compact semantic status badge for neutral, info, success, warning, and danger states', +]; \ No newline at end of file diff --git a/site/examples/web-app-starter/img/cat01.jpg b/site/examples/web-app-starter/img/cat01.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/web-app-starter/img/cat01.jpg differ diff --git a/site/examples/web-app-starter/img/cat02.jpg b/site/examples/web-app-starter/img/cat02.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/web-app-starter/img/cat02.jpg differ diff --git a/site/examples/web-app-starter/img/cat03.jpg b/site/examples/web-app-starter/img/cat03.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/web-app-starter/img/cat03.jpg differ diff --git a/site/examples/web-app-starter/img/cat04.jpg b/site/examples/web-app-starter/img/cat04.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/web-app-starter/img/cat04.jpg differ diff --git a/site/examples/web-app-starter/img/cat05.jpg b/site/examples/web-app-starter/img/cat05.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/web-app-starter/img/cat05.jpg differ diff --git a/site/examples/web-app-starter/img/cat06.jpg b/site/examples/web-app-starter/img/cat06.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/examples/web-app-starter/img/cat06.jpg differ diff --git a/site/examples/web-app-starter/index.php b/site/examples/web-app-starter/index.php new file mode 100755 index 0000000..59d10d7 --- /dev/null +++ b/site/examples/web-app-starter/index.php @@ -0,0 +1,40 @@ +'; + echo '

404 Not Found

'; + echo '

'.safe(URL::$error).'

'; + echo ''; + } + URL::$fragments['main'] = ob_get_clean(); + + Profiler::log('main content: end', -1); + + $page_template = cfg('theme/path').'/page.'.URL::$page_type.'.php'; + if(!file_exists($page_template)) + $page_template = 'themes/common/page.'.URL::$page_type.'.php'; + if(!file_exists($page_template)) + die('fatal error: page template not found ('.$page_template.')'); + require($page_template); + + Log::audit('page:'.URL::$route['page'], URL::$route['l-path']); diff --git a/site/examples/web-app-starter/js/ag-grid/ag-grid-community.min.js b/site/examples/web-app-starter/js/ag-grid/ag-grid-community.min.js new file mode 100755 index 0000000..1a78246 --- /dev/null +++ b/site/examples/web-app-starter/js/ag-grid/ag-grid-community.min.js @@ -0,0 +1,8 @@ +/** + * @ag-grid-community/all-modules - Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue + * @version v31.1.0 + * @link https://www.ag-grid.com/ + * @license MIT + */ +// @ag-grid-community/all-modules v31.1.0 +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.agGrid=t():e.agGrid=t()}(window,(function(){return function(e){var t={};function a(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,a),o.l=!0,o.exports}return a.m=e,a.c=t,a.d=function(e,t,r){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(a.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)a.d(r,o,function(t){return e[t]}.bind(null,o));return r},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=6)}([function(e,t,a){"use strict";a.r(t),a.d(t,"ColumnFactory",(function(){return it})),a.d(t,"ColumnModel",(function(){return kt})),a.d(t,"ColumnKeyCreator",(function(){return M})),a.d(t,"ColumnUtils",(function(){return At})),a.d(t,"DisplayedGroupCreator",(function(){return Ot})),a.d(t,"GroupInstanceIdCreator",(function(){return gt})),a.d(t,"GROUP_AUTO_COLUMN_ID",(function(){return ct})),a.d(t,"ComponentUtil",(function(){return Nt})),a.d(t,"AgStackComponentsRegistry",(function(){return Ut})),a.d(t,"UserComponentRegistry",(function(){return un})),a.d(t,"UserComponentFactory",(function(){return Pn})),a.d(t,"ColDefUtil",(function(){return Ln})),a.d(t,"BeanStub",(function(){return at})),a.d(t,"Context",(function(){return ie})),a.d(t,"Autowired",(function(){return de})),a.d(t,"PostConstruct",(function(){return le})),a.d(t,"PreConstruct",(function(){return ne})),a.d(t,"Optional",(function(){return ce})),a.d(t,"Bean",(function(){return ge})),a.d(t,"Qualifier",(function(){return pe})),a.d(t,"PreDestroy",(function(){return se})),a.d(t,"QuerySelector",(function(){return po})),a.d(t,"RefSelector",(function(){return ho})),a.d(t,"ExcelFactoryMode",(function(){return Nn})),a.d(t,"DragAndDropService",(function(){return Ii})),a.d(t,"DragSourceType",(function(){return Ni})),a.d(t,"DragService",(function(){return In})),a.d(t,"VirtualListDragFeature",(function(){return _n})),a.d(t,"Column",(function(){return Ee})),a.d(t,"ColumnGroup",(function(){return lt})),a.d(t,"ProvidedColumnGroup",(function(){return xe})),a.d(t,"RowNode",(function(){return Ai})),a.d(t,"RowHighlightPosition",(function(){return Bn})),a.d(t,"FilterManager",(function(){return Jn})),a.d(t,"ProvidedFilter",(function(){return ko})),a.d(t,"SimpleFilter",(function(){return Ho})),a.d(t,"ScalarFilter",(function(){return _o})),a.d(t,"NumberFilter",(function(){return ei})),a.d(t,"TextFilter",(function(){return ai})),a.d(t,"DateFilter",(function(){return Wo})),a.d(t,"TextFloatingFilter",(function(){return si})),a.d(t,"HeaderFilterCellComp",(function(){return $n})),a.d(t,"FloatingFilterMapper",(function(){return On})),a.d(t,"GridBodyComp",(function(){return $l})),a.d(t,"GridBodyCtrl",(function(){return cl})),a.d(t,"RowAnimationCssClasses",(function(){return gl})),a.d(t,"ScrollVisibleService",(function(){return ts})),a.d(t,"MouseEventService",(function(){return os})),a.d(t,"NavigationService",(function(){return ns})),a.d(t,"RowContainerComp",(function(){return us})),a.d(t,"RowContainerName",(function(){return Wl})),a.d(t,"RowContainerCtrl",(function(){return Jl})),a.d(t,"RowContainerType",(function(){return Ul})),a.d(t,"getRowContainerTypeForName",(function(){return Kl})),a.d(t,"BodyDropPivotTarget",(function(){return hs})),a.d(t,"BodyDropTarget",(function(){return bs})),a.d(t,"CssClassApplier",(function(){return Rl})),a.d(t,"HeaderRowContainerComp",(function(){return Xs})),a.d(t,"GridHeaderComp",(function(){return og})),a.d(t,"GridHeaderCtrl",(function(){return ag})),a.d(t,"HeaderRowComp",(function(){return ks})),a.d(t,"HeaderRowType",(function(){return Rs})),a.d(t,"HeaderRowCtrl",(function(){return Ks})),a.d(t,"HeaderCellCtrl",(function(){return Vs})),a.d(t,"SortIndicatorComp",(function(){return ci})),a.d(t,"HeaderFilterCellCtrl",(function(){return Ls})),a.d(t,"HeaderGroupCellCtrl",(function(){return Ws})),a.d(t,"AbstractHeaderCellCtrl",(function(){return As})),a.d(t,"HeaderRowContainerCtrl",(function(){return Qs})),a.d(t,"HorizontalResizeService",(function(){return ng})),a.d(t,"MoveColumnFeature",(function(){return fs})),a.d(t,"StandardMenuFactory",(function(){return sg})),a.d(t,"TabbedLayout",(function(){return hg})),a.d(t,"ResizeObserverService",(function(){return vg})),a.d(t,"AnimationFrameService",(function(){return wg})),a.d(t,"ExpansionService",(function(){return yg})),a.d(t,"MenuService",(function(){return Eg})),a.d(t,"LargeTextCellEditor",(function(){return wi})),a.d(t,"PopupEditorWrapper",(function(){return ss})),a.d(t,"SelectCellEditor",(function(){return Ci})),a.d(t,"TextCellEditor",(function(){return Ri})),a.d(t,"NumberCellEditor",(function(){return $i})),a.d(t,"DateCellEditor",(function(){return tn})),a.d(t,"DateStringCellEditor",(function(){return on})),a.d(t,"CheckboxCellEditor",(function(){return gn})),a.d(t,"Beans",(function(){return bl})),a.d(t,"AnimateShowChangeCellRenderer",(function(){return ki})),a.d(t,"AnimateSlideCellRenderer",(function(){return Ti})),a.d(t,"GroupCellRenderer",(function(){return ji})),a.d(t,"GroupCellRendererCtrl",(function(){return Wi})),a.d(t,"SetLeftFeature",(function(){return Os})),a.d(t,"PositionableFeature",(function(){return Ro})),a.d(t,"AutoWidthCalculator",(function(){return xg})),a.d(t,"CheckboxSelectionComponent",(function(){return Pi})),a.d(t,"CellComp",(function(){return gs})),a.d(t,"CellCtrl",(function(){return kl})),a.d(t,"RowCtrl",(function(){return Al})),a.d(t,"RowRenderer",(function(){return Ag})),a.d(t,"ValueFormatterService",(function(){return Mg})),a.d(t,"CssClassManager",(function(){return so})),a.d(t,"CheckboxCellRenderer",(function(){return ln})),a.d(t,"PinnedRowModel",(function(){return Lg})),a.d(t,"ServerSideTransactionResultStatus",(function(){return Ng})),a.d(t,"ChangedPath",(function(){return Fg})),a.d(t,"RowNodeBlock",(function(){return Ig})),a.d(t,"RowNodeBlockLoader",(function(){return _g})),a.d(t,"PaginationProxy",(function(){return qg})),a.d(t,"ClientSideRowModelSteps",(function(){return bg})),a.d(t,"StylingService",(function(){return Ug})),a.d(t,"LayoutCssClasses",(function(){return el})),a.d(t,"AgAbstractField",(function(){return Ao})),a.d(t,"AgCheckbox",(function(){return Io})),a.d(t,"AgRadioButton",(function(){return Go})),a.d(t,"AgToggleButton",(function(){return jg})),a.d(t,"AgInputTextField",(function(){return Jo})),a.d(t,"AgInputTextArea",(function(){return Kg})),a.d(t,"AgInputNumberField",(function(){return Xo})),a.d(t,"AgInputDateField",(function(){return Yg})),a.d(t,"AgInputRange",(function(){return Qg})),a.d(t,"AgRichSelect",(function(){return td})),a.d(t,"AgSelect",(function(){return Lo})),a.d(t,"AgSlider",(function(){return rd})),a.d(t,"AgGroupComponent",(function(){return id})),a.d(t,"AgMenuItemRenderer",(function(){return dn})),a.d(t,"AgMenuItemComponent",(function(){return cd})),a.d(t,"AgMenuList",(function(){return ld})),a.d(t,"AgMenuPanel",(function(){return gd})),a.d(t,"AgDialog",(function(){return md})),a.d(t,"AgPanel",(function(){return pd})),a.d(t,"Component",(function(){return uo})),a.d(t,"ManagedFocusFeature",(function(){return So})),a.d(t,"TabGuardComp",(function(){return ug})),a.d(t,"TabGuardCtrl",(function(){return cg})),a.d(t,"TabGuardClassNames",(function(){return gg})),a.d(t,"PopupComponent",(function(){return vi})),a.d(t,"PopupService",(function(){return Cd})),a.d(t,"TouchListener",(function(){return gi})),a.d(t,"VirtualList",(function(){return $g})),a.d(t,"AgAbstractLabel",(function(){return To})),a.d(t,"AgPickerField",(function(){return Oo})),a.d(t,"AgAutocomplete",(function(){return xd})),a.d(t,"CellRangeType",(function(){return pl})),a.d(t,"SelectionHandleType",(function(){return ul})),a.d(t,"AutoScrollService",(function(){return Gn})),a.d(t,"VanillaFrameworkOverrides",(function(){return zd})),a.d(t,"CellNavigationService",(function(){return Ad})),a.d(t,"AlignedGridsService",(function(){return Md})),a.d(t,"KeyCode",(function(){return Wr})),a.d(t,"VerticalDirection",(function(){return Di})),a.d(t,"HorizontalDirection",(function(){return Oi})),a.d(t,"Grid",(function(){return _u})),a.d(t,"GridCoreCreator",(function(){return qu})),a.d(t,"createGrid",(function(){return Hu})),a.d(t,"GridApi",(function(){return Un})),a.d(t,"Events",(function(){return st})),a.d(t,"FocusService",(function(){return rc})),a.d(t,"GridOptionsService",(function(){return mu})),a.d(t,"EventService",(function(){return fe})),a.d(t,"SelectableService",(function(){return kc})),a.d(t,"RowNodeSorter",(function(){return Kc})),a.d(t,"CtrlsService",(function(){return Jc})),a.d(t,"GridComp",(function(){return Xd})),a.d(t,"GridCtrl",(function(){return Qd})),a.d(t,"Logger",(function(){return Kd})),a.d(t,"LoggerFactory",(function(){return jd})),a.d(t,"SortController",(function(){return ec})),a.d(t,"TemplateService",(function(){return qd})),a.d(t,"LocaleService",(function(){return fu})),a.d(t,"_",(function(){return $r})),a.d(t,"NumberSequence",(function(){return eo})),a.d(t,"AgPromiseStatus",(function(){return to})),a.d(t,"AgPromise",(function(){return ao})),a.d(t,"Timer",(function(){return ro})),a.d(t,"ValueService",(function(){return Gd})),a.d(t,"ValueCache",(function(){return cc})),a.d(t,"ExpressionService",(function(){return _d})),a.d(t,"ValueParserService",(function(){return xu})),a.d(t,"CellPositionUtils",(function(){return Lc})),a.d(t,"RowPositionUtils",(function(){return Mc})),a.d(t,"HeaderPositionUtils",(function(){return _c})),a.d(t,"HeaderNavigationService",(function(){return eg})),a.d(t,"HeaderNavigationDirection",(function(){return Zs})),a.d(t,"DataTypeService",(function(){return Eu})),a.d(t,"PropertyKeys",(function(){return Pt})),a.d(t,"ColumnApi",(function(){return Fd})),a.d(t,"BaseComponentWrapper",(function(){return Wu})),a.d(t,"Environment",(function(){return yc})),a.d(t,"TooltipFeature",(function(){return fl})),a.d(t,"CustomTooltipFeature",(function(){return lo})),a.d(t,"DEFAULT_CHART_GROUPS",(function(){return Uu})),a.d(t,"CHART_TOOL_PANEL_ALLOW_LIST",(function(){return ju})),a.d(t,"CHART_TOOLBAR_ALLOW_LIST",(function(){return Ku})),a.d(t,"CHART_TOOL_PANEL_MENU_OPTIONS",(function(){return Yu})),a.d(t,"__FORCE_MODULE_DETECTION",(function(){return Qu})),a.d(t,"BarColumnLabelPlacement",(function(){return Ju})),a.d(t,"ModuleNames",(function(){return re})),a.d(t,"ModuleRegistry",(function(){return oe}));var r={};a.r(r),a.d(r,"makeNull",(function(){return y})),a.d(r,"exists",(function(){return S})),a.d(r,"missing",(function(){return E})),a.d(r,"missingOrEmpty",(function(){return R})),a.d(r,"toStringOrNull",(function(){return x})),a.d(r,"attrToNumber",(function(){return k})),a.d(r,"attrToBoolean",(function(){return z})),a.d(r,"attrToString",(function(){return T})),a.d(r,"jsonEquals",(function(){return A})),a.d(r,"defaultComparator",(function(){return D})),a.d(r,"values",(function(){return O}));var o={};a.r(o),a.d(o,"iterateObject",(function(){return P})),a.d(o,"cloneObject",(function(){return L})),a.d(o,"deepCloneDefinition",(function(){return N})),a.d(o,"getAllValuesInObject",(function(){return F})),a.d(o,"mergeDeep",(function(){return I})),a.d(o,"getValueUsingField",(function(){return G})),a.d(o,"removeAllReferences",(function(){return V})),a.d(o,"isNonNullObject",(function(){return H}));var i={};a.r(i),a.d(i,"doOnce",(function(){return B})),a.d(i,"warnOnce",(function(){return q})),a.d(i,"errorOnce",(function(){return W})),a.d(i,"getFunctionName",(function(){return U})),a.d(i,"isFunction",(function(){return j})),a.d(i,"executeInAWhile",(function(){return K})),a.d(i,"executeNextVMTurn",(function(){return J})),a.d(i,"executeAfter",(function(){return X})),a.d(i,"debounce",(function(){return Z})),a.d(i,"throttle",(function(){return $})),a.d(i,"waitUntil",(function(){return ee})),a.d(i,"compose",(function(){return te})),a.d(i,"noop",(function(){return ae}));var n={};a.r(n),a.d(n,"existsAndNotEmpty",(function(){return ze})),a.d(n,"last",(function(){return Te})),a.d(n,"areEqual",(function(){return Ae})),a.d(n,"shallowCompare",(function(){return De})),a.d(n,"sortNumerically",(function(){return Oe})),a.d(n,"removeRepeatsFromArray",(function(){return Me})),a.d(n,"removeFromUnorderedArray",(function(){return Pe})),a.d(n,"removeFromArray",(function(){return Le})),a.d(n,"removeAllFromUnorderedArray",(function(){return Ne})),a.d(n,"removeAllFromArray",(function(){return Fe})),a.d(n,"insertIntoArray",(function(){return Ie})),a.d(n,"insertArrayIntoArray",(function(){return Ge})),a.d(n,"moveInArray",(function(){return Ve})),a.d(n,"includes",(function(){return He})),a.d(n,"flatten",(function(){return _e})),a.d(n,"pushAll",(function(){return Be})),a.d(n,"toStrings",(function(){return qe})),a.d(n,"forEachReverse",(function(){return We}));var l={};a.r(l),a.d(l,"stopPropagationForAgGrid",(function(){return Ke})),a.d(l,"isStopPropagationForAgGrid",(function(){return Ye})),a.d(l,"isEventSupported",(function(){return Qe})),a.d(l,"getCtrlForEventTarget",(function(){return Je})),a.d(l,"isElementInEventPath",(function(){return Xe})),a.d(l,"createEventPath",(function(){return Ze})),a.d(l,"getEventPath",(function(){return $e})),a.d(l,"addSafePassiveEventListener",(function(){return et}));var s={};a.r(s),a.d(s,"utf8_encode",(function(){return mt})),a.d(s,"capitalise",(function(){return vt})),a.d(s,"escapeString",(function(){return ft})),a.d(s,"camelCaseToHumanText",(function(){return wt})),a.d(s,"camelCaseToHyphenated",(function(){return bt}));var g={};a.r(g),a.d(g,"convertToMap",(function(){return Ct})),a.d(g,"mapById",(function(){return yt})),a.d(g,"keys",(function(){return St}));var d={};a.r(d),a.d(d,"setAriaRole",(function(){return Jt})),a.d(d,"getAriaSortState",(function(){return Xt})),a.d(d,"getAriaLevel",(function(){return Zt})),a.d(d,"getAriaPosInSet",(function(){return $t})),a.d(d,"getAriaLabel",(function(){return ea})),a.d(d,"setAriaLabel",(function(){return ta})),a.d(d,"setAriaLabelledBy",(function(){return aa})),a.d(d,"setAriaDescribedBy",(function(){return ra})),a.d(d,"setAriaLive",(function(){return oa})),a.d(d,"setAriaAtomic",(function(){return ia})),a.d(d,"setAriaRelevant",(function(){return na})),a.d(d,"setAriaLevel",(function(){return la})),a.d(d,"setAriaDisabled",(function(){return sa})),a.d(d,"setAriaHidden",(function(){return ga})),a.d(d,"setAriaActiveDescendant",(function(){return da})),a.d(d,"setAriaExpanded",(function(){return ca})),a.d(d,"removeAriaExpanded",(function(){return ua})),a.d(d,"setAriaSetSize",(function(){return pa})),a.d(d,"setAriaPosInSet",(function(){return ha})),a.d(d,"setAriaMultiSelectable",(function(){return ma})),a.d(d,"setAriaRowCount",(function(){return va})),a.d(d,"setAriaRowIndex",(function(){return fa})),a.d(d,"setAriaColCount",(function(){return wa})),a.d(d,"setAriaColIndex",(function(){return ba})),a.d(d,"setAriaColSpan",(function(){return Ca})),a.d(d,"setAriaSort",(function(){return ya})),a.d(d,"removeAriaSort",(function(){return Sa})),a.d(d,"setAriaSelected",(function(){return Ea})),a.d(d,"setAriaChecked",(function(){return Ra})),a.d(d,"setAriaControls",(function(){return xa})),a.d(d,"getAriaCheckboxStateName",(function(){return ka}));var c={};a.r(c),a.d(c,"isBrowserSafari",(function(){return za})),a.d(c,"getSafariVersion",(function(){return Ta})),a.d(c,"isBrowserChrome",(function(){return Aa})),a.d(c,"isBrowserFirefox",(function(){return Da})),a.d(c,"isMacOsUserAgent",(function(){return Oa})),a.d(c,"isIOSUserAgent",(function(){return Ma})),a.d(c,"browserSupportsPreventScroll",(function(){return Pa})),a.d(c,"getTabIndex",(function(){return La})),a.d(c,"getMaxDivHeight",(function(){return Na})),a.d(c,"getBodyWidth",(function(){return Fa})),a.d(c,"getBodyHeight",(function(){return Ia})),a.d(c,"getScrollbarWidth",(function(){return Ga})),a.d(c,"isInvisibleScrollbar",(function(){return Ha}));var u={};a.r(u),a.d(u,"padStartWidthZeros",(function(){return _a})),a.d(u,"createArrayOfNumbers",(function(){return Ba})),a.d(u,"cleanNumber",(function(){return qa})),a.d(u,"decToHex",(function(){return Wa})),a.d(u,"formatNumberTwoDecimalPlacesAndCommas",(function(){return Ua})),a.d(u,"formatNumberCommas",(function(){return ja})),a.d(u,"sum",(function(){return Ka}));var p={};a.r(p),a.d(p,"serialiseDate",(function(){return Ya})),a.d(p,"dateToFormattedString",(function(){return Ja})),a.d(p,"parseDateTimeFromString",(function(){return Xa}));var h={};a.r(h),a.d(h,"radioCssClass",(function(){return $a})),a.d(h,"FOCUSABLE_SELECTOR",(function(){return er})),a.d(h,"FOCUSABLE_EXCLUDE",(function(){return tr})),a.d(h,"isFocusableFormField",(function(){return ar})),a.d(h,"setDisplayed",(function(){return rr})),a.d(h,"setVisible",(function(){return or})),a.d(h,"setDisabled",(function(){return ir})),a.d(h,"isElementChildOfClass",(function(){return nr})),a.d(h,"getElementSize",(function(){return lr})),a.d(h,"getInnerHeight",(function(){return sr})),a.d(h,"getInnerWidth",(function(){return gr})),a.d(h,"getAbsoluteHeight",(function(){return dr})),a.d(h,"getAbsoluteWidth",(function(){return cr})),a.d(h,"getElementRectWithOffset",(function(){return ur})),a.d(h,"isRtlNegativeScroll",(function(){return pr})),a.d(h,"getScrollLeft",(function(){return hr})),a.d(h,"setScrollLeft",(function(){return mr})),a.d(h,"clearElement",(function(){return vr})),a.d(h,"removeFromParent",(function(){return fr})),a.d(h,"isInDOM",(function(){return wr})),a.d(h,"isVisible",(function(){return br})),a.d(h,"loadTemplate",(function(){return Cr})),a.d(h,"ensureDomOrder",(function(){return yr})),a.d(h,"setDomChildOrder",(function(){return Sr})),a.d(h,"insertWithDomOrder",(function(){return Er})),a.d(h,"addStylesToElement",(function(){return Rr})),a.d(h,"isHorizontalScrollShowing",(function(){return xr})),a.d(h,"isVerticalScrollShowing",(function(){return kr})),a.d(h,"setElementWidth",(function(){return zr})),a.d(h,"setFixedWidth",(function(){return Tr})),a.d(h,"setElementHeight",(function(){return Ar})),a.d(h,"setFixedHeight",(function(){return Dr})),a.d(h,"formatSize",(function(){return Or})),a.d(h,"isNodeOrElement",(function(){return Mr})),a.d(h,"copyNodeList",(function(){return Pr})),a.d(h,"iterateNamedNodeMap",(function(){return Lr})),a.d(h,"addOrRemoveAttribute",(function(){return Nr})),a.d(h,"nodeListForEach",(function(){return Fr})),a.d(h,"bindCellRendererToHtmlElement",(function(){return Ir}));var m={};a.r(m),a.d(m,"fuzzyCheckStrings",(function(){return Gr})),a.d(m,"fuzzySuggestions",(function(){return Vr}));var v={};a.r(v),a.d(v,"iconNameClassMap",(function(){return _r})),a.d(v,"createIcon",(function(){return Br})),a.d(v,"createIconNoSpan",(function(){return qr}));var f={};a.r(f),a.d(f,"isEventFromPrintableCharacter",(function(){return Ur})),a.d(f,"isUserSuppressingKeyboardEvent",(function(){return jr})),a.d(f,"isUserSuppressingHeaderKeyboardEvent",(function(){return Kr})),a.d(f,"normaliseQwertyAzerty",(function(){return Yr})),a.d(f,"isDeleteKey",(function(){return Qr}));var w={};a.r(w),a.d(w,"areEventsNear",(function(){return Jr}));var b={};a.r(b),a.d(b,"sortRowNodesByOrder",(function(){return Xr}));var C={};function y(e){return null==e||""===e?null:e}function S(e,t=!1){return null!=e&&(""!==e||t)}function E(e){return!S(e)}function R(e){return null==e||0===e.length}function x(e){return null!=e&&"function"==typeof e.toString?e.toString():null}function k(e){if(void 0===e)return;if(null===e||""===e)return null;if("number"==typeof e)return isNaN(e)?void 0:e;const t=parseInt(e,10);return isNaN(t)?void 0:t}function z(e){if(void 0!==e)return null!==e&&""!==e&&("boolean"==typeof e?e:/true/i.test(e))}function T(e){if(null!=e&&""!==e)return e}function A(e,t){return(e?JSON.stringify(e):null)===(t?JSON.stringify(t):null)}function D(e,t,a=!1){const r=null==e,o=null==t;if(e&&e.toNumber&&(e=e.toNumber()),t&&t.toNumber&&(t=t.toNumber()),r&&o)return 0;if(r)return-1;if(o)return 1;function i(e,t){return e>t?1:et.push(e)),t}return Object.values(e)}a.r(C),a.d(C,"convertToSet",(function(){return Zr}));class M{constructor(){this.existingKeys={}}addExistingKeys(e){for(let t=0;t{if(t&&t.indexOf(e)>=0)return;const o=a[e],i=H(o)&&o.constructor===Object;r[e]=i?N(o):o}),r}function F(e){if(!e)return[];const t=Object;if("function"==typeof t.values)return t.values(e);const a=[];for(const t in e)e.hasOwnProperty(t)&&e.propertyIsEnumerable(t)&&a.push(e[t]);return a}function I(e,t,a=!0,r=!1){S(t)&&P(t,(t,o)=>{let i=e[t];if(i!==o){if(r){if(null==i&&null!=o){"object"==typeof o&&o.constructor===Object&&(i={},e[t]=i)}}H(o)&&H(i)&&!Array.isArray(i)?I(i,o,a,r):(a||void 0!==o)&&(e[t]=o)}})}function G(e,t,a){if(!t||!e)return;if(!a)return e[t];const r=t.split(".");let o=e;for(let e=0;e{"object"!=typeof e[a]||t.includes(a)||(e[a]=void 0)});const r=Object.getPrototypeOf(e),o={};Object.getOwnPropertyNames(r).forEach(e=>{if("function"==typeof r[e]&&!t.includes(e)){const t=()=>{console.warn((e=>`AG Grid: Grid API function ${e}() cannot be called as the grid has been destroyed.\n It is recommended to remove local references to the grid api. Alternatively, check gridApi.isDestroyed() to avoid calling methods against a destroyed grid.\n To run logic when the grid is about to be destroyed use the gridPreDestroy event. See: ${a}`)(e))};o[e]={value:t,writable:!0}}}),Object.defineProperties(e,o)}function H(e){return"object"==typeof e&&null!==e}const _={};function B(e,t){_[t]||(e(),_[t]=!0)}function q(e){B(()=>console.warn("AG Grid: "+e),e)}function W(e){B(()=>console.error("AG Grid: "+e),e)}function U(e){if(e.name)return e.name;const t=/function\s+([^\(]+)/.exec(e.toString());return t&&2===t.length?t[1].trim():null}function j(e){return!!(e&&e.constructor&&e.call&&e.apply)}function K(e){X(e,400)}const Y=[];let Q=!1;function J(e){Y.push(e),Q||(Q=!0,window.setTimeout(()=>{const e=Y.slice();Y.length=0,Q=!1,e.forEach(e=>e())},0))}function X(e,t=0){e.length>0&&window.setTimeout(()=>e.forEach(e=>e()),t)}function Z(e,t){let a;return function(...r){const o=this;window.clearTimeout(a),a=window.setTimeout((function(){e.apply(o,r)}),t)}}function $(e,t){let a=0;return function(...r){const o=(new Date).getTime();o-a{const l=(new Date).getTime()-o>a;(e()||l)&&(t(),n=!0,null!=i&&(window.clearInterval(i),i=null),l&&r&&console.warn(r))};l(),n||(i=window.setInterval(l,10))}function te(...e){return t=>e.reduce((e,t)=>t(e),t)}const ae=()=>{};var re;!function(e){e.CommunityCoreModule="@ag-grid-community/core",e.InfiniteRowModelModule="@ag-grid-community/infinite-row-model",e.ClientSideRowModelModule="@ag-grid-community/client-side-row-model",e.CsvExportModule="@ag-grid-community/csv-export",e.EnterpriseCoreModule="@ag-grid-enterprise/core",e.RowGroupingModule="@ag-grid-enterprise/row-grouping",e.ColumnsToolPanelModule="@ag-grid-enterprise/column-tool-panel",e.FiltersToolPanelModule="@ag-grid-enterprise/filter-tool-panel",e.MenuModule="@ag-grid-enterprise/menu",e.SetFilterModule="@ag-grid-enterprise/set-filter",e.MultiFilterModule="@ag-grid-enterprise/multi-filter",e.StatusBarModule="@ag-grid-enterprise/status-bar",e.SideBarModule="@ag-grid-enterprise/side-bar",e.RangeSelectionModule="@ag-grid-enterprise/range-selection",e.MasterDetailModule="@ag-grid-enterprise/master-detail",e.RichSelectModule="@ag-grid-enterprise/rich-select",e.GridChartsModule="@ag-grid-enterprise/charts",e.ViewportRowModelModule="@ag-grid-enterprise/viewport-row-model",e.ServerSideRowModelModule="@ag-grid-enterprise/server-side-row-model",e.ExcelExportModule="@ag-grid-enterprise/excel-export",e.ClipboardModule="@ag-grid-enterprise/clipboard",e.SparklinesModule="@ag-grid-enterprise/sparklines",e.AdvancedFilterModule="@ag-grid-enterprise/advanced-filter",e.AngularModule="@ag-grid-community/angular",e.ReactModule="@ag-grid-community/react",e.VueModule="@ag-grid-community/vue"}(re||(re={}));class oe{static register(e){oe.__register(e,!0,void 0)}static registerModules(e){oe.__registerModules(e,!0,void 0)}static __register(e,t,a){oe.runVersionChecks(e),void 0!==a?(oe.areGridScopedModules=!0,void 0===oe.gridModulesMap[a]&&(oe.gridModulesMap[a]={}),oe.gridModulesMap[a][e.moduleName]=e):oe.globalModulesMap[e.moduleName]=e,oe.setModuleBased(t)}static __unRegisterGridModules(e){delete oe.gridModulesMap[e]}static __registerModules(e,t,a){oe.setModuleBased(t),e&&e.forEach(e=>oe.__register(e,t,a))}static isValidModuleVersion(e){const[t,a]=e.version.split(".")||[],[r,o]=oe.currentModuleVersion.split(".")||[];return t===r&&a===o}static runVersionChecks(e){if(oe.currentModuleVersion||(oe.currentModuleVersion=e.version),e.version?oe.isValidModuleVersion(e)||console.error(`AG Grid: You are using incompatible versions of AG Grid modules. Major and minor versions should always match across modules. '${e.moduleName}' is version ${e.version} but the other modules are version ${this.currentModuleVersion}. Please update all modules to the same version.`):console.error(`AG Grid: You are using incompatible versions of AG Grid modules. Major and minor versions should always match across modules. '${e.moduleName}' is incompatible. Please update all modules to the same version.`),e.validate){const t=e.validate();if(!t.isValid){const e=t;console.error("AG Grid: "+e.message)}}}static setModuleBased(e){void 0===oe.moduleBased?oe.moduleBased=e:oe.moduleBased!==e&&B(()=>{console.warn("AG Grid: You are mixing modules (i.e. @ag-grid-community/core) and packages (ag-grid-community) - you can only use one or the other of these mechanisms."),console.warn("Please see https://www.ag-grid.com/javascript-grid/packages-modules/ for more information.")},"ModulePackageCheck")}static __setIsBundled(){oe.isBundled=!0}static __assertRegistered(e,t,a){var r;if(this.__isRegistered(e,a))return!0;const o=t+e;let i;if(oe.isBundled)i=`AG Grid: unable to use ${t} as 'ag-grid-enterprise' has not been loaded. Check you are using the Enterprise bundle:\n \n + + + diff --git a/site/examples/web-app-starter/js/u-events.js b/site/examples/web-app-starter/js/u-events.js new file mode 100755 index 0000000..3a0d4fe --- /dev/null +++ b/site/examples/web-app-starter/js/u-events.js @@ -0,0 +1,92 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.EventEmitter = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + +class EventEmitter { + constructor() { + /** Map> */ + this._topics = new Map(); + } + + /** + * Subscribe to `topic`. If `slot_key` is provided (truthy), + * it dedupes by that key; otherwise a unique Symbol is used. + * Returns an unsubscribe fn. + */ + on(topic, handler, slot_key = null) { + let map = this._topics.get(topic); + if (!map) { + map = new Map(); + this._topics.set(topic, map); + } + // use the provided slot_key or a fresh Symbol() + const key = slot_key != null ? slot_key : Symbol(); + map.set(key, handler); + return () => this.off(topic, key); + } + + /** + * Unsubscribe by handler function or by slot_key. + */ + off(topic, handlerOrSlotKey) { + const map = this._topics.get(topic); + if (!map) return; + + // if it matches a slotKey directly, remove it + if (map.has(handlerOrSlotKey)) { + map.delete(handlerOrSlotKey); + } else { + // otherwise assume it's a function: remove all matching fns + for (const [key, fn] of map.entries()) { + if (fn === handlerOrSlotKey) { + map.delete(key); + } + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + } + + /** + * Emit to all handlers on `topic`. Handlers returning + * 'remove_handler' are auto-removed. + * Returns the number of handlers invoked. + */ + emit(topic, ...args) { + let count = 0; + const map = this._topics.get(topic); + if (!map) return count; + + for (const [key, fn] of Array.from(map.entries())) { + const res = fn(...args); + count++; + if (res === 'remove_handler') { + map.delete(key); + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + return count; + } + + clear(topic) { + if (topic) { + this._topics.delete(topic); + } + return this; + } +} + +return EventEmitter; + +})); diff --git a/site/examples/web-app-starter/js/u-format.js b/site/examples/web-app-starter/js/u-format.js new file mode 100644 index 0000000..a10b75c --- /dev/null +++ b/site/examples/web-app-starter/js/u-format.js @@ -0,0 +1,136 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UFormat = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + + const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + + function scaleBytes(bytes) { + let value = Number(bytes || 0); + let unitIndex = 0; + while (Math.abs(value) >= 1024 && unitIndex < BYTE_UNITS.length - 1) { + value /= 1024; + unitIndex += 1; + } + return { value, unitIndex }; + } + + function formatBytes(bytes) { + if (bytes == null || bytes === '') return '--'; + const scaled = scaleBytes(bytes); + const decimals = scaled.unitIndex === 0 ? 0 : 1; + return `${scaled.value.toFixed(decimals)} ${BYTE_UNITS[scaled.unitIndex]}`; + } + + function formatDiskBytes(bytes) { + if (bytes == null || bytes === '') return '--'; + const scaled = scaleBytes(bytes); + const decimals = scaled.unitIndex >= 4 ? 2 : scaled.unitIndex >= 1 ? 1 : 0; + return `${scaled.value.toFixed(decimals)} ${BYTE_UNITS[scaled.unitIndex]}`; + } + + function formatCount(value) { + const number = Number(value); + if (!Number.isFinite(number)) return '--'; + return number.toLocaleString(); + } + + function formatDurationMs(value) { + const number = Number(value); + if (!Number.isFinite(number)) return '--'; + if (Math.abs(number) >= 1000) { + return `${(number / 1000).toFixed(number >= 10000 ? 0 : 1)} s`; + } + return `${number.toFixed(number >= 100 ? 0 : 1)} ms`; + } + + function parseUnitNumber(text) { + const normalized = String(text || '').trim().toLowerCase().replace(/,/g, ''); + if (!normalized) return null; + + const pure = normalized.match(/^([-+]?\d*\.?\d+)$/); + if (pure) { + return Number(pure[1]); + } + + const withUnit = normalized.match(/^([-+]?\d*\.?\d+)\s*([a-z%][a-z0-9\/_-]*)$/); + if (!withUnit) { + return null; + } + + const value = Number(withUnit[1]); + let unit = withUnit[2]; + if (!Number.isFinite(value)) { + return null; + } + + if (unit.endsWith('/s')) { + unit = unit.slice(0, -2); + } + + const bytes = { + b: 1, + kb: 1024, + kib: 1024, + mb: 1024 ** 2, + mib: 1024 ** 2, + gb: 1024 ** 3, + gib: 1024 ** 3, + tb: 1024 ** 4, + tib: 1024 ** 4, + pb: 1024 ** 5, + pib: 1024 ** 5, + }; + + if (Object.prototype.hasOwnProperty.call(bytes, unit)) { + return value * bytes[unit]; + } + + const durations = { + ms: 0.001, + s: 1, + sec: 1, + secs: 1, + second: 1, + seconds: 1, + m: 60, + min: 60, + mins: 60, + minute: 60, + minutes: 60, + h: 3600, + hr: 3600, + hrs: 3600, + hour: 3600, + hours: 3600, + d: 86400, + day: 86400, + days: 86400, + }; + + if (Object.prototype.hasOwnProperty.call(durations, unit)) { + return value * durations[unit]; + } + + if (unit === '%') { + return value; + } + + return null; + } + + return { + formatBytes, + formatCount, + formatDiskBytes, + formatDurationMs, + parseUnitNumber, + scaleBytes, + }; +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-howler-demo.html b/site/examples/web-app-starter/js/u-howler-demo.html new file mode 100755 index 0000000..7d86387 --- /dev/null +++ b/site/examples/web-app-starter/js/u-howler-demo.html @@ -0,0 +1,1488 @@ + + + + + + U-Howler.js Demo + + + +
+

U-Howler.js Demo

+ +
+
+

Basic Controls

+
+
+
+
+
+
+
+ + + + +
+ +
+ + + 440 Hz +
+ +
+ + + 1.0 +
+
+ +
+
+ + +
+ +
+
+ +
+

Global Controls

+
+ + +
+ +
+ + + 1.0 +
+
+
+ +
+

API Overview

+ +
+

Howler (Global)

+
volume(vol?) → Howler|number
+
mute(muted?) → Howler
+
stop() → Howler
+
unload() → Howler
+
codecs(ext) → boolean
+
environment(env) → Howler
+
+ +
+

Howl Constructor

+
new Howl(options)
+
+ Options: src, volume, rate, loop, mute, preload, autoplay, sprite, format, html5, pool, xhr, spatial, on* +
+
+ +
+

Core Methods

+
load() → Promise<Howl>
+
play(sprite?) → number|Promise
+
pause(id?) → Howl
+
stop(id?) → Howl
+
mute(muted?, id?) → Howl|boolean
+
volume(vol?, id?) → Howl|number
+
fade(from, to, duration, id?) → Howl
+
rate(rate?, id?) → Howl|number
+
seek(seek?, id?) → Howl|number
+
+ +
+

Examples

+
+// Promise support
+const howl = new Howl({src: 'audio.mp3'});
+await howl.load();
+const id = howl.play();
+
+// Sprite usage
+const soundFx = new Howl({
+  src: 'sounds.mp3',
+  sprite: {
+    jump: [0, 500],
+    shoot: [600, 300]
+  }
+});
+soundFx.play('jump');
+
+
+
+
+ +
+
+

Audio Sprites

+
+

Audio Sprites Demo

+
+
+
+
+
+ +
+ + +
+ +
+ + + + + + + + +
+ +
+
+ +
+

Audio Sprites API

+ +
+

Sprite Methods

+
play(spriteName) → number
+
+ Play a specific sprite by name from the sprite collection. +
+
+ +
+

Sprite Configuration

+
+ Sprite Format: { name: [start_ms, duration_ms] }
+ • start_ms: Starting position in milliseconds
+ • duration_ms: Length of the sprite in milliseconds
+ • Sprites can be tightly packed or have gaps between them
+
+
+const spriteMap = {
+  jump: [0, 500],       // 0ms - 500ms
+  coin: [500, 300],     // 500ms - 800ms
+  laser: [800, 400],    // 800ms - 1200ms
+  explosion: [1200, 800] // 1200ms - 2000ms
+};
+
+const gameAudio = new Howl({
+  src: 'game-sounds.wav',
+  sprite: spriteMap
+});
+
+gameAudio.play('jump');
+
+
+
+
+ +
+
+

Audio Effects

+
+

Filters

+
+ + + + + + +
+
+ + +
+
+ +
+
+

Rate Control

+
+ + + 1.0x +
+ +
+ +
+

Fade Effects

+
+ + +
+
+
+
+ +
+

Effects API

+ +
+

Effect Methods

+
addEffect(type, options?) → AudioNode
+
effectsArray<AudioNode>
+
updateEffectsChain() → Howl
+
enableEQ(bands?, callback?, boost?) → Howl
+
disableEQ() → Howl
+
+ +
+

Audio Effects

+
+ addEffect(type, options) - Add audio effect to the sound
+ • Returns the Web Audio node for direct manipulation
+ • Types: 'lowpass', 'highpass', 'bandpass', 'notch', 'delay', 'reverb', 'gain', 'distortion'
+ • Options vary by type (frequency, Q, delay, gain, etc.)

+ effects - Getter for effects array (for direct manipulation)
+ updateEffectsChain() - Rebuild audio chain after effects manipulation +
+
+// Add effects and manipulate them
+const filter = howl.addEffect('lowpass', {
+  frequency: 1000, Q: 1
+});
+
+// Direct effects array manipulation
+howl.effects.splice(1, 1); // Remove delay effect
+howl.effects.reverse(); // Reorder effects
+howl.updateEffectsChain(); // Apply changes
+
+
+ +
+

Frequency Analysis

+
+ enableEQ(bands, callback, boost) - Enable real-time frequency analysis
+ • bands: Number of frequency bands (default: 16)
+ • callback: Function called with frequency data array
+ • boost: Gain multiplier for visualization (default: 4.0)

+ disableEQ() - Disable frequency analysis
+
+
+
+
+ +
+
+

3D Spatial Audio

+
+
+
+
+
+
+
+ 🟢 Green is you! + Drag the dots to move them around the 3D space! + 🏛️ Cathedral environment provides realistic reverb. +
+
+ + +
+
+
+ +
+

Spatial API

+ +
+

3D Spatial Audio

+
+ spatial - Object containing 3D audio properties
+ • Properties: position{x,y,z}, orientation{x,y,z}, velocity{x,y,z}
+ • distance{ref, max, rolloff, model}, cone{inner, outer, outerGain}
+ • occlusion, obstruction, panningModel

+ Howler.listener - Global listener (camera/player) object
+ Howler.environment(env) - Set reverb environment
+ • Accepts preset strings: 'room', 'hall', 'cathedral'
+ • Or custom objects: { size, decay, dampening }
+ • Pass null to disable environment reverb +
+
+ +
+

Environment Presets

+
+ Built-in Reverb Environments: 'room', 'hall', 'cathedral', 'cave', 'underwater'

+ + Custom Environment Parameters:
+ • size: 'small' | 'medium' | 'large' - Room size
+ • decay: 0.1-20.0 - Reverb tail length in seconds
+ • dampening: 0.0-1.0 - High frequency absorption (0=bright, 1=muffled) +
+
+ +
+

Spatial Examples

+
+const spatial = new Howl({
+  src: 'footsteps.mp3',
+  spatial: {
+    position: [10, 0, -5],
+    distance: { ref: 2, max: 100 },
+    cone: { inner: 45, outer: 90, outerGain: 0.2 }
+  }
+});
+
+Howler.listener.position = { x: 0, y: 1.8, z: 0 };
+Howler.listener.update();
+
+spatial.spatial.position = { x: enemy.x, y: enemy.y, z: enemy.z };
+spatial.spatial.update();
+
+// Built-in presets
+Howler.environment('room');      // Small room reverb
+Howler.environment('hall');      // Medium hall reverb
+Howler.environment('cathedral'); // Large cathedral reverb
+
+// Custom environment
+Howler.environment({
+  size: 'medium',
+  decay: 3.0,
+  dampening: 0.4
+});
+
+Howler.environment(null); // Disable environment
+
+
+
+
+ +
+
+

Library Info

+
+
Loading library information...
+ +

Supported Codecs:

+
+
+
+ +
+

Events & Utilities

+ +
+

Event Listeners

+
on(event, fn, id?) → Howl
+
off(event?, fn?, id?) → Howl
+
once(event, fn, id?) → Howl
+
+ +
+

Utility Methods

+
playing(id?) → boolean
+
duration(id?) → number
+
state() → string
+
unload() → null
+
+ +
+

Events

+
load() - Audio loaded successfully
+
loaderror(id, error) - Failed to load audio
+
play(id) - Sound started playing
+
pause(id) - Sound paused
+
stop(id) - Sound stopped
+
end(id) - Sound finished playing
+
fade(id) - Fade effect completed
+
volume(id) - Volume changed
+
rate(id) - Playback rate changed
+
seek(id) - Playback position changed
+
mute(id) - Mute state changed
+
unlock() - Audio context unlocked (user interaction)
+
playerror(id, error) - Playback error occurred
+
+
+
+
+ + + + + diff --git a/site/examples/web-app-starter/js/u-howler.js b/site/examples/web-app-starter/js/u-howler.js new file mode 100755 index 0000000..d322751 --- /dev/null +++ b/site/examples/web-app-starter/js/u-howler.js @@ -0,0 +1,1162 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + var exports_obj = factory(); + module.exports = exports_obj; + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + var exports_obj = factory(); + root.Howler = exports_obj.Howler; + root.Howl = exports_obj.Howl; + } +}(typeof self !== 'undefined' ? self : this, function () { + +/** + * based on Howler.js - (c) 2013-2020, James Simpson of GoldFire Studios + * Mostly compatible API + */ + +class HowlerGlobal { + constructor() { + this._counter = 1000; + this._howls = []; + this._volume = 1; + this._muted = false; + this._html5Pool = []; + this._codecs = {}; + this.ctx = null; + this.masterGain = null; + this.usingWebAudio = true; + this.autoUnlock = true; + this._audioUnlocked = false; + this._environment = null; + this._setup(); + } + + volume(vol) { + if (vol === undefined) return this._volume; + this._volume = Math.max(0, Math.min(1, vol)); + + if (!this._muted) { + if (this.usingWebAudio && this.masterGain) { + this.masterGain.gain.setValueAtTime(this._volume, this.ctx.currentTime); + } + this._howls.forEach(howl => howl._updateVolume()); + } + return this; + } + + mute(muted = true) { + this._muted = muted; + let vol = muted ? 0 : this._volume; + + if (this.usingWebAudio && this.masterGain) { + this.masterGain.gain.setValueAtTime(vol, this.ctx.currentTime); + } + this._howls.forEach(howl => howl._updateVolume()); + return this; + } + + stop() { + this._howls.forEach(howl => howl.stop()); + return this; + } + + unload() { + this._howls.slice().forEach(howl => howl.unload()); + if (this.ctx?.close) { + this.ctx.close(); + this.ctx = null; + this._setupAudioContext(); + } + return this; + } + + codecs(ext) { + return this._codecs[ext?.replace(/^x-/, '')]; + } + + _setup() { + this._setupAudioContext(); + this._setupCodecs(); + if (this.autoUnlock) this._setupUnlock(); + } + + _setupAudioContext() { + if (!this.usingWebAudio) return; + + try { + this.ctx = new (window.AudioContext || window.webkitAudioContext)(); + this.masterGain = this.ctx.createGain(); + this.masterGain.connect(this.ctx.destination); + this.masterGain.gain.setValueAtTime(this._muted ? 0 : this._volume, this.ctx.currentTime); + + this.listener = { + position: { x: 0, y: 0, z: 0 }, + orientation: { + forward: { x: 0, y: 0, z: -1 }, + up: { x: 0, y: 1, z: 0 } + }, + velocity: { x: 0, y: 0, z: 0 }, + update: () => this._updateListener() + }; + } catch (e) { + this.usingWebAudio = false; + } + } + + _setupCodecs() { + if (typeof Audio === 'undefined') return; + + let audio = new Audio(); + let test = (mime) => !!audio.canPlayType(mime).replace(/^no$/, ''); + + this._codecs = { + mp3: test('audio/mpeg;'), + opus: test('audio/ogg; codecs="opus"'), + ogg: test('audio/ogg; codecs="vorbis"'), + wav: test('audio/wav; codecs="1"') || test('audio/wav'), + aac: test('audio/aac;'), + m4a: test('audio/x-m4a;') || test('audio/m4a;') || test('audio/aac;'), + mp4: test('audio/x-mp4;') || test('audio/mp4;') || test('audio/aac;'), + webm: test('audio/webm; codecs="vorbis"'), + flac: test('audio/x-flac;') || test('audio/flac;') + }; + } + + _setupUnlock() { + if (this._audioUnlocked || !this.ctx) return; + + let unlock = () => { + if (this._audioUnlocked) return; + + let buffer = this.ctx.createBuffer(1, 1, 22050); + let source = this.ctx.createBufferSource(); + source.buffer = buffer; + source.connect(this.ctx.destination); + source.start(0); + + if (this.ctx.resume) this.ctx.resume(); + + source.onended = () => { + this._audioUnlocked = true; + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('click', unlock, true); + this._howls.forEach(howl => howl._emit('unlock')); + }; + }; + + document.addEventListener('touchstart', unlock, true); + document.addEventListener('click', unlock, true); + } + + _getHtml5Audio() { + return this._html5Pool.pop() || new Audio(); + } + + _releaseHtml5Audio(audio) { + if (audio._unlocked && this._html5Pool.length < 10) { + this._html5Pool.push(audio); + } + } + + environment(env) { + if(!env) + return this._environment; + if (typeof env === 'string') { + let presets = { + room: { size: 'small', decay: 1.5, dampening: 0.8 }, + hall: { size: 'medium', decay: 2.5, dampening: 0.6 }, + cathedral: { size: 'large', decay: 8.0, dampening: 0.1 }, + cave: { size: 'large', decay: 6.0, dampening: 0.2 }, + underwater: { size: 'medium', decay: 3.0, dampening: 0.9 } + }; + this._environment = presets[env] || null; + } else { + this._environment = env; + } + this._howls.forEach(howl => howl._updateEnvironment?.()); + return this; + } + + _updateListener() { + if (!this.ctx?.listener) return; + + let { position, orientation, velocity } = this.listener; + let l = this.ctx.listener; + + if (l.positionX) { + l.positionX.setValueAtTime(position.x, this.ctx.currentTime); + l.positionY.setValueAtTime(position.y, this.ctx.currentTime); + l.positionZ.setValueAtTime(position.z, this.ctx.currentTime); + l.forwardX.setValueAtTime(orientation.forward.x, this.ctx.currentTime); + l.forwardY.setValueAtTime(orientation.forward.y, this.ctx.currentTime); + l.forwardZ.setValueAtTime(orientation.forward.z, this.ctx.currentTime); + l.upX.setValueAtTime(orientation.up.x, this.ctx.currentTime); + l.upY.setValueAtTime(orientation.up.y, this.ctx.currentTime); + l.upZ.setValueAtTime(orientation.up.z, this.ctx.currentTime); + } else { + l.setPosition(position.x, position.y, position.z); + l.setOrientation( + orientation.forward.x, orientation.forward.y, orientation.forward.z, + orientation.up.x, orientation.up.y, orientation.up.z + ); + } + + if (l.setVelocity) { + l.setVelocity(velocity.x, velocity.y, velocity.z); + } + } +} + +class Howl { + constructor(options = {}) { + if(options.url) options.src = options.url; // backwards compat + if(options.urls) options.src = options.urls; // backwards compat + if (!options.src?.length) throw new Error('src required'); + + Object.assign(this, { + _src: Array.isArray(options.src) ? options.src : [options.src], + _volume: options.volume ?? 1, + _rate: options.rate ?? 1, + _loop: options.loop ?? false, + _muted: options.mute ?? false, + _preload: options.preload ?? true, + _autoplay: options.autoplay ?? false, + _sprite: options.sprite ?? {}, + _format: options.format ? (Array.isArray(options.format) ? options.format : [options.format]) : null, + _html5: options.html5 ?? false, + _pool: options.pool ?? 5, + _xhr: { method: 'GET', ...options.xhr }, + _duration: 0, + _state: 'unloaded', + _sounds: [], + _queue: [], + _listeners: {}, + _webAudio: Howler.usingWebAudio && !options.html5, + + _analyzer: null, + _frequencyCallback: null, + _frequencyData: null, + _frequencyBands: options.frequencyBands || 16, + _frequencyBoost: 4.0, + _frequencyEnabled: false, + + _effects: [] + }); + + if (options.spatial) { + this.spatial = { + enabled: true, + position: { x: 0, y: 0, z: 0 }, + orientation: { x: 0, y: 0, z: -1 }, + velocity: { x: 0, y: 0, z: 0 }, + distance: { + ref: 1, + max: 10000, + rolloff: 1, + model: 'inverse' + }, + cone: { + inner: 360, + outer: 360, + outerGain: 0 + }, + occlusion: 0, + obstruction: 0, + panningModel: 'HRTF', + update: () => this._updateSpatial() + }; + + if (Array.isArray(options.spatial.position)) { + let [x, y, z] = options.spatial.position; + Object.assign(this.spatial.position, { x, y, z }); + } + if (options.spatial.distance) { + Object.assign(this.spatial.distance, options.spatial.distance); + } + if (options.spatial.cone) { + Object.assign(this.spatial.cone, options.spatial.cone); + } + if (options.spatial.panningModel) { + this.spatial.panningModel = options.spatial.panningModel; + } + } + + ['load', 'loaderror', 'play', 'pause', 'stop', 'end', 'fade', 'volume', 'rate', 'seek', 'mute', 'unlock'].forEach(event => { + if (options[`on${event}`]) this.on(event, options[`on${event}`]); + }); + + Howler._howls.push(this); + + if (this._autoplay) this._queue.push({ event: 'play', action: () => this.play() }); + if (this._preload) this.load(); + } + + load() { + if (this._state !== 'unloaded') return Promise.resolve(this); + + this._state = 'loading'; + + return new Promise((resolve, reject) => { + let url = this._selectSource(); + if (!url) { + let error = 'No compatible source found'; + this._emit('loaderror', null, error); + return reject(new Error(error)); + } + + this._src = url; + + if (this._webAudio) { + this._loadWebAudio(url).then(resolve).catch(reject); + } else { + this._loadHtml5(url).then(resolve).catch(reject); + } + }); + } + + play(sprite) { + if (typeof sprite === 'number') { + let sound = this._sounds.find(s => s._id === sprite); + return sound ? this._playSound(sound) : null; + } + + if (this._state !== 'loaded') { + if (this._state === 'loading') { + return new Promise(resolve => { + this._queue.push({ event: 'play', action: () => resolve(this.play(sprite)) }); + }); + } + return this.load().then(() => this.play(sprite)); + } + + let sound = this._getSound(); + sound._sprite = sprite || '__default'; + return this._playSound(sound); + } + + pause(id) { + this._getSounds(id).forEach(sound => { + if (!sound._paused) { + sound._paused = true; + if (sound._node) { + if (this._webAudio && sound._source) { + sound._source.stop(); + this._cleanupSound(sound); + } else { + sound._node.pause(); + } + } + this._emit('pause', sound._id); + } + }); + return this; + } + + stop(id) { + this._getSounds(id).forEach(sound => { + sound._paused = true; + sound._ended = true; + if (sound._node) { + if (this._webAudio) { + if (sound._source) { + sound._source.stop(); + } + } else { + sound._node.pause(); + sound._node.currentTime = 0; + } + this._cleanupSound(sound); + } + this._emit('stop', sound._id); + }); + return this; + } + + mute(muted, id) { + if (muted === undefined) return this._muted; + + if (id === undefined) this._muted = muted; + + this._getSounds(id).forEach(sound => { + sound._muted = muted; + this._updateSoundVolume(sound); + this._emit('mute', sound._id); + }); + return this; + } + + volume(vol, id) { + if (vol === undefined) { + let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0]; + return sound ? sound._volume : this._volume; + } + + vol = Math.max(0, Math.min(1, vol)); + if (id === undefined) this._volume = vol; + + this._getSounds(id).forEach(sound => { + sound._volume = vol; + this._updateSoundVolume(sound); + this._emit('volume', sound._id); + }); + return this; + } + + fade(from, to, duration, id) { + this._getSounds(id).forEach(sound => { + sound._volume = from; + this._updateSoundVolume(sound); + + let startTime = Date.now(); + let fadeStep = () => { + let elapsed = Date.now() - startTime; + let progress = Math.min(elapsed / duration, 1); + let currentVol = from + (to - from) * progress; + + sound._volume = currentVol; + this._updateSoundVolume(sound); + + if (progress < 1) { + requestAnimationFrame(fadeStep); + } else { + this._emit('fade', sound._id); + } + }; + + requestAnimationFrame(fadeStep); + }); + return this; + } + + rate(rate, id) { + if (rate === undefined) { + let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0]; + return sound ? sound._rate : this._rate; + } + + if (id === undefined) this._rate = rate; + + this._getSounds(id).forEach(sound => { + sound._rate = rate; + if (sound._node) { + if (this._webAudio && sound._source) { + sound._source.playbackRate.setValueAtTime(rate, Howler.ctx.currentTime); + } else { + sound._node.playbackRate = rate; + } + } + this._emit('rate', sound._id); + }); + return this; + } + + seek(seek, id) { + if (seek === undefined) { + let sound = id ? this._sounds.find(s => s._id === id) : this._sounds[0]; + if (!sound) return 0; + + if (this._webAudio) { + let elapsed = sound._playStart ? Howler.ctx.currentTime - sound._playStart : 0; + return sound._seek + elapsed * sound._rate; + } + return sound._node?.currentTime || 0; + } + + this._getSounds(id).forEach(sound => { + let wasPlaying = !sound._paused; + if (wasPlaying) this.pause(sound._id); + + sound._seek = seek; + if (!this._webAudio && sound._node) { + sound._node.currentTime = seek; + } + + if (wasPlaying) this.play(sound._id); + this._emit('seek', sound._id); + }); + return this; + } + + playing(id) { + if (id) { + let sound = this._sounds.find(s => s._id === id); + return sound ? !sound._paused : false; + } + return this._sounds.some(s => !s._paused); + } + + duration(id) { + if (id) { + let sound = this._sounds.find(s => s._id === id); + if (sound && this._sprite[sound._sprite]) { + return this._sprite[sound._sprite][1] / 1000; + } + } + return this._duration; + } + + state() { + return this._state; + } + + unload() { + this.stop(); + this._stopFrequencyAnalysis(); + Howler._howls.splice(Howler._howls.indexOf(this), 1); + this._sounds = []; + this._state = 'unloaded'; + return null; + } + + enableEQ(bands = 16, callback, boost = 4.0) { + if (!this._webAudio) { + console.warn('Frequency analysis requires Web Audio API'); + return this; + } + + this._frequencyCallback = callback; + this._frequencyBands = bands; + this._frequencyBoost = boost; + this._frequencyEnabled = true; + + if (!this._analyzer) { + this._analyzer = Howler.ctx.createAnalyser(); + this._analyzer.fftSize = 2048; + this._analyzer.smoothingTimeConstant = 0.3; + this._frequencyData = new Uint8Array(this._analyzer.frequencyBinCount); + this._analyzer.connect(Howler.masterGain); + } + + this._rebuildEffectChain(); + this._startFrequencyAnalysis(); + return this; + } + + disableEQ() { + this._frequencyEnabled = false; + this._stopFrequencyAnalysis(); + this._rebuildEffectChain(); + return this; + } + + getFrequencyData() { + if (!this._analyzer || !this._frequencyEnabled) return null; + + this._analyzer.getByteFrequencyData(this._frequencyData); + + let bands = new Array(this._frequencyBands); + let nyquist = Howler.ctx.sampleRate / 2; + let dataLength = this._frequencyData.length; + + let minFreq = 20; + let maxFreq = nyquist; + let logMin = Math.log(minFreq); + let logMax = Math.log(maxFreq); + let logRange = logMax - logMin; + + for (let i = 0; i < this._frequencyBands; i++) { + // Logarithmic frequency distribution + let logStart = logMin + (i / this._frequencyBands) * logRange; + let logEnd = logMin + ((i + 1) / this._frequencyBands) * logRange; + + let startFreq = Math.exp(logStart); + let endFreq = Math.exp(logEnd); + + let startBin = Math.floor((startFreq / nyquist) * dataLength); + let endBin = Math.ceil((endFreq / nyquist) * dataLength); + + let sum = 0; + let count = 0; + for (let j = startBin; j < Math.min(endBin, dataLength); j++) { + sum += this._frequencyData[j]; + count++; + } + + bands[i] = count > 0 ? (sum / count / 255) : 0; // Normalize to 0-1 + } + + for (let i = 0; i < bands.length; i++) { + bands[i] = Math.min(1.0, bands[i] * this._frequencyBoost); + } + + return bands; + } + + _startFrequencyAnalysis() { + if (!this._frequencyEnabled || this._frequencyAnimationId) return; + + let analyze = () => { + if (!this._frequencyEnabled) return; + + let frequencyData = this.getFrequencyData(); + if (frequencyData && this._frequencyCallback) { + this._frequencyCallback(frequencyData); + } + + this._frequencyAnimationId = requestAnimationFrame(analyze); + }; + + analyze(); + } + + _stopFrequencyAnalysis() { + if (this._frequencyAnimationId) { + cancelAnimationFrame(this._frequencyAnimationId); + this._frequencyAnimationId = null; + } + } + + addEffect(type, options = {}) { + if (!this._webAudio) { + console.warn('Audio effects require Web Audio API'); + return null; + } + + let effect; + + switch (type.toLowerCase()) { + case 'lowpass': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'lowpass'; + effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime); + break; + + case 'highpass': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'highpass'; + effect.frequency.setValueAtTime(options.frequency || 300, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime); + break; + + case 'bandpass': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'bandpass'; + effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 1, Howler.ctx.currentTime); + break; + + case 'notch': + effect = Howler.ctx.createBiquadFilter(); + effect.type = 'notch'; + effect.frequency.setValueAtTime(options.frequency || 1000, Howler.ctx.currentTime); + effect.Q.setValueAtTime(options.Q || 30, Howler.ctx.currentTime); + break; + + case 'delay': + effect = Howler.ctx.createDelay(options.maxDelay || 1); + effect.delayTime.setValueAtTime(options.delay || 0.3, Howler.ctx.currentTime); + break; + + case 'reverb': + effect = Howler.ctx.createConvolver(); + let length = Howler.ctx.sampleRate * (options.duration || 2); + let impulse = Howler.ctx.createBuffer(2, length, Howler.ctx.sampleRate); + let decay = options.decay || 2; + + for (let channel = 0; channel < 2; channel++) { + let channelData = impulse.getChannelData(channel); + for (let i = 0; i < length; i++) { + channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay); + } + } + effect.buffer = impulse; + break; + + case 'gain': + effect = Howler.ctx.createGain(); + effect.gain.setValueAtTime(options.gain || 1, Howler.ctx.currentTime); + break; + + case 'distortion': + effect = Howler.ctx.createWaveShaper(); + let samples = 44100; + let curve = new Float32Array(samples); + let amount = options.amount || 50; + for (let i = 0; i < samples; i++) { + let x = (i * 2) / samples - 1; + curve[i] = ((3 + amount) * x * 20 * Math.PI / 180) / (Math.PI + amount * Math.abs(x)); + } + effect.curve = curve; + effect.oversample = options.oversample || '4x'; + break; + + default: + console.warn(`Unknown effect type: ${type}`); + return null; + } + + if (effect) { + this._effects.push(effect); + this._rebuildEffectChain(); + } + + return effect; + } + + get effects() { + return this._effects; + } + + updateEffectsChain() { + this._rebuildEffectChain(); + return this; + } + + _rebuildEffectChain() { + this._sounds.forEach(sound => { + if (sound._node && this._webAudio) { + let startNode = sound._panner || sound._node; + this._connectSoundToChain(sound, startNode); + } + }); + } + + _connectSoundToChain(sound, startNode = null) { + if (!sound._node || !this._webAudio) return; + + let sourceNode = startNode || sound._node; + sourceNode.disconnect(); + + let currentNode = sourceNode; + + this._effects.forEach(effect => { + currentNode.connect(effect); + currentNode = effect; + }); + + if (this._environmentReverb && sound._panner) { + this._ensureEnvironmentSend(sound); + currentNode.connect(sound._environmentSend); + sound._environmentSend.connect(this._environmentReverb); + } + + if (this._analyzer && this._frequencyEnabled) { + currentNode.connect(this._analyzer); + sound._analyzerConnected = true; + } else { + currentNode.connect(Howler.masterGain); + sound._analyzerConnected = false; + } + } + + _ensureEnvironmentSend(sound) { + if (!sound._environmentSend) { + sound._environmentSend = Howler.ctx.createGain(); + sound._environmentSend.gain.setValueAtTime(0.7, Howler.ctx.currentTime); + } + } + + on(event, fn, id) { + (this._listeners[event] = this._listeners[event] || []).push({ fn, id }); + return this; + } + + off(event, fn, id) { + let listeners = this._listeners[event]; + if (!listeners) return this; + + if (!fn && !id) { + this._listeners[event] = []; + } else { + this._listeners[event] = listeners.filter(l => + (fn && l.fn !== fn) || (id && l.id !== id) + ); + } + return this; + } + + once(event, fn, id) { + let onceFn = (...args) => { + fn(...args); + this.off(event, onceFn, id); + }; + return this.on(event, onceFn, id); + } + + _selectSource() { + for (let src of this._src) { + let ext = this._format?.[this._src.indexOf(src)] || + src.match(/\.([^.?]+)(\?|$)/)?.[1] || + src.match(/^data:audio\/([^;,]+)/)?.[1]; + + if (ext && Howler.codecs(ext)) return src; + } + return null; + } + + async _loadWebAudio(url) { + try { + let arrayBuffer; + + if (url.startsWith('data:')) { + let data = atob(url.split(',')[1]); + arrayBuffer = new Uint8Array(data.length); + for (let i = 0; i < data.length; i++) { + arrayBuffer[i] = data.charCodeAt(i); + } + arrayBuffer = arrayBuffer.buffer; + } else { + let response = await fetch(url, this._xhr); + arrayBuffer = await response.arrayBuffer(); + } + + let audioBuffer = await Howler.ctx.decodeAudioData(arrayBuffer); + this._buffer = audioBuffer; + this._duration = audioBuffer.duration; + this._finishLoad(); + return this; + } catch (error) { + this._emit('loaderror', null, error.message); + throw error; + } + } + + async _loadHtml5(url) { + return new Promise((resolve, reject) => { + let audio = Howler._getHtml5Audio(); + + let onLoad = () => { + this._duration = Math.ceil(audio.duration * 10) / 10; + this._finishLoad(); + audio.removeEventListener('canplaythrough', onLoad); + audio.removeEventListener('error', onError); + resolve(this); + }; + + let onError = () => { + let error = `Failed to load: ${url}`; + audio.removeEventListener('canplaythrough', onLoad); + audio.removeEventListener('error', onError); + this._emit('loaderror', null, error); + reject(new Error(error)); + }; + + audio.addEventListener('canplaythrough', onLoad); + audio.addEventListener('error', onError); + audio.src = url; + audio.load(); + }); + } + + _finishLoad() { + if (!Object.keys(this._sprite).length) { + this._sprite = { __default: [0, this._duration * 1000] }; + } + + this._state = 'loaded'; + this._emit('load'); + this._processQueue(); + } + + _processQueue() { + if (this._queue.length) { + let { action } = this._queue.shift(); + action(); + this._processQueue(); + } + } + + _getSound() { + let sound = this._sounds.find(s => s._ended); + + if (!sound) { + if (this._sounds.length >= this._pool) { + return this._sounds.find(s => s._paused) || this._sounds[0]; + } + sound = this._createSound(); + } + + return this._resetSound(sound); + } + + _createSound() { + let sound = { + _id: ++Howler._counter, + _volume: this._volume, + _rate: this._rate, + _seek: 0, + _paused: true, + _ended: true, + _muted: this._muted, + _sprite: '__default' + }; + + this._sounds.push(sound); + this._createSoundNode(sound); + return sound; + } + + _resetSound(sound) { + Object.assign(sound, { + _volume: this._volume, + _rate: this._rate, + _seek: 0, + _paused: true, + _ended: true, + _muted: this._muted, + _sprite: '__default' + }); + return sound; + } + + _createSoundNode(sound) { + if (this._webAudio) { + sound._node = Howler.ctx.createGain(); + + if (this.spatial?.enabled) { + sound._panner = Howler.ctx.createPanner(); + // Set the panning model (this doesn't change dynamically) + sound._panner.panningModel = this.spatial.panningModel; + sound._node.connect(sound._panner); + this._updateSoundSpatial(sound); + + if (Howler._environment) { + this._updateEnvironment(); + } + + this._connectSoundToChain(sound, sound._panner); + } else { + this._connectSoundToChain(sound); + } + } else { + sound._node = Howler._getHtml5Audio(); + sound._node.src = this._src; + } + } + + _playSound(sound) { + if (!sound || this._state !== 'loaded') return null; + + let sprite = this._sprite[sound._sprite]; + if (!sprite) return null; + + let [start, duration] = sprite; + let seek = Math.max(0, sound._seek || start / 1000); + + sound._paused = false; + sound._ended = false; + + if (this._webAudio) { + this._playWebAudio(sound, seek, duration / 1000); + } else { + this._playHtml5(sound, seek); + } + + this._emit('play', sound._id); + return sound._id; + } + + _playWebAudio(sound, seek, duration) { + this._cleanupSound(sound); + + sound._source = Howler.ctx.createBufferSource(); + sound._source.buffer = this._buffer; + sound._source.connect(sound._node); + + sound._source.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime); + sound._playStart = Howler.ctx.currentTime; + + if (sound._loop) { + sound._source.loop = true; + sound._source.start(0, seek); + } else { + // Fix: duration is the sprite duration, not total audio duration + sound._source.start(0, seek, duration); + sound._source.onended = () => this._onSoundEnd(sound); + } + + this._updateSoundVolume(sound); + } + + _playHtml5(sound, seek) { + let { _node: node } = sound; + node.currentTime = seek; + node.volume = this._getEffectiveVolume(sound); + node.playbackRate = sound._rate; + node.muted = sound._muted || this._muted || Howler._muted; + + let promise = node.play(); + if (promise) { + promise.catch(() => this._emit('playerror', sound._id, 'Playback failed')); + } + + if (!sound._loop) { + node.onended = () => this._onSoundEnd(sound); + } + } + + _onSoundEnd(sound) { + sound._paused = true; + sound._ended = true; + this._emit('end', sound._id); + + if (sound._loop) { + sound._ended = false; + this._playSound(sound); + } + } + + _updateSoundVolume(sound) { + if (!sound._node) return; + + let volume = this._getEffectiveVolume(sound); + + if (this._webAudio) { + sound._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + } else { + sound._node.volume = volume; + } + } + + _getEffectiveVolume(sound) { + if (sound._muted || this._muted || Howler._muted) return 0; + return sound._volume * this._volume * Howler._volume; + } + + _updateVolume() { + this._sounds.forEach(sound => this._updateSoundVolume(sound)); + } + + _cleanupSound(sound) { + if (sound._source) { + sound._source.disconnect(); + sound._source = null; + } + if (sound._occlusionFilter) { + sound._occlusionFilter.disconnect(); + sound._occlusionFilter = null; + } + } + + _getSounds(id) { + return id ? this._sounds.filter(s => s._id === id) : this._sounds; + } + + _emit(event, id, data) { + let listeners = this._listeners[event]; + if (!listeners) return; + + listeners.forEach(({ fn, id: listenerId }) => { + if (!listenerId || listenerId === id) { + setTimeout(() => fn(id, data), 0); + } + }); + } + + _updateSpatial() { + if (!this.spatial?.enabled) return; + + this._sounds.forEach(sound => { + if (sound._panner) { + this._updateSoundSpatial(sound); + } + }); + } + + _updateSoundSpatial(sound) { + if (!sound._panner || !this.spatial?.enabled) return; + + let { position, orientation, velocity, distance, cone, occlusion, obstruction } = this.spatial; + let p = sound._panner; + + if (p.positionX) { + p.positionX.setValueAtTime(position.x, Howler.ctx.currentTime); + p.positionY.setValueAtTime(position.y, Howler.ctx.currentTime); + p.positionZ.setValueAtTime(position.z, Howler.ctx.currentTime); + p.orientationX.setValueAtTime(orientation.x, Howler.ctx.currentTime); + p.orientationY.setValueAtTime(orientation.y, Howler.ctx.currentTime); + p.orientationZ.setValueAtTime(orientation.z, Howler.ctx.currentTime); + } else { + p.setPosition(position.x, position.y, position.z); + p.setOrientation(orientation.x, orientation.y, orientation.z); + } + + if (p.setVelocity && velocity) { + p.setVelocity(velocity.x, velocity.y, velocity.z); + } + + p.refDistance = distance.ref; + p.maxDistance = distance.max; + p.rolloffFactor = distance.rolloff; + p.distanceModel = distance.model; + + p.coneInnerAngle = cone.inner; + p.coneOuterAngle = cone.outer; + p.coneOuterGain = cone.outerGain; + + this._applyOcclusion(sound, occlusion || 0, obstruction || 0); + } + + _applyOcclusion(sound, occlusion, obstruction) { + if (!sound._occlusionFilter && (occlusion > 0 || obstruction > 0)) { + sound._occlusionFilter = Howler.ctx.createBiquadFilter(); + sound._occlusionFilter.type = 'lowpass'; + sound._node.disconnect(); + sound._node.connect(sound._occlusionFilter); + sound._occlusionFilter.connect(sound._panner || Howler.masterGain); + } + + if (sound._occlusionFilter) { + let freq = 20000 * (1 - Math.max(occlusion, obstruction) * 0.9); + let gain = 1 - occlusion * 0.8; + sound._occlusionFilter.frequency.setValueAtTime(freq, Howler.ctx.currentTime); + sound._node.gain.setValueAtTime(gain * this._getEffectiveVolume(sound), Howler.ctx.currentTime); + } + } + + _updateEnvironment() { + if (!Howler._environment || !this._webAudio) return; + + if (!this._environmentReverb) { + this._environmentReverb = Howler.ctx.createConvolver(); + let { size, decay, dampening } = Howler._environment; + let length = Howler.ctx.sampleRate * (size === 'large' ? 6 : size === 'medium' ? 3 : 1.5); + let impulse = Howler.ctx.createBuffer(2, length, Howler.ctx.sampleRate); + + for (let channel = 0; channel < 2; channel++) { + let channelData = impulse.getChannelData(channel); + for (let i = 0; i < length; i++) { + let progress = i / length; + let sample = 0; + + if (i < Howler.ctx.sampleRate * 0.2) { + for (let j = 0; j < 8; j++) { + let delay = (j + 1) * 0.02 * Howler.ctx.sampleRate; + if (Math.abs(i - delay) < 100) { + sample += (Math.random() * 2 - 1) * 0.3 * Math.exp(-j * 0.5); + } + } + } + + let envelope = Math.pow(1 - progress, decay); + let reverb = (Math.random() * 2 - 1) * envelope; + sample += reverb * (1 - dampening + Math.random() * dampening * 0.3); + + if (size === 'large') { + let lowFreq = Math.sin(i * 0.01) * envelope * 0.2; + sample += lowFreq; + } + + channelData[i] = Math.tanh(sample * 0.8); + } + } + this._environmentReverb.buffer = impulse; + + this._environmentGain = Howler.ctx.createGain(); + this._environmentGain.gain.setValueAtTime(0.8, Howler.ctx.currentTime); + this._environmentReverb.connect(this._environmentGain); + this._environmentGain.connect(Howler.masterGain); + } + + this._rebuildEffectChain(); + } +} + +let Howler = new HowlerGlobal(); + +return { Howler, Howl }; + +})); diff --git a/site/examples/web-app-starter/js/u-macrobars-demo.html b/site/examples/web-app-starter/js/u-macrobars-demo.html new file mode 100755 index 0000000..63dc607 --- /dev/null +++ b/site/examples/web-app-starter/js/u-macrobars-demo.html @@ -0,0 +1,977 @@ + + + + + + U-Macrobars.js Demo + + + +
+

U-Macrobars.js Demo

+ +
+
+

Basic Field Output

+
+
{{name}} is {{age}} years old and works as {{job or "unemployed"}}
+
+
+ + +
+
+ +
+

Number Formatting

+
Price: ${{%price}} | Large Number: {{~bigNumber}}
+
+
+ + +
+
+ +
+

Default Values & Safety

+
{{username or "Guest"}} | {{profile.bio or "No bio available"}}
+
+
+ + +
+
+
+ +
+

Field Output API

+ +
+

Basic Syntax

+
{{field}}Safe HTML output
+
{{{field}}}Raw HTML output
+
{{:variable}}Direct variable
+
{{field or "default"}}With fallback
+
+ +
+

Number Formatting

+
{{%number}}2 decimal places
+
{{~number}}Rounded (1k, 1M)
+
+ +
+

Examples

+
+// Basic field output
+const template = Macrobars.compile('{{name}}');
+const result = template({name: 'John'});
+
+// With defaults
+const withDefault = '{{title or "Untitled"}}';
+
+// Number formatting
+const price = '${{%cost}}'; // $12.34
+const count = '{{~views}}';  // 1.2k
+
+
+
+
+ +
+
+

Control Structures

+
+

Conditionals

+
{{#if isLoggedIn}} +Welcome back, {{username}}! +{{#else}} +Please log in to continue. +{{/if}}
+
+
+ + +
+
+ +
+

Loops & Iteration

+
{{#each items}} +• {{number}}. {{name}} - ${{price}} +{{/each}}
+
+
+ + +
+
+ +
+

Equality & Lookup

+
{{#eq status "active"}}User is active{{/eq}} +{{lookup user "permissions"}}
+
+
+ + +
+
+
+ +
+

Control Flow API

+ +
+

Conditionals

+
{{#if condition}}...{{/if}}
+
{{#else}}Alternative branch
+
+ Conditional rendering based on truthy values. Supports nested conditions. +
+
+ +
+

Loops

+
{{#each items}}...{{/each}}
+
{{#each items as item}}...{{/each}}
+
+ • Standard: data becomes current item
+ • Named: item becomes current item, data preserved +
+
+ +
+

Comparison & Lookup

+
{{#eq val1 val2}}...{{/eq}}
+
{{eq val1 val2}}"true" or ""
+
{{#lookup obj key}}...{{/lookup}}
+
{{lookup obj key}}value or ""
+
+ +
+

Examples

+
+// Conditionals
+'{{#if user.isAdmin}}Admin Panel{{/if}}'
+
+// Named loops
+'{{#each products as product}}'
+  '{{product.name}} - {{data.storeName}}'
+'{{/each}}'
+
+// Equality & lookup
+'{{#eq user.role "admin"}}Secret{{/eq}}'
+'{{lookup config "theme"}}'
+
+
+
+
+ +
+
+

Advanced Features

+
+

Event Binding

+
<button {{@click="handleClick"}}>Click Count: {{clickCount}}</button>
+
+
+ + +
+
+ +
+

Code Blocks

+
<script>var computed = data.value * 2;</script> +Result: {{:computed}}
+
+
+ + +
+
+ +
+

Components

+
{{#component userCard}}
+
+
+ + +
+
+
+ +
+

Advanced API

+ +
+

Event Binding

+
{{@event="handler"}}DOM attribute
+
template.renderTo(container, data)
+
+ Events are automatically bound when using renderTo(). Handler can reference data properties or global functions. +
+
+ +
+

Code Execution

+
<script>...</script>
+
<defer>...</defer>
+
<? code ?>
+
<?= expression ?>
+
+ +
+

Components & Compilation

+
Macrobars.compile(template, options)
+
Macrobars.createComponents(definitions)
+
+ +
+

Examples

+
+// Event binding with renderTo
+const template = Macrobars.compile(
+  '<button {{@click="increment"}}>{{count}}</button>'
+);
+template.renderTo('#container', {
+  count: 0,
+  increment: function() { this.count++; }
+});
+
+// Components
+const components = Macrobars.createComponents({
+  userCard: '<div>{{name}} - {{email}}</div>'
+});
+
+
+
+
+ +
+
+

Template Editor

+
+ + + + + + + +
+ + + + +
+ +
Click "Render Template"
+ +
+

Template Information

+
Ready.
+
+ +
+
+ +
+

Compilation & Debugging

+ +
+

Compilation Options

+
decimals: numberNumber precision
+
strict: booleanStrict mode
+
components: objectReusable templates
+
+ +
+

Debugging Properties

+
template.tokensParsed tokens
+
template.gensourceGenerated JS
+
template.event_bindingsEvent data
+
+ +
+

Utility Functions

+
Macrobars.safe_out(text, default)
+
Macrobars.num_out(number, decimals)
+
Macrobars.num_out_round(number)
+
+ +
+

Example Templates

+
+// Complete example
+const template = Macrobars.compile(`
+<div class="user-card">
+  <h3>{{name or "Unknown User"}}</h3>
+  {{#if profile.verified}}✅ Verified{{/if}}
+  <p>Balance: ${{%balance}}</p>
+  {{#each achievements}}
+    <span class="badge">{{data}}</span>
+  {{/each}}
+</div>
+`, { decimals: 2 });
+
+const result = template(userData);
+
+
+
+
+
+ + + + + diff --git a/site/examples/web-app-starter/js/u-macrobars.js b/site/examples/web-app-starter/js/u-macrobars.js new file mode 100755 index 0000000..b09b46d --- /dev/null +++ b/site/examples/web-app-starter/js/u-macrobars.js @@ -0,0 +1,411 @@ +(function (root, factory) { // I really hate this convoluted bullshit + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.Macrobars = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + + if(!String.prototype.replaceAll) String.prototype.replaceAll = function(search, replacement) { + var target = this; + return target.split(search).join(replacement); + }; + + var safe_out = (s, defaultValue = '') => { + if(typeof s == 'undefined' || s === null || s === false || s === '') s = defaultValue; + return((''+s).replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')); + }; + + var num_out = (n, decimals = 2) => { + if(typeof n == 'undefined' || n === false) n = 0; + if(decimals == 0) return(Math.round(n)); + return(n.toFixed(decimals)); + } + + var num_out_round = (n) => { + if(typeof n == 'undefined' || n === false || n === null) n = 0; + + if(typeof n === 'string') { + n = parseFloat(n); + if(isNaN(n)) return '0'; + } + + if(Math.abs(n) >= 1e15) { + return n.toExponential(2); + } + + if(Math.abs(n) > 0 && Math.abs(n) < 0.001) { + return n.toExponential(2); + } + + if(Math.abs(n) >= 1e12) { + return (n/1e12).toFixed(1) + 'T'; + } else if(Math.abs(n) >= 1e9) { + return (n/1e9).toFixed(1) + 'B'; + } else if(Math.abs(n) >= 1e6) { + return (n/1e6).toFixed(1) + 'M'; + } else if(Math.abs(n) >= 1e3) { + return (n/1e3).toFixed(1) + 'k'; + } else if(Math.abs(n) >= 1) { + return Math.round(n); + } else if(Math.abs(n) >= 0.01) { + return n.toFixed(2); + } else if(Math.abs(n) >= 0.001) { + return n.toFixed(3); + } else if(n === 0) { + return 0; + } + + return n.toString(); + } + + var debug_out = (ee) => { + var es = ee.stack.split("\n"); + var loc = es[1]; + var p0 = loc.indexOf(''); + if(p0 != -1) { + loc = loc.substr(p0+(''.length+1)); + loc = loc.substr(0, loc.length-1); + } + return('Macrobars '+es[0]+' ('+loc+')'); + } + + var signposts = [ + { start : '', type : 'code' }, + { start : '', end : '', type : 'defer' }, + + { start : '', type : 'field' }, + { start : '', type : 'var_out' }, + { start : '', type : 'code' }, + { start : '', type : 'code' }, + + { start : '{{:', end : '}}', type : 'var_out' }, + { start : '{{#eq ', end : '}}', type : 'eq_block_start' }, + { start : '{{#lookup ', end : '}}', type : 'lookup_block_start' }, + { start : '{{eq ', end : '}}', type : 'eq_inline' }, + { start : '{{lookup ', end : '}}', type : 'lookup_inline' }, + { start : '{{#default ', end : '}}', type : 'default_empty' }, + { start : '{{#if ', end : '}}', type : 'if_start' }, + { start : '{{#else}}', end : '', type : 'else' }, + { start : '{{#component ', end : '}}', type : 'component' }, + { start : '{{#each ', end : '}}', type : 'each_start' }, + { start : '{{/each}}', end : '', type : 'each_end' }, + { start : '{{/if}}', end : '', type : 'if_end' }, + { start : '{{/eq}}', end : '', type : 'eq_block_end' }, + { start : '{{/lookup}}', end : '', type : 'lookup_block_end' }, + { start : '{{/', end : '}}', type : 'block_end' }, + { start : '{{@', end : '}}', type : 'event_bind' }, + { start : '{{{', end : '}}}', type : 'field_unsafe' }, + { start : '{{%', end : '}}', type : 'field_number' }, + { start : '{{~', end : '}}', type : 'field_number_round' }, + { start : '{{', end : '}}', type : 'field' }, + ]; + + if(!each) function each(o, f) { + if(!o) + return; + if(o.forEach) { + o.forEach(f); + } else { + for(var prop in o) if(o.hasOwnProperty(prop)) { + f(o[prop], prop); + } + } + } + + var data_prefix = (identifier) => { + if(identifier.substr(0, 1) == ':') + return(identifier.substr(1)); + else + return('data.'+identifier); + } + + var parse_field_with_default = (fieldText) => { + var orMatch = fieldText.match(/^(.+?)\s+or\s+(['"])(.*?)\2$/); + if (orMatch) { + return { + field: orMatch[1].trim(), + default: orMatch[3] + }; + } + return { + field: fieldText.trim(), + default: null + }; + } + + var compile = function(text, options = {}) { + + var crash_counter = 0; + var tokens = []; + var gensource = []; + var event_bindings = []; + var components = options.components || {}; + var condition_stack = []; + + var emit = { + text : (token) => { + gensource.push('output += '+JSON.stringify(token.text)+';'); + }, + code : (token) => { + gensource.push(token.text); + }, + defer : (token) => { + gensource.push('output += "";'); + }, + var_out : (token) => { + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out('+(token.text)+', '+defaultVal+');'); + }, + field : (token) => { + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out('+data_prefix(token.text)+', '+defaultVal+');'); + }, + field_unsafe : (token) => { + if (token.default) { + gensource.push('output += ('+data_prefix(token.text)+' || '+JSON.stringify(token.default)+');'); + } else { + gensource.push('output += ('+data_prefix(token.text)+' || default_empty_field);'); + } + }, + field_number : (token) => { + gensource.push('output += num_out('+data_prefix(token.text)+', this.decimals);'); + }, + field_number_round : (token) => { + gensource.push('output += num_out_round('+data_prefix(token.text)+');'); + }, + each_start : (token) => { + var parts = token.text.split(' as '); + var collection = parts[0].trim(); + var itemName = parts.length > 1 ? parts[1].trim() : 'data'; + + gensource.push('each('+data_prefix(collection)+', ('+itemName+', index) => {'); + + }, + if_start : (token) => { + condition_stack.push('if'); + gensource.push('if ('+data_prefix(token.text)+') {'); + }, + else : (token) => { + gensource.push('} else {'); + }, + if_end : (token) => { + condition_stack.pop(); + gensource.push('} /* if end */'); + }, + each_end : (token) => { + gensource.push('}); /* each end */'); + }, + block_end : (token) => { + var block_type = condition_stack.pop(); + gensource.push('}); /* '+(block_type || 'each')+' end */'); + }, + component : (token) => { + var comp_name = token.text.trim(); + if (components[comp_name]) { + gensource.push('output += components['+JSON.stringify(comp_name)+'](data);'); + } else { + gensource.push('output += "[Component '+comp_name+' not found]";'); + } + }, + event_bind : (token) => { + // Parse event binding: @click="functionName" or @click="data.handler" + var parts = token.text.split('='); + if (parts.length === 2) { + var event_type = parts[0].trim(); + var handler_ref = parts[1].trim().replace(/"/g, ''); + var binding_id = 'mb_bind_' + event_bindings.length; + event_bindings.push({ + id: binding_id, + event: event_type, + handler: handler_ref + }); + gensource.push('output += " data-mb-bind=\\"'+binding_id+'\\"";'); + } + }, + eq_block_start : (token) => { + // Parse "value1 value2" for equality comparison block + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var val1 = parts[0].startsWith('"') || parts[0].startsWith("'") ? parts[0] : data_prefix(parts[0]); + var val2 = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + condition_stack.push('eq'); + gensource.push('if ('+val1+' == '+val2+') {'); + } + }, + lookup_block_start : (token) => { + // Parse "object key" for dynamic property lookup block (checks if property exists and is truthy) + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var obj = data_prefix(parts[0]); + var key = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + condition_stack.push('lookup'); + gensource.push('if ('+obj+' && '+obj+'['+key+']) {'); + } + }, + eq_inline : (token) => { + // Parse "value1 value2" for equality comparison - outputs result directly + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var val1 = parts[0].startsWith('"') || parts[0].startsWith("'") ? parts[0] : data_prefix(parts[0]); + var val2 = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out(('+val1+' == '+val2+') ? "true" : "", '+defaultVal+');'); + } + }, + lookup_inline : (token) => { + // Parse "object key" for dynamic property lookup - outputs result directly + var parts = token.text.trim().split(/\s+/); + if (parts.length >= 2) { + var obj = data_prefix(parts[0]); + var key = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]); + var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field'; + gensource.push('output += safe_out(('+obj+' && '+obj+'['+key+']) || "", '+defaultVal+');'); + } + }, + eq_block_end : (token) => { + condition_stack.pop(); + gensource.push('} /* eq end */'); + }, + lookup_block_end : (token) => { + condition_stack.pop(); + gensource.push('} /* lookup end */'); + }, + default_empty : (token) => { + gensource.push('default_empty_field = '+JSON.stringify(token.text)+';'); + }, + } + + while(text != '' && crash_counter < 100) { + + crash_counter+=1; + var p0 = -1; + var sp_found = false; + signposts.forEach((sp) => { + var ps = text.indexOf(sp.start); + if(ps != -1 && (p0 == -1 || p0 > ps)) { + sp_found = sp; + p0 = ps; + } + }); + + if(sp_found) + { + tokens.push({ type : 'text', text : text.substr(0, p0) }); + text = text.substr(p0 + sp_found.start.length); + var p1 = text.indexOf(sp_found.end); + if(p1 == -1) p1 = text.length; + + var tokenText = text.substr(0, p1); + var token = { type : sp_found.type, text : tokenText }; + + if (sp_found.type === 'field' || sp_found.type === 'var_out' || + sp_found.type === 'field_unsafe' || sp_found.type === 'field_number' || + sp_found.type === 'field_number_round') { + var parsed = parse_field_with_default(tokenText); + token.text = parsed.field; + token.default = parsed.default; + } + + tokens.push(token); + text = text.substr(p1+sp_found.end.length); + } + else + { + tokens.push({ type : 'text', text : text }); + text = ''; + } + + } + + if(typeof options.decimals == 'undefined') + options.decimals = 2; + this.decimals = options.decimals; + + gensource.push('(data) => { '); + if(options.strict) + gensource.push('"use strict";'); + gensource.push(' try {'); + gensource.push(' if(!data) data = {}; var data_root = data;'); + gensource.push(' var output = ""; var default_empty_field = "";'); + gensource.push(' var echo = (s) => { output += s; };'); + tokens.forEach((tok) => { + emit[tok.type](tok); + }); + gensource.push(' } catch (ee) { console.error(ee); output += debug_out(ee); }'); + gensource.push(' return(output);'); + gensource.push('}'); + + var f = {}; + try { + f = eval('('+gensource.join("\n")+')'); + } catch(ce) { + console.error('Macrobars compilation error:', ce); + console.error('Template source:', text.substring(0, 200) + (text.length > 200 ? '...' : '')); + console.error('Generated JavaScript:', gensource.join("\n")); + } + + f.tokens = tokens; + f.gensource = gensource; + f.event_bindings = event_bindings; + f.options = options; + f.components = components; + + f.renderTo = function(container_or_query, data) { + var container; + if (typeof container_or_query === 'string') { + container = document.querySelector(container_or_query); + if (!container) { + return null; + } + } else { + container = container_or_query; + } + + try { + var html = f(data); + container.innerHTML = html; + event_bindings.forEach(binding => { + var elements = container.querySelectorAll('[data-mb-bind="' + binding.id + '"]'); + elements.forEach(element => { + var handler = data[binding.handler] || eval(binding.handler); + if (typeof handler === 'function') { + element.addEventListener(binding.event, handler); + } + }); + }); + } catch(renderError) { + console.error('Macrobars renderTo error:', renderError); + console.error('Data passed to template:', data); + throw renderError; + } + + return container; + }; + + return(f); + } + + return({ + + compile : compile, + + createComponents : function(definitions) { + var components = {}; + each(definitions, (template, name) => { + components[name] = compile(template); + }); + return components; + }, + + num_out : num_out, + safe_out : safe_out, + num_out_round : num_out_round, + debug_out : debug_out, + each : each, + + }); + +})); diff --git a/site/examples/web-app-starter/js/u-macrobars.md b/site/examples/web-app-starter/js/u-macrobars.md new file mode 100755 index 0000000..191aedb --- /dev/null +++ b/site/examples/web-app-starter/js/u-macrobars.md @@ -0,0 +1,137 @@ +# macrobars.js + +A lightweight JavaScript templating engine with PHP-style syntax and Handlebars-inspired features. + +## Basic Usage + +```javascript +const template = Macrobars.compile('

{{title}}

{{content}}

'); +const html = template({title: 'Hello', content: 'World'}); +``` + +## Field Output + +```javascript +{{field}} // Safe HTML output (escaped) +{{{field}}} // Unsafe HTML output (raw) +{{field or "default"}} // With default value +{{:variable}} // Direct variable (no data. prefix) +``` + +## Numbers + +```javascript +{{%number}} // Formatted number (2 decimals) +{{~number}} // Rounded number (1k, 1M format) +``` + +## Control Structures + +### Conditionals +```javascript +{{#if condition}} + Content when true +{{#else}} + Content when false +{{/if}} +``` + +### Equality Blocks +```javascript +{{#eq value1 value2}} + Content when equal +{{/eq}} + +{{eq value1 value2}} // Inline: outputs "true" or "" +``` + +### Loops +```javascript +{{#each items}} + {{name}} - {{index}} +{{/each}} + +{{#each items as item}} + {{item.name}} +{{/each}} +``` + +### Property Lookup +```javascript +{{#lookup object key}} + {{value}} exists +{{/lookup}} + +{{lookup object key}} // Inline: outputs value or "" +``` + +## Code Blocks + +```javascript + + + + // Deferred JavaScript execution + + + // PHP-style output + // PHP-style code +``` + +## Event Binding + +```javascript + +``` + +Events are automatically bound when using `renderTo()`. + +## Components + +```javascript +{{#component myComponent}} // Render registered component +``` + +## Compilation Options + +```javascript +const template = Macrobars.compile(templateString, { + decimals: 2, // Number formatting precision + strict: true, // Use strict mode + components: {...} // Reusable components +}); +``` + +## DOM Integration + +```javascript +template.renderTo('#container', data); // Render to element +template.renderTo(document.getElementById('x'), data); +``` + +## Utility Functions + +```javascript +Macrobars.safe_out(text, default) // HTML escape +Macrobars.num_out(number, decimals) // Format number +Macrobars.num_out_round(number) // Round with k/M suffix +``` + +## Template Debugging + +Access compiled template information: +```javascript +template.tokens // Parsed tokens +template.gensource // Generated JavaScript +template.event_bindings // Event binding data +``` + +## Default Values + +```javascript +{{#default "N/A"}} // Set default for empty fields +{{field}} // Will show "N/A" if empty +``` + diff --git a/site/examples/web-app-starter/js/u-popup-menu.css b/site/examples/web-app-starter/js/u-popup-menu.css new file mode 100644 index 0000000..fb30ca3 --- /dev/null +++ b/site/examples/web-app-starter/js/u-popup-menu.css @@ -0,0 +1,34 @@ +:root { + --vp-size-popup-min-width: 160px; +} + +.vp-popup-menu { + position: absolute; + background: var(--vp-color-black); + border: 1px solid var(--color-border); + color: var(--vp-color-white); + min-width: var(--vp-size-popup-min-width); + z-index: var(--vp-z-popup); + border-radius: var(--vp-space-l); + overflow: hidden; +} + +.vp-popup-item { + padding: var(--vp-space-l); + cursor: pointer; + white-space: nowrap; +} + +.vp-popup-item:hover { + color: var(--color-highlight); +} + +.vp-popup-item.focused { + background: var(--color-highlight); + color: var(--vp-color-black); +} + +.vp-popup-empty { + padding: var(--vp-space-l); + color: var(--vp-color-gray); +} \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-popup-menu.js b/site/examples/web-app-starter/js/u-popup-menu.js new file mode 100644 index 0000000..e30498b --- /dev/null +++ b/site/examples/web-app-starter/js/u-popup-menu.js @@ -0,0 +1,84 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.showPopupMenu = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + let showPopupMenu = (x, y, items, prop = {}) => { + document.querySelectorAll('.vp-popup-menu').forEach(menu => { + if(menu.parentNode) menu.parentNode.removeChild(menu); + }); + + let container = prop.container || document.body; + let menu = $('
').css({ + position: 'absolute', + left: (x|0) + 'px', + top: (y|0) + 'px' + })[0]; + + let elems = []; + if(!items || !items.length) { + $(menu).append(`
${prop.emptyText || 'No items'}
`); + } else { + items.forEach((it, idx) => { + let label = typeof it === 'string' ? it : (it.label || it.screen || it.name || JSON.stringify(it)); + let $item = $(`
${label}
`) + .attr('data-idx', idx) + .on('click', ev => { ev.stopPropagation(); try { (prop.onSelect || (()=>{}))(it); } catch(e){ console.error(e); } removeMenu(); }) + .on('keydown', e => { if(e.key === 'Enter') { e.preventDefault(); $item[0].click(); } }); + $(menu).append($item[0]); + elems.push({el: $item[0], data: it}); + }); + } + + let focusedIndex = elems.length ? 0 : -1; + let focusAt = i => { + elems.forEach((it, idx) => { $(it.el).removeClass('focused'); if(idx === i) { $(it.el).addClass('focused'); try{ it.el.focus(); }catch(e){} } }); + focusedIndex = i; + }; + + let removeMenu = () => { + if(menu.parentNode) menu.parentNode.removeChild(menu); + $(document).off('mousedown', onDoc).off('keydown', onKey); + $(window).off('resize', onResize); + }; + let onDoc = ev => { if(!menu.contains(ev.target)) removeMenu(); }; + let onResize = () => reposition(); + let onKey = ev => { + if(!elems.length) { if(ev.key === 'Escape') removeMenu(); return; } + if(ev.key === 'Escape') { ev.preventDefault(); removeMenu(); return; } + if(ev.key === 'ArrowDown') { ev.preventDefault(); focusAt((focusedIndex + 1) % elems.length); return; } + if(ev.key === 'ArrowUp') { ev.preventDefault(); focusAt((focusedIndex - 1 + elems.length) % elems.length); return; } + if(ev.key === 'Home') { ev.preventDefault(); focusAt(0); return; } + if(ev.key === 'End') { ev.preventDefault(); focusAt(elems.length - 1); return; } + if(ev.key === 'Enter') { ev.preventDefault(); if(focusedIndex >= 0) elems[focusedIndex].el.click(); } + }; + + container.appendChild(menu); + let reposition = () => { + let mRect = menu.getBoundingClientRect(); + let winW = window.innerWidth, winH = window.innerHeight; + let left = parseInt($(menu).css('left'),10) || 0, top = parseInt($(menu).css('top'),10) || 0; + if(mRect.right > winW) left = Math.max(4, winW - Math.ceil(mRect.width) - 8); + if(mRect.bottom > winH) top = Math.max(4, winH - Math.ceil(mRect.height) - 8); + if(left < 4) left = 4; if(top < 4) top = 4; + $(menu).css({left: left + 'px', top: top + 'px'}); + }; + + $(document).on('mousedown', onDoc).on('keydown', onKey); + $(window).on('resize', onResize); + if(elems.length) { focusAt(0); } + setTimeout(reposition, 0); + return menu; + }; + + return showPopupMenu; +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-query-demo.html b/site/examples/web-app-starter/js/u-query-demo.html new file mode 100755 index 0000000..ce48d39 --- /dev/null +++ b/site/examples/web-app-starter/js/u-query-demo.html @@ -0,0 +1,976 @@ + + + + + + U-Query.js Demo + + + +
+

U-Query.js Demo

+ +
+
+

Element Selection & DOM Manipulation

+
+

DOM Queries

+
+ Select elements to see them highlighted +
+
Test Element 1
+
Test Element 2
+
Test Element 3 (special)
+ +
+ + + + +
+ +
+
+ +
+

DOM Manipulation

+
Original content
+ +
+ + + + +
+ +
+
+ +
+

CSS & Classes

+
CSS Target Element
+ +
+ + + + +
+ +
+
+
+ +
+

Selection API

+ +
+

Core Selectors

+
$(selector) → NodeList
+
$('.class') → NodeList
+
$('#id') → NodeList
+
$('tag') → NodeList
+
+ +
+

DOM Manipulation

+
html(content?) → NodeList|string
+
text(content?) → NodeList|string
+
append(content) → NodeList
+
prepend(content) → NodeList
+
remove() → NodeList
+
+ +
+

CSS & Classes

+
css(styles) → NodeList
+
addClass(className) → NodeList
+
removeClass(className) → NodeList
+
toggleClass(className) → NodeList
+
+ +
+

Examples

+
+// Element selection
+const elements = $('.my-class');
+const byId = $('#my-id');
+
+$('.target').html('<b>New content</b>');
+$('.target').text('Plain text');
+$('.container').append('<p>Added</p>');
+
+$('.element').css({
+  'color': 'red',
+  'background': 'blue'
+});
+$('.element').addClass('active');
+
+
+
+
+ +
+
+

Event Handling & Visibility

+
+

Event System

+ + +
+ + + +
+ +
+
+ +
+

Show/Hide/Toggle

+
Toggle me!
+ +
+ + + + +
+ +
+
+ +
+

Method Chaining

+
+
Chain Target 1
+
Chain Target 2
+
Chain Target 3
+
+ +
+ + +
+ +
+
+
+ +
+

Events & Visibility API

+ +
+

Event Methods

+
on(event, handler) → NodeList
+
off(event, handler?) → NodeList
+
trigger(event, data?) → NodeList
+
once(event, handler) → NodeList
+
+ +
+

Visibility Control

+
show() → NodeList
+
hide() → NodeList
+
toggle() → NodeList
+
fadeIn(duration?) → NodeList
+
fadeOut(duration?) → NodeList
+
+ +
+

Method Chaining

+
+ All u-query methods return the NodeList, enabling method chaining. +
+
+$('.button').on('click', function() {
+  console.log('Clicked!');
+});
+
+$('.element')
+  .addClass('active')
+  .css({'color': 'red'})
+  .show()
+  .on('click', handler);
+
+$('.modal').fadeIn(300);
+$('.tooltip').fadeOut(200);
+
+
+
+
+ +
+
+

AJAX & Global Events

+
+

AJAX Utilities

+
+
+ AJAX Request Status +
+ +
+ + + +
+ +
+
+ +
+

Global Event System

+ + +
+ + + +
+ +
+
+ +
+

Utility Functions

+
Utility Element 1
+
Utility Element 2
+
Utility Element 3
+ +
+ + + +
+ +
+
+
+ +
+

AJAX & Utilities API

+ +
+

AJAX Methods

+
$.get(url, callback) → Promise
+
$.post(url, data, callback) → Promise
+
$.ajax(options) → Promise
+
$.json(url) → Promise
+
+ +
+

Global Event System

+
$.on(event, handler) → $
+
$.off(event, handler?) → $
+
$.emit(event, data?) → number
+
$.trigger(event, data?) → number
+
+ +
+

Utility Functions

+
$.each(selector, callback) → $
+
$.ready(callback) → $
+
$.extend(target, ...sources) → object
+
$.map(selector, callback) → Array
+
+ +
+

Examples

+
+const data = await $.get('/api/users');
+$.post('/api/users', {name: 'John'})
+  .then(response => console.log(response));
+
+$.on('user-login', user => {
+  console.log('User logged in:', user);
+});
+$.emit('user-login', {id: 123});
+
+$.each('.item', (el, i) => {
+  el.textContent = `Item ${i}`;
+});
+
+$.ready(() => {
+  console.log('DOM ready!');
+});
+
+
+
+
+ +
+
+

Browser Info

+
+
Loading library information...
+ +

This browser supports

+
+ +
+ + +
+
+
+ +
+

Advanced Features

+ +
+

Advanced Selectors

+
$(':visible') → NodeList
+
$(':hidden') → NodeList
+
$(':checked') → NodeList
+
$(':first') → NodeList
+
$(':last') → NodeList
+
+ +
+

Data Attributes

+
data(key, value?) → NodeList|any
+
attr(name, value?) → NodeList|string
+
prop(name, value?) → NodeList|any
+
val(value?) → NodeList|string
+
+ +
+

Traversal Methods

+
parent() → NodeList
+
children() → NodeList
+
siblings() → NodeList
+
find(selector) → NodeList
+
closest(selector) → NodeList
+
+ +
+

DOM Diff Updates

+
+ u-query can make use of morphdom.js for efficient DOM diff updates. +
+
$.options.alwaysDoDifferentialUpdate = true
+
+ Global flag for morphdom. +
+
+ +
+
+
+ + + + + diff --git a/site/examples/web-app-starter/js/u-query.js b/site/examples/web-app-starter/js/u-query.js new file mode 100755 index 0000000..907e8ba --- /dev/null +++ b/site/examples/web-app-starter/js/u-query.js @@ -0,0 +1,589 @@ +(function (root, factory) { + // this is the stupid UMD pattern, but eh we lost that battle a long time ago + if (typeof exports === 'object' && typeof module !== 'undefined') { + var exports_obj = factory(); + module.exports = exports_obj.$; + // Also export individual components + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + exports[key] = exports_obj[key]; + } + } + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + var exports_obj = factory(); + root.$ = exports_obj.$; + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + root[key] = exports_obj[key]; + } + } + } +}(typeof self !== 'undefined' ? self : this, function () { + +// a collection of DOM utility functions that I thought were cool in jQuery + +class EventEmitter { + constructor() { + /** Map> */ + this._topics = new Map(); + } + + /** + * Subscribe to `topic`. If `slot_key` is provided (truthy), + * it dedupes by that key; otherwise a unique Symbol is used. + * Returns an unsubscribe fn. + */ + on(topic, handler, slot_key = null) { + let map = this._topics.get(topic); + if (!map) { + map = new Map(); + this._topics.set(topic, map); + } + // use the provided slot_key or a fresh Symbol() + const key = slot_key != null ? slot_key : Symbol(); + map.set(key, handler); + return () => this.off(topic, key); + } + + /** + * Unsubscribe by handler function or by slot_key. + */ + off(topic, handlerOrSlotKey) { + const map = this._topics.get(topic); + if (!map) return; + + // if it matches a slotKey directly, remove it + if (map.has(handlerOrSlotKey)) { + map.delete(handlerOrSlotKey); + } else { + // otherwise assume it's a function: remove all matching fns + for (const [key, fn] of map.entries()) { + if (fn === handlerOrSlotKey) { + map.delete(key); + } + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + } + + /** + * Emit to all handlers on `topic`. Handlers returning + * 'remove_handler' are auto-removed. + * Returns the number of handlers invoked. + */ + emit(topic, ...args) { + let count = 0; + const map = this._topics.get(topic); + if (!map) return count; + + for (const [key, fn] of Array.from(map.entries())) { + const res = fn(...args); + count++; + if (res === 'remove_handler') { + map.delete(key); + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + return count; + } +} + +class QueryWrapper extends Array { + constructor(elements) { + super(); + if (elements) { + this.push(...(Array.isArray(elements) ? elements : [elements])); + } + } +} + +$ = function(selector_or_element) { + let elements; + if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) { + elements = [selector_or_element]; + } else if (typeof selector_or_element === 'string') { + // Check if string looks like HTML (starts with < and ends with >) + if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') { + // Create element from HTML string + let temp = document.createElement('div'); + temp.innerHTML = selector_or_element.trim(); + elements = Array.from(temp.children); + } else { + // Treat as CSS selector + elements = Array.from(document.querySelectorAll(selector_or_element)); + } + } else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) { + elements = Array.from(selector_or_element); + } else { + elements = [selector_or_element]; + } + return new QueryWrapper(elements); +}; + +$.options = { + alwaysDoDifferentialUpdate: true, +}; + +$.events = new EventEmitter(); +$.on = $.events.on.bind($.events); +$.off = $.events.off.bind($.events); +$.emit = $.events.emit.bind($.events); + +$.each = function(selector, callback) { + let elements; + if (typeof selector === 'string') { + elements = document.querySelectorAll(selector); + } else if (selector instanceof QueryWrapper) { + elements = selector; + } else { + elements = selector; + } + for (let i = 0; i < elements.length; i++) { + callback(elements[i], i); + } +} + +$.post = function(url, data, callback) { + return $.ajax({ + method: 'POST', + url: url, + data: data, + success: callback, + error: function(xhr) { + console.error('POST request failed:', xhr); + } + }); +} + +$.get = function(url, callback) { + return $.ajax({ + method: 'GET', + url: url, + success: callback, + error: function(xhr) { + console.error('GET request failed:', xhr); + } + }); +} + +$.ajax = function(options) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open(options.method || 'GET', options.url, true); + + // Set default headers + if (options.method === 'POST' || options.data) { + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + } + + // Set custom headers + if (options.headers) { + for (var key in options.headers) { + xhr.setRequestHeader(key, options.headers[key]); + } + } + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + var response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + // If JSON parsing fails, use raw response + response = xhr.responseText; + } + + if (xhr.status >= 200 && xhr.status < 300) { + if (options.success) options.success(response); + resolve(response); + } else { + if (options.error) options.error(xhr); + reject(xhr); + } + } + }; + + var data = null; + if (options.data) { + data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data); + } + + xhr.send(data); + }); +} + +$.ready = function(callback) { + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } +} + +// Helper function to execute scripts in HTML content +function executeScripts(htmlContent) { + // Create a temporary container to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // Find all script tags and disable them temporarily + const scripts = tempDiv.querySelectorAll('script'); + const scriptData = []; + + scripts.forEach((script) => { + // Store script data for later execution + scriptData.push({ + content: script.textContent || script.innerText || '', + src: script.src, + type: script.type, + nonce: script.nonce, + async: script.async, + defer: script.defer + }); + + // Disable the script by changing its type + script.type = 'text/disabled-script'; + }); + + // Return the modified HTML and script data + return { + html: tempDiv.innerHTML, + scripts: scriptData + }; +} + +// Helper function to execute a single script +function executeScript(scriptInfo) { + if (scriptInfo.src) { + // External script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + if (scriptInfo.async) newScript.async = scriptInfo.async; + if (scriptInfo.defer) newScript.defer = scriptInfo.defer; + newScript.src = scriptInfo.src; + + document.head.appendChild(newScript); + } else if (scriptInfo.content.trim()) { + // Inline script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + newScript.text = scriptInfo.content; + + // Execute by inserting and immediately removing + document.head.appendChild(newScript); + document.head.removeChild(newScript); + } +} + +// Add all the jQuery-like methods to QueryWrapper +QueryWrapper.prototype.parent = function() { + const parents = Array.from(this).map(el => el.parentNode).filter(el => el); + return new QueryWrapper(parents); +} + +QueryWrapper.prototype.load = function(url, opt = {}) { + return new Promise((resolve, reject) => { + const ajaxOptions = { + method: opt.method || (opt.postData ? 'POST' : 'GET'), + url: url, + success: (response) => { + if(opt.replace) + this.replaceWith(response, opt); + else + this.html(response, opt); + if (opt.onLoad) opt.onLoad(response, null); + resolve(this); + }, + error: (xhr) => { + if (opt.onError) opt.onError(xhr); + reject(new Error('Failed to load: ' + url)); + } + }; + + if (opt.postData) { + ajaxOptions.data = opt.postData; + } + + $.ajax(ajaxOptions); + }); +} + +QueryWrapper.prototype.html = function(opt_content = false, prop = {}) { + if (opt_content === false) { + return Array.from(this).map(el => el.innerHTML).join(''); + } + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + const temp = document.createElement('div'); + temp.innerHTML = opt_content; + this.forEach(el => { + morphdom(el, temp, { childrenOnly: true }); + }); + } else { + this.forEach(el => { + const result = executeScripts(opt_content); + el.innerHTML = result.html; + result.scripts.forEach(executeScript); + }); + } + return this; +} + +QueryWrapper.prototype.text = function(opt_content = false) { + if (opt_content === false) { + return Array.from(this).map(el => el.textContent).join(''); + } + this.forEach(el => { + el.textContent = opt_content; + }); + return this; +} + +QueryWrapper.prototype.replaceWith = function(content, prop = {}) { + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + this.forEach(el => { + morphdom(el, content); + }); + } else { + if(typeof content === 'string') { + this.forEach(el => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content; + el.parentNode.replaceChild(tempDiv.firstChild, el); + }); + } else if(content instanceof Element) { + this.forEach(el => { + el.parentNode.replaceChild(content.cloneNode(true), el); + }); + } + } + return this; +} + +QueryWrapper.prototype.query = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.find = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.on = function(event, handler) { + this.forEach(function(el) { + el.addEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.off = function(event, handler) { + this.forEach(function(el) { + el.removeEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.hide = function() { + return this.css({ display: 'none' }); +} + +QueryWrapper.prototype.show = function() { + return this.css({ display: '' }); +} + +QueryWrapper.prototype.toggle = function() { + this.forEach(function(el) { + el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none'; + }); + return this; +} + +QueryWrapper.prototype.attr = function(name, value) { + if (value === undefined) { + return this.length > 0 ? this[0].getAttribute(name) : null; + } + this.forEach(function(el) { + el.setAttribute(name, value); + }); + return this; +} + +QueryWrapper.prototype.addClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.add(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.removeClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.remove(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.toggleClass = function(classNameOrList, force) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + if (typeof force !== 'undefined') { + el.classList.toggle(className, force); + } else { + el.classList.toggle(className); + } + }); + }); + }); + return this; +} + +QueryWrapper.prototype.empty = function() { + this.forEach(function(el) { + el.innerHTML = ''; + }); + return this; +} + +QueryWrapper.prototype.css = function(styles, optOrValue) { + // If no arguments or first argument is a string (getter mode) + if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) { + if (this.length === 0) return undefined; + var el = this[0]; + if (typeof styles === 'string') { + // Get computed style for a specific property + return window.getComputedStyle(el)[styles]; + } else { + // Return computed styles object (not commonly used) + return window.getComputedStyle(el); + } + } + + // Setter mode + this.forEach(function(el) { + if (typeof styles === 'string' && arguments.length >= 2) { + // Single property setter: .css('prop', 'value') + el.style[styles] = optOrValue; + } else if (typeof styles === 'object') { + // Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'}) + for (var key in styles) { + el.style[key] = styles[key]; + } + } + }); + return this; +} + +QueryWrapper.prototype.each = function(callback) { + this.forEach(function(el, index) { + callback(el, index); + }); + return this; +} + +QueryWrapper.prototype.append = function(child_or_html) { + this.forEach(function(el) { + if (typeof child_or_html === 'string' && el.insertAdjacentHTML) { + if (child_or_html.includes(' max) return max; + return v; +} + +function pick_entry_from_range(array, value) { + if (!array) return {}; + let result = {}; + for (const [pv] of Object.entries(array)) { + if (value >= pv.from && value <= pv.to) result = pv; + } + return result; +} + +return { + $: $, + EventEmitter: EventEmitter, + QueryWrapper: QueryWrapper, + first: first, + clamp: clamp, + pick_entry_from_range: pick_entry_from_range +}; + +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-sortable-table.js b/site/examples/web-app-starter/js/u-sortable-table.js new file mode 100644 index 0000000..e9fee4f --- /dev/null +++ b/site/examples/web-app-starter/js/u-sortable-table.js @@ -0,0 +1,168 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.USortableTable = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getTable(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target.nodeType === 1 ? target : null; + } + + function getStorageKey(table, options) { + if (options && options.storageKey) return options.storageKey; + if (table && table.id) return `u-sortable-table.${table.id}`; + return null; + } + + function loadSort(table, options) { + const storageKey = getStorageKey(table, options); + if (!storageKey || !globalScope.localStorage) return null; + try { + const raw = globalScope.localStorage.getItem(storageKey); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (!parsed || !Number.isInteger(parsed.column) || (parsed.direction !== 'asc' && parsed.direction !== 'desc')) { + return null; + } + return parsed; + } catch (error) { + return null; + } + } + + function saveSort(table, column, direction, options) { + const storageKey = getStorageKey(table, options); + if (!storageKey || !globalScope.localStorage) return; + try { + globalScope.localStorage.setItem(storageKey, JSON.stringify({ column, direction })); + } catch (error) { + // Ignore storage errors. + } + } + + function parseSortValue(text) { + const normalized = String(text || '').trim(); + if (!normalized) return { type: 'text', value: '' }; + + if (globalScope.UFormat && typeof globalScope.UFormat.parseUnitNumber === 'function') { + const unitValue = globalScope.UFormat.parseUnitNumber(normalized); + if (unitValue != null) { + return { type: 'number', value: unitValue }; + } + } + + const timestamp = Date.parse(normalized); + if (!Number.isNaN(timestamp)) { + return { type: 'number', value: timestamp }; + } + + return { type: 'text', value: normalized.toLowerCase() }; + } + + function setHeaderState(table, columnIndex, direction) { + Array.from(table.querySelectorAll('thead th')).forEach((header, index) => { + header.classList.remove('sorted-asc', 'sorted-desc'); + header.setAttribute('aria-sort', 'none'); + if (index === columnIndex) { + header.classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc'); + header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending'); + } + }); + } + + function sortTable(table, columnIndex, direction, options, persist) { + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.children.length > 0 && !row.hasAttribute('data-empty-row')); + if (!rows.length) return; + + rows.sort((rowA, rowB) => { + const cellA = rowA.children[columnIndex]; + const cellB = rowB.children[columnIndex]; + const rawA = cellA ? (cellA.getAttribute('data-sort-value') || cellA.textContent) : ''; + const rawB = cellB ? (cellB.getAttribute('data-sort-value') || cellB.textContent) : ''; + const valueA = parseSortValue(rawA); + const valueB = parseSortValue(rawB); + + let comparison = 0; + if (valueA.type === 'number' && valueB.type === 'number') { + comparison = valueA.value - valueB.value; + } else { + comparison = String(valueA.value).localeCompare(String(valueB.value), undefined, { + numeric: true, + sensitivity: 'base', + }); + } + return direction === 'asc' ? comparison : -comparison; + }); + + rows.forEach((row) => tbody.appendChild(row)); + setHeaderState(table, columnIndex, direction); + if (persist !== false) { + saveSort(table, columnIndex, direction, options); + } + } + + function getInitialSort(table, options) { + const saved = loadSort(table, options); + if (saved) return saved; + if (options && options.initialSort) { + return { + column: Number(options.initialSort.column || 0), + direction: options.initialSort.direction === 'desc' ? 'desc' : 'asc', + }; + } + return null; + } + + function attachHeader(header, table, columnIndex, options) { + if (header.getAttribute('data-sortable') === 'false') return; + header.classList.add('sortable'); + header.tabIndex = 0; + header.addEventListener('click', function () { + const current = loadSort(table, options); + const nextDirection = current && current.column === columnIndex && current.direction === 'asc' ? 'desc' : 'asc'; + sortTable(table, columnIndex, nextDirection, options, true); + }); + header.addEventListener('keydown', function (event) { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + header.click(); + }); + } + + function init(target, options = {}) { + const table = getTable(target); + if (!table || table.dataset.sortableTableInit === '1') return table; + + Array.from(table.querySelectorAll('thead th')).forEach((header, index) => attachHeader(header, table, index, options)); + table.dataset.sortableTableInit = '1'; + + const initialSort = getInitialSort(table, options); + if (initialSort) { + sortTable(table, initialSort.column, initialSort.direction, options, false); + } + + return table; + } + + function initAll(selector = '.u-sortable-table') { + Array.from(document.querySelectorAll(selector)).forEach((table) => init(table)); + } + + return { + init, + initAll, + parseSortValue, + sortTable, + }; +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-tiling-viewport.css b/site/examples/web-app-starter/js/u-tiling-viewport.css new file mode 100644 index 0000000..63976c7 --- /dev/null +++ b/site/examples/web-app-starter/js/u-tiling-viewport.css @@ -0,0 +1,165 @@ +:root { + --vp-color-primary: rgba(255, 125, 55, 1); + --vp-color-primary-focus: rgba(255, 120, 60, 0.4); + --vp-background: rgba(0,0,0,0.2); + + --vp-color-black: #000; + --vp-color-gray: #666; + --vp-color-white: #fff; + --vp-space-s: 4px; + --vp-space-l: 6px; + + --vp-size-icon: 12px; + --vp-size-nav-height: 48px; + + --vp-opacity: 0.4; + --vp-opacity-active: 0.9; + + --vp-z-pane: 2; + --vp-z-toolbar: 3; + --vp-z-popup: 9999; + --vp-duration: 0.25s; + --vp-easing: ease; +} + +#fabric { + position: absolute; + top: var(--vp-size-nav-height); + left: 0; right: 0; bottom: 0; + display: flex; + overflow: hidden; +} + +.vp-split { + flex: 1 1 auto; + display: flex; + position: relative; + overflow: hidden; +} + +.vp-split.row { flex-direction: row; } +.vp-split.col { flex-direction: column; } + +.viewport-pane { + flex: 1 1 0; + position: relative; + border: var(--vp-space-s) solid var(--color-border); + background: var(--vp-background); + padding: var(--vp-space-l); + overflow: auto; + font-family: console; + transition: + border-color var(--vp-duration) var(--vp-easing), + box-shadow var(--vp-duration) var(--vp-easing), + background var(--vp-duration) var(--vp-easing); +} + +.vp-split > .viewport-pane { + transition: + flex var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); +} + +.vp-no-transition, +.vp-no-transition * { + transition: none !important; +} + +.viewport-pane:before { + content: attr(data-title); + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + opacity: var(--vp-opacity-medium); + pointer-events: none; +} + +.viewport-pane.focused { + border-color: var(--vp-color-primary-focus); + z-index: var(--vp-z-pane); +} + +.viewport-pane.focused:before { + opacity: var(--vp-opacity-active); + color: var(--color-highlight); +} + +.vp-divider { + background: var(--color-border); + opacity: var(--vp-opacity); + position: relative; + flex: 0 0 auto; +} + +.vp-divider.row { + width: var(--vp-space-l); + cursor: col-resize; +} + +.vp-divider.col { + height: var(--vp-space-l); + cursor: row-resize; +} + +.vp-divider:after { + content:''; + position: absolute; + inset: 0; + transition: + background var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); + background: var(--color-border); +} + +.vp-divider.row:after { + background: var(--color-border); +} + +.vp-divider:hover:after { + background: var(--color-highlight); + opacity: var(--vp-opacity); +} + +.vp-divider.dragging:after { + background: var(--color-highlight); + opacity: var(--vp-opacity-active); +} + +.vp-help-overlay { + position: absolute; + top: var(--vp-space-s); + left: var(--vp-space-s); + opacity: var(--vp-opacity); + pointer-events: none; +} + +.vp-pane-toolbar { + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + display: flex; + gap: var(--vp-space-s); + z-index: var(--vp-z-toolbar); +} + +.vp-pane-toolbar button { + padding: 0 var(--vp-space-l); + border: 1px solid var(--color-border); + background: var(--vp-background); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.vp-pane-toolbar button:hover { + background: var(--color-highlight); + color: var(--vp-color-black); + text-shadow: none; +} + +.vp-icon { + width: var(--vp-size-icon); + height: var(--vp-size-icon); + display: block; +} \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-tiling-viewport.js b/site/examples/web-app-starter/js/u-tiling-viewport.js new file mode 100644 index 0000000..3762033 --- /dev/null +++ b/site/examples/web-app-starter/js/u-tiling-viewport.js @@ -0,0 +1,388 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.TilingViewportManager = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + // Note: This module expects showPopupMenu to be available globally + // or you may need to adjust the dependency list above to include it + + let TilingViewportManager = function(container, options = {}) { + + let idCounter = 1, rootNode = null, focusedPane = null, containerEl = null; + let nextId = () => 'vp_' + (idCounter++); + let paneMeta = new Map(); + let pendingViewLoads = []; + let defaultoptions = { + minPaneSize: 100, + keyboard: true, + showHelp: true, + closeTransitionTimeout: 400, + toolbarButtons: ['split-v','split-h','views','close'], + standardViewList: [], + cssVars: { '--vp-duration': '.22s', '--vp-easing': 'ease' } + }; + options = Object.assign({}, defaultoptions, options); + + let createPane = (content, props = {}) => { + let id = nextId(); + let $el = $('
').attr('data-id', id); + if(props.title) $el.attr('data-title', props.title); + $el.html(content || `
Use Alt+<H V O X ↑ ← → ↓>
`) + .on('mousedown', () => focusPaneByEl($el[0])); + attachPaneToolbar($el[0]); + paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props}); + if(props.view?.screen) pendingViewLoads.push({id, view: props.view}); + return {type: 'pane', id, el: $el[0]}; + }; + + let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); }; + let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); }; + let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id'))); + let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r); + let getNodeById = id => findNodeById(rootNode, id) || null; + let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r); + let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc); + let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); }; + let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); }; + + let split = (direction, newPaneProps = {}) => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canSplit === false) return; + + let paneRect = focusedPane.el.getBoundingClientRect(); + let availableSpace = direction === 'row' ? paneRect.width : paneRect.height; + let requiredSpace = 2 * options.minPaneSize; + if(availableSpace < requiredSpace) { + //console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`); + return; + } + + let parentPath = findParentPath(rootNode, focusedPane.id); + let newPane = createPane(undefined, newPaneProps); + let animatedSplit = null, newIndex = -1; + if(!parentPath?.length) { + rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + animatedSplit = rootNode; newIndex = 1; + } else { + let parent = parentPath.at(-1); + if(parent.type === 'split' && parent.direction === direction) { + let idx = parent.children.indexOf(focusedPane); + parent.children.splice(idx+1, 0, newPane); + parent.sizes = parent.sizes || parent.children.map(()=>1); + parent.sizes.splice(idx+1, 0, 0); + renormalizeSizes(parent); + animatedSplit = parent; newIndex = idx+1; + } else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) { + parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0]; + animatedSplit = parent; newIndex = parent.children.length-1; + } else { + let idx = parent.children.indexOf(focusedPane); + let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + parent.children.splice(idx, 1, created); + animatedSplit = created; newIndex = 1; + } + } + render(); + if(animatedSplit && newIndex >= 0) { + requestAnimationFrame(() => requestAnimationFrame(() => { + animatedSplit.sizes = animatedSplit.children.map(()=>1); + renormalizeSizes(animatedSplit); + animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`)); + })); + } + focusPane(newPane); return newPane; + }; + + let splitVertical = () => split('row'); + let splitHorizontal = () => split('col'); + + let closeFocused = () => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canClose === false) return; + if(rootNode === focusedPane) { buildInitial(); return; } + if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id); + if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id); + let parentPath = findParentPath(rootNode, focusedPane.id); + if(!parentPath?.length) return; + let parent = parentPath.at(-1); + if(parent.type !== 'split') return; + let idx = parent.children.indexOf(focusedPane); + + let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice(); + let oldSize = oldSizes[idx] || 0; + let remaining = 1 - oldSize; + let newSizes = oldSizes.slice(); + if(remaining > 0) { + for(let i=0;i ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`)); + if(focusedPane.el) { + focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease'; + focusedPane.el.style.opacity = '0'; + } + let done = false; + let cleanup = () => { + if(done) return; done = true; + parent.children.splice(idx,1); + parent.sizes?.splice(idx,1); + paneMeta.delete(focusedPane.id); + if(parent.children.length === 1) { + let only = parent.children[0]; + parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only); + } else if(parent.sizes) renormalizeSizes(parent); + render(); focusPane(paneList(rootNode)[0]); + }; + let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } }; + if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd); + setTimeout(() => cleanup(), 350); + }; + + let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; }; + let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); }; + let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); }; + + let serialize = (node=rootNode) => { + let ser = n => !n ? null : n.type==='pane' ? + {type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} : + {type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)}; + return {focus: focusedPane?.id, layout: ser(node)}; + }; + + let restore = data => { + if(!data) return; + let wrapper = data.layout ? data : {layout:data}; + paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0; + let idNums = []; + let build = l => l && l.type==='pane' ? + (() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view}); + if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); } + let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); } + return pane; })() : + l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null; + rootNode = build(wrapper.layout) || createPane(); + if(idNums.length) idCounter = Math.max(...idNums) + 1; + render(); + pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0; + focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]); + }; + + let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); }; + + let renderNode = node => { + if(node.type==='pane') return node.el; + node.el = node.el || $('
')[0]; + $(node.el).empty().addClass(`vp-split ${node.direction}`); + if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); } + node.children.forEach((ch,i) => { + let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl); + if(i < node.children.length-1) { let div = $('
').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); } + }); + return node.el; + }; + + let setupDivider = (div, splitNode, leftIdx) => { + $(div).on('mousedown', e => { + e.preventDefault(); $(div).addClass('dragging'); + $(containerEl).addClass('vp-no-transition'); + let isRow = splitNode.direction === 'row'; + let startPos = isRow ? e.clientX : e.clientY; + let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect()); + let prop = isRow ? 'width' : 'height'; + let startPixels = childRects.map(r => r[prop]); + let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]]; + let totalPixels = startPixels.reduce((a,b)=>a+b,0); + let onMove = ev => { + let curPos = isRow ? ev.clientX : ev.clientY; + let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos]; + if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize]; + if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize]; + startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx; + splitNode.sizes = startPixels.map(p => p / totalPixels); + splitNode.children.forEach((ch,i)=> { + if(ch.el) { + let flexValue = `${splitNode.sizes[i]} 1 0`; + $(ch.el).css('flex', flexValue); + } + }); + }; + let onUp = () => { + $(div).removeClass('dragging'); + $(containerEl).removeClass('vp-no-transition'); + $(document).off('mousemove', onMove).off('mouseup', onUp); + }; + $(document).on('mousemove', onMove).on('mouseup', onUp); + }); + $(div).on('dblclick', () => { + $(containerEl).addClass('vp-no-transition'); + let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len); + splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`)); + setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0); + }); + }; + + let attachPaneToolbar = el => { + let bar = $('
').addClass('vp-pane-toolbar').html(` + + + + `)[0]; + el.appendChild(bar); + $(bar).on('mousedown', ev => ev.stopPropagation()); + $(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el); + let button = ev.target.closest('button[data-act]'); + let act = button ? button.getAttribute('data-act') : null; + if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused(); + else if(act==='views') { + // compute button position + let rect = button.getBoundingClientRect(); + let items = options.standardViewList.slice(); + showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => { + // support string or object {screen} + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(el.dataset.id, view); + }, container: document.body, emptyText: 'No views available' }); + } + }); + refreshToolbarState(el.dataset.id); + }; + + let refreshToolbarState = paneId => { + let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; + if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return; + let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false]; + ['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } }); + let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); } + }; + + let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props); + if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; } + if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload; + paneMeta.set(paneId, meta); refreshToolbarState(paneId); }; + let getPaneProps = paneId => paneMeta.get(paneId); + + let focusDirection = (dx, dy) => { + if(!focusedPane) return; + let panes = Array.from(containerEl.querySelectorAll('.viewport-pane')); + let cur = focusedPane.el.getBoundingClientRect(); + let candidates = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); + if(dx) { + if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null; + let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top)); + let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.height - overlap)}; + } + if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null; + let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left)); + let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.width - overlap)}; + }).filter(Boolean); + if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el); + let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2]; + let fallback = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2]; + let [vx, vy] = [px-cx, py-cy]; + if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null; + return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))}; + }).filter(Boolean); + if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el); + }; + + let injectHelpOverlay = () => { + if(!options.showHelp) return; + if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``})); + }; + + let getPaneEl = id => findNodeById(rootNode, id)?.el || null; + + let openViewsPopupForPane = (paneId) => { + let node = findNodeById(rootNode, paneId || focusedPane?.id); + if(!node || !node.el) return; + let rect = node.el.getBoundingClientRect(); + let x = Math.max(8, rect.right - 80); + let y = rect.top + 24; + let items = options.standardViewList.slice(); + showPopupMenu(x, y, items, { onSelect: it => { + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(node.id, view); + }, container: document.body, emptyText: 'No views available' }); + }; + + let loadView = (paneId, view) => { + let node = findNodeById(rootNode, paneId); + if(!node || !node.el) return; // nothing to load into + let meta = paneMeta.get(paneId); + try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); } + if(meta) meta.view = view; + node.el.innerHTML = `
Loading ${view.screen||'...'}...
`; + $(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})}); + }; + + let handleKeys = e => { + if(!e.altKey || !options.keyboard) return; + let actions = { + 'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(), + 'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(), + 'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(), + 'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(), + 'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext() + }; + if(actions[e.code] || actions[e.key]) { + (actions[e.code] || actions[e.key])(); + e.preventDefault(); + } + }; + + containerEl = typeof container === 'string' ? document.querySelector(container) : container; + if(!containerEl) throw new Error('TilingViewportManager: container not found'); + if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k])); + buildInitial(); + if(options.keyboard) document.addEventListener('keydown', handleKeys); + + this.splitVertical = splitVertical; + this.splitHorizontal = splitHorizontal; + this.closeFocused = closeFocused; + this.focusNext = focusNext; + this.focusPrev = focusPrev; + this.serialize = serialize; + this.restore = restore; + this.setPaneProps = setPaneProps; + this.getPaneProps = getPaneProps; + this.options = options; + this.loadView = loadView; + this.getPaneEl = getPaneEl; + this.getNodeById = getNodeById; + + Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) }); + Object.defineProperty(this, 'focused', { get: () => focusedPane?.id }); + }; + + return TilingViewportManager; +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-timeseries-chart.js b/site/examples/web-app-starter/js/u-timeseries-chart.js new file mode 100644 index 0000000..b19f3eb --- /dev/null +++ b/site/examples/web-app-starter/js/u-timeseries-chart.js @@ -0,0 +1,354 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UTimeSeriesChart = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + class TimeSeriesChart { + constructor(canvasOrId, options = {}) { + this.canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId; + if (!this.canvas || !this.canvas.getContext) { + throw new Error('UTimeSeriesChart requires a canvas element'); + } + + this.ctx = this.canvas.getContext('2d'); + const theme = this.resolveThemeColors(); + this.options = Object.assign({ + xAxisLabel: 'Time', + yAxisLeftLabel: 'Value', + yAxisRightLabel: '', + yAxisLeftFormat: 'number', + yAxisRightFormat: 'number', + padding: { top: 18, right: 62, bottom: 34, left: 58 }, + gridColor: theme.gridColor, + axisColor: theme.axisColor, + textColor: theme.textColor, + legendBackground: theme.legendBackground, + legendTextColor: theme.legendTextColor, + tooltipBackground: theme.tooltipBackground, + tooltipBorder: theme.tooltipBorder, + tooltipTitleColor: theme.tooltipTitleColor, + tooltipTextColor: theme.tooltipTextColor, + hoverLineColor: 'rgba(255,255,255,0.35)', + }, options); + + this.series = []; + this.xLabels = []; + this.hoverIndex = null; + this.hoverX = 0; + this.width = 0; + this.height = 0; + + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + this.onResize = this.onResize.bind(this); + + this.canvas.addEventListener('mousemove', this.onMouseMove); + this.canvas.addEventListener('mouseleave', this.onMouseLeave); + globalScope.addEventListener('resize', this.onResize); + if (typeof ResizeObserver !== 'undefined') { + this.resizeObserver = new ResizeObserver(this.onResize); + this.resizeObserver.observe(this.canvas); + } + + this.resize(); + } + + resolveThemeColors() { + const styles = globalScope.getComputedStyle ? globalScope.getComputedStyle(document.documentElement) : null; + const pick = function (name, fallback) { + if (!styles) return fallback; + const value = styles.getPropertyValue(name).trim(); + return value || fallback; + }; + return { + gridColor: pick('--border', 'rgba(148, 163, 184, 0.20)'), + axisColor: pick('--border-hover', pick('--border', 'rgba(148, 163, 184, 0.42)')), + textColor: pick('--text-secondary', '#64748b'), + legendBackground: pick('--surface', 'rgba(15, 23, 42, 0.72)'), + legendTextColor: pick('--text-primary', '#0f172a'), + tooltipBackground: pick('--surface', '#0f172a'), + tooltipBorder: pick('--border', 'rgba(148, 163, 184, 0.30)'), + tooltipTitleColor: pick('--primary', '#3b82f6'), + tooltipTextColor: pick('--text-primary', '#0f172a'), + }; + } + + destroy() { + this.canvas.removeEventListener('mousemove', this.onMouseMove); + this.canvas.removeEventListener('mouseleave', this.onMouseLeave); + globalScope.removeEventListener('resize', this.onResize); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + } + + onResize() { + this.resize(); + } + + onMouseLeave() { + this.hoverIndex = null; + this.draw(); + } + + resize() { + const rect = this.canvas.getBoundingClientRect(); + const cssWidth = Math.max(280, Math.round(rect.width || this.canvas.width || 640)); + const cssHeight = Math.max(180, Math.round(rect.height || this.canvas.height || 320)); + const pixelRatio = globalScope.devicePixelRatio || 1; + + this.width = cssWidth; + this.height = cssHeight; + this.canvas.width = Math.round(cssWidth * pixelRatio); + this.canvas.height = Math.round(cssHeight * pixelRatio); + this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + this.draw(); + } + + setData(series, xLabels = []) { + this.series = (Array.isArray(series) ? series : []).map((item) => ({ + key: item.key || '', + label: item.label || 'Series', + color: item.color || '#60a5fa', + axis: item.axis === 'right' ? 'right' : 'left', + unit: item.unit || '', + format: item.format || '', + maxHint: Number(item.maxHint || 0), + decimals: Number.isInteger(item.decimals) ? item.decimals : 2, + values: Array.isArray(item.values) ? item.values.map((value) => Number(value || 0)) : [], + })); + this.xLabels = Array.isArray(xLabels) ? xLabels : []; + this.draw(); + } + + getPlotRect() { + const padding = this.options.padding; + return { + x: padding.left, + y: padding.top, + w: this.width - padding.left - padding.right, + h: this.height - padding.top - padding.bottom, + }; + } + + getMaxForAxis(axis) { + const relevant = this.series.filter((item) => item.axis === axis); + if (!relevant.length) return 1; + let max = 1; + relevant.forEach((item) => { + const localMax = Math.max(...(item.values.length ? item.values : [0]), item.maxHint || 0, 1); + if (localMax > max) max = localMax; + }); + return max; + } + + xForIndex(index, pointCount, plot) { + if (pointCount <= 1) return plot.x; + return plot.x + (index / (pointCount - 1)) * plot.w; + } + + yForValue(value, axisMax, plot) { + const clamped = Math.max(0, Number(value || 0)); + return plot.y + plot.h - ((clamped / axisMax) * plot.h); + } + + formatValue(value, format, unit, decimals) { + const number = Number(value || 0); + if (globalScope.UFormat) { + if (format === 'bytes' && globalScope.UFormat.formatBytes) return globalScope.UFormat.formatBytes(number); + if (format === 'disk-bytes' && globalScope.UFormat.formatDiskBytes) return globalScope.UFormat.formatDiskBytes(number); + if (format === 'count' && globalScope.UFormat.formatCount) return globalScope.UFormat.formatCount(number); + if (format === 'duration-ms' && globalScope.UFormat.formatDurationMs) return globalScope.UFormat.formatDurationMs(number); + } + return `${number.toFixed(decimals)}${unit || ''}`; + } + + drawGridAndAxes(plot, maxLeft, maxRight) { + const ctx = this.ctx; + ctx.strokeStyle = this.options.gridColor; + ctx.lineWidth = 1; + + for (let index = 0; index <= 4; index += 1) { + const y = plot.y + (plot.h / 4) * index; + ctx.beginPath(); + ctx.moveTo(plot.x, y); + ctx.lineTo(plot.x + plot.w, y); + ctx.stroke(); + + const leftValue = maxLeft - ((maxLeft / 4) * index); + const leftTick = this.formatValue(leftValue, this.options.yAxisLeftFormat, '', 1); + ctx.fillStyle = this.options.textColor; + ctx.font = '11px Inter, sans-serif'; + const leftWidth = ctx.measureText(leftTick).width; + ctx.fillText(leftTick, Math.max(4, plot.x - leftWidth - 8), y + 4); + + if (this.options.yAxisRightLabel) { + const rightValue = maxRight - ((maxRight / 4) * index); + const rightTick = this.formatValue(rightValue, this.options.yAxisRightFormat, '', 1); + ctx.fillText(rightTick, plot.x + plot.w + 8, y + 4); + } + } + + ctx.strokeStyle = this.options.axisColor; + ctx.beginPath(); + ctx.moveTo(plot.x, plot.y); + ctx.lineTo(plot.x, plot.y + plot.h); + ctx.lineTo(plot.x + plot.w, plot.y + plot.h); + ctx.stroke(); + + ctx.fillStyle = this.options.textColor; + ctx.font = '12px Inter, sans-serif'; + ctx.save(); + ctx.translate(14, plot.y + (plot.h / 2)); + ctx.rotate(-Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.yAxisLeftLabel, 0, 0); + ctx.restore(); + + if (this.options.yAxisRightLabel) { + ctx.save(); + ctx.translate(this.width - 14, plot.y + (plot.h / 2)); + ctx.rotate(Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.yAxisRightLabel, 0, 0); + ctx.restore(); + } + + const xLabel = this.options.xAxisLabel; + const xLabelWidth = ctx.measureText(xLabel).width; + ctx.fillText(xLabel, plot.x + plot.w - xLabelWidth, this.height - 10); + } + + drawSeries(plot, maxLeft, maxRight) { + const ctx = this.ctx; + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) return; + + this.series.forEach((item) => { + if (!item.values.length) return; + const axisMax = item.axis === 'right' ? maxRight : maxLeft; + ctx.beginPath(); + item.values.forEach((value, index) => { + const x = this.xForIndex(index, item.values.length, plot); + const y = this.yForValue(value, axisMax, plot); + if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); + }); + ctx.strokeStyle = item.color; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + if (this.series.length > 1) { + ctx.fillStyle = this.options.legendBackground; + ctx.fillRect(plot.x + 4, plot.y + 4, Math.min(plot.w - 8, 320), 24); + let offsetX = plot.x + 12; + this.series.forEach((item) => { + ctx.fillStyle = item.color; + ctx.fillRect(offsetX, plot.y + 13, 14, 3); + offsetX += 20; + ctx.fillStyle = this.options.legendTextColor; + ctx.font = '11px Inter, sans-serif'; + ctx.fillText(item.label, offsetX, plot.y + 17); + offsetX += ctx.measureText(item.label).width + 18; + }); + } + } + + drawHover(plot, maxLeft, maxRight) { + if (this.hoverIndex == null) return; + const ctx = this.ctx; + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) return; + + const index = Math.max(0, Math.min(pointCount - 1, this.hoverIndex)); + const x = this.xForIndex(index, pointCount, plot); + + ctx.strokeStyle = this.options.hoverLineColor; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, plot.y); + ctx.lineTo(x, plot.y + plot.h); + ctx.stroke(); + + const lines = []; + lines.push(this.xLabels[index] || `Sample ${index + 1}`); + + this.series.forEach((item) => { + if (index >= item.values.length) return; + const axisMax = item.axis === 'right' ? maxRight : maxLeft; + const value = item.values[index] ?? 0; + const y = this.yForValue(value, axisMax, plot); + ctx.fillStyle = item.color; + ctx.beginPath(); + ctx.arc(x, y, 3.5, 0, Math.PI * 2); + ctx.fill(); + lines.push(`${item.label}: ${this.formatValue(value, item.format || (item.axis === 'right' ? this.options.yAxisRightFormat : this.options.yAxisLeftFormat), item.unit || '', item.decimals)}`); + }); + + ctx.font = '11px Inter, sans-serif'; + const padding = 8; + const lineHeight = 14; + const tooltipWidth = Math.max(...lines.map((line) => ctx.measureText(line).width)) + padding * 2; + const tooltipHeight = lines.length * lineHeight + padding * 2 - 2; + let tooltipX = this.hoverX + 12; + let tooltipY = plot.y + 8; + + if (tooltipX + tooltipWidth > this.width - 4) tooltipX = this.hoverX - tooltipWidth - 12; + if (tooltipX < 4) tooltipX = 4; + if (tooltipY + tooltipHeight > this.height - 4) tooltipY = this.height - tooltipHeight - 4; + + ctx.fillStyle = this.options.tooltipBackground; + ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight); + ctx.strokeStyle = this.options.tooltipBorder; + ctx.strokeRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight); + + lines.forEach((line, lineIndex) => { + ctx.fillStyle = lineIndex === 0 ? this.options.tooltipTitleColor : this.options.tooltipTextColor; + ctx.fillText(line, tooltipX + padding, tooltipY + padding + 10 + lineIndex * lineHeight); + }); + } + + onMouseMove(event) { + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + this.hoverX = x; + + const plot = this.getPlotRect(); + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) { + this.hoverIndex = null; + this.draw(); + return; + } + + const relative = Math.max(0, Math.min(plot.w, x - plot.x)); + this.hoverIndex = pointCount <= 1 ? 0 : Math.round((relative / plot.w) * (pointCount - 1)); + this.draw(); + } + + draw() { + const ctx = this.ctx; + ctx.clearRect(0, 0, this.width, this.height); + + const plot = this.getPlotRect(); + if (plot.w <= 0 || plot.h <= 0) return; + const maxLeft = this.getMaxForAxis('left'); + const maxRight = this.getMaxForAxis('right'); + + this.drawGridAndAxes(plot, maxLeft, maxRight); + this.drawSeries(plot, maxLeft, maxRight); + this.drawHover(plot, maxLeft, maxRight); + } + } + + return TimeSeriesChart; +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-workspace-shell.js b/site/examples/web-app-starter/js/u-workspace-shell.js new file mode 100644 index 0000000..90fbd31 --- /dev/null +++ b/site/examples/web-app-starter/js/u-workspace-shell.js @@ -0,0 +1,68 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UWorkspaceShell = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getElement(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target && target.nodeType === 1 ? target : null; + } + + function bindShell(options) { + const sidebar = getElement(options.sidebarId || options.sidebar); + const overlay = getElement(options.overlayId || options.overlay); + const toggle = getElement(options.toggleButtonId || options.toggle); + const closeOnNav = options.closeOnNav !== false; + + if (!sidebar || !overlay) return null; + + function setOpen(open) { + sidebar.classList.toggle('is-open', !!open); + overlay.classList.toggle('is-open', !!open); + } + + function toggleOpen() { + setOpen(!sidebar.classList.contains('is-open')); + } + + if (toggle) { + toggle.addEventListener('click', toggleOpen); + } + + overlay.addEventListener('click', function () { + setOpen(false); + }); + + if (closeOnNav) { + sidebar.querySelectorAll('a').forEach(function (link) { + link.addEventListener('click', function () { + setOpen(false); + }); + }); + } + + globalScope.addEventListener('resize', function () { + if (globalScope.innerWidth > 860) { + setOpen(false); + } + }); + + return { + open: function () { setOpen(true); }, + close: function () { setOpen(false); }, + toggle: toggleOpen, + }; + } + + return { + init: bindShell, + }; +})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-wsconnection.js b/site/examples/web-app-starter/js/u-wsconnection.js new file mode 100755 index 0000000..9c3b40f --- /dev/null +++ b/site/examples/web-app-starter/js/u-wsconnection.js @@ -0,0 +1,111 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.Connection = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + +var Connection = { + + auto_reconnect : false, + established : false, + + update_indicator : (status) => { + var status_colors = { + 'offline' : 'red', + 'online' : 'lightgreen', + 'error' : 'DarkOrange', + }; + $('#connection-status').text(status).css('color', status_colors[status] || 'gray'); + }, + + debug : true, + auto_reconnect : true, + cmd_waiting_rels : {}, + + server_url : '', + + init : () => { + new EventSystem(Connection); + }, + + start : (url = null) => { + + if(url) Connection.server_url = url; + + Connection.update_indicator('offline'); + if(Connection.socket) Connection.socket.close(); + Connection.socket = new WebSocket(Connection.server_url); + Connection.socket.onmessage = function(rawmsg) { + var msg = JSON.parse(rawmsg.data); + if(Connection.debug) console.log('CONNECTION MSG', msg); + if(msg.type) Connection.trigger(msg.type, msg); + Connection.trigger('message', msg); + } + Connection.socket.onclose = function() { + Connection.update_indicator('offline'); + if(Connection.debug) console.log('CONNECTION CLOSED'); + Connection.established = false; + Connection.trigger('close', {}); + } + Connection.socket.onerror = function(error) { + Connection.update_indicator('error'); + console.error('CONNECTION', error); + Connection.established = false; + } + Connection.socket.onopen = function() { + Connection.update_indicator('online'); + if(Connection.debug) console.log('CONNECTION ESTABLISHED'); + Connection.established = true; + Connection.trigger('open', {}); + } + setTimeout(Connection.reconnect, 2000); + + }, + + deauth : () => { + Game.session = {}; + }, + + reconnect : () => { + if(!Connection.established && Connection.auto_reconnect) + Connection.start(); + else + setTimeout(Connection.reconnect, 2000); + }, + + queue : [], + + dequeue : () => { + var dq = Connection.queue; + Connection.queue = []; + dq.forEach(function(fm) { + Connection.send(fm); + }); + }, + + send : (msg) => { + if(Connection.established) { + if(typeof msg == 'function') + msg(); + else + Connection.socket.send(JSON.stringify(msg)); + } else { + Connection.queue.push(msg); + } + }, + + close : () => { + Connection.auto_reconnect = false; + Connection.socket.close(); + }, + +} + +return Connection; + +})); + diff --git a/site/examples/web-app-starter/lib/components.php b/site/examples/web-app-starter/lib/components.php new file mode 100755 index 0000000..d1179e4 --- /dev/null +++ b/site/examples/web-app-starter/lib/components.php @@ -0,0 +1,199 @@ + function($prop, &$context) { + $context['current_component_id'] = $prop['id']; + return('
'); + }, + + 'end' => function($prop) { + ?>'); + }, + + 'render' => function($prop, &$context) { + return($context['begin']($prop, $context) . $context['end']($prop)); + }, + + 'about' => 'This is an example component that wraps content in a div and logs when it is finalized.', + + ]; */ + + $GLOBALS['render_funcs'] = array(); + $GLOBALS['id_counter'] = $GLOBALS['id_counter'] ?? 1; + + function component_error_banner($s) + { + return ''; + } + + function component_caller_dir() + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + return isset($trace[1]['file']) ? dirname($trace[1]['file']) : getcwd(); + } + + function component_normalize_name($file_name) + { + return preg_replace('/\.php$/', '', trim((string)$file_name)); + } + + function component_resolve_file($file_name, $search_path = false) + { + $component_name = component_normalize_name($file_name); + $candidates = array($component_name.'.php'); + if($search_path && !str_starts_with($component_name, 'components/')) + $candidates[] = rtrim($search_path, '/').'/'.$component_name.'.php'; + if(!str_starts_with($component_name, 'components/')) + $candidates[] = 'components/'.$component_name.'.php'; + foreach(array_unique($candidates) as $candidate) + { + if(file_exists($candidate)) + return $candidate; + } + return false; + } + + /** + * Loads a component from file system and registers it in the global registry + * + * Component files are searched in this order: + * 1. exact file path passed to the loader + * 2. caller-relative path (for shorthand component names) + * 3. components/{component_name}.php + * + * Component files should return an array with render functions like: + * return ['render' => function($prop) { ... }]; + */ + function component_get_func($file_name, $return_false_if_not_found = false, $search_path = false) + { + $component_name = component_normalize_name($file_name); + $result = []; + // Check if component is already loaded in global registry + if(!isset($GLOBALS['render_funcs'][$component_name])) + { + $component_file = component_resolve_file($component_name, $search_path); + if($component_file) + { + $result['component_file'] = $component_file; + } + else + { + // Component file not found - create error component + if($return_false_if_not_found) return(false); + $result['component_file'] = false; + $result['error'] = 'Component not found: '.$component_name; + $result['render'] = function() use($component_name) { + return component_error_banner('component not found: '.$component_name); + }; + } + if($result['component_file']) + foreach(require($result['component_file']) as $k => $v) + $result[$k] = $v; + // Register component in global registry for future use + $GLOBALS['render_funcs'][$component_name] = $result; + } + return($GLOBALS['render_funcs'][$component_name]); + } + +/** + * Checks if a component exists without loading it + * Returns true if component file can be found, false otherwise + */ +function component_exists($file_name) +{ + $caller_dir = component_caller_dir(); + return(component_get_func($file_name, true, $caller_dir) !== false); +} + +/** + * Loads a component and returns its definition + * Same as component_get_func but with caller directory detection + */ +function component_load($file_name) +{ + $caller_dir = component_caller_dir(); + return(component_get_func($file_name, true, $caller_dir)); +} + +function component_call($file_name, $render_call, $prop = array()) +{ + $prop['render_call'] = $render_call; + return component($file_name, $prop); +} + +/** + * Declares an inline component directly in code (not from file) + * + * @param string $file_name - Component name for registry + * @param array $render_prop - Component definition with render functions + * + * Example: + * component_declare('my-button', [ + * 'render' => function($prop) { return ""; } + * ]); + */ +function component_declare($file_name, $render_prop) +{ + // Register the inline component directly in global registry + $GLOBALS['render_funcs'][$file_name] = $render_prop; +} + +/** + * Main component rendering function - the heart of the component system + * + * @param string $file_name - Component name or path + * @param array $prop - Properties/data to pass to component + * @return mixed - Component output from the selected render method + * + * Component calling examples: + * component('components/example/theme-switcher') + * component_call('components/workspace/panel', 'render', ['title' => 'Status']) + * component('workspace/panel', ['render_call' => 'render']) + */ +function component($file_name, $prop = array()) +{ + Profiler::log('component '.$file_name, 1); + $caller_dir = component_caller_dir(); + if($file_name == '') + return(component_error_banner('[component name empty]')); + $file_name = component_normalize_name($file_name); + + // Default render method is 'render', but can be overridden + $prop['render_call'] = first($prop['render_call'] ?? false, 'render'); + + // Support for calling specific render methods: 'component:method' + if(stristr($file_name, ':')) // you can specify which render function to call + { + $prop['render_call'] = $file_name; + $file_name = nibble(':', $prop['render_call']); + } + + // Get component definition from registry (load if not already loaded) + $renderer = $GLOBALS['render_funcs'][$file_name] ?? false; + if(!$renderer) + { + // Component not in registry - try to load from file + component_get_func($file_name, false, $caller_dir); + $renderer = $GLOBALS['render_funcs'][$file_name] ?? false; + } + + // Generate unique ID for component instance if not provided + $prop['id'] = !empty($prop['id']) ? $prop['id'] : 'c'.($GLOBALS['id_counter']++); + $prop['filename'] = $file_name; + + if(isset($renderer[$prop['render_call']]) && is_callable($renderer[$prop['render_call']])) + $result = ($renderer[$prop['render_call']]($prop, $renderer)); + else if(isset($renderer[$prop['render_call']])) + $result = ($renderer[$prop['render_call']]); + else + $result = component_error_banner('component render method not found: '.$file_name.':'.$prop['render_call']); + + Profiler::log(false, -1); + return($result); +} + diff --git a/site/examples/web-app-starter/lib/db.class.php b/site/examples/web-app-starter/lib/db.class.php new file mode 100755 index 0000000..f12d80d --- /dev/null +++ b/site/examples/web-app-starter/lib/db.class.php @@ -0,0 +1,381 @@ + array(), 'info' => array()); + foreach(self::Get('SHOW FULL COLUMNS FROM #'.$table) as $fld) + { + $ds = array(); + foreach($fld as $k => $v) + { + $k = strtolower($k); + if($k == 'comment') + { + $p = explode(',', $v); + $v = false; + if(sizeof($p) > 0) foreach($p as $pi) + { + $pk = trim(nibble('=', $pi)); + $ds['_'.$pk] = trim($pi); + } + } + if($v) + $ds[$k] = $v; + } + $ds['caption'] = first($ds['_title'], $ds['field']); + $result['fields'][$ds['field']] = $ds; + } + return($result); + } + + # updates/creates the $dataset in the $tablename + static function Insert($tablename, $dataset) + { + self::$writeOps++; + $query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset). + ') VALUES('.DB::MakeValuesList($dataset).')'; + + mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).'{ '.$query.' }'); + self::$affectedRows += mysqli_affected_rows(self::$link); + return(mysqli_insert_id(self::$link)); + } + + # updates/creates the $dataset in the $tablename + static function Commit($tablename, &$dataset) + { + self::$writeOps++; + $keynames = self::Keys($tablename); + $keyname = $keynames[0]; + $keyvalue = $dataset[$keyname]; + Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') start'); + + $cache_entry = $tablename.':'.$keyname.':'.$keyvalue; + unset(self::$dataCache[$cache_entry]); + + $query='REPLACE INTO '.$tablename.' ('.DB::MakeNamesList($dataset). + ') VALUES('.DB::MakeValuesList($dataset).');'; + + # keeping this around just in case, but performance seems the same: + # $query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset). + # ') VALUES('.DB::MakeValuesList($dataset).') + # ON DUPLICATE KEY UPDATE '.DB::MakeSetList($dataset).';'; + + mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { '.$query.' }'); + $dataset[$keyname] = first($dataset[$keyname], mysqli_insert_id(self::$link)); + self::$dataCache[$cache_entry] = $dataset; + + Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') done'); + return($dataset[$keyname]); + } + + static function GetRowsMatch($table, $matchOptions, $fillIfEmpty = true) + { + self::$readOps++; + $where = array('1'); + foreach($matchOptions as $k => $v) + $where[] = '('.$k.'="'.DB::Safe($v).'")'; + $iwhere = implode(' AND ', $where); + $query = 'SELECT * FROM '.($table). + ' WHERE '.$iwhere; + $resultDS = self::GetRowWithQuery($query); + if ($fillIfEmpty && sizeof($resultDS) == 0) + foreach($matchOptions as $k => $v) + $resultDS[$k] = $v; + Profiler::Log('DB::GetRowsMatch('.$table.') done'); + return($resultDS); + } + + # from table $tablename, get dataset with key $keyvalue + static function GetRow($tablename, $keyvalue, $keyname = '', $options = array()) + { + if($keyvalue == '0') return(array()); + $fields = @$options['fields']; + $fields = first($fields, '*'); + if (!self::$link) return(array()); + + if ($keyname == '') + { + $keynames = self::Keys($tablename); + $keyname = $keynames[0]; + } + + $cache_entry = $tablename.':'.$keyname.':'.$keyvalue; + if(isset(self::$dataCache[$cache_entry])) return(self::$dataCache[$cache_entry]); + + $join = isset($options['join']) ? ' '.$options['join'] : ''; + $query = 'SELECT '.$fields.' FROM '.$tablename.$join.' WHERE '.$keyname.'="'.DB::Safe($keyvalue).'";'; + $queryResult = mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { Query: "'.$query.'" }'); + self::$readOps++; + + if ($line = @mysqli_fetch_array($queryResult, MYSQLI_ASSOC)) + { + mysqli_free_result($queryResult); + self::$dataCache[$cache_entry] = $line; + Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.')'); + return($line); + } + else + $result = array(); + + Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.') #fail'); + return($result); + } + + static function RemoveRow($tablename, $keyvalue, $keyname = null) + { + if ($keyname == null) + { + $keynames = self::Keys($tablename); + $keyname = $keynames[0]; + } + $res = (mysqli_query(self::$link, 'DELETE FROM '.$tablename.' WHERE '.$keyname.'="'. + DB::Safe($keyvalue).'";') + or critical(' Cannot remove dataset // '.mysqli_error(self::$link))); + Profiler::Log('DB::RemoveRow('.$tablename.', '.$keyvalue.') done'); + self::$affectedRows += mysqli_affected_rows(self::$link); + self::$writeOps++; + return($res); + } + + // retrieve dataset identified by SQL $query + static function GetRowWithQuery($query, $parameters = null) + { + $query = self::ParseQueryParams($query, $parameters); + + $queryResult = mysqli_query(self::$link, $query); + + if(!$queryResult) + return(critical('Error getting data // '.mysqli_error(self::$link).'{ '.$query.' }')); + + if ($line = mysqli_fetch_array($queryResult, MYSQLI_ASSOC)) + { + $result = $line; + mysqli_free_result($queryResult); + } + else + $result = array(); + + Profiler::Log('DB::GetRowWithQuery('.$query.')'); + self::$readOps++; + return($result); + } + + # execute a simple update $query + static function Query($query, $parameters = null) + { + $query = self::parseQueryParams($query, $parameters); + if (substr($query, -1, 1) == ';') + $query = substr($query, 0, -1); + $result = (mysqli_query(self::$link, $query) + or critical(' Query error // '.mysqli_error(self::$link))); + Profiler::Log('DB::Query('.$query.') done'); + self::$affectedRows += mysqli_affected_rows(self::$link); + self::$writeOps++; + return($result); + } + + # create a comma-separated list of keys in $dataset + static function MakeNamesList(&$dataset) + { + $result = ''; + if (sizeof($dataset) > 0) + foreach (array_keys($dataset) as $k) + { + if ($k!='') + $result = $result.','.$k; + } + return(substr($result, 1)); + } + + # make a name-value list for UPDATE-queries + static function MakeValuesList(&$dataset) + { + $result = ''; + if (sizeof($dataset) > 0) + foreach ($dataset as $k => $v) + { + if ($k!='') + $result = $result.',"'.DB::safe($v).'"'; + } + return(substr($result, 1)); + } + + static function MakeSetList(&$dataset, $concat = ', ') + { + $result = array(); + if (sizeof($dataset) > 0) foreach ($dataset as $k => $v) + { + if(substr($k, -1) == '+' || substr($k, -1) == '-') + { + $op = substr($k, -1); + $k = substr($k, 0, -1); + $result[] = $k.' = '.$k.' '.$op.' "'.DB::safe($v).'"'; + } + else + { + $result[] = $k.' = "'.DB::safe($v).'"'; + } + } + return(implode($concat, $result)); + } + + static function ParseQueryParams($query, $parameters = null) + { + if ($parameters != null) + { + $pctr = 0; + $result = ''; + for($a = 0; $a < strlen($query); $a++) + { + $chr = substr($query, $a, 1); + if ($chr == '?') + { + $result .= '"'.DB::Safe($parameters[$pctr]).'"'; + $pctr++; + } + else if ($chr == '&') + { + $result .= ''.intval($parameters[$pctr]).''; + $pctr++; + } + else if ($chr == ':') + { + $paramName = ''; + $a += 1; + $pFormat = 'string'; + if($query[$a] == ':') + { + $pFormat = 'number'; + $a += 1; + } + while(!ctype_space($chr = substr($query, $a, 1)) && $a < strlen($query)) + { + $paramName .= $chr; + $a += 1; + } + if($pFormat == 'number') + $result .= ' '.($parameters[$paramName]+0).' '; + else + $result .= ' "'.DB::Safe($parameters[$paramName]).'" '; + } + else + $result .= $chr; + } + } + else + $result = $query; + $q = str_replace('#', cfg('db/prefix'), $result); + self::$lastQuery = $q; + return($q); + } + + static function Safe($raw) + { + if(!self::$link) + return(addslashes($raw)); + else + return(mysqli_real_escape_string(self::$link, $raw)); + } + +} + +DB::connect(); + diff --git a/site/examples/web-app-starter/lib/db.md b/site/examples/web-app-starter/lib/db.md new file mode 100755 index 0000000..4620087 --- /dev/null +++ b/site/examples/web-app-starter/lib/db.md @@ -0,0 +1,144 @@ +# db.class.php + +A PHP database abstraction layer for MySQL/MariaDB with caching, parameter binding, and profiling. + +## Connection + +```php +DB::connect() // Auto-connects using config +DB::isConnected() // Check connection status +``` + +Uses configuration from `cfg('db/host')`, `cfg('db/user')`, etc. + +## Basic Queries + +```php +DB::Query($sql, $params) // Execute any SQL query +DB::Get($sql, $params) // Get multiple rows as array +DB::GetRowWithQuery($sql, $params) // Get single row +DB::GetCached($sql, $params) // Cached version of Get() +``` + +## CRUD Operations + +### Reading Data +```php +DB::GetRow($table, $keyvalue) // Get row by primary key +DB::GetRow($table, $id, $keyname) // Get row by specific key +DB::GetRowsMatch($table, $criteria) // Get rows matching criteria +``` + +### Writing Data +```php +DB::Insert($table, $data) // Insert new row, returns ID +DB::Commit($table, $data) // Insert or update (REPLACE) +DB::Update($table, $where, $data) // Update existing rows +DB::RemoveRow($table, $keyvalue) // Delete row by key +``` + +## Parameter Binding + +### Positional Parameters +```php +DB::Query('SELECT * FROM users WHERE id = ? AND name = ?', [$id, $name]); +DB::Query('SELECT * FROM users WHERE age > & AND active = ?', [$age, $active]); +``` + +### Named Parameters +```php +DB::Query('SELECT * FROM users WHERE name = :name', ['name' => $username]); +DB::Query('SELECT * FROM users WHERE age > ::age', ['age' => 25]); // Number format +``` + +Parameter formats: +- `?` - String (escaped) +- `&` - Number (unescaped integer) +- `:name` - Named string parameter +- `::name` - Named number parameter + +## Table Information + +```php +DB::Keys($table) // Get primary key column names +DB::Info($table) // Get full column information +``` + +## Utility Methods + +```php +DB::Safe($string) // Escape string for SQL +DB::MakeNamesList($array) // Create column list for INSERT +DB::MakeValuesList($array) // Create values list for INSERT +DB::MakeSetList($array, $separator) // Create SET clause for UPDATE +``` + +## Advanced Features + +### Table Prefix Support +```php +DB::Query('SELECT * FROM #users') // # replaced with cfg('db/prefix') +``` + +### Increment/Decrement Operations +```php +DB::Update('users', ['id' => 1], ['score+' => 10]); // score = score + 10 +DB::Update('users', ['id' => 1], ['lives-' => 1]); // lives = lives - 1 +``` + +### Caching +```php +$users = DB::GetCached('SELECT * FROM users WHERE active = 1'); +// Subsequent calls return cached results +``` + +### Statistics +```php +DB::$affectedRows // Rows affected by last operation +DB::$readOps // Count of read operations +DB::$writeOps // Count of write operations +DB::$lastQuery // Last executed query +``` + +## Examples + +### Basic Usage +```php +// Insert new user +$userId = DB::Insert('users', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'active' => 1 +]); + +// Get user by ID +$user = DB::GetRow('users', $userId); + +// Update user +DB::Update('users', ['id' => $userId], ['last_login' => date('Y-m-d H:i:s')]); + +// Find users +$activeUsers = DB::Get('SELECT * FROM users WHERE active = ?', [1]); +``` + +### Complex Queries +```php +// Named parameters +$results = DB::Get(' + SELECT * FROM #posts + WHERE author_id = :author + AND created_date > :date + AND status = :status +', [ + 'author' => $authorId, + 'date' => '2023-01-01', + 'status' => 'published' +]); + +// Mixed parameter types +DB::Query('UPDATE #users SET score = score + ::points WHERE id = :id', [ + 'points' => 100, // Number (unescaped) + 'id' => $userId // String (escaped) +]); +``` + diff --git a/site/examples/web-app-starter/lib/filebase.class.php b/site/examples/web-app-starter/lib/filebase.class.php new file mode 100644 index 0000000..298e6a6 --- /dev/null +++ b/site/examples/web-app-starter/lib/filebase.class.php @@ -0,0 +1,217 @@ +', 'file_put_contents_ex('.$filename.') could not write file'); + return; + } + + if (flock($fp, LOCK_EX)) + { + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, $data); + } + else + { + Log::text('', 'file_put_contents_ex('.$filename.') could not acquire lock'); + } + + fclose($fp); + } + + static function delete_file($file_name) + { + unlink($file_name); + } + + static function read_file($file_name) + { + $fsz = filesize($file_name); + if($fsz == 0) + return(''); + + $fp = fopen($file_name, "rb+"); + + if(!$fp) + return(''); + + if (flock($fp, LOCK_SH)) { + $content = fread($fp, $fsz); + flock($fp, LOCK_UN); + } else { + Log::text('', 'read_file('.$file_name.') could not acquire lock'); + } + + fclose($fp); + + return($content); + } + + static function write_data($class, $bucket, $type, $data) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true); + $fn = $storage_path.'/'.$type.'.json'; + self::write_file($fn, json_encode($data)); + } + + static function read_data($class, $bucket, $type) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + $fn = $storage_path.'/'.$type.'.json'; + return(json_decode(self::read_file($fn), true)); + } + + static function delete_data($class, $bucket, $type) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + return(self::delete_file($storage_path.'/'.$type.'.json')); + } + + static function get_data_filename($class, $bucket, $type) + { + return(first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket).'/'.$type.'.json'); + } + + static function list_bucket($class, $bucket) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + foreach(explode(chr(10), trim(shell_exec('ls -1 '.escapeshellarg($storage_path)))) as $name) + { + if(substr($name, 0, 1) != '_' && trim($name) != '') + $items[] = $name; + } + return($items); + } + + static function search_bucket($class, $bucket, $q) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + foreach(explode(chr(10), trim(shell_exec('grep -irlF '.escapeshellarg($q).' '.escapeshellarg($storage_path)))) as $l) + { + $name = substr($l, strlen($storage_path)+1, -5); + if(substr($name, 0, 1) != '_' && trim($name) != '') + $items[] = $name; + } + return($items); + } + + static function delete_bucket($class, $bucket) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + if(stristr($storage_path, '*') !== false) return; + if(stristr($storage_path, '?') !== false) return; + $result = trim(shell_exec('rm -r '.escapeshellarg($storage_path).' 2>&1')); + return($result); + } + + static function write_log($class, $bucket, $type, $data) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true); + WriteToFile($storage_path.'/'.$type.'.log', json_encode($data).chr(10)); + } + + static function read_log($class, $bucket, $type, $line_count = 8, $offset = false) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + return(get_json_tail($storage_path.'/'.$type.'.log', $line_count, $offset)); + } + + static function line_count($class, $bucket, $type) + { + $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); + return(intval(trim(shell_exec('wc -l '.escapeshellarg($storage_path.'/'.$type.'.log'))))); + } + + static function get_json_tail($from_file, $line_count = 8, $offset = false) + { + if($offset > 0) + { + $lines = trim(shell_exec( + 'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count))); + } + else + { + $lines = trim(shell_exec( + 'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file))); + } + return(json_lines($lines)); + } + + static function get_tail($from_file, $line_count = 8, $offset = false) + { + $line_count = intval($line_count); + if($offset > 0) + { + $lines = trim(shell_exec( + 'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count))); + } + else + { + $lines = trim(shell_exec( + 'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file))); + } + return(explode(chr(10), $lines)); + } + + static function get_all_lines($from_file) + { + return(explode(chr(10), trim(file_get_contents($from_file)))); + } + + static function truncate_log($from_file, $lines = 128) + { + $lines = intval($lines); + $tmp = tempnam(sys_get_temp_dir(), 'trunc_'); + shell_exec('tail -n '.$lines.' '.escapeshellarg($from_file).' > '.escapeshellarg($tmp).' && cp '.escapeshellarg($tmp).' '.escapeshellarg($from_file)); + @unlink($tmp); + return true; + } + + static function json_lines($lines) + { + if($lines == '') + { + return(array()); + } + else + { + $result = array(); + foreach(explode(chr(10), $lines) as $line) + $result[] = json_decode($line, true); + return($result); + } + } + +} \ No newline at end of file diff --git a/site/examples/web-app-starter/lib/http.class.php b/site/examples/web-app-starter/lib/http.class.php new file mode 100644 index 0000000..128df62 --- /dev/null +++ b/site/examples/web-app-starter/lib/http.class.php @@ -0,0 +1,111 @@ + $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => is_array($data) ? http_build_query($data) : $data, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3 + ]); + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + return ['error' => 'cURL Error: ' . $error, 'http_code' => 0]; + } + + $decoded = json_decode($response, true); + + return [ + 'success' => $http_code >= 200 && $http_code < 300, + 'http_code' => $http_code, + 'raw' => $response, + 'data' => $decoded ?: $response, + 'error' => $http_code >= 400 ? "HTTP Error $http_code" : null + ]; + } + + /** + * Make a GET request + */ + public static function get($url, $params = [], $headers = []) + { + if (!empty($params)) { + $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params); + } + + $ch = curl_init(); + + $default_headers = [ + 'Accept: application/json', + 'User-Agent: Web-App-Starter/1.0' + ]; + + $headers = array_merge($default_headers, $headers); + + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3 + ]); + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + return ['error' => 'cURL Error: ' . $error, 'http_code' => 0]; + } + + $decoded = json_decode($response, true); + + return [ + 'success' => $http_code >= 200 && $http_code < 300, + 'http_code' => $http_code, + 'raw' => $response, + 'data' => $decoded ?: $response, + 'error' => $http_code >= 400 ? "HTTP Error $http_code" : null + ]; + } + + /** + * Make a request with Bearer token authentication + */ + public static function get_with_token($url, $token, $params = []) + { + return self::get($url, $params, [ + 'Authorization: Bearer ' . $token + ]); + } +} diff --git a/site/examples/web-app-starter/lib/log.class.php b/site/examples/web-app-starter/lib/log.class.php new file mode 100755 index 0000000..c1f1814 --- /dev/null +++ b/site/examples/web-app-starter/lib/log.class.php @@ -0,0 +1,41 @@ +prepare()) { + * $data = [ + * 'customer_name' => 'John Doe', + * 'invoice_date' => time(), + * 'total_amount' => 1250.50, + * 'items' => [ + * ['name' => 'Product A', 'price' => 500.00, 'qty' => 1], + * ['name' => 'Product B', 'price' => 750.50, 'qty' => 1] + * ] + * ]; + * + * $odt->replace_placeholders($data); + * $pdf_path = $odt->create_output('/path/to/output.odt'); + * + * if ($pdf_path) { + * echo "PDF created: " . $pdf_path; + * } else { + * echo "Error: " . $odt->error_msg; + * } + * } else { + * echo "Error: " . $odt->error_msg; + * } + * + * PLACEHOLDER SYNTAX: + * + * In your ODT template, use LibreOffice placeholders (Insert > Field > Other > Variables > User Field) + * or direct placeholder markup: + * + * Simple placeholders: + * customer_name + * + * Formatted placeholders with JSON properties in description: + * invoice_date + * total_amount + * + * SUPPORTED FORMATS: + * + * - "date" - Formats timestamp as date (uses cfg('date_format') or custom template) + * - "time" - Formats timestamp as time (uses cfg('time_format') or custom template) + * - "datetime" - Formats timestamp as datetime (uses cfg('datetime_format') or custom template) + * - "currency" - Formats number as currency with € symbol + * - "number" - Formats number with specified decimal places + * - "duration" - Converts seconds to "Xh Ymin" format + * + * FORMAT OPTIONS: + * + * - "template": Custom date/time format string (e.g., "Y-m-d H:i:s") + * - "decimals": Number of decimal places for currency/number formatting + * - "default": Default value if placeholder data is empty + * - "emptyifzero": Show empty string if numeric value is zero + * - "delsegmentifzero": Delete entire segment if numeric value is zero + * - "newline": Add newlines "before", "after", or "before,after" the value + * + * LISTS AND TABLES: + * + * For repeating table rows, use a special placeholder in a table row: + * items + * + * This will repeat the table row for each item in the 'items' array. + * Within the repeated row, access item properties with 'item.' prefix: + * item.name + * item.price + * + * The class automatically calculates sums for numeric fields: + * sums.items.price + * + * ERROR HANDLING: + * + * Always check the return values and $odt->error_msg for error details: + * - prepare() returns false on failure + * - create_output() returns false on failure, PDF path on success + * - Check $odt->error_msg for specific error messages + * + * PROPERTIES: + * + * - $template_name: Path to the ODT template file + * - $content_xml: Loaded and processed content.xml from ODT + * - $temp_dir: Temporary directory for ODT extraction + * - $error_msg: Last error message + * - $odt_filename: Path to generated ODT file + * - $pdf_filename: Path to generated PDF file + * - $debug_output: Debug output from shell commands + * + * NOTES: + * + * - Temporary files are automatically cleaned up in destructor + * - PDF conversion requires LibreOffice to be installed and accessible + * - The class assumes UTF-8 encoding for all text content + * - Newlines in data are converted to ODT line breaks + * - All string values are properly escaped for XML + */ + +class ODT +{ + + public $template_name; + public $content_xml; + public $temp_dir; + public $error_msg; + public $odt_filename; + public $pdf_filename; + public $debug_output; + + function __construct($template_name) + { + $this->template_name = $template_name; + if(!file_exists($this->template_name)) { + $this->error_msg = 'Template not found: '.$template_name; + } + $this->temp_dir = '/tmp/'.uniqid(); + } + + /** + * Check if all required command line tools are available and working + * @return array Array with 'status' (bool) and 'messages' (array of status messages) + */ + static function check_requirements() + { + $results = [ + 'status' => true, + 'messages' => [] + ]; + + $unzip_check = shell_exec('which unzip 2>/dev/null'); + if (empty(trim($unzip_check))) { + $results['status'] = false; + $results['messages'][] = 'ERROR: unzip command not found in PATH'; + } else { + $results['messages'][] = 'OK: unzip found at ' . trim($unzip_check); + $unzip_version = shell_exec('unzip -v 2>&1 | head -1'); + if ($unzip_version) { + $results['messages'][] = 'INFO: ' . trim($unzip_version); + } + } + + $zip_check = shell_exec('which zip 2>/dev/null'); + if (empty(trim($zip_check))) { + $results['status'] = false; + $results['messages'][] = 'ERROR: zip command not found in PATH'; + } else { + $results['messages'][] = 'OK: zip found at ' . trim($zip_check); + } + + $soffice_check = shell_exec('which soffice 2>/dev/null') || ''; + if (empty(trim($soffice_check))) { + $results['status'] = false; + $results['messages'][] = 'ERROR: LibreOffice (soffice) not found in PATH'; + } else { + $results['messages'][] = 'OK: LibreOffice found at ' . trim($soffice_check); + + $soffice_version = shell_exec('soffice --version 2>&1'); + if ($soffice_version) { + $results['messages'][] = 'INFO: ' . trim($soffice_version); + } + + $headless_test = shell_exec('timeout 10 soffice --headless --help 2>&1') || ''; + if (strpos($headless_test, 'headless') !== false || strpos($headless_test, 'convert-to') !== false) { + $results['messages'][] = 'OK: LibreOffice headless mode is working'; + } else { + $results['status'] = false; + $results['messages'][] = 'ERROR: LibreOffice headless mode test failed'; + $results['messages'][] = 'DEBUG: ' . trim($headless_test); + } + } + + if (!is_dir('/tmp')) { + $results['status'] = false; + $results['messages'][] = 'ERROR: /tmp directory does not exist'; + } elseif (!is_writable('/tmp')) { + $results['status'] = false; + $results['messages'][] = 'ERROR: /tmp directory is not writable'; + } else { + $results['messages'][] = 'OK: /tmp directory exists and is writable'; + } + + $test_temp_dir = '/tmp/odt_test_' . uniqid(); + if (!mkdir($test_temp_dir, 0777, true)) { + $results['status'] = false; + $results['messages'][] = 'ERROR: Cannot create temporary directories in /tmp'; + } else { + $results['messages'][] = 'OK: Can create temporary directories'; + rmdir($test_temp_dir); + } + + return $results; + } + + static function parse_attributes($tag) { + $attributes = []; + while($tag != '') + { + $a_name = nibble('="', $tag); + $a_value = nibble('"', $tag); + $attributes[trim($a_name)] = trim($a_value); + } + return $attributes; + } + + function prepare() + { + // Create the temporary directory + if (!mkdir($this->temp_dir, 0777, true)) { + $this->error_msg = 'Failed to create temp directory'; + return false; + } + + // Unzip the template into the temp directory using shell_exec() + $command = 'unzip ' . escapeshellarg($this->template_name) . ' -d ' . escapeshellarg($this->temp_dir) . ' 2>&1'; + $output = shell_exec($command); + if ($output === null) { + $this->error_msg = 'Failed to unzip the template'; + return false; + } + + // Load content.xml into memory + $content_file = $this->temp_dir . '/content.xml'; + if (!file_exists($content_file)) { + $this->error_msg = 'content.xml not found'; + return false; + } + + $this->content_xml = file_get_contents($content_file); + if ($this->content_xml === false) { + $this->error_msg = 'Failed to read content.xml'; + return false; + } + + return true; + } + + function odt_entities($raw) + { + return str_replace(chr(10), '', safe($raw, ENT_XML1 | ENT_QUOTES, 'UTF-8')); + } + + function break_into_segments($s) + { + $segment_handlers = [ + ' function(&$cake, &$seg) { + $rest_of_row = nibble('', $cake); + # check whether this segment contains an items marker indicating we should + # iterate this segment over a list in the data + $items_marker = ''; + if(strpos($rest_of_row, $items_marker) !== false) + { + $r0 = nibble($items_marker, $rest_of_row); + $items_param = trim(str_replace(['<', '>'], '', + nibble('', $rest_of_row))); + $seg[] = [ + 'type' => 'list', + 'content' => '', + 'fields' => $items_param]; + } + else $seg[] = [ + 'type' => 'flat', + 'content' => '']; + }, + ]; + $seg = []; + while($s != '') + { + $seg_start_found = false; + $sc = nibble(array_keys($segment_handlers), $s, $seg_start_found); + $seg[] = ['type' => 'flat', 'content' => $sc]; + if($seg_start_found !== false) + { + $segment_handlers[$seg_start_found]($s, $seg); + } + } + return($seg); + } + + function run_segment($seg, $params) + { + $result = ''; + $sc = $seg['content']; + while($sc != '') + { + $placeholder_found = false; + $result .= nibble('', $sc)); + $p_prop = []; + if(isset($p_attributes['text:description']) && $p_attributes['text:description']) + { + if(substr($p_attributes['text:description'], 0, 1) == '{') + { + $decoded = json_decode( + str_replace('"', '"', $p_attributes['text:description']), true); + if($decoded !== null) { + $p_prop = $decoded; + } + } + else + { + $p_prop['format'] = $p_attributes['text:description']; + } + } + $p_content = str_replace(array('>', '<'), '', + nibble('', $sc)); + $placeholder_key = $p_content; + $value = first($params[$placeholder_key] ?? null, $p_prop['default'] ?? null, ''); + switch($p_prop['format'] ?? '') + { + case('date'): + { + if($value == 0) $value = '-'; else + $value = date(first($p_prop['template'], cfg('date_format')), intval($value)); + } break; + case('time'): + { + if($value == 0) $value = '-'; else + $value = date(first($p_prop['template'], cfg('time_format')), intval($value)); + } break; + case('datetime'): + { + if($value == 0) $value = '-'; else + $value = date(first($p_prop['template'], cfg('datetime_format')), intval($value)); + } break; + case('currency'): + { + $value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', '').' €'; + } break; + case('number'): + { + $value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', ''); + } break; + case('duration'): + { + $value = intval($value); + $h = floor($value/3600); $value -= $h*3600; + $m = floor($value/60); $value -= $m*60; + $s = $value; + $value = ''; + if($h > 0) $value .= $h.'h '; + $value .= $m.'min'; + } break; + } + if(($p_prop['emptyifzero'] ?? false) && floatval($value) == 0) + $value = ''; + if(($p_prop['delsegmentifzero'] ?? false) && floatval($value) == 0) + return(''); + if(!is_string($value)) + $value = trim($value); + if(($p_prop['newline'] ?? false) && $value != '') + { + if(str_contains($p_prop['newline'], 'before')) + $value = chr(10).$value; + if(str_contains($p_prop['newline'], 'after')) + $value = $value.chr(10); + } + if(!is_array($value)) + $result .= $this->odt_entities($value); + } + } + return $result; + } + + function replace_placeholders(&$params) + { + $result = ''; + foreach($this->break_into_segments($this->content_xml) as $seg) + { + if($seg['type'] == 'list') + { + if(isset($params[$seg['fields']]) && is_array($params[$seg['fields']])) { + foreach($params[$seg['fields']] as $item) + { + $np = $params; + foreach($item as $k => $v) + { + $np['item.'.$k] = $v; + $params['sums.'.$seg['fields'].'.'.$k] += floatval($v); + } + $result .= $this->run_segment($seg, $np); + } + } + } + else + { + $result .= $this->run_segment($seg, $params); + } + } + return $this->content_xml = $result; + } + + function create_output($out_filename) + { + $this->odt_filename = $out_filename; + $content_file = $this->temp_dir . '/content.xml'; + if (file_put_contents($content_file, $this->content_xml) === false) { + $this->error_msg = 'Failed to write content.xml'; + return false; // + } + + $this->debug_output = trim( + shell_exec('cd '.escapeshellarg($this->temp_dir).' ; zip -r ../tmp.odt * 2>&1')); + $zip_output = dirname($this->temp_dir).'/tmp.odt'; + if(!file_exists($zip_output)) + { + $this->error_msg = 'Failed to create tmp ODT file (shell "'.$this->debug_output.'")'; + return false; + } + if(file_exists($out_filename)) { + unlink($out_filename); + } + shell_exec('mv '.escapeshellarg($zip_output).' '.escapeshellarg($out_filename)); + + if (!file_exists($out_filename)) { + $this->error_msg = 'Failed to create ODT file "'.$out_filename.'"'; + return false; // + } + + $this->pdf_filename = $pdf_output = pathinfo($out_filename, PATHINFO_DIRNAME) . '/' . pathinfo($out_filename, PATHINFO_FILENAME) . '.pdf'; + $command = 'HOME=/tmp ; soffice --headless --convert-to pdf ' . escapeshellarg($out_filename) . ' --outdir ' . escapeshellarg(pathinfo($out_filename, PATHINFO_DIRNAME)) . ' 2>&1'; + $this->debug_output = shell_exec($command); + + if (!file_exists($pdf_output)) { + $this->error_msg = 'PDF conversion failed'; + return false; // + } + + return $pdf_output; + } + + function __destruct() + { + $this->delete_directory($this->temp_dir); + } + + function delete_directory($dir) + { + if (!is_dir($dir)) return; + $items = array_diff(scandir($dir), ['.', '..']); + foreach ($items as $item) { + $path = "$dir/$item"; + is_dir($path) ? $this->delete_directory($path) : unlink($path); + } + rmdir($dir); + } + +} diff --git a/site/examples/web-app-starter/lib/profiler.class.php b/site/examples/web-app-starter/lib/profiler.class.php new file mode 100755 index 0000000..f4f1937 --- /dev/null +++ b/site/examples/web-app-starter/lib/profiler.class.php @@ -0,0 +1,42 @@ + 0) // if greater than zero, update indent after logging + self::$indent_str = str_repeat(' ', self::$indent_level * 2); + self::$last = $thistime; + self::$current = $absoluteMS; + return($thistime); + } + + static function get_time() + { + return(1000*(microtime(true) - self::$start)); + } + + static function start() + { + self::$start = self::$last = microtime(true); + } + +} diff --git a/site/examples/web-app-starter/lib/svg.class.php b/site/examples/web-app-starter/lib/svg.class.php new file mode 100755 index 0000000..a7d038b --- /dev/null +++ b/site/examples/web-app-starter/lib/svg.class.php @@ -0,0 +1,32 @@ += 2 * M_PI) $start_angle -= 2 * M_PI; + while ($end_angle >= 2 * M_PI) $end_angle -= 2 * M_PI; + + $angle_diff = $end_angle - $start_angle; + if ($angle_diff < 0) $angle_diff += 2 * M_PI; + + $large_arc_flag = ($angle_diff > M_PI) ? 1 : 0; + $sweep_flag = 1; + + ?> + + + + <?= first(URL::$route['page-title'] ?? false, cfg('site/default_page_title')).' | '.cfg('site/name') ?> + + + + + + $signed_in, + 'profile' => $profile, + 'avatar' => first($profile['avatar_url'] ?? false, $profile['profile_image'] ?? false), + 'display_name' => first($profile['username'] ?? false, $profile['email'] ?? false, 'Account'), + ); +} + +function theme_render_account_links($options = array()) +{ + $context = theme_get_user_context(); + $wrapper_class = trim((string)first($options['wrapper_class'] ?? false, 'nav-account')); + $name_class = trim((string)first($options['name_class'] ?? false, 'account-name')); + $links_wrapper_class = trim((string)($options['links_wrapper_class'] ?? '')); + $show_avatar = !empty($options['show_avatar']); + $show_name = array_key_exists('show_name', $options) ? (bool)$options['show_name'] : true; + ?>> + + $account_wrapper_class, + 'name_class' => 'account-name', + 'show_avatar' => $show_avatar, + )); ?> +

tag with cache busting +include_css('css/style.css') // Outputs tag with cache busting +get_file_location($file) // Find file in include paths +``` + +Assets are automatically versioned with `filemtime()` for cache busting. + +## HTML/Security Functions + +```php +safe($text) // HTML escape for content +asafe($text) // HTML escape for attributes (strips newlines) +jsafe($data) // JSON encode (alias for json_encode) +``` + +## Configuration + +```php +cfg('database/host') // Get config value with slash notation +cfg('url/root') // Access nested config arrays +``` + +Reads from `$GLOBALS['config']` with support for nested keys using `/` separator. + +## Utility Functions + +```php +first($a, $b, $c) // Return first non-empty value +alnum($text, '_', true) // Keep only alphanumeric + spaces +write_to_file($file, $content) // Append content to file +``` + +### first() Examples +```php +$name = first($user_input, $default_name, 'Anonymous'); +$config = first($_GET['theme'], $_COOKIE['theme'], 'default'); +``` + +## String Processing + +### String Parsing +```php +nibble(':', $path, $found) // Extract substring before delimiter +// Example: nibble(':', 'user:pass', $found) → 'user', $path becomes 'pass' +``` + +### Date/Time +```php +age_to_string($timestamp) // Human-friendly time differences +// Examples: "just now", "5 min ago", "2 h ago", "Mon 14:30" +``` + +### Hash Generation +```php +make_hash() // Generate random hash (10 chars) +make_hash($input, 20) // Hash specific input (20 chars) +``` + +### Base Conversion +```php +base_convert_any($num, $from, $to) // Convert between any number bases +// Example: base_convert_any('FF', '0123456789ABCDEF', '0123456789') → '255' +``` + +## Advanced Features + +- **Include Path Resolution**: Searches multiple paths for files +- **Automatic Permissions**: Sets 0777 on written files +- **Cache Busting**: Automatic versioning for JS/CSS +- **Reference Parameters**: Functions like `nibble()` modify input variables +- **Flexible Base Conversion**: Support for custom character sets + +## Common Usage Patterns + +```php +// Configuration-driven asset loading +include_css(cfg('theme/css_file')); + +// Safe template output +echo '
' . safe($content) . '
'; + +// Fallback values +$title = first($_POST['title'], $default_title, 'Untitled'); + +// URL parsing +$protocol = nibble('://', $url); // Extract 'http' from 'http://example.com' + +// Time display +echo 'Posted ' . age_to_string($post_timestamp); +``` + +## Error Handling + +Functions include built-in error handling: +- `get_file_location()` dies with error message if file not found +- `write_to_file()` uses error suppression for chmod +- Most functions return safe defaults for invalid input diff --git a/site/examples/web-app-starter/lib/ulib.php b/site/examples/web-app-starter/lib/ulib.php new file mode 100755 index 0000000..07dce6b --- /dev/null +++ b/site/examples/web-app-starter/lib/ulib.php @@ -0,0 +1,254 @@ + $max) return $max; + return $v; +} + +function pick_entry_from_range($array, $value) +{ + if(!is_array($array)) return []; + $result = []; + foreach($array as $pv) + if($value >= $pv['from'] && $value <= $pv['to']) $result = $pv; + return $result; +} + +/** + * Can have any number of arguments. Returns the first of its arguments that is not false, empty string, or null. + */ +function first() +{ + $args = func_get_args(); + foreach($args as $v) + { + if(isset($v) && $v !== false && $v !== '' && $v !== null) + return($v); + } + return(''); +} + +function alnum($s, $replace_with = '_', $trim = true) +{ + if($trim) $s = trim(strtolower($s)); + return(preg_replace("/[^[:alnum:][:space:]]/u", $replace_with, $s)); +} + +/** + * Append a string to the given file. + */ +function write_to_file($filename, $content) +{ + if (is_array($content)) $content = json_encode($content); + $open = fopen($filename, 'a+'); + fwrite($open, $content); + fclose($open); + @chmod($filename, 0777); +} + +# **************************** ARRAY FUNCTIONS ****************************** + +/** + * Returns a value from the $GLOBALS['config'] array identified by $key. + * Sub-array values can be addressed by using the '/' character as a separator. + */ +function cfg($key) +{ + $config = $GLOBALS['config']; + $seg = explode('/', $key); + $lastSeg = array_pop($seg); + foreach($seg as $s) + { + if(is_array($config) && array_key_exists($s, $config) && is_array($config[$s])) + $config = $config[$s]; + else + return null; + } + if(!is_array($config) || !array_key_exists($lastSeg, $config)) + return null; + return($config[$lastSeg]); +} + + +# **************************** STRING/FORMATTING FUNCTIONS ****************************** + +/** + * Convert any base number into another number of another base system. + */ +function base_convert_any($numberInput, $fromBaseInput, $toBaseInput) +{ + if ($fromBaseInput==$toBaseInput) return $numberInput; + $fromBase = str_split($fromBaseInput,1); + $toBase = str_split($toBaseInput,1); + $number = str_split($numberInput,1); + $fromLen=strlen($fromBaseInput); + $toLen=strlen($toBaseInput); + $numberLen=strlen($numberInput); + $retval=''; + if ($toBaseInput == '0123456789') + { + $retval=0; + for ($i = 1;$i <= $numberLen; $i++) + $retval = bcadd($retval, bcmul(array_search($number[$i-1], $fromBase),bcpow($fromLen,$numberLen-$i))); + return $retval; + } + if ($fromBaseInput != '0123456789') + $base10=base_convert_any($numberInput, $fromBaseInput, '0123456789'); + else + $base10 = $numberInput; + if ($base10 $content_file.'.php'); + $dir_index_file = $content_file.'/index.php'; + if(file_exists($dir_index_file)) + return array('file' => $dir_index_file); + $lpath_parts = array_values(array_filter(explode('/', $lpath), function($segment) { + return $segment !== ''; + })); + if(sizeof($lpath_parts) > 1) + { + $last_seg = array_pop($lpath_parts); + $parent_file = $base_dir.'/'.implode('/', $lpath_parts).'/index.php'; + if(file_exists($parent_file)) + return array('file' => $parent_file, 'param' => $last_seg); + } + return false; + } + + static function Link($path, $params = false) + { + $path = ltrim((string)$path, '?'); + $query = $params !== false ? http_build_query($params) : ''; + if(cfg('url/pretty')) + { + return($GLOBALS['config']['url']['root'].$path.($query !== '' ? '?'.$query : '')); + } + else + { + if($path === '') + return($GLOBALS['config']['url']['root'].($query !== '' ? '?'.$query : '')); + return($GLOBALS['config']['url']['root'].'?'.$path.($query !== '' ? '&'.$query : '')); + } + } + + # redirect to URL and quit + static function Redirect($url = '', $params = array()) + { + header('location: '.self::Link($url, $params)); + die(); + } + + +} diff --git a/site/examples/web-app-starter/lib/url.md b/site/examples/web-app-starter/lib/url.md new file mode 100755 index 0000000..249244e --- /dev/null +++ b/site/examples/web-app-starter/lib/url.md @@ -0,0 +1,134 @@ +# url.class.php + +A PHP URL routing and handling class for web applications with support for pretty URLs and request parsing. + +## Static Properties + +```php +URL::$locator // Current URL path +URL::$route // Parsed route information +URL::$error // Error message for 404s +URL::$title // Page title +URL::$page_type // Response type (default: 'html') +URL::$fragments // URL fragments array +URL::$tried // Attempted routes (debugging) +``` + +## Request Parsing + +```php +URL::ParseRequestURI() // Extract locator from REQUEST_URI +``` + +Parses URLs and populates `$_REQUEST` with query parameters. Handles both pretty URLs and query string format. + +## Route Generation + +```php +URL::MakeRoute($locator) // Generate route from URL path +``` + +Creates route array with: +- `l-path` - Clean URL path segments +- `page` - Target page/view (defaults to 'index') +- URL config values + +### Special Route Handling +- Paths starting with `:` use the entire path as page name +- Strips dots and empty segments for security +- Removes root path prefix + +## URL Generation + +```php +URL::Link($path, $params) // Generate application URL +``` + +Creates URLs based on `cfg('url/pretty')` setting: +- **Pretty URLs**: `/path?param=value` +- **Query URLs**: `/?path¶m=value` + +## Navigation + +```php +URL::Redirect($url, $params) // Redirect and exit +URL::NotFound($message) // Send 404 response +``` + +## Examples + +### Basic Routing +```php +// Parse current request +$locator = URL::ParseRequestURI(); // Returns 'users/profile' +$route = URL::MakeRoute(); // Creates route array + +// Access route data +echo URL::$route['l-path']; // 'users/profile' +echo URL::$route['page']; // 'index' (default) +``` + +### URL Generation +```php +// Generate links +$userLink = URL::Link('users/123'); // '/users/123' +$searchLink = URL::Link('search', ['q' => 'term']); // '/search?q=term' + +// Redirect examples +URL::Redirect('login'); // Redirect to login +URL::Redirect('dashboard', ['tab' => 'settings']); // With parameters +``` + +### Special Page Routes +```php +// URL: /special/admin/users +// If URL starts with ':special', entire path becomes page name +URL::MakeRoute(':special/admin/users'); +// Results in: page = 'special/admin/users' +``` + +### Error Handling +```php +// Set 404 status +URL::NotFound('Page not found'); +echo URL::$error; // 'Page not found' + +// Check attempted routes +var_dump(URL::$tried); // Array of attempted route resolutions +``` + +## Configuration Integration + +Uses configuration values: +- `cfg('url/root')` - Application root path +- `cfg('url/pretty')` - Enable/disable pretty URLs +- `$GLOBALS['config']['url']` - URL configuration array + +## URL Parsing Logic + +1. **Extract Path**: Removes query string from REQUEST_URI +2. **Parse Parameters**: Converts query string to $_REQUEST entries +3. **Clean Segments**: Removes dots, empty segments, security prefixes +4. **Route Resolution**: Determines target page and path +5. **Special Handling**: Processes colon-prefixed paths + +## Security Features + +- **Dot Prevention**: Strips segments starting with '.' +- **Path Sanitization**: Removes empty and problematic segments +- **Root Stripping**: Removes application root from paths + +## Pretty URL Support + +Supports both URL formats: +```php +// Pretty URLs (url/pretty = true) +/users/profile/edit +/search?q=term + +// Query URLs (url/pretty = false) +/?users/profile/edit +/?search&q=term +``` + + diff --git a/site/examples/web-app-starter/lib/user.class.php b/site/examples/web-app-starter/lib/user.class.php new file mode 100755 index 0000000..ce2ff97 --- /dev/null +++ b/site/examples/web-app-starter/lib/user.class.php @@ -0,0 +1,111 @@ + false, 'message' => 'email_and_password_required']; + } + + $email = strtolower(trim($basic_profile['email'])); + + $existing = Filebase::read_data('users', Filebase::hash($email), 'account'); + if ($existing) return ['result' => false, 'message' => 'user_exists']; + + $now = time(); + $stored = $basic_profile; + $stored['email'] = $email; + $stored['password_hash'] = password_hash($basic_profile['password'], PASSWORD_DEFAULT); + $stored['created'] = $now; + if (!isset($stored['roles'])) $stored['roles'] = ['user']; + + Filebase::write_data('users', Filebase::hash($email), 'account', $stored); + return ['result' => true, 'id' => $email, 'profile' => $stored]; + } + + static function AuthWithPassword($password, $basic_profile = false) + { + if ($basic_profile === false) { + if (!isset($_POST['email'])) return ['result' => false, 'message' => 'email_missing']; + $email = strtolower(trim($_POST['email'])); + $basic_profile = Filebase::read_data('users', Filebase::hash($email), 'account'); + if ($basic_profile) $basic_profile['id'] = $email; + } + + if (!$basic_profile) return ['result' => false, 'message' => 'no_such_user']; + if (!isset($basic_profile['password_hash'])) return ['result' => false, 'message' => 'no_password_set']; + + if (password_verify($password, $basic_profile['password_hash'])) { + if (session_status() !== PHP_SESSION_ACTIVE) session_start(); + $_SESSION[self::$session_key] = $basic_profile['id']; + self::$current_profile = $basic_profile; + return ['result' => true, 'profile' => $basic_profile]; + } + + return ['result' => false, 'message' => 'invalid_password']; + } + + static function Permission($thing_name) + { + if (!self::IsSignedIn()) return false; + $roles = self::$current_profile['roles'] ?? []; + if (in_array('admin', $roles)) return true; + + $perm_map = [ + 'edit' => ['admin', 'editor'], + 'view' => ['admin', 'editor', 'user'], + ]; + + $allowed = $perm_map[$thing_name] ?? ['admin']; + return count(array_intersect($roles, $allowed)) > 0; + } + + static function IsSignedIn() + { + if (session_status() !== PHP_SESSION_ACTIVE) session_start(); + if (isset($_SESSION[self::$session_key]) && $_SESSION[self::$session_key]) { + if (self::$current_profile) return true; + $id = $_SESSION[self::$session_key]; + $profile = Filebase::read_data('users', Filebase::hash($id), 'account'); + if ($profile) { + $profile['id'] = $id; + self::$current_profile = $profile; + return true; + } + unset($_SESSION[self::$session_key]); + } + return false; + } + + static function Logout() + { + if (session_status() !== PHP_SESSION_ACTIVE) session_start(); + unset($_SESSION[self::$session_key]); + self::$current_profile = null; + return true; + } + +} diff --git a/site/examples/web-app-starter/tests/bootstrap.php b/site/examples/web-app-starter/tests/bootstrap.php new file mode 100644 index 0000000..84cebc8 --- /dev/null +++ b/site/examples/web-app-starter/tests/bootstrap.php @@ -0,0 +1,18 @@ + 'portal-dark']) === '/web-app-starter/?theme=portal-dark', 'root theme link omits the empty route segment'); +test_assert(URL::Link('themes', ['theme' => 'portal-dark']) === '/web-app-starter/?themes&theme=portal-dark', 'named route links keep the route segment and query parameters'); + +test_assert(component_exists('components/example/theme-switcher'), 'component existence works for explicit component paths'); +test_assert(component_exists('example/theme-switcher'), 'component existence works for shorthand component paths'); + +ob_start(); +component('example/theme-switcher'); +$theme_switcher_html = ob_get_clean(); +test_assert(str_contains($theme_switcher_html, 'theme-switcher'), 'component rendering works through the shorthand component API'); + +$portal_dark = cfg('theme/options/portal-dark'); +test_assert(!empty($portal_dark['description']) && !empty($portal_dark['footer_text']), 'theme metadata is centralized in config'); + +echo "All smoke tests passed.\n"; \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/css/gauges.css b/site/examples/web-app-starter/themes/common/css/gauges.css new file mode 100644 index 0000000..1c465da --- /dev/null +++ b/site/examples/web-app-starter/themes/common/css/gauges.css @@ -0,0 +1,348 @@ +.gauge-demo-row { + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: stretch; + margin-bottom: 1.5rem; +} + +.gauge-control-panel { + flex: 1 1 18rem; + min-width: 16rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-control-panel h4 { + margin: 0 0 0.75rem; + color: var(--text-primary); +} + +.gauge-slider-group { + display: grid; + gap: 0.35rem; + margin-bottom: 0.9rem; +} + +.gauge-slider-group label { + color: var(--text-secondary); + font-weight: 600; + font-size: 0.92rem; +} + +.gauge-slider-group input[type="range"] { + width: 100%; + accent-color: var(--primary); +} + +.gauge-set, +.arcgauge-set { + flex: 1 1 24rem; + min-width: 20rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-set-header, +.arcgauge-set-header { + margin-bottom: 1rem; +} + +.gauge-set-header h3, +.arcgauge-set-header h3 { + margin: 0 0 0.3rem; + color: var(--text-primary); +} + +.gauge-set-header p, +.arcgauge-set-header p { + margin: 0; + color: var(--text-secondary); +} + +.gauge-card, +.arcgauge-card { + display: flex; + flex-direction: column; + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-elevated, var(--surface)); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: relative; + overflow: hidden; +} + +.gauge-card::before, +.arcgauge-card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.5; +} + +.gauge-metric-label { + color: var(--text-secondary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.gauge-metric-value { + color: var(--text-primary); + font-size: 1.1rem; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); + letter-spacing: -0.03em; +} + +.gauge-metric-meta { + color: var(--text-muted); + font-size: 0.84rem; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.progressbar-set-horizontal, +.progressbar-set-vertical { + align-self: stretch; +} + +.progressbar-grid-horizontal { + display: grid; + gap: 0.85rem; +} + +.progressbar-grid-vertical { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + align-items: stretch; +} + +.progressbar-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.7rem; +} + +.progressbar-card-horizontal { + gap: 0.7rem; +} + +.progressbar-card-vertical { + align-items: center; + justify-content: flex-end; + text-align: center; + gap: 0.7rem; +} + +.progressbar-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.progressbar-background { + position: relative; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent 30%); + background: color-mix(in srgb, var(--bg-color) 72%, var(--surface) 28%); + display: flex; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); +} + +.progressbar-background-horizontal { + height: 0.9rem; + border-radius: 999px; +} + +.progressbar-background-vertical { + height: var(--progressbar-height, 240px); + width: 100%; + max-width: 4rem; + margin: 0 auto; + border-radius: 1.2rem; + justify-content: flex-end; + padding: 0.35rem; + flex-direction: column; +} + + +.progressbar-bar { + transition: all 0.35s ease; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12), 0 0 24px color-mix(in srgb, currentColor 28%, transparent 72%); +} + +.progressbar-background-horizontal .progressbar-bar { + height: 100%; + border-radius: 999px; +} + +.progressbar-background-vertical .progressbar-bar { + width: 100%; + border-radius: 0.95rem; +} + +.progressbar-marker { + position: absolute; + z-index: 10; + opacity: 0.72; + background: var(--text-color, var(--text-primary, #333)); + box-shadow: 0 0 0 1px rgba(255,255,255,0.16); +} + +.progressbar-marker:hover { + opacity: 1; +} + +.progressbar-background-horizontal .progressbar-marker { + height: 100%; + top: 0; + width: 3px; + transform: translateX(-50%); +} + +.progressbar-background-vertical .progressbar-marker { + width: 100%; + left: 0; + height: 3px; + transform: translateY(50%); +} + +.needlegauge-svg .needle { + transition: all 0.3s ease; +} + +.progressbar-meta { + margin-top: 0.1rem; +} + +.needlegauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); +} + +.needlegauge-card { + align-items: center; + text-align: center; + gap: 0.55rem; +} + +.needlegauge-label-head { + align-self: flex-start; +} + +.needlegauge-visual { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.needlegauge-svg { + max-width: 100%; + overflow: visible; +} + + +.needlegauge-svg .ticks line { + opacity: 0.8; +} + +.needlegauge-svg .ticks text { + font-family: var(--font-mono, ui-monospace, monospace); + fill: var(--text-muted); +} + +.needlegauge-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.arcgauge-label { + margin-bottom: 0.35rem; +} + +.arcgauge-svg { + width: 130px; + height: 72px; + margin: 0.1rem 0 0.35rem; + overflow: visible; +} + +.arcgauge-track { + stroke: color-mix(in srgb, var(--border) 70%, transparent 30%); +} + +.arcgauge-arc-dyn { + transition: stroke-dasharray 0.6s ease, stroke 0.6s ease; +} + +.arcgauge-watermark { + stroke-width: 1.5; + stroke-linecap: round; +} + +.arcgauge-watermark-lo { + stroke: var(--primary); +} + +.arcgauge-watermark-hi { + stroke: var(--accent); +} + +.arcgauge-value { + fill: var(--text-primary); + font-size: 17px; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.arcgauge-caption { + fill: var(--text-muted); + font-size: 7px; + letter-spacing: 1.5px; + font-family: var(--font-sans, system-ui, sans-serif); +} + +.arcgauge-meta { + width: 100%; + text-align: center; +} + +.arcgauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); +} + +.arcgauge-card { + align-items: center; + text-align: center; +} + +@media (max-width: 768px) { + .gauge-demo-row { + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/css/workspace.css b/site/examples/web-app-starter/themes/common/css/workspace.css new file mode 100644 index 0000000..b0575cb --- /dev/null +++ b/site/examples/web-app-starter/themes/common/css/workspace.css @@ -0,0 +1,522 @@ +.ws-app { + --ws-sidebar-width: 280px; + --ws-frame-bg: var(--surface); + --ws-sidebar-bg: color-mix(in srgb, var(--bg-secondary) 80%, var(--surface) 20%); + --ws-main-bg: var(--surface); + --ws-surface-alt: color-mix(in srgb, var(--bg-secondary) 72%, var(--surface) 28%); + --ws-shadow: var(--shadow-md); + display: flex; + width: 100%; + min-height: min(76vh, 900px); + overflow: hidden; + position: relative; + background: var(--ws-frame-bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--ws-shadow); +} + +.ws-sidebar { + width: var(--ws-sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + min-height: 0; + background: var(--ws-sidebar-bg); + border-right: 1px solid var(--border); + position: relative; + z-index: 2; +} + +.ws-main { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--ws-main-bg); + padding: 0; +} + +.ws-sidebar-top, +.ws-panel-header, +.ws-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.ws-sidebar-top { + padding: 0.9rem; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + flex-direction: column; + align-items: stretch; + background: color-mix(in srgb, var(--surface) 78%, var(--bg-secondary) 22%); +} + +.ws-search-wrap { + position: relative; +} + +.ws-search-icon { + position: absolute; + left: 0.7rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 0.8rem; + pointer-events: none; +} + +.ws-search-input { + width: 100%; + min-height: 36px; + padding: 0 0.8rem 0 2rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + font-size: 0.9rem; + color: var(--text-primary); +} + +.ws-search-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent 80%); +} + +.ws-panel-title-group, +.ws-header-actions { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.ws-panel-title-group { + flex-direction: column; + align-items: flex-start; +} + +.ws-panel-title-group h2, +.ws-section-head h3, +.ws-empty-state h2 { + margin: 0; +} + +.ws-subtitle { + margin: 0; + color: var(--text-secondary); + font-size: 0.92rem; +} + +.ws-sidebar-overlay { + display: none; +} + +.ws-mobile-bar { + display: none; + align-items: center; + gap: 0.7rem; + padding: 0.9rem 1rem; + border-bottom: 1px solid var(--border); + background: color-mix(in srgb, var(--surface) 92%, var(--bg-secondary) 8%); + position: sticky; + top: 0; + z-index: 1; +} + +.ws-mobile-title { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); +} + +.ws-empty-state { + min-height: 320px; + display: grid; + place-items: center; + align-content: center; + gap: 0.75rem; + text-align: center; + padding: 2rem; + max-width: 40rem; + margin: auto; +} + +.ws-empty-state p { + max-width: 32rem; + color: var(--text-secondary); + margin: 0; +} + +.ws-empty-icon { + width: 64px; + height: 64px; + display: grid; + place-items: center; + border-radius: var(--radius); + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: #fff; + font-size: 24px; + box-shadow: var(--shadow-sm); +} + +.ws-panel { + min-height: 0; + display: flex; + flex-direction: column; + padding: 1.1rem 1.25rem; + gap: 1rem; +} + +.ws-section { + display: flex; + flex-direction: column; + gap: 0.8rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--ws-surface-alt); +} + +.ws-section + .ws-section { + margin-top: 0.25rem; +} + +.ws-icon-btn { + min-width: 34px; + height: 34px; + padding: 0 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + flex-shrink: 0; + background: var(--bg-secondary); + color: var(--text-secondary); + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease, border-color 0.2s ease; + text-decoration: none; +} + +.ws-icon-btn:hover { + background: var(--surface-hover); + border-color: var(--border-hover); + color: var(--text-primary); + text-decoration: none; +} + +.ws-icon-btn:active { + transform: translateY(1px); +} + +.ws-primary-btn, +.ws-sidebar-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + width: 100%; + min-height: 38px; + padding: 0.55rem 0.85rem; + border: none; + border-radius: var(--radius-sm); + background: var(--primary); + color: #fff; + font-size: 0.92rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; +} + +.ws-primary-btn:hover, +.ws-sidebar-action-btn:hover { + background: var(--primary-dark); + text-decoration: none; + color: #fff; +} + +.ws-primary-btn:active, +.ws-sidebar-action-btn:active { + transform: translateY(1px); +} + +.ws-list-state { + padding: 1rem; + color: var(--text-secondary); + font-size: 0.86rem; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + border-top: 1px solid var(--border); +} + +.ws-nav-group-label { + padding: 0.7rem 0.95rem 0.35rem; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + user-select: none; +} + +.ws-nav-list { + display: flex; + flex-direction: column; + padding: 0.3rem 0.4rem 0.9rem; + gap: 0.2rem; + overflow: auto; + min-height: 0; + flex: 1; +} + +.ws-nav-item { + display: block; + padding: 0 0.2rem; + text-decoration: none; + color: inherit; +} + +.ws-nav-item-inner { + display: flex; + align-items: center; + gap: 0.55rem; + width: 100%; + padding: 0.58rem 0.65rem; + border-radius: var(--radius-sm); + min-width: 0; + transition: background 0.2s ease, border-color 0.2s ease, padding-left 0.2s ease; + border-left: 3px solid transparent; +} + +.ws-nav-item:hover .ws-nav-item-inner { + background: color-mix(in srgb, var(--surface-hover) 85%, transparent 15%); +} + +.ws-nav-item.active .ws-nav-item-inner, +.ws-nav-item.is-active .ws-nav-item-inner { + background: color-mix(in srgb, var(--primary) 12%, transparent 88%); + border-left-color: var(--primary); + padding-left: calc(0.65rem - 3px); +} + +.ws-nav-icon { + flex-shrink: 0; + width: 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; +} + +.ws-nav-item.active .ws-nav-icon, +.ws-nav-item.is-active .ws-nav-icon { + color: var(--primary); +} + +.ws-nav-item-text { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.45rem; + overflow: hidden; +} + +.ws-nav-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.9rem; + color: var(--text-primary); + line-height: 1.3; +} + +.ws-nav-item.active .ws-nav-title, +.ws-nav-item.is-active .ws-nav-title { + font-weight: 700; + color: var(--primary-dark); +} + +.ws-nav-meta { + flex-shrink: 0; + font-size: 0.72rem; + color: var(--text-muted); + transition: opacity 0.2s ease; +} + +.ws-status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0.2rem 0.6rem; + border-radius: 999px; + border: 1px solid transparent; + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.ws-status-pill-neutral { + background: color-mix(in srgb, var(--text-muted) 16%, transparent 84%); + border-color: color-mix(in srgb, var(--text-muted) 28%, transparent 72%); + color: var(--text-secondary); +} + +.ws-status-pill-success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +.ws-status-pill-warn { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + +.ws-status-pill-danger { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); +} + +.ws-status-pill-info { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); +} + +.ws-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.85rem; +} + +.ws-stat-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.ws-stat-card-label { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + color: var(--text-secondary); +} + +.ws-stat-card-value { + font-size: 1.45rem; + font-weight: 700; + line-height: 1.1; + font-variant-numeric: tabular-nums; +} + +.ws-stat-card-meta, +.ws-section-copy, +.ws-inline-note, +.ws-detail-copy { + margin: 0; + color: var(--text-secondary); +} + +.ws-detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.85rem; +} + +.ws-detail-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--bg-secondary) 85%, var(--surface) 15%); + min-height: 100%; +} + +.ws-detail-card h4 { + margin: 0 0 0.35rem; + font-size: 0.95rem; +} + +.ws-detail-card p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.ws-demo-sidebar-copy { + padding: 0 1rem 1rem; + font-size: 0.84rem; + color: var(--text-secondary); +} + +.ws-demo-sidebar-copy p { + margin: 0; +} + +@media (max-width: 860px) { + .ws-app { + display: block; + min-height: 72vh; + } + + .ws-sidebar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: min(88vw, max(var(--ws-sidebar-width), 320px)); + transform: translateX(-100%); + transition: transform 160ms ease; + box-shadow: var(--shadow-lg); + z-index: 10; + } + + .ws-sidebar.is-open { + transform: translateX(0); + } + + .ws-sidebar-overlay.is-open { + display: block; + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.38); + z-index: 5; + } + + .ws-mobile-bar { + display: flex; + } + + .ws-panel { + padding: 1rem; + } + + .ws-stat-grid, + .ws-detail-grid { + grid-template-columns: 1fr; + } + + .ws-section-head, + .ws-panel-header { + align-items: flex-start; + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/fontawesome/LICENSE.txt b/site/examples/web-app-starter/themes/common/fontawesome/LICENSE.txt new file mode 100755 index 0000000..f31bef9 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/site/examples/web-app-starter/themes/common/fontawesome/css/all.min.css b/site/examples/web-app-starter/themes/common/fontawesome/css/all.min.css new file mode 100755 index 0000000..0f922d8 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fontawesome/css/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/fontawesome/css/fontawesome.min.css b/site/examples/web-app-starter/themes/common/fontawesome/css/fontawesome.min.css new file mode 100755 index 0000000..ab52c55 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fontawesome/css/fontawesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot new file mode 100755 index 0000000..30a2784 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg new file mode 100755 index 0000000..47bb690 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg @@ -0,0 +1,3451 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf new file mode 100755 index 0000000..a7ab4d4 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff new file mode 100755 index 0000000..ec5b613 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100755 index 0000000..df11cea Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot new file mode 100755 index 0000000..b5440c9 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg new file mode 100755 index 0000000..5ec81a8 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf new file mode 100755 index 0000000..87693a8 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff new file mode 100755 index 0000000..917bf73 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100755 index 0000000..0f10115 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot new file mode 100755 index 0000000..305fc64 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg new file mode 100755 index 0000000..eb47f6a --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,4649 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:21 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf new file mode 100755 index 0000000..8fb4d53 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff new file mode 100755 index 0000000..69f4474 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff differ diff --git a/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100755 index 0000000..20e4ce2 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-bold-italic.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-bold-italic.ttf new file mode 100755 index 0000000..049965e Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-bold-italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-bold.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-bold.ttf new file mode 100755 index 0000000..3c92e2e Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-bold.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-italic.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-italic.ttf new file mode 100755 index 0000000..01ec3c5 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf new file mode 100755 index 0000000..ec2a812 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-bold.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-bold.ttf new file mode 100755 index 0000000..fb26114 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-bold.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-italic.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-italic.ttf new file mode 100755 index 0000000..af20348 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-regular.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-regular.ttf new file mode 100755 index 0000000..aaac636 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-mono-regular.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/b612/b612-regular.ttf b/site/examples/web-app-starter/themes/common/fonts/b612/b612-regular.ttf new file mode 100755 index 0000000..c685a23 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/b612/b612-regular.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf new file mode 100755 index 0000000..4c0ee56 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf new file mode 100755 index 0000000..fa51576 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf new file mode 100755 index 0000000..3cb9269 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf new file mode 100755 index 0000000..010ed45 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf new file mode 100755 index 0000000..8550ed9 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf new file mode 100755 index 0000000..2e93072 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf new file mode 100755 index 0000000..592caec Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf new file mode 100755 index 0000000..1f42361 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf new file mode 100755 index 0000000..ff1769f Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf new file mode 100755 index 0000000..dcd2c27 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf new file mode 100755 index 0000000..18da8c6 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf new file mode 100755 index 0000000..0af4e38 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf new file mode 100755 index 0000000..09cc053 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf new file mode 100755 index 0000000..e25a540 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/press-start-2p/OFL.txt b/site/examples/web-app-starter/themes/common/fonts/press-start-2p/OFL.txt new file mode 100644 index 0000000..22796df --- /dev/null +++ b/site/examples/web-app-starter/themes/common/fonts/press-start-2p/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf b/site/examples/web-app-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Black.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Black.ttf new file mode 100755 index 0000000..689fe5c Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Black.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100755 index 0000000..0b4e0ee Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Bold.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Bold.ttf new file mode 100755 index 0000000..d3f01ad Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Bold.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100755 index 0000000..41cc1e7 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Italic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Italic.ttf new file mode 100755 index 0000000..6a1cee5 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Light.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Light.ttf new file mode 100755 index 0000000..219063a Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Light.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf new file mode 100755 index 0000000..0e81e87 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Medium.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Medium.ttf new file mode 100755 index 0000000..1a7f3b0 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Medium.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100755 index 0000000..0030295 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Regular.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Regular.ttf new file mode 100755 index 0000000..2c97eea Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Regular.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Thin.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Thin.ttf new file mode 100755 index 0000000..b74a4fd Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-Thin.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100755 index 0000000..dd0ddb8 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf new file mode 100755 index 0000000..fc28868 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf new file mode 100755 index 0000000..e1a648f Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf new file mode 100755 index 0000000..97ff9f1 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf new file mode 100755 index 0000000..2dae31e Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf new file mode 100755 index 0000000..da108d3 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf differ diff --git a/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf new file mode 100755 index 0000000..c2304c1 Binary files /dev/null and b/site/examples/web-app-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf differ diff --git a/site/examples/web-app-starter/themes/common/page.blank.php b/site/examples/web-app-starter/themes/common/page.blank.php new file mode 100755 index 0000000..92858e3 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/page.blank.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/common/page.json.php b/site/examples/web-app-starter/themes/common/page.json.php new file mode 100755 index 0000000..89fe5e2 --- /dev/null +++ b/site/examples/web-app-starter/themes/common/page.json.php @@ -0,0 +1,12 @@ + a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +nav.nav-scrolled { + background: rgba(30, 41, 59, 0.95) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} + +h1::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: var(--bg-gradient); + border-radius: 3px; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px var(--success-bg); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px var(--error-bg); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #8b5cf6; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: var(--bg-color); +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/examples/web-app-starter/themes/dark/icon.png b/site/examples/web-app-starter/themes/dark/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/examples/web-app-starter/themes/dark/icon.png differ diff --git a/site/examples/web-app-starter/themes/dark/page.html.php b/site/examples/web-app-starter/themes/dark/page.html.php new file mode 100755 index 0000000..f3b3b1f --- /dev/null +++ b/site/examples/web-app-starter/themes/dark/page.html.php @@ -0,0 +1,21 @@ + + + + + +> + + false, + 'account_wrapper_class' => 'nav-account nav-menu', + )); ?> +
> + +
+ $embed_mode)); ?> + + + diff --git a/site/examples/web-app-starter/themes/light/css/style.css b/site/examples/web-app-starter/themes/light/css/style.css new file mode 100755 index 0000000..b872b44 --- /dev/null +++ b/site/examples/web-app-starter/themes/light/css/style.css @@ -0,0 +1,540 @@ +:root { + /* Light theme color palette */ + --bg-color: #f8fafc; + --bg-secondary: #ffffff; + --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --surface: #ffffff; + --surface-elevated: #ffffff; + --surface-hover: #f1f5f9; + --primary: #3b82f6; + --primary-dark: #1d4ed8; + --primary-light: #93c5fd; + --secondary: #8b5cf6; + --accent: #06b6d4; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --border: #e2e8f0; + --border-hover: #cbd5e1; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + + /* Light theme specific variables */ + --glass-bg: rgba(255, 255, 255, 0.95); + --glass-border: rgba(0, 0, 0, 0.1); + --overlay: rgba(0, 0, 0, 0.3); + --success: #10b981; + --success-bg: #dcfce7; + --success-border: #16a34a; + --success-text: #065f46; + --warning: #f59e0b; + --warning-bg: #fef3c7; + --warning-border: #d97706; + --warning-text: #92400e; + --error: #ef4444; + --error-bg: #fee2e2; + --error-border: #dc2626; + --error-text: #991b1b; + --info: #3b82f6; + --info-bg: #dbeafe; + --info-border: #2563eb; + --info-text: #1e40af; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* Links */ +a { + color: var(--primary); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--primary-dark); + text-decoration: underline; +} + +/* Modern Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +nav > a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + margin-right: 0.5rem; + font-size: 0.95rem; +} + +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} + +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary-dark); +} + +/* Navigation scroll state */ +nav.nav-scrolled { + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +/* Main Content Area */ +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2.5rem; + background: var(--surface); + padding: 1.75rem 2rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--border); + position: relative; + overflow: hidden; +} + +h1::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--bg-gradient); +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #7c3aed; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/examples/web-app-starter/themes/light/icon.png b/site/examples/web-app-starter/themes/light/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/examples/web-app-starter/themes/light/icon.png differ diff --git a/site/examples/web-app-starter/themes/light/img/favicon.png b/site/examples/web-app-starter/themes/light/img/favicon.png new file mode 100755 index 0000000..6653d41 Binary files /dev/null and b/site/examples/web-app-starter/themes/light/img/favicon.png differ diff --git a/site/examples/web-app-starter/themes/light/page.html.php b/site/examples/web-app-starter/themes/light/page.html.php new file mode 100755 index 0000000..1df8f11 --- /dev/null +++ b/site/examples/web-app-starter/themes/light/page.html.php @@ -0,0 +1,19 @@ + + + + + +> + + false)); ?> +
> + +
+ $embed_mode)); ?> + + + + diff --git a/site/examples/web-app-starter/themes/localfirst/css/style.css b/site/examples/web-app-starter/themes/localfirst/css/style.css new file mode 100644 index 0000000..088ce45 --- /dev/null +++ b/site/examples/web-app-starter/themes/localfirst/css/style.css @@ -0,0 +1,382 @@ +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.28); } + 50% { opacity: 0.6; box-shadow: 0 0 4px var(--accent), 0 0 8px rgba(255,107,26,0.16); } +} + +:root { + --brand-cyan: #00b7f9; + --brand-cyan-soft: #58d2ff; + --brand-cyan-bright: #b6eeff; + --brand-cyan-deep: #0088bb; + --brand-orange: #ff6b1a; + --brand-orange-soft: #ff9f6a; + --brand-orange-bright: #ffd0b3; + --bg-color: #020913; + --bg-secondary: #07131f; + --surface: rgba(7, 19, 31, 0.9); + --surface-elevated: rgba(10, 28, 43, 0.96); + --surface-hover: rgba(0, 183, 249, 0.08); + --card: rgba(7, 19, 31, 0.84); + --text-primary: var(--brand-cyan-soft); + --text-secondary: rgba(139, 222, 255, 0.82); + --text-muted: rgba(101, 161, 188, 0.78); + --primary: var(--brand-cyan); + --primary-light: rgba(0, 183, 249, 0.18); + --primary-dark: var(--brand-cyan-deep); + --secondary: var(--brand-cyan-soft); + --accent: var(--brand-orange); + --success: #34d399; + --success-bg: rgba(52, 211, 153, 0.1); + --success-border: rgba(52, 211, 153, 0.3); + --success-text: #7ef0bb; + --warning: var(--brand-orange); + --warning-bg: rgba(255, 107, 26, 0.12); + --warning-border: rgba(255, 107, 26, 0.32); + --warning-text: #ffc29f; + --error: #fb7185; + --error-bg: rgba(251, 113, 133, 0.12); + --error-border: rgba(251, 113, 133, 0.3); + --error-text: #fecdd3; + --info: var(--brand-cyan); + --info-bg: rgba(0, 183, 249, 0.1); + --info-border: rgba(0, 183, 249, 0.28); + --info-text: var(--brand-cyan-bright); + --border: rgba(67, 149, 182, 0.22); + --border-hover: rgba(98, 205, 246, 0.36); + --shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.18); + --shadow-md: 0 14px 32px rgba(0, 0, 0, 0.24); + --shadow-lg: 0 18px 38px rgba(0, 0, 0, 0.28); + --shadow-xl: 0 22px 48px rgba(0, 0, 0, 0.34); + --radius: 10px; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 20px; + --font-sans: 'B612', 'Segoe UI', sans-serif; + --font-mono: 'B612 Mono', ui-monospace, monospace; +} + +* { box-sizing: border-box; margin: 0; } +html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +body { + font-family: var(--font-sans); + font-size: 14px; + background: var(--bg-color); + color: var(--text-primary); + min-height: 100vh; +} +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 10% 0%, rgba(0,183,249,0.12) 0%, transparent 60%), + radial-gradient(ellipse 60% 50% at 90% 100%, rgba(255,107,26,0.08) 0%, transparent 60%); + pointer-events: none; + z-index: 0; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +body.admin-page { overflow: hidden; height: 100vh; } +.admin-shell { display: flex; height: 100vh; overflow: hidden; position: relative; z-index: 1; } +.admin-nav { + width: 210px; + flex-shrink: 0; + background: + linear-gradient(180deg, rgba(5, 16, 26, 0.98), rgba(2, 10, 18, 0.98)), + linear-gradient(180deg, rgba(0, 174, 240, 0.06), rgba(255, 107, 26, 0.04)); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0 0 16px; +} +.admin-nav-header { + border-bottom: 1px solid var(--border); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 9px; + padding: 10px 10px 12px; +} +.admin-nav-title { display: block; width: 100%; } +.admin-nav-logo-img { display: block; width: 100%; height: auto; } +.admin-nav-item { + display: block; + padding: 9px 16px 9px 14px; + text-decoration: none; + color: var(--text-secondary); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; + border-left: 3px solid transparent; +} +.admin-nav-item:hover { + color: var(--brand-cyan-bright); + background: linear-gradient(90deg, rgba(0, 174, 240, 0.12), rgba(255, 107, 26, 0.03)); + text-decoration: none; +} +.admin-nav-item.active { + color: var(--accent); + border-left-color: var(--accent); + background: linear-gradient(90deg, rgba(255, 107, 26, 0.16), rgba(255, 107, 26, 0.04)); + box-shadow: inset 0 0 0 1px rgba(255, 107, 26, 0.08); +} + +.admin-main { + flex: 1; + min-width: 0; + position: relative; + display: flex; + flex-direction: column; +} + +.admin-toolbar { + position: fixed; + top: 16px; + right: 20px; + z-index: 40; + display: flex; + justify-content: flex-end; + pointer-events: none; +} + +.admin-account-card { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(7, 19, 31, 0.78); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} +.admin-account-name { + color: var(--brand-cyan-bright); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} +.admin-account-links { + display: inline-flex; + gap: 10px; + align-items: center; +} +.admin-account-links a { + color: var(--accent); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} + +.admin-content { + flex: 1; + overflow-y: auto; + padding: 16px 20px 28px; + position: relative; + min-width: 0; +} + +h1 { + font-size: 18px; + font-weight: 600; + letter-spacing: 1.4px; + text-transform: uppercase; + color: var(--brand-cyan-bright); + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 14px; +} +h1::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background: var(--accent); + border-radius: 2px; + box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.24); + animation: pulse-glow 2.5s ease-in-out infinite; +} +h2, h3, h4, h5, h6 { + color: var(--brand-cyan-bright); + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} +p { margin: 0 0 12px; color: var(--text-secondary); } + +.card, +.block, +.dashboard-panel, +#content > div:not(.ws-app):not(.demo-section) { + background: var(--card); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + position: relative; + transition: border-color 0.25s ease, box-shadow 0.25s ease; + margin-bottom: 14px; +} +.card::before, +.block::before, +.dashboard-panel::before, +#content > div:not(.ws-app):not(.demo-section)::before { + content: ''; + position: absolute; + top: 0; + left: 12px; + right: 12px; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.55; +} +.card:hover, +.block:hover, +.dashboard-panel:hover, +#content > div:not(.ws-app):not(.demo-section):hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-md); +} + +form { max-width: 760px; } +form > div { display: grid; gap: 6px; margin-bottom: 12px; } +label { + color: var(--text-muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} +input, select, textarea { + width: 100%; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(3, 7, 18, 0.55); + color: var(--brand-cyan-bright); + padding: 10px 12px; + font: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--border-hover); + box-shadow: 0 0 0 3px rgba(0, 183, 249, 0.12); +} +button, .btn, input[type="submit"] { + border: 1px solid var(--border); + background: rgba(255, 107, 26, 0.12); + color: var(--brand-orange-bright); + border-radius: var(--radius); + padding: 10px 14px; + font: inherit; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: rgba(255, 107, 26, 0.18); + border-color: rgba(255, 107, 26, 0.42); + text-decoration: none; +} +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: var(--radius); + padding: 10px 12px; + border: 1px solid var(--border); + background: rgba(0, 183, 249, 0.08); + color: var(--text-secondary); + margin-bottom: 12px; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: rgba(3, 7, 18, 0.42); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + background: rgba(0, 183, 249, 0.08); +} +tr:last-child td { border-bottom: none; } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 12px; + background: rgba(3, 7, 18, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +@media (max-width: 980px) { + .admin-shell { flex-direction: column; height: auto; min-height: 100vh; } + .admin-nav { + width: 100%; + flex-direction: row; + overflow-x: auto; + padding-bottom: 0; + } + .admin-nav-header { min-width: 180px; margin-bottom: 0; border-bottom: 0; border-right: 1px solid var(--border); } + .admin-nav-item { white-space: nowrap; border-left: 0; border-bottom: 3px solid transparent; } + .admin-nav-item.active { border-bottom-color: var(--accent); border-left-color: transparent; } + .admin-toolbar { position: static; justify-content: flex-start; margin: 16px 20px 0; } + .admin-content { padding-top: 12px; } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/localfirst/icon.png b/site/examples/web-app-starter/themes/localfirst/icon.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/examples/web-app-starter/themes/localfirst/icon.png differ diff --git a/site/examples/web-app-starter/themes/localfirst/img/local_first_logo.png b/site/examples/web-app-starter/themes/localfirst/img/local_first_logo.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/examples/web-app-starter/themes/localfirst/img/local_first_logo.png differ diff --git a/site/examples/web-app-starter/themes/localfirst/page.html.php b/site/examples/web-app-starter/themes/localfirst/page.html.php new file mode 100644 index 0000000..6c9db4d --- /dev/null +++ b/site/examples/web-app-starter/themes/localfirst/page.html.php @@ -0,0 +1,38 @@ + + + + + + + false)); ?> +
+ +
+
+ 'admin-account-card', + 'links_wrapper_class' => 'admin-account-links', + 'name_class' => 'admin-account-name', + )); ?> +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/portal-dark/css/style.css b/site/examples/web-app-starter/themes/portal-dark/css/style.css new file mode 100644 index 0000000..90001c3 --- /dev/null +++ b/site/examples/web-app-starter/themes/portal-dark/css/style.css @@ -0,0 +1,313 @@ +:root { + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --surface: #1e293b; + --surface-elevated: #334155; + --surface-hover: #475569; + --primary: #60a5fa; + --primary-dark: #3b82f6; + --primary-light: #93c5fd; + --secondary: #a78bfa; + --accent: #22d3ee; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --border: #334155; + --border-hover: #475569; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + --glass-bg: rgba(30, 41, 59, 0.8); + --success: #10b981; + --success-bg: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.3); + --success-text: #10b981; + --warning: #f59e0b; + --warning-bg: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.3); + --warning-text: #f59e0b; + --error: #ef4444; + --error-bg: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.3); + --error-text: #ef4444; + --info: #3b82f6; + --info-bg: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.3); + --info-text: #3b82f6; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'SFMono-Regular', Consolas, monospace; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; } +body { + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +a { color: var(--primary); text-decoration: none; transition: all 0.2s ease; } +a:hover { color: var(--primary-light); text-decoration: underline; } + +::selection { + background: rgba(96, 165, 250, 0.3); + color: var(--text-primary); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 50% at 20% -10%, rgba(96, 165, 250, 0.05) 0%, transparent 60%), + radial-gradient(ellipse 60% 40% at 85% 110%, rgba(34, 211, 238, 0.04) 0%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} +nav::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.3; +} + +.nav-menu { display: flex; align-items: center; gap: 0.25rem; } +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding: 1rem 1.5rem; + margin: 0.5rem 0.25rem; +} +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary); +} +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + font-size: 0.95rem; + margin-right: 0.5rem; +} +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid var(--border); +} +.nav-account .nav-avatar { width: 30px; height: 30px; } +.profile-avatar { width: 96px; height: 96px; } + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 3px; +} +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } + +#content > div:not(.ws-app), .block, .card, .dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover, .dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +form { max-width: 600px; margin: 0 auto; } +form > div { display: flex; flex-direction: column; margin-bottom: 1.5rem; gap: 0.5rem; } +label { font-weight: 500; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; } +input, select, textarea { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); + text-decoration: none; +} + +.banner { + border-radius: var(--radius); + padding: 0.8rem 1rem; + border: 1px solid var(--border); + margin-bottom: 1rem; + background: rgba(255,255,255,0.03); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--border); + background: rgba(255,255,255,0.02); + border-radius: var(--radius); + overflow: hidden; +} +th, td { padding: 0.8rem 0.9rem; border-bottom: 1px solid var(--border); text-align: left; } +th { background: var(--surface-elevated); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(255,255,255,0.02); } +code, pre { font-family: var(--font-mono); } +pre { + padding: 1rem; + background: rgba(15, 23, 42, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + padding: 1.25rem 1rem; + color: var(--text-muted); + font-size: 0.875rem; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-secondary); + outline: none; + border: none; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} + +@media (max-width: 860px) { + nav { flex-wrap: wrap; padding-bottom: 0.6rem; } + .nav-account { width: 100%; margin-left: 0; justify-content: flex-start; } + #content { margin-top: 7.5rem; padding: 1rem 0.75rem; } + #content > div:not(.ws-app), .block, .card, .dashboard-panel { padding: 1.25rem; margin-bottom: 1rem; } + h1 { font-size: 2rem; padding: 1.25rem; } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/portal-dark/icon.png b/site/examples/web-app-starter/themes/portal-dark/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/examples/web-app-starter/themes/portal-dark/icon.png differ diff --git a/site/examples/web-app-starter/themes/portal-dark/page.html.php b/site/examples/web-app-starter/themes/portal-dark/page.html.php new file mode 100644 index 0000000..ae86edd --- /dev/null +++ b/site/examples/web-app-starter/themes/portal-dark/page.html.php @@ -0,0 +1,18 @@ + + + + + + + + 'nav-account nav-menu')); ?> +
> + +
+ $embed_mode)); ?> + + + \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/portal-light/css/style.css b/site/examples/web-app-starter/themes/portal-light/css/style.css new file mode 100644 index 0000000..141b15f --- /dev/null +++ b/site/examples/web-app-starter/themes/portal-light/css/style.css @@ -0,0 +1,392 @@ +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-italic.ttf") format("truetype"); + font-style: italic; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +:root { + --kg-blue: #0f68ad; + --kg-blue-dark: #0b4f85; + --kg-blue-light: #2d86ca; + --kg-bg: #e9e9ea; + --kg-surface: #ffffff; + --kg-border: rgba(0, 0, 0, 0.2); + --kg-text: #121417; + --kg-muted: #6c737b; + --kg-orange: #f08a00; + --kg-success: #149b2e; + --kg-danger: #e11818; + --topbar-height: 56px; + --bg-color: var(--kg-bg); + --bg-secondary: #ffffff; + --surface: var(--kg-surface); + --surface-elevated: #ffffff; + --surface-hover: #e8eef3; + --primary: var(--kg-blue); + --primary-dark: var(--kg-blue-dark); + --primary-light: var(--kg-blue-light); + --secondary: #596572; + --accent: var(--kg-orange); + --text-primary: var(--kg-text); + --text-secondary: #4b5560; + --text-muted: var(--kg-muted); + --border: var(--kg-border); + --border-hover: rgba(15, 104, 173, 0.3); + --success: var(--kg-success); + --success-bg: #e7f5ea; + --success-border: #86c88f; + --success-text: #0f6e22; + --warning: #c97a14; + --warning-bg: #fff4e4; + --warning-border: #f0c07b; + --warning-text: #92550a; + --error: var(--kg-danger); + --error-bg: #ffe7e7; + --error-border: #ea9999; + --error-text: #b80f0f; + --info: var(--primary); + --info-bg: #e8f2ff; + --info-border: #b9d4f3; + --info-text: #0b4f85; + --shadow-sm: 0 1px 2px rgba(18, 20, 23, 0.06); + --shadow-md: 0 8px 22px rgba(18, 20, 23, 0.06); + --shadow-lg: 0 16px 34px rgba(18, 20, 23, 0.1); + --shadow-xl: 0 20px 40px rgba(18, 20, 23, 0.14); + --radius: 8px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --font-sans: "B612", "Segoe UI", sans-serif; + --font-mono: "B612 Mono", "SFMono-Regular", Consolas, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body { min-height: 100%; } +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.4; + color: var(--text-primary); + background: var(--bg-color); +} +a { color: var(--primary-dark); text-decoration: none; } +a:hover { text-decoration: underline; } + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--topbar-height); + display: flex; + align-items: stretch; + background: var(--primary); + border-bottom: 1px solid var(--primary-dark); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); + z-index: 60; +} + +.nav-menu { + display: flex; + align-items: stretch; + overflow-x: auto; + white-space: nowrap; + flex: 1; +} + +.nav-menu > a { + display: inline-flex; + align-items: center; + padding: 0 0.9rem; + color: #fff; + border-right: 1px solid rgba(255, 255, 255, 0.16); + font-size: 14px; + font-weight: 700; + text-decoration: none; + background: transparent; +} + +.nav-menu > a:first-child { + font-size: 18px; + min-width: 250px; + background: rgba(0, 0, 0, 0.08); +} + +.nav-menu > a:hover, +.nav-menu > a:focus { + background: var(--primary-dark); + text-decoration: none; +} + +.nav-account { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0 0.7rem; + border-left: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.12); + flex-shrink: 0; +} + +.nav-account .account-name { + color: #d8f8df; + font-size: 14px; + font-weight: 700; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid rgba(255, 255, 255, 0.4); +} + +.nav-account .nav-avatar { + width: 30px; + height: 30px; +} + +.profile-avatar { + width: 96px; + height: 96px; + border-color: var(--border); +} + +.nav-account a { + color: #fff; + font-size: 14px; + font-weight: 700; + padding: 0.22rem 0.5rem; + border-radius: 3px; +} + +.nav-account a:hover { + background: rgba(255, 255, 255, 0.16); + text-decoration: none; +} + +#content { + margin-top: var(--topbar-height); + padding: 1rem; + min-height: calc(100vh - var(--topbar-height)); +} + +#content > div:not(.ws-app), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: 1.25rem; + margin-bottom: 1rem; + transition: box-shadow 0.2s ease, border-color 0.2s ease; +} + +#content > div:not(.ws-app):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: rgba(15, 104, 173, 0.2); +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + color: #252b31; + margin-bottom: 0.7rem; +} +h1 { font-size: 22px; padding-bottom: 0.5rem; border-bottom: 2px solid #d2d3d7; position: relative; } +h1::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 50px; + height: 2px; + background: var(--primary); + border-radius: 2px; +} +h2 { font-size: 18px; } +h3 { font-size: 16px; } +h4, h5, h6 { font-size: 14px; } + +p { margin: 0.4rem 0 0.85rem; } + +form { max-width: 780px; } +form > div { margin-bottom: 0.8rem; } +label { + display: block; + font-weight: 700; + font-size: 14px; + color: #3f464f; + margin-bottom: 0.3rem; +} +input, select, textarea { + width: 100%; + min-height: 36px; + padding: 0 0.7rem; + border: 1px solid #a8aaaf; + border-radius: 4px; + background: #fff; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} +textarea { min-height: 100px; padding: 0.5rem 0.7rem; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-light); + box-shadow: 0 0 0 1px var(--primary-light); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 0.9rem; + border: 1px solid #7f8084; + border-radius: 4px; + background: #f7f7f7; + color: #1a1e23; + font-size: 14px; + font-weight: 700; + cursor: pointer; + font-family: inherit; +} +button:hover, .btn:hover, input[type="submit"]:hover { background: #ececef; text-decoration: none; } +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: 4px; + padding: 0.65rem 0.8rem; + border: 1px solid #b4b5ba; + margin-bottom: 0.8rem; + background: #fafafa; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 0.7rem 0.8rem; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { background: #e7e7e8; font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 0, 0, 0.02); } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.9rem; + background: #fff; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + background: #ffffff; + color: var(--text-muted); + padding: 1rem 1rem 1.5rem; + font-size: 13px; +} +.footer-inner { max-width: 1200px; margin: 0 auto; } + +input[type="range"] { + height: 6px; + padding: 0; + min-height: auto; + border: none; + background: #d4d5d9; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +::selection { + background: rgba(15, 104, 173, 0.2); + color: var(--text-primary); +} + +@media (max-width: 860px) { + nav { + height: auto; + flex-direction: column; + } + .nav-menu { + flex-wrap: wrap; + } + .nav-menu > a:first-child { + min-width: 0; + } + .nav-account { + justify-content: flex-start; + padding: 0.6rem 0.7rem; + } + #content { + margin-top: 104px; + padding: 0.75rem; + } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/portal-light/icon.png b/site/examples/web-app-starter/themes/portal-light/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/examples/web-app-starter/themes/portal-light/icon.png differ diff --git a/site/examples/web-app-starter/themes/portal-light/page.html.php b/site/examples/web-app-starter/themes/portal-light/page.html.php new file mode 100644 index 0000000..0382b0f --- /dev/null +++ b/site/examples/web-app-starter/themes/portal-light/page.html.php @@ -0,0 +1,18 @@ + + + + + + + + +
> + +
+ $embed_mode, 'inner_class' => 'footer-inner')); ?> + + + \ No newline at end of file diff --git a/site/examples/web-app-starter/themes/retro-gaming/css/style.css b/site/examples/web-app-starter/themes/retro-gaming/css/style.css new file mode 100644 index 0000000..f187fa6 --- /dev/null +++ b/site/examples/web-app-starter/themes/retro-gaming/css/style.css @@ -0,0 +1,523 @@ +/* ──────────────────────────────────────────────────────── + RETRO GAMING THEME + CRT scanlines · pixel font · neon glow · 8-bit palette + ──────────────────────────────────────────────────────── */ + +@font-face { + font-family: 'Press Start 2P'; + src: url('../../common/fonts/press-start-2p/press-start-2p-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} + +/* ── tokens ─────────────────────────────────────────── */ +:root { + /* background */ + --bg-color: #0a0a1a; + --bg-secondary: #10102a; + --surface: #12122e; + --surface-elevated: #1a1a3e; + --surface-hover: #22224e; + + /* neon palette */ + --primary: #00ff88; + --primary-dark: #00cc6a; + --primary-light: #66ffbb; + --secondary: #ff00ff; + --accent: #ffff00; + --text-primary: #e0e0ff; + --text-secondary: #a0a0cc; + --text-muted: #6868a0; + + /* borders, shadows */ + --border: #2a2a5a; + --border-hover: #3a3a7a; + --shadow-sm: 0 2px 0 #000; + --shadow-md: 0 4px 0 #000, 0 0 12px rgba(0, 255, 136, 0.08); + --shadow-lg: 0 6px 0 #000, 0 0 24px rgba(0, 255, 136, 0.12); + --shadow-xl: 0 8px 0 #000, 0 0 40px rgba(0, 255, 136, 0.16); + + /* semantic */ + --success: #00ff88; + --success-bg: rgba(0, 255, 136, 0.1); + --success-border: rgba(0, 255, 136, 0.3); + --success-text: #00ff88; + --warning: #ffff00; + --warning-bg: rgba(255, 255, 0, 0.1); + --warning-border: rgba(255, 255, 0, 0.3); + --warning-text: #ffff00; + --error: #ff3366; + --error-bg: rgba(255, 51, 102, 0.1); + --error-border: rgba(255, 51, 102, 0.3); + --error-text: #ff3366; + --info: #00ccff; + --info-bg: rgba(0, 204, 255, 0.1); + --info-border: rgba(0, 204, 255, 0.3); + --info-text: #00ccff; + + /* geometry */ + --radius: 0px; + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 2px; + --radius-xl: 4px; + + /* typography */ + --font-sans: 'Press Start 2P', monospace; + --font-mono: 'Press Start 2P', monospace; +} + +/* ── reset ──────────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; image-rendering: pixelated; } + +body { + font-family: var(--font-sans); + font-size: 10px; + line-height: 2; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: none; + -moz-osx-font-smoothing: unset; +} + +/* ── CRT scanline overlay ───────────────────────────── */ +body::before { + content: ''; + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.15) 2px, + rgba(0, 0, 0, 0.15) 4px + ); + pointer-events: none; + z-index: 9000; +} + +/* subtle vignette */ +body::after { + content: ''; + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.5) 100%); + pointer-events: none; + z-index: 8999; +} + +::selection { + background: var(--primary); + color: var(--bg-color); +} + +/* ── links ──────────────────────────────────────────── */ +a { + color: var(--primary); + text-decoration: none; + transition: color 0.1s step-end; +} +a:hover { + color: var(--accent); + text-decoration: none; + text-shadow: 0 0 6px var(--accent); +} + +/* ── navigation ─────────────────────────────────────── */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--bg-secondary); + border-bottom: 3px solid var(--primary); + padding: 0 0.5rem; + display: flex; + align-items: center; + box-shadow: 0 3px 0 #000, 0 6px 20px rgba(0, 255, 136, 0.15); +} + +.nav-menu { + display: flex; + align-items: center; + gap: 0; + flex: 1; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; +} +.nav-menu::-webkit-scrollbar { display: none; } + +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.75rem 0.8rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 8px; + letter-spacing: 0.5px; + text-transform: uppercase; + border-right: 1px solid var(--border); + transition: background 0.1s step-end, color 0.1s step-end; +} + +.nav-menu > a:first-child { + color: var(--primary); + text-shadow: 0 0 8px rgba(0, 255, 136, 0.5); + font-size: 10px; + padding: 0.75rem 1rem; + border-right: 2px solid var(--primary); +} + +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--accent); + text-shadow: 0 0 6px var(--accent); + text-decoration: none; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0; + flex: 0 0 auto; + overflow: visible; +} +.nav-account a { + border-right: none; + border-left: 1px solid var(--border); +} + +.nav-account .account-name { + color: var(--primary); + font-size: 8px; + padding: 0 0.5rem; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 0; + border: 2px solid var(--primary); + image-rendering: pixelated; +} +.nav-account .nav-avatar { width: 24px; height: 24px; } +.profile-avatar { width: 64px; height: 64px; } + +/* ── main content ───────────────────────────────────── */ +#content { + margin-top: 3.5rem; + min-height: calc(100vh - 7rem); + padding: 1.5rem 1rem; + max-width: 1100px; + margin-left: auto; + margin-right: auto; + position: relative; + z-index: 1; +} + +/* ── typography ─────────────────────────────────────── */ +h1, h2, h3, h4, h5, h6 { + font-weight: 400; + line-height: 2; + margin-bottom: 0.75rem; + color: var(--primary); + text-shadow: 0 0 10px rgba(0, 255, 136, 0.35); + text-transform: uppercase; +} + +h1 { + font-size: 16px; + padding: 0.75rem 0; + border-bottom: 3px double var(--primary); + margin-bottom: 1.25rem; + position: relative; +} +h1::before { + content: '►'; + margin-right: 0.5rem; + animation: blink-cursor 1s step-end infinite; +} +h2 { font-size: 12px; color: var(--info-text); text-shadow: 0 0 8px rgba(0, 204, 255, 0.3); } +h3 { font-size: 10px; color: var(--secondary); text-shadow: 0 0 8px rgba(255, 0, 255, 0.3); } +h4, h5, h6 { font-size: 10px; } + +p { margin: 0 0 0.75rem; } + +@keyframes blink-cursor { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* ── cards / blocks ─────────────────────────────────── */ +#content > div:not(.ws-app):not(.demo-section), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 2px solid var(--border); + padding: 1rem; + margin-bottom: 1.25rem; + box-shadow: var(--shadow-md); + position: relative; + transition: border-color 0.1s step-end; +} + +#content > div:not(.ws-app):not(.demo-section)::before, +.block::before, +.card::before, +.dashboard-panel::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, var(--primary), var(--secondary), var(--accent)); +} + +#content > div:not(.ws-app):not(.demo-section):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-lg); +} + +/* ── forms ──────────────────────────────────────────── */ +form { max-width: 640px; } +form > div { display: grid; gap: 0.35rem; margin-bottom: 1rem; } + +label { + color: var(--accent); + font-size: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +input, select, textarea { + width: 100%; + padding: 0.6rem 0.7rem; + border: 2px solid var(--border); + background: var(--bg-color); + color: var(--primary); + font-family: var(--font-mono); + font-size: 10px; + caret-color: var(--primary); +} +textarea { min-height: 100px; } + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary), 0 0 12px rgba(0, 255, 136, 0.2); +} + +input::placeholder, textarea::placeholder { + color: var(--text-muted); + opacity: 1; +} + +/* ── buttons ────────────────────────────────────────── */ +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1.2rem; + border: 2px solid var(--primary); + background: transparent; + color: var(--primary); + font-family: var(--font-sans); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.1s step-end; + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--primary); +} + +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary); + color: var(--bg-color); + text-shadow: none; + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--primary); + transform: translate(2px, 2px); + text-decoration: none; +} + +button:active, .btn:active, input[type="submit"]:active { + box-shadow: none; + transform: translate(4px, 4px); +} + +.btn-secondary { + border-color: var(--secondary); + color: var(--secondary); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--secondary); +} +.btn-secondary:hover { + background: var(--secondary); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--secondary); +} + +.btn-outline { + border-color: var(--accent); + color: var(--accent); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--accent); +} +.btn-outline:hover { + background: var(--accent); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--accent); +} + +/* ── range slider ───────────────────────────────────── */ +input[type="range"] { + height: 8px; + padding: 0; + border: 2px solid var(--border); + background: var(--bg-color); + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} +input[type="range"]::-moz-range-thumb { + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + border-radius: 0; + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} + +/* ── banners ────────────────────────────────────────── */ +.banner { + padding: 0.6rem 0.8rem; + border: 2px solid var(--border); + margin-bottom: 0.75rem; + background: var(--surface); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +/* ── tables ─────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border: 2px solid var(--border); +} +th, td { + padding: 0.5rem 0.65rem; + border-bottom: 1px solid var(--border); + text-align: left; + font-size: 9px; +} +th { + background: var(--surface-elevated); + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid var(--accent); +} +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 255, 136, 0.04); } + +/* ── code ───────────────────────────────────────────── */ +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.75rem; + background: var(--bg-color); + border: 2px solid var(--border); + overflow: auto; + color: var(--primary); +} + +/* ── footer ─────────────────────────────────────────── */ +footer { + border-top: 3px solid var(--primary); + padding: 1rem; + color: var(--text-muted); + font-size: 8px; + background: var(--bg-secondary); + box-shadow: 0 -3px 0 #000; + position: relative; + z-index: 1; +} +footer::before { + content: '// '; + color: var(--text-muted); +} + +/* ── pixel decoration classes (for page template) ──── */ +.retro-stars { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(1px 1px at 10% 15%, var(--primary), transparent), + radial-gradient(1px 1px at 30% 45%, var(--accent), transparent), + radial-gradient(1px 1px at 55% 20%, var(--secondary), transparent), + radial-gradient(1px 1px at 70% 65%, var(--info-text), transparent), + radial-gradient(1px 1px at 85% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 15% 75%, var(--accent), transparent), + radial-gradient(1px 1px at 45% 85%, var(--secondary), transparent), + radial-gradient(1px 1px at 90% 80%, var(--primary), transparent), + radial-gradient(1px 1px at 25% 55%, var(--info-text), transparent), + radial-gradient(1px 1px at 60% 50%, var(--accent), transparent), + radial-gradient(1px 1px at 5% 40%, var(--error-text), transparent), + radial-gradient(1px 1px at 75% 10%, var(--accent), transparent), + radial-gradient(1px 1px at 40% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 95% 55%, var(--secondary), transparent), + radial-gradient(1px 1px at 50% 70%, var(--primary), transparent), + radial-gradient(1px 1px at 20% 90%, var(--info-text), transparent); + animation: twinkle 4s ease-in-out infinite alternate; +} +@keyframes twinkle { + 0% { opacity: 0.4; } + 100% { opacity: 0.8; } +} + +/* ── responsive ─────────────────────────────────────── */ +@media (max-width: 860px) { + nav { flex-wrap: wrap; } + .nav-menu { overflow-x: auto; white-space: nowrap; } + .nav-account { + width: 100%; + margin-left: 0; + border-top: 1px solid var(--border); + justify-content: flex-start; + } + #content { margin-top: 5rem; padding: 1rem 0.75rem; } + h1 { font-size: 12px; } + h2 { font-size: 10px; } +} + +/* ── focus accessibility ────────────────────────────── */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* ── print ──────────────────────────────────────────── */ +@media print { + nav, footer, .retro-stars { display: none; } + body::before, body::after { display: none; } + #content { margin-top: 0; } + * { background: white !important; color: black !important; box-shadow: none !important; text-shadow: none !important; } +} diff --git a/site/examples/web-app-starter/themes/retro-gaming/icon.png b/site/examples/web-app-starter/themes/retro-gaming/icon.png new file mode 100644 index 0000000..12dfe2c Binary files /dev/null and b/site/examples/web-app-starter/themes/retro-gaming/icon.png differ diff --git a/site/examples/web-app-starter/themes/retro-gaming/page.html.php b/site/examples/web-app-starter/themes/retro-gaming/page.html.php new file mode 100644 index 0000000..acf2c11 --- /dev/null +++ b/site/examples/web-app-starter/themes/retro-gaming/page.html.php @@ -0,0 +1,22 @@ + + + + + +> +
+ + false, + 'account_wrapper_class' => 'nav-account nav-menu', + )); ?> +
> + +
+ $embed_mode)); ?> + + + diff --git a/site/examples/web-app-starter/views/account/login.php b/site/examples/web-app-starter/views/account/login.php new file mode 100644 index 0000000..5d32d40 --- /dev/null +++ b/site/examples/web-app-starter/views/account/login.php @@ -0,0 +1,32 @@ + + + + + Login + + +

Login

+ +
+ +
+
+
+ +
+

Register

+ + diff --git a/site/examples/web-app-starter/views/account/logout.php b/site/examples/web-app-starter/views/account/logout.php new file mode 100644 index 0000000..1a91ca9 --- /dev/null +++ b/site/examples/web-app-starter/views/account/logout.php @@ -0,0 +1,4 @@ + + + + + Profile + + +

Profile

+

Email:

+

Roles:

+

Created:

+

Logout

+ + diff --git a/site/examples/web-app-starter/views/account/register.php b/site/examples/web-app-starter/views/account/register.php new file mode 100644 index 0000000..0fad35b --- /dev/null +++ b/site/examples/web-app-starter/views/account/register.php @@ -0,0 +1,36 @@ + $email, 'password' => $password]); + if ($res['result']) { + $success = true; + } else { + $errors[] = $res['message']; + } +} + +?> + + + + Register + + +

Register

+ +
Registration successful. Log in
+ + +
+ +
+
+
+ +
+ + diff --git a/site/examples/web-app-starter/views/auth/callback.php b/site/examples/web-app-starter/views/auth/callback.php new file mode 100644 index 0000000..ee30b64 --- /dev/null +++ b/site/examples/web-app-starter/views/auth/callback.php @@ -0,0 +1,485 @@ + [ + 'client_id' => 'YOUR_GOOGLE_CLIENT_ID', + 'client_secret' => 'YOUR_GOOGLE_CLIENT_SECRET', + 'token_url' => 'https://oauth2.googleapis.com/token', + 'userinfo_url' => 'https://www.googleapis.com/oauth2/v2/userinfo', + 'redirect_uri' => URL::Link('auth/callback') + ] +]; + +// Check for OAuth errors +if ($error) { + $error_message = $error_description ?: $error; + URL::$fragments['error'] = "OAuth Error: " . htmlspecialchars($error_message); + URL::$fragments['error_type'] = 'oauth_error'; +} else if ($code && $state) { + // Determine which OAuth provider this is for (you'd need to store this during the auth initiation) + $service = $_SESSION['oauth_service'] ?? 'google'; + + if (!isset($oauth_config[$service])) { + URL::$fragments['error'] = "Unknown OAuth service: $service"; + URL::$fragments['error_type'] = 'invalid_service'; + } else { + $config = $oauth_config[$service]; + + // Check if configuration is complete + if ($config['client_id'] === 'YOUR_GOOGLE_CLIENT_ID' || + $config['client_secret'] === 'YOUR_GOOGLE_CLIENT_SECRET') { + + URL::$fragments['demo_mode'] = true; + URL::$fragments['success'] = "OAuth callback received successfully! (Demo Mode)"; + URL::$fragments['code'] = substr($code, 0, 20) . '...'; + URL::$fragments['state'] = $state; + URL::$fragments['service'] = $service; + } else { + // STEP 1: Exchange authorization code for access token + $token_response = HTTP::post($config['token_url'], [ + 'client_id' => $config['client_id'], + 'client_secret' => $config['client_secret'], + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $config['redirect_uri'] + ]); + + if (!$token_response['success']) { + URL::$fragments['error'] = "Token exchange failed: " . ($token_response['error'] ?? 'Unknown error'); + URL::$fragments['error_type'] = 'token_exchange_failed'; + URL::$fragments['debug'] = $token_response; + } else { + $tokens = $token_response['data']; + $access_token = $tokens['access_token']; + + // STEP 2: Get user profile information + $profile_response = HTTP::get_with_token($config['userinfo_url'], $access_token); + + if (!$profile_response['success']) { + URL::$fragments['error'] = "Failed to get user profile: " . ($profile_response['error'] ?? 'Unknown error'); + URL::$fragments['error_type'] = 'profile_fetch_failed'; + URL::$fragments['debug'] = $profile_response; + } else { + $profile = $profile_response['data']; + + // STEP 3: Create or login user + // This is where you'd typically: + // 1. Check if user exists by email + // 2. Create new user if doesn't exist + // 3. Update user profile with OAuth data + // 4. Set session variables + + // For demo purposes, just set session data + $_SESSION['user_id'] = $profile['id']; + $_SESSION['user_email'] = $profile['email']; + $_SESSION['user_name'] = $profile['name']; + $_SESSION['user_picture'] = $profile['picture'] ?? null; + $_SESSION['oauth_provider'] = $service; + $_SESSION['logged_in_at'] = time(); + + URL::$fragments['success'] = "Successfully logged in with " . ucfirst($service) . "!"; + URL::$fragments['user_profile'] = $profile; + URL::$fragments['redirect_to'] = 'dashboard'; // Where to redirect after success + } + } + } + } + + // Clean up temporary OAuth session data + unset($_SESSION['oauth_service']); + unset($_SESSION['oauth_state']); +} else { + URL::$fragments['error'] = "Invalid OAuth callback - missing required parameters"; + URL::$fragments['error_type'] = 'invalid_callback'; +} + +?> + +
+
+ +
+
+

Authentication Failed

+

+ +
+ +
+
+

Authentication Successful

+

+ + + + + + + +
+

Demo Mode - Next Steps for Implementation:

+
+

Service:

+

Code:

+

State:

+
+ +
    +
  1. Configure OAuth credentials +
    + Replace 'YOUR_GOOGLE_CLIENT_ID' and 'YOUR_GOOGLE_CLIENT_SECRET' with actual values +
    +
  2. +
  3. Exchange authorization code for access token
  4. +
  5. Use access token to get user profile
  6. +
  7. Create or login user account
  8. +
  9. Set session variables
  10. +
  11. Redirect to dashboard/profile
  12. +
+ +
+

Complete Implementation Example:

+
+
+
+
+
+
+ oauth-handler.php +
+
 $client_id,
+    "client_secret" => $client_secret, 
+    "code" => $code,
+    "grant_type" => "authorization_code",
+    "redirect_uri" => $redirect_uri
+]);
+
+// Get user profile
+$profile_response = HTTP::get_with_token(
+    "https://www.googleapis.com/oauth2/v2/userinfo",
+    $token_response["data"]["access_token"]
+);
+
+// Login/create user
+$profile = $profile_response["data"];
+$user = User::FindByEmail($profile["email"]) ?: User::Create([
+    "email" => $profile["email"],
+    "name" => $profile["name"],
+    "picture" => $profile["picture"],
+    "oauth_provider" => "google",
+    "oauth_id" => $profile["id"]
+]);
+
+// Set session
+$_SESSION["user_id"] = $user["id"];
+$_SESSION["user_email"] = $user["email"];
+$_SESSION["logged_in_at"] = time();
+
+// Redirect to dashboard
+URL::Redirect("dashboard");') ?>
+
+
+
+ + + + + +
+

Debug Information

+
+
+ +
+ +
+
+ + + + diff --git a/site/examples/web-app-starter/views/auth/demo.php b/site/examples/web-app-starter/views/auth/demo.php new file mode 100644 index 0000000..0de8bb8 --- /dev/null +++ b/site/examples/web-app-starter/views/auth/demo.php @@ -0,0 +1,170 @@ + + +

Authentication Demo

+ +
+

OAuth Authentication

+

This component provides secure OAuth authentication with popular identity providers.

+ + 'Sign In to Your Account', + 'subtitle' => 'Choose your preferred authentication method to continue', + 'google_client_id' => 'YOUR_GOOGLE_CLIENT_ID', + 'github_client_id' => 'YOUR_GITHUB_CLIENT_ID', + 'discord_client_id' => 'YOUR_DISCORD_CLIENT_ID', + 'twitch_client_id' => 'YOUR_TWITCH_CLIENT_ID', + 'callback_url' => URL::Link('auth/callback'), + 'debug' => true // Enable debug mode to see OAuth details + ]) ?> +
+ +
+

Setup Instructions

+
+
+

1. Create OAuth Applications

+

Create OAuth applications with your preferred providers:

+ +

For all providers, add callback URL:

+
+ +
+

2. Configure OAuth Component

+

Update the OAuth component with your client IDs from each provider:

+
+
+
+
+
+
+ views/account.php +
+
<?php component('components/auth/oauth-client', [
+    'google_client_id' => 'your-google-client-id',
+    'github_client_id' => 'your-github-client-id',
+    'discord_client_id' => 'your-discord-client-id',
+    'twitch_client_id' => 'your-twitch-client-id',
+    'callback_url' => URL::Link('auth/callback')
+]); ?>
+
+

Note: Only providers with valid client IDs will appear as login options.

+
+ +
+

3. Implement Backend Handler

+

Create views/account/callback.php to handle the OAuth callback and exchange the authorization code for tokens:

+
+
+
+
+
+
+ views/account/callback.php +
+
<?php
+// Handle OAuth callback
+if ($_GET['code']) {
+    // Exchange code for access token
+    // Get user profile from provider
+    // Create/login user account
+    // Set session and redirect
+}
+?>
+
+
+
+
+ +
+

Built-in Provider Support

+

The OAuth component comes with built-in support for popular providers:

+ +
+
+

Google OAuth

+
+
+
+
+
+
+ google.config +
+
// Scope: openid, email, profile
+// Additional params: access_type=offline
+'google_client_id' => 'your-client-id'
+
+
+ +
+

GitHub OAuth

+
+
+
+
+
+
+ github.config +
+
// Scope: user:email
+// Additional params: allow_signup=true
+'github_client_id' => 'your-client-id'
+
+
+ +
+

Discord OAuth

+
+
+
+
+
+
+ discord.config +
+
// Scope: identify, email
+// Additional params: prompt=consent
+'discord_client_id' => 'your-client-id'
+
+
+ +
+

Twitch OAuth

+
+
+
+
+
+
+ twitch.config +
+
// Scope: user:read:email
+// Additional params: force_verify=true
+'twitch_client_id' => 'your-client-id'
+
+
+
+
+ +
+

Current Session Status

+ +
+

✓ Logged In

+

User ID:

+ +

Email:

+ +
+ +
+

ⓘ Not Logged In

+

Use the OAuth component above to sign in with Google, GitHub, Discord, or Twitch.

+
+ +
\ No newline at end of file diff --git a/site/examples/web-app-starter/views/auth/store-oauth-session.php b/site/examples/web-app-starter/views/auth/store-oauth-session.php new file mode 100644 index 0000000..bb4a4d1 --- /dev/null +++ b/site/examples/web-app-starter/views/auth/store-oauth-session.php @@ -0,0 +1,21 @@ + 'success']); + } else { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Invalid input']); + } +} else { + http_response_code(405); + echo json_encode(['status' => 'error', 'message' => 'Method not allowed']); +} diff --git a/site/examples/web-app-starter/views/dark-demo.php b/site/examples/web-app-starter/views/dark-demo.php new file mode 100755 index 0000000..9e55459 --- /dev/null +++ b/site/examples/web-app-starter/views/dark-demo.php @@ -0,0 +1,165 @@ + 'Dark Theme Demo', + 'subtitle' => 'Experience our beautiful dark mode with enhanced readability and modern aesthetics.', + 'cta_text' => 'Explore Features', + 'cta_link' => '#features' +]) ?> + + [ + [ + 'icon' => '🌙', + 'title' => 'Dark Mode', + 'description' => 'Beautiful dark theme with carefully chosen colors for optimal readability.' + ], + [ + 'icon' => '⚡', + 'title' => 'Performance', + 'description' => 'Optimized for speed with reduced eye strain in low-light environments.' + ], + [ + 'icon' => '🎨', + 'title' => 'Design', + 'description' => 'Modern dark UI that adapts seamlessly across all components.' + ] + ] +]) ?> + + [ + [ + 'name' => 'Dark Starter', + 'price' => 'Free', + 'period' => '', + 'description' => 'Perfect for trying out dark mode', + 'features' => [ + 'Dark theme support', + 'Basic components', + 'Community support' + ], + 'cta' => 'Try Dark Mode', + 'popular' => false + ], + [ + 'name' => 'Dark Pro', + 'price' => '$19', + 'period' => '/month', + 'description' => 'Professional dark theme experience', + 'features' => [ + 'Advanced dark components', + 'Theme customization', + 'Priority support', + 'Custom color schemes' + ], + 'cta' => 'Go Dark Pro', + 'popular' => true + ] + ] +]) ?> + +
+
+

Dark Theme Components

+

See how beautiful our components look in dark mode

+ +
+
+

Dark Forms

+
+
+ + +
+
+ + +
+ +
+
+ +
+

Notifications

+
+ + + +
+
+
+
+
+ + + + diff --git a/site/examples/web-app-starter/views/dashboard.css b/site/examples/web-app-starter/views/dashboard.css new file mode 100644 index 0000000..ce90e84 --- /dev/null +++ b/site/examples/web-app-starter/views/dashboard.css @@ -0,0 +1,205 @@ +.dashboard-intro p, +.dashboard-panel-header p, +.dashboard-note { + color: var(--text-secondary); + max-width: 72ch; +} + +.dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + margin-bottom: 2rem; + overflow: hidden; + padding: 1.5rem; +} + +.dashboard-panel-header { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-bottom: 1.25rem; +} + +.dashboard-panel-header h2 { + font-size: 1.4rem; + margin-bottom: 0; +} + +.dashboard-stat-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.dashboard-stat-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + color: inherit; + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1rem; + text-decoration: none; + transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease; + position: relative; + overflow: hidden; +} + +.dashboard-stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + text-decoration: none; +} + +.dashboard-stat-card::before { + content: ''; + position: absolute; + inset: 0 auto 0 0; + width: 4px; + background: var(--primary); +} + +.dashboard-stat-card.tone-success::before { + background: var(--success); +} + +.dashboard-stat-card.tone-warning::before { + background: var(--warning); +} + +.dashboard-stat-card.tone-danger::before { + background: var(--error); +} + +.dashboard-stat-card.tone-info::before { + background: var(--info); +} + +.dashboard-stat-label { + color: var(--text-secondary); + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.dashboard-stat-value { + font-size: clamp(1.6rem, 2vw, 2.1rem); + font-variant-numeric: tabular-nums; + font-weight: 700; + line-height: 1.1; +} + +.dashboard-stat-meta { + color: var(--text-secondary); + font-size: 0.92rem; +} + +.dashboard-chart-canvas { + background: + radial-gradient(circle at top left, rgba(255, 255, 255, 0.05), transparent 45%), + linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(15, 23, 42, 0.08)); + border: 1px solid var(--border); + border-radius: var(--radius); + display: block; + width: 100%; + min-height: 180px; +} + +.dashboard-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.u-sortable-table { + border-collapse: collapse; + font-size: 0.95rem; + min-width: 720px; + width: 100%; +} + +.u-sortable-table thead th { + background: var(--surface-elevated); + border-bottom: 1px solid var(--border); + color: var(--text-primary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.05em; + padding: 0.85rem 1rem; + position: relative; + text-transform: uppercase; + white-space: nowrap; +} + +.u-sortable-table thead th.sortable { + cursor: pointer; + padding-right: 2rem; + user-select: none; +} + +.u-sortable-table thead th.sortable::after { + content: ':'; + color: var(--text-muted); + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + font-size: 0.8rem; +} + +.u-sortable-table thead th.sorted-asc::after { + content: '^'; + color: var(--primary); +} + +.u-sortable-table thead th.sorted-desc::after { + content: 'v'; + color: var(--primary); +} + +.u-sortable-table tbody tr:nth-child(odd) { + background: rgba(148, 163, 184, 0.05); +} + +.u-sortable-table tbody tr:hover { + background: rgba(96, 165, 250, 0.08); +} + +.u-sortable-table td { + border-bottom: 1px solid var(--border); + padding: 0.85rem 1rem; + vertical-align: top; + font-variant-numeric: tabular-nums; +} + +.u-sortable-table tbody tr:last-child td { + border-bottom: none; +} + +.u-sortable-table .align-right { + text-align: right; +} + +.u-sortable-table .align-center { + text-align: center; +} + +.u-sortable-table .muted { + color: var(--text-secondary); + text-align: center; +} + +@media (max-width: 768px) { + .dashboard-panel { + padding: 1rem; + } + + .u-sortable-table { + min-width: 600px; + } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/views/dashboard.php b/site/examples/web-app-starter/views/dashboard.php new file mode 100644 index 0000000..9d30a46 --- /dev/null +++ b/site/examples/web-app-starter/views/dashboard.php @@ -0,0 +1,90 @@ + 'requests', + 'label' => 'Requests', + 'color' => '#60a5fa', + 'axis' => 'left', + 'format' => 'count', + 'decimals' => 0, + 'values' => [240, 268, 294, 322, 301, 356, 388], + ], + [ + 'key' => 'latency', + 'label' => 'Latency', + 'color' => '#f59e0b', + 'axis' => 'right', + 'format' => 'duration-ms', + 'values' => [182, 176, 191, 204, 188, 166, 159], + ], + ]; + + $serviceRows = [ + ['service' => 'router', 'uptime' => '12 days', 'requests' => 142890, 'memory' => 402653184, 'p95_latency' => 148, 'healthy' => true], + ['service' => 'queue-worker', 'uptime' => '8 days', 'requests' => 98214, 'memory' => 654311424, 'p95_latency' => 231, 'healthy' => true], + ['service' => 'vector-index', 'uptime' => '5 days', 'requests' => 44892, 'memory' => 1241513984, 'p95_latency' => 312, 'healthy' => true], + ['service' => 'sandbox', 'uptime' => '19 hours', 'requests' => 12810, 'memory' => 295698432, 'p95_latency' => 418, 'healthy' => false], + ]; +?> + +

Dashboard Primitives

+ +
+

+ This page is the first backport slice from the LocalAI dashboard frontend. It keeps the parts that are generic enough for the starter itself: + metric cards, a canvas time-series chart, and a lightweight sortable table that remembers its last sort choice. +

+
+ + 'Starter-Friendly Overview Cards', + 'subtitle' => 'Small summary tiles work well across admin pages, internal tools, and SSR dashboards.', + 'items' => [ + ['label' => '24h Requests', 'value' => '18,420', 'meta' => '+12.8% vs yesterday', 'tone' => 'info'], + ['label' => 'Median Latency', 'value' => '182 ms', 'meta' => 'stable over last 7 samples', 'tone' => 'success'], + ['label' => 'Resident Memory', 'value' => '2.4 GB', 'meta' => 'combined across workers', 'tone' => 'warning'], + ['label' => 'Healthy Services', 'value' => '3 / 4', 'meta' => 'one degraded background worker', 'tone' => 'danger'], + ], +]) ?> + + 'dashboard-demo-traffic', + 'title' => 'Requests vs Latency', + 'subtitle' => 'Same generic chart primitive can track throughput, job backlog, token volume, or queue time.', + 'height' => 320, + 'x_axis_label' => 'Last 7 Hours', + 'y_axis_left_label' => 'Requests', + 'y_axis_right_label' => 'Latency', + 'y_axis_left_format' => 'count', + 'y_axis_right_format' => 'duration-ms', + 'x_labels' => ['08:00', '09:00', '10:00', '11:00', '12:00', '13:00', '14:00'], + 'series' => $trafficSeries, +]) ?> + + 'dashboard-service-table', + 'title' => 'Service Snapshot', + 'subtitle' => 'Vanilla HTML table enhancement for cases where ag-Grid is overkill.', + 'storage_key' => 'starter.dashboard.services', + 'sort' => ['column' => 2, 'direction' => 'desc'], + 'columns' => [ + ['key' => 'service', 'label' => 'Service'], + ['key' => 'uptime', 'label' => 'Uptime'], + ['key' => 'requests', 'label' => 'Requests', 'align' => 'right', 'format' => 'number'], + ['key' => 'memory', 'label' => 'Memory', 'align' => 'right', 'format' => 'bytes'], + ['key' => 'p95_latency', 'label' => 'P95 Latency', 'align' => 'right', 'format' => 'duration-ms'], + ['key' => 'healthy', 'label' => 'Healthy', 'align' => 'center', 'format' => 'bool'], + ], + 'rows' => $serviceRows, +]) ?> + +
+

What Was Backported

+

+ The charting logic and data-formatting ideas come directly from the more complex dashboard frontend on uh-llm2, but the starter version is stripped down to generic building blocks. + That keeps the repo useful as a baseline instead of baking in LocalAI-specific assumptions. +

+
\ No newline at end of file diff --git a/site/examples/web-app-starter/views/elements/error.php b/site/examples/web-app-starter/views/elements/error.php new file mode 100755 index 0000000..50a91a5 --- /dev/null +++ b/site/examples/web-app-starter/views/elements/error.php @@ -0,0 +1,5 @@ + + + +

Gauge Components Demo

+ +
+ + 'horizontal_demo', + 'title' => 'Horizontal Progress Bar', + 'subtitle' => 'The original bar gauges, restyled to share the same card and token language as the new arc gauges.', + 'layout' => 'horizontal', + 'style' => 'flex:1 1 24rem', + 'listen' => true, + 'markers' => [ + 'zero' => [ + 'value' => 0, + 'label' => 'Zero', + 'color' => 'var(--bg-color)', + ], + 'high' => [ + 'value' => 100, + 'label' => 'High', + ], + ], + 'items' => [ + 'cpu' => [ + 'value' => 45, + 'min' => 0, + 'max' => 200, + 'label' => 'CPU', + 'tooltip' => 'CPU Usage', + 'color' => [ + ['from' => -50, 'to' => 20, 'color' => 'var(--text-muted)'], + ['from' => 20, 'to' => 80, 'color' => 'var(--primary)'], + ['from' => 80, 'to' => 100, 'color' => 'var(--warning)'], + ], + ], + 'memory' => [ + 'value' => 92, + 'min' => -50, + 'max' => 100, + 'label' => 'Memory', + 'tooltip' => 'Memory Usage', + ], + 'disk' => [ + 'value' => 28, + 'min' => 0, + 'max' => 100, + 'label' => 'Disk I/O', + 'tooltip' => 'Disk I/O', + ], + ], + ]) ?> + + 'vertical_demo', + 'title' => 'Vertical Progress Bar', + 'subtitle' => 'Same abstraction, but stacked as compact KPI cards.', + 'layout' => 'vertical', + 'style' => 'flex:1 1 24rem', + 'height' => 350, + 'listen' => true, + 'markers' => [ + 'zero' => [ + 'value' => 0, + 'label' => 'Zero', + 'color' => 'var(--bg-color)', + ], + 'high' => [ + 'value' => 100, + 'label' => 'High', + ], + ], + 'items' => [ + 'cpu' => [ + 'value' => 45, + 'min' => 0, + 'max' => 200, + 'label' => 'CPU', + 'tooltip' => 'CPU Usage', + ], + 'memory' => [ + 'value' => 92, + 'min' => -50, + 'max' => 100, + 'label' => 'RAM', + 'tooltip' => 'Memory Usage', + ], + 'disk' => [ + 'value' => 28, + 'min' => 0, + 'max' => 100, + 'label' => 'I/O', + 'tooltip' => 'Disk I/O', + ], + ], + ]) ?> + +
+
+

Event Binding

+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ + 'Needle Gauge', + 'subtitle' => 'The original analog gauge now uses the same elevated panels, typography, and theme-token palette as the arc gauges.', + 'style' => 'flex:1 1 24rem', + 'listen' => true, + 'label' => 'CPU', + 'tooltip' => 'CPU Usage', + 'scale' => [ + 'angle_start' => -1.2*pi(), + 'angle_end' => 0.2*pi(), + 'max' => 100, + 'unit' => '%', + 'ticks-every' => 10, + 'value-labels-every' => 20, + 'tick-color' => 'var(--text-muted)', + 'color' => [ + ['from' => -50, 'to' => 10, 'color' => 'var(--primary)'], + ['from' => 10, 'to' => 70, 'color' => 'var(--text-muted)'], + ['from' => 70, 'to' => 90, 'color' => 'var(--warning)'], + ['from' => 90, 'to' => 200, 'color' => 'var(--error)'], + ], + ], + 'items' => [ + 'cpu' => [ + 'max' => 200, + 'value' => 45, + 'label' => 'CPU', + ], + 'memory' => [ + 'value' => 92, + 'min' => -50, + 'label' => 'Memory', + 'tooltip' => 'Memory Usage', + ], + 'disk' => [ + 'value' => 28, + 'label' => 'Disk I/O', + 'tooltip' => 'Disk I/O', + ], + ], + ]) ?> + +
+ +
+ 'arc_demo', + 'title' => 'SVG Arc Gauges', + 'subtitle' => 'Backported from the llm2 overview as reusable KPI-style gauges with optional watermark tracking.', + 'style' => 'flex: 2 1 36rem', + 'listen' => true, + 'items' => [ + 'load' => [ + 'label' => 'System Load', + 'value' => 1.8, + 'max' => 8, + 'precision' => 1, + 'caption' => 'LOAD 1M', + 'meta' => '4 cores available', + 'watermark_prefix' => 'loadDemo', + 'color' => [ + ['from' => 0, 'to' => 3.5, 'color' => 'var(--success, #10b981)'], + ['from' => 3.5, 'to' => 6, 'color' => 'var(--warning, #f59e0b)'], + ['from' => 6, 'to' => 8, 'color' => 'var(--error, #ef4444)'], + ], + ], + 'memory_arc' => [ + 'label' => 'Memory', + 'value' => 62, + 'max' => 100, + 'precision' => 0, + 'unit' => '%', + 'caption' => 'MEMORY', + 'meta' => '9.9 / 16 GB', + 'watermark_prefix' => 'memoryDemo', + ], + 'network' => [ + 'label' => 'Network', + 'value' => 18, + 'max' => 100, + 'precision' => 0, + 'unit' => ' MB/s', + 'caption' => 'THROUGHPUT', + 'meta' => 'Inbound + outbound', + 'watermark_prefix' => 'networkDemo', + ], + ], + ]) ?> + +
+

Arc Gauge Controls

+
+ + +
+
+ + +
+
+ + +
+
+
+ diff --git a/site/examples/web-app-starter/views/index.php b/site/examples/web-app-starter/views/index.php new file mode 100755 index 0000000..4a991b0 --- /dev/null +++ b/site/examples/web-app-starter/views/index.php @@ -0,0 +1,146 @@ + + + 'Stunning Apps', + 'subtitle' => 'Experience the power of super bloated PHP development with our gigantic and truly unwieldy component-based framework. + Seriously though this page only serves as an example repository of + different styles and blocks.', + 'cta_text' => 'Get Started Free(mium)', + 'cta_link' => '#features' +]) ?> + + + + [ + ['number' => '10K+', 'label' => 'Happy Vibe Coders'], + ['number' => '<1s', 'label' => 'Page Load Time'], + ['number' => '99.9%', 'label' => 'Uptime'], + ['number' => '24/7', 'label' => 'Support'] + ] +]) ?> + + + + + + + +
+
+

Demo

+

Try our UNBELIEVABLE components in action

+ +
+
+

Modern Forms

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Button Variations

+
+ + + + +
+ +
+ + + +
+
+
+
+
+ +
+

Theme Families

+

Compare the starter’s built-in theme families and jump to the live gallery.

+
+ +
+

+

+ Preview Theme +
+ +
+

Starter Themes

+

The original light and dark starter skins remain available in the same gallery for side-by-side checks.

+ Open Gallery +
+
+
+ + 'Ready to Transform Your Development?', + 'subtitle' => 'Join thousands of developers who have already modernized their workflow with our framework.', + 'cta_text' => 'Start Your Project', + 'secondary_text' => 'View GitHub' +]) ?> + +
+

Component Development Guidelines

+
+
+ 🏷️ + Use semantic HTML5 elements +
+
+ 🎨 + Implement CSS custom properties for theming +
+
+ 📱 + Add responsive design with mobile-first approach +
+
+ + Include accessibility attributes (ARIA, alt text) +
+
+ + Use progressive enhancement for JavaScript features +
+
+
+ +
+

Simple Data Table (ag-Grid)

+

Basic data table with auto-generated columns, sorting, and filtering:

+ [ + ['name' => 'John Doe', 'age' => 25, 'gender' => 'male', 'department' => 'Engineering', 'salary' => 75000, 'active' => true], + ['name' => 'Jane Smith', 'age' => 30, 'gender' => 'female', 'department' => 'Design', 'salary' => 82000, 'active' => true], + ['name' => 'Bob Johnson', 'age' => 35, 'gender' => 'male', 'department' => 'Marketing', 'salary' => 68000, 'active' => false], + ['name' => 'Alice Brown', 'age' => 40, 'gender' => 'female', 'department' => 'Engineering', 'salary' => 95000, 'active' => true], + ['name' => 'Dave Wilson', 'age' => 45, 'gender' => 'male', 'department' => 'Sales', 'salary' => 72000, 'active' => true], + ['name' => 'Eve Davis', 'age' => 50, 'gender' => 'non-binary', 'department' => 'Management', 'salary' => 110000, 'active' => true], + ['name' => 'Charlie Miller', 'age' => 28, 'gender' => 'male', 'department' => 'Engineering', 'salary' => 78000, 'active' => true], + ['name' => 'Sarah Taylor', 'age' => 33, 'gender' => 'female', 'department' => 'Design', 'salary' => 85000, 'active' => false], + ], + 'height' => '350px' +]) ?> +
+ diff --git a/site/examples/web-app-starter/views/marketing.css b/site/examples/web-app-starter/views/marketing.css new file mode 100755 index 0000000..77c5028 --- /dev/null +++ b/site/examples/web-app-starter/views/marketing.css @@ -0,0 +1,233 @@ + +/* Shared base styles */ +.mono-text, +.code-block { + font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; +} + +/* Code block container with terminal styling */ +.code-block-container { + background: linear-gradient(135deg, var(--surface) 0%, var(--surface-elevated) 100%); + padding: 2rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border); + margin: 1rem 0; + position: relative; + overflow: hidden; +} + +/* Terminal header bar */ +.terminal-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; +} + +.terminal-header.primary-gradient { + background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%); +} + +.terminal-header.success-gradient { + background: linear-gradient(90deg, var(--success) 0%, var(--primary) 50%, var(--secondary) 100%); +} + +.terminal-header.warning-gradient { + background: linear-gradient(90deg, var(--warning) 0%, var(--accent) 50%, var(--primary) 100%); +} + +/* Terminal window controls */ +.terminal-controls { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.window-dot { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 8px; +} + +.window-dot.red { background: #ff5f56; } +.window-dot.yellow { background: #ffbd2e; } +.window-dot.green { + background: #27ca3f; + margin-right: 1rem; +} + +/* Monospace text styling */ +.mono-text { + font-size: 0.875rem; + color: var(--text-muted); +} + +/* Code block styling */ +.code-block { + margin: 0; + font-size: 0.9rem; + line-height: 1.6; + color: var(--text-primary); + background: none; +} + +/* Card components - using shared base styles */ +.component-card, +.success-card, +.enhancement-card { + padding: 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); +} + +.component-card { + background: var(--surface-elevated); +} + +.success-card { + background: var(--success-bg); + border-color: var(--success-border); +} + +.enhancement-card { + color: var(--bg-color); + text-align: center; + border: none; +} + +/* Grid layouts - shared base properties */ +.components-grid, +.theme-grid, +.enhancements-grid, +.guidelines-grid { + display: grid; + gap: 1rem; +} + +.components-grid { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + margin: 1rem 0; +} + +.theme-grid { + grid-template-columns: 1fr 1fr; + margin: 1rem 0; +} + +.enhancements-grid { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.guidelines-grid { + gap: 0.5rem; +} + +/* Guideline items */ +.guideline-item { + display: flex; + align-items: center; + background: var(--surface-elevated); + padding: 1rem; + border-radius: var(--radius); + border-left: 4px solid; +} + +.guideline-icon { + font-size: 1.5rem; + margin-right: 1rem; +} + +/* Demo section styling */ +.demo-section { + padding: 4rem 0; + background: var(--bg-color); +} + +.demo-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + text-align: center; +} + +.demo-container h2 { + font-size: 2.5rem; + margin-bottom: 1rem; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.demo-container > p { + font-size: 1.25rem; + color: var(--text-secondary); + margin-bottom: 3rem; +} + +.demo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); + gap: 2rem; + margin-top: 3rem; +} + +.demo-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + padding: 2.5rem; + box-shadow: var(--shadow-md); + text-align: left; +} + +.demo-card h3 { + margin-bottom: 2rem; + color: var(--text-primary); + text-align: center; +} + +.demo-form { + max-width: none; +} + +/* Flex layouts */ +.button-showcase, +.notification-demo { + display: flex; + gap: 1rem; +} + +.button-showcase { + flex-wrap: wrap; + margin-bottom: 2rem; + justify-content: center; +} + +.notification-demo { + flex-direction: column; +} + +/* Responsive design lol */ +@media (max-width: 768px) { + .demo-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .demo-card { + padding: 2rem 1.5rem; + } + + .button-showcase { + flex-direction: column; + align-items: center; + } + + .button-showcase .btn { + width: 100%; + max-width: 250px; + } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/views/page1.php b/site/examples/web-app-starter/views/page1.php new file mode 100755 index 0000000..3af458c --- /dev/null +++ b/site/examples/web-app-starter/views/page1.php @@ -0,0 +1,126 @@ +

Component System

+ +
+

Component Declaration

+ + + +
+
+
+
+
+
+ component.php +
+
<?php return [
+
+    'render' => function($prop) {
+        // render the component 
+    },
+    
+    'about' => 'A floating theme switcher button that toggles between light and dark themes',
+
+]; 
+
+
+
+ +
+

Example Components

+
+
+

hero-section

+

Landing page hero with CTA buttons

+
+
+

features-grid

+

3-column feature showcase

+
+
+

stats-section

+

Animated statistics display

+
+
+

testimonials

+

Customer testimonial carousel

+
+
+

cta-section

+

Call-to-action with background

+
+
+

pricing-table

+

Responsive pricing tiers

+
+
+

brands-showcase

+

Logo grid with animations

+
+
+

theme-switcher

+

Light/dark theme toggle

+
+
+
+ +
+

Usage Examples

+
+
+
+
+
+
+ usage-examples.php +
+
// Basic component
+<?php component('components/example/hero-section'); ?>
+
+// Component with data
+<?php component('components/example/stats-section', [
+    'title' => 'Our Growth',
+    'stats' => [
+        ['number' => '50K+', 'label' => 'Users'],
+        ['number' => '99.9%', 'label' => 'Uptime'],
+        ['number' => '24/7', 'label' => 'Support']
+    ]
+]); ?>
+
+
+ +
+

Theme Info

+

This starter pack includes a light and a dark theme

+
+
+

Light Theme

+ themes/light/css/style.css +
+
+

Dark Theme

+ themes/dark/css/style.css +
+
+ +
+
+
+
+
+
+ variables.css +
+

CSS Variables

+
/* Core theme variables */
+--bg-color, --bg-secondary, --surface
+--text-primary, --text-secondary, --text-muted
+--primary, --primary-dark, --primary-light
+--border, --border-hover
+--shadow-sm, --shadow-md, --shadow-lg
+
+
+ + +
+ diff --git a/site/examples/web-app-starter/views/page2-section1.php b/site/examples/web-app-starter/views/page2-section1.php new file mode 100755 index 0000000..44e7e19 --- /dev/null +++ b/site/examples/web-app-starter/views/page2-section1.php @@ -0,0 +1,9 @@ +Can haz ODT? '); + print_r(ODT::check_requirements()); + print(''); \ No newline at end of file diff --git a/site/examples/web-app-starter/views/page2.php b/site/examples/web-app-starter/views/page2.php new file mode 100755 index 0000000..ac490aa --- /dev/null +++ b/site/examples/web-app-starter/views/page2.php @@ -0,0 +1,15 @@ +

Ajax Demo

+ +
+

This is the content of Page 2. You can add more information here.

+ + +
+ + + +
diff --git a/site/examples/web-app-starter/views/theme-preview.php b/site/examples/web-app-starter/views/theme-preview.php new file mode 100644 index 0000000..582215c --- /dev/null +++ b/site/examples/web-app-starter/views/theme-preview.php @@ -0,0 +1,102 @@ + 'Current Theme', 'value' => $themeLabel], + ['label' => 'Mode', 'value' => ucfirst($themeMode)], + ['label' => 'Preview Route', 'value' => '/?theme-preview'], + ]; +?> +
+
+ Starter Theme Preview +

+

This route renders the same neutral content inside each theme so downstream projects can compare layout, typography, color tokens, and chrome without switching between unrelated pages.

+ +
+ +
+
+

Tokens at a Glance

+
+ +
+ + +
+ +
+
+ Primary + Secondary + Accent + Surface +
+
+ +
+

System Message States

+ + + +
+ +
+

Form Controls

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Dense Content Block

+ + + + + + + + + + + + + + + + + + + + + + + + + +
AreaExpectationStatus
NavigationShould remain readable when menus get long.Verified
CardsShould keep spacing and hierarchy on both desktop and mobile.Verified
EmbedsShould render cleanly inside the gallery iframe.Verified
+
+
+
\ No newline at end of file diff --git a/site/examples/web-app-starter/views/themes.css b/site/examples/web-app-starter/views/themes.css new file mode 100644 index 0000000..aea3292 --- /dev/null +++ b/site/examples/web-app-starter/views/themes.css @@ -0,0 +1,180 @@ +.theme-gallery-shell, +.theme-preview-shell { + display: grid; + gap: 1.5rem; +} + +.theme-gallery-kicker, +.theme-preview-kicker { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); + margin-bottom: 0.75rem; +} + +.theme-gallery-hero h1, +.theme-preview-hero h1 { + margin-bottom: 0.75rem; +} + +.theme-gallery-hero p, +.theme-preview-hero p { + max-width: 72ch; + color: var(--text-secondary); +} + +.theme-gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 1.25rem; +} + +.theme-gallery-card, +.theme-preview-card, +.theme-preview-hero { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg, var(--radius)); + padding: 1.25rem; + box-shadow: var(--shadow-sm); +} + +.theme-gallery-card.is-active { + border-color: var(--primary); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 35%, transparent 65%), var(--shadow-md); +} + +.theme-gallery-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.theme-gallery-head p { + margin: 0.4rem 0 0; + color: var(--text-secondary); +} + +.theme-gallery-badge { + display: inline-flex; + align-items: center; + padding: 0.3rem 0.65rem; + border-radius: 999px; + background: color-mix(in srgb, var(--primary) 15%, transparent 85%); + color: var(--primary); + font-size: 0.78rem; + font-weight: 700; + white-space: nowrap; +} + +.theme-gallery-actions, +.theme-preview-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.theme-gallery-frame-wrap { + border: 1px solid var(--border); + border-radius: calc(var(--radius-lg, var(--radius)) - 4px); + overflow: hidden; + background: var(--bg-secondary); +} + +.theme-gallery-frame-wrap iframe { + display: block; + width: 100%; + height: 430px; + border: 0; + background: #fff; +} + +.theme-preview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +.theme-preview-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.theme-preview-stat { + display: grid; + gap: 0.2rem; + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-elevated, var(--bg-secondary)); +} + +.theme-preview-stat span { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-muted); +} + +.theme-preview-swatches { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 0.75rem; +} + +.theme-preview-swatches span { + display: flex; + align-items: end; + min-height: 92px; + padding: 0.75rem; + border-radius: var(--radius); + color: #fff; + font-weight: 700; + box-shadow: inset 0 -20px 30px rgba(0, 0, 0, 0.12); +} + +.theme-preview-form { + max-width: none; + display: grid; + gap: 0.85rem; +} + +.embed-mode nav, +.embed-mode footer, +.embed-mode #theme-switcher, +.embed-mode .admin-toolbar { + display: none !important; +} + +.embed-mode #content, +.embed-mode .admin-content { + margin-top: 0 !important; + min-height: 100vh !important; + padding-top: 1rem !important; +} + +@media (max-width: 720px) { + .theme-gallery-grid, + .theme-preview-grid { + grid-template-columns: 1fr; + } + + .theme-gallery-head { + flex-direction: column; + } + + .theme-gallery-frame-wrap iframe { + height: 360px; + } +} \ No newline at end of file diff --git a/site/examples/web-app-starter/views/themes.php b/site/examples/web-app-starter/views/themes.php new file mode 100644 index 0000000..cbf6bfe --- /dev/null +++ b/site/examples/web-app-starter/views/themes.php @@ -0,0 +1,35 @@ + + \ No newline at end of file diff --git a/site/examples/web-app-starter/views/workspace/index.php b/site/examples/web-app-starter/views/workspace/index.php new file mode 100644 index 0000000..56a499a --- /dev/null +++ b/site/examples/web-app-starter/views/workspace/index.php @@ -0,0 +1,182 @@ + [ + 'title' => 'Workspace overview', + 'subtitle' => 'A generic app-shell pattern for tools, admin consoles, and internal products.', + 'status' => ['label' => 'Ready', 'variant' => 'success'], + 'description' => 'This slice comes from the uh-ai portal app: a reusable workspace shell with sidebar navigation, compact mobile controls, and semantic panel primitives.', + 'highlights' => [ + ['title' => 'Shell layout', 'text' => 'Sidebar plus main panel, responsive overlay behavior, and a compact mobile header.'], + ['title' => 'Semantic primitives', 'text' => 'Panels, section heads, status pills, list states, and empty-state placeholders.'], + ['title' => 'Starter-friendly', 'text' => 'Backported against the starter theme variables instead of portal-specific branding tokens.'], + ], + ], + 'projects' => [ + 'title' => 'Projects queue', + 'subtitle' => 'Nested path fallback lets one controller serve multiple panes cleanly.', + 'status' => ['label' => '3 active', 'variant' => 'info'], + 'description' => 'This page is served from views/workspace/index.php, while the route remains /workspace/projects. That path fallback was backported from uh-ai as part of this slice.', + 'highlights' => [ + ['title' => 'Content review', 'text' => 'Design a shared docs/workspace explorer for internal tools.'], + ['title' => 'Component extraction', 'text' => 'Promote shell primitives into stable starter components.'], + ['title' => 'Routing cleanup', 'text' => 'Use nested routes without multiplying single-file views.'], + ], + ], + 'activity' => [ + 'title' => 'Recent activity', + 'subtitle' => 'Sidebar-first tools often need a live or recent-events pane.', + 'status' => ['label' => 'Monitoring', 'variant' => 'warn'], + 'description' => 'The workspace shell is a better fit than the marketing-style home page when the app is navigation-heavy and stateful.', + 'highlights' => [ + ['title' => 'Deploy preview', 'text' => 'Workspace layout validated on desktop and mobile widths.'], + ['title' => 'Shell behavior', 'text' => 'Sidebar toggle is abstracted in js/u-workspace-shell.js.'], + ['title' => 'Surface consistency', 'text' => 'All blocks inherit existing starter color and radius tokens.'], + ], + ], + ]; + + if (!isset($sections[$section])) { + $section = 'overview'; + } + + $current = $sections[$section]; + + $navItems = [ + ['key' => 'overview', 'label' => 'Overview', 'icon' => 'fas fa-compass', 'meta' => 'Shell'], + ['key' => 'projects', 'label' => 'Projects', 'icon' => 'fas fa-folder-tree', 'meta' => 'Routes'], + ['key' => 'activity', 'label' => 'Activity', 'icon' => 'fas fa-wave-square', 'meta' => 'State'], + ]; + + ob_start(); + ?> +
Workspace areas
+ +
+

This sidebar and mobile shell pattern was extracted from the AI portal app and normalized against starter theme tokens.

+
+ 'fas fa-circle-info', + 'text' => 'Use /workspace/overview, /workspace/projects, or /workspace/activity', + ]) ?> + 'Open shell', + 'search_input_id' => 'workspace-demo-search', + 'search_input_name' => 'workspace_demo_search', + 'search_placeholder' => 'Search workspace sections', + ]); + + $sidebar = component('components/workspace/sidebar-shell', [ + 'id' => 'workspace-demo-sidebar', + 'top_html' => $sidebarTop, + 'body_html' => $sidebarBody, + ]); + + $mobileBar = component('components/workspace/mobile-bar', [ + 'button_id' => 'workspace-demo-toggle', + 'title' => 'Starter Workspace', + ]); + + $header = component('components/workspace/panel-header', [ + 'title' => $current['title'], + 'subtitle' => $current['subtitle'], + 'actions_html' => component('components/workspace/status-pill', [ + 'label' => $current['status']['label'], + 'variant' => $current['status']['variant'], + ]), + ]); + + $stats = [ + ['label' => 'Sidebar pattern', 'value' => 'Responsive', 'meta' => 'Overlay on mobile'], + ['label' => 'Routing mode', 'value' => 'Nested', 'meta' => 'Parent-path fallback'], + ['label' => 'Source app', 'value' => 'uh-ai', 'meta' => 'Portal shell'], + ]; + + ob_start(); + ?> +
+ +
+
+
+
+
+ +
+ +
+ +
+

+

+
+ +
+ component('components/workspace/section-head', ['title' => 'Why this belongs in the starter']), + 'body_html' => '

' . safe($current['description']) . '

' . $statsHtml, + ]) . + component('components/workspace/section', [ + 'header_html' => component('components/workspace/section-head', ['title' => 'Starter demo content']), + 'body_html' => $detailHtml . '

Try visiting ' . safe(URL::link('workspace/projects')) . ' or ' . safe(URL::link('workspace/activity')) . ' to see the nested route fallback in action.

', + ]); + + if ($section === 'activity') { + $mainBody .= component('components/workspace/empty-state', [ + 'icon_class' => 'fas fa-clock-rotate-left', + 'title' => 'No live stream wired yet', + 'text' => 'The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one.', + 'action_html' => 'Open dashboard demo', + ]); + } + + $main = $mobileBar . component('components/workspace/panel', [ + 'header_html' => $header, + 'body_html' => $mainBody, + ]); + + echo component('components/workspace/app-frame', [ + 'id' => 'workspace-demo-shell', + 'overlay_id' => 'workspace-demo-overlay', + 'sidebar_html' => $sidebar, + 'main_html' => $main, + ]); + ?> + \ No newline at end of file diff --git a/test/call_file.uce b/site/test/call_file.uce similarity index 65% rename from test/call_file.uce rename to site/test/call_file.uce index 4987707..c74dd25 100644 --- a/test/call_file.uce +++ b/site/test/call_file.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -12,10 +12,9 @@ RENDER()
-
params) ?>
+
} - diff --git a/test/call_file_funcs.uce b/site/test/call_file_funcs.uce similarity index 50% rename from test/call_file_funcs.uce rename to site/test/call_file_funcs.uce index 1fcd914..f8aa873 100644 --- a/test/call_file_funcs.uce +++ b/site/test/call_file_funcs.uce @@ -1,4 +1,4 @@ -// some bullshit? +// Minimal exported helper used by the `call_file()` demo page. EXPORT void test_func() { diff --git a/site/test/components.uce b/site/test/components.uce new file mode 100644 index 0000000..7be9ba2 --- /dev/null +++ b/site/test/components.uce @@ -0,0 +1,33 @@ + +RENDER(Request& context) +{ + DTree card_props; + card_props["title"] = "Component Example"; + card_props["body"] = "This card body comes from context.call and is rendered through component()."; + + DTree named_props; + named_props["title"] = "Named Render Example"; + named_props["body"] = "This content is rendered through the RENDER:BODY(Request& context) entry point."; + + <> + +

+ UCE Test: + Components +

+

+ component_exists("components/card"): + +

+

+ component_resolve("components/card"): + +

+

Default Render

+
+ +
+

Named Render

+ + +} diff --git a/site/test/components/card.uce b/site/test/components/card.uce new file mode 100644 index 0000000..0b071f9 --- /dev/null +++ b/site/test/components/card.uce @@ -0,0 +1,24 @@ + +RENDER(Request& context) +{ + <> +
+ + +
+ +} + +RENDER:TITLE(Request& context) +{ + <> +

+ +} + +RENDER:BODY(Request& context) +{ + <> +

+ +} diff --git a/site/test/components/markdown/code_block.uce b/site/test/components/markdown/code_block.uce new file mode 100644 index 0000000..ba69458 --- /dev/null +++ b/site/test/components/markdown/code_block.uce @@ -0,0 +1,10 @@ +RENDER(Request& context) +{ + String lang = first(context.call["lang"].to_string(), "plain"); + <> +
+

Code block:

+
+
+ +} diff --git a/site/test/components/markdown/warning.uce b/site/test/components/markdown/warning.uce new file mode 100644 index 0000000..74cefb2 --- /dev/null +++ b/site/test/components/markdown/warning.uce @@ -0,0 +1,15 @@ +RENDER(Request& context) +{ + String title = first( + context.call["node"]["attrs"]["title"].to_string(), + context.call["argument"].to_string(), + "Notice" + ); + + <> + + +} diff --git a/test/cookie.uce b/site/test/cookie.uce similarity index 69% rename from test/cookie.uce rename to site/test/cookie.uce index b898cb5..4bb83ea 100644 --- a/test/cookie.uce +++ b/site/test/cookie.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -14,17 +14,17 @@ RENDER() set_cookie("test-cookie-1", "test-value-1", time() + 30*60); set_cookie("test-cookie-2", "test-value-2", time() + 30*60*24); - print(var_dump(context->set_cookies)); + print(var_dump(context.set_cookies)); ?> Get
cookies));
+		print(var_dump(context.cookies));
 
 		?>
Params -
params) ?>
+
} diff --git a/test/dtree.uce b/site/test/dtree.uce similarity index 92% rename from test/dtree.uce rename to site/test/dtree.uce index 97a7ff4..42a7bcd 100644 --- a/test/dtree.uce +++ b/site/test/dtree.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -48,7 +48,7 @@ RENDER() ?> Params -
params) ?>
+
} diff --git a/test/empty.uce b/site/test/empty.uce similarity index 100% rename from test/empty.uce rename to site/test/empty.uce diff --git a/site/test/error-reporting.uce b/site/test/error-reporting.uce new file mode 100644 index 0000000..7781640 --- /dev/null +++ b/site/test/error-reporting.uce @@ -0,0 +1,26 @@ + +RENDER(Request& context) +{ + String mode = context.get["mode"]; + + if(mode == "exception") + throw std::runtime_error("Intentional test exception from /test/error-reporting.uce"); + if(mode == "abort") + raise(SIGABRT); + if(mode == "segfault") + raise(SIGSEGV); + + <> + +

+ UCE Test: + Error reporting +

+

These actions intentionally trigger failures so you can verify that UCE returns a usable `500` response instead of dropping the upstream connection.

+ + +} diff --git a/test/file_append.uce b/site/test/file_append.uce similarity index 66% rename from test/file_append.uce rename to site/test/file_append.uce index c9adf67..f0e1f2b 100644 --- a/test/file_append.uce +++ b/site/test/file_append.uce @@ -2,7 +2,7 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -17,10 +17,10 @@ RENDER()
get["cmd"] == "clear")
+			if(context.get["cmd"] == "clear")
 				file_put_contents("/tmp/test.txt", "");
 
-			file_append("/tmp/test.txt", context->server->request_count, "\thello world\t", 2, "\t", time(), "\t", microtime(), "\n");
+			file_append("/tmp/test.txt", context.server->request_count, "\thello world\t", 2, "\t", time(), "\t", microtime(), "\n");
 
 			print(file_get_contents("/tmp/test.txt"));
 
@@ -30,7 +30,7 @@ RENDER()
 		
 
 		Params
-		
params) ?>
+
} diff --git a/test/fileio.uce b/site/test/fileio.uce similarity index 77% rename from test/fileio.uce rename to site/test/fileio.uce index 69474bc..b7b2c30 100644 --- a/test/fileio.uce +++ b/site/test/fileio.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -16,7 +16,7 @@ RENDER()
Params -
params) ?>
+
} diff --git a/test/header.uce b/site/test/header.uce similarity index 67% rename from test/header.uce rename to site/test/header.uce index 4427128..3071d1c 100644 --- a/test/header.uce +++ b/site/test/header.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -14,12 +14,12 @@ RENDER() Response Headers
header));
+		print(var_dump(context.header));
 
 		?>
Params -
params) ?>
+
} diff --git a/test/hello.uce b/site/test/hello.uce similarity index 95% rename from test/hello.uce rename to site/test/hello.uce index e29732d..92326f6 100644 --- a/test/hello.uce +++ b/site/test/hello.uce @@ -1,12 +1,12 @@ -void show_stuff() +void show_stuff(Request& context) { - //context->header["Content-Type"] = "text/plain"; + //context.header["Content-Type"] = "text/plain"; <> Mwahahaaha -
hello world: params["HTTP_HOST"] ?>
+
hello world:
 Display the date using any or all of the following elements:
 
@@ -173,7 +173,7 @@ Lifewire is part of the Dotdash publishing family.
 
 }
 
-RENDER()
+RENDER(Request& context)
 {
 
 	<>
@@ -182,8 +182,8 @@ RENDER()
 			UCE Test:
 			Index
 		
-		
-		
params) ?>
+ +
} diff --git a/test/inc-test.cpp b/site/test/inc-test.cpp similarity index 100% rename from test/inc-test.cpp rename to site/test/inc-test.cpp diff --git a/test/index.uce b/site/test/index.uce similarity index 76% rename from test/index.uce rename to site/test/index.uce index b1e9d8d..0d34f48 100644 --- a/test/index.uce +++ b/site/test/index.uce @@ -1,9 +1,9 @@ -RENDER() +RENDER(Request& context) { DTree p; - p.set(context->params); + p.set(context.params); <> @@ -35,20 +35,23 @@ RENDER()
  • call_file()
  • parse_time()
  • header
  • +
  • Components
  • +
  • Markdown
  • +
  • Error reporting
  • WebSockets
  • ob->str().length(), " \n");
    -			print("Request #", context->server->request_count, "\n");
    +			print("Output buffer size: ", context.ob->str().length(), " \n");
    +			print("Request #", context.server->request_count, "\n");
     		?>
    ob->str().length(), " \n"); + print("Output buffer size: ", context.ob->str().length(), " \n"); ?>
    - //context->flags.log_request = false; + //context.flags.log_request = false; } diff --git a/test/json.uce b/site/test/json.uce similarity index 89% rename from test/json.uce rename to site/test/json.uce index 3110f7e..11881f4 100644 --- a/test/json.uce +++ b/site/test/json.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -26,7 +26,7 @@ RENDER() t["g"]["x"]["y2"] = &t; t["g"]["x"]["y3"] = time(); t["g"]["x"]["y4"] = "Ünicödä"; - t["l"] = context->params; + t["l"] = context.params; String j; print(j = json_encode(t)); @@ -50,7 +50,7 @@ RENDER() ?>
    Params -
    params) ?>
    +
    } diff --git a/site/test/markdown-example.md b/site/test/markdown-example.md new file mode 100644 index 0000000..174cc5a --- /dev/null +++ b/site/test/markdown-example.md @@ -0,0 +1,36 @@ +# Markdown Demo + +This page exercises **strong**, *emphasis*, ~~strikethrough~~, `code spans`, and a bare URL: https://uce.openfu.com/doc/index.uce + +## Task List + +- [x] Parse markdown into an AST +- [x] Render markdown into HTML +- [ ] Add even more extensions later + +## Table + +| Feature | Status | Notes | +| :--- | :---: | ---: | +| Headings | Ready | 1 | +| Tables | Ready | 2 | +| Components | Ready | 3 | + +## Quote + +> Markdown in UCE should be composable. +> +> Components make that much more interesting. + +:::warning title="Component-backed directive" +This `:::warning` block is rendered through a normal UCE component selected from `options["components"]`. +::: + +## Code + +```cpp +RENDER(Request& context) +{ + print(markdown_to_html("# hello")); +} +``` diff --git a/site/test/markdown.uce b/site/test/markdown.uce new file mode 100644 index 0000000..69f8337 --- /dev/null +++ b/site/test/markdown.uce @@ -0,0 +1,46 @@ +RENDER(Request& context) +{ + DTree options; + options["components"][":::warning"] = "components/markdown/warning"; + options["components"]["node.code_block"] = "components/markdown/code_block"; + + String markdown_src = first( + context.post["markdown"], + file_get_contents("markdown-example.md") + ); + + DTree ast = markdown_to_ast(markdown_src, options); + String html = markdown_to_html(markdown_src, options); + + <> + +

    + UCE Test: + Markdown +

    +

    + This page exercises `markdown_to_ast()` and `markdown_to_html()` with component hooks for `:::warning` directives and fenced code blocks. +

    +
    +
    +
    +

    Source

    + +
    + +
    +
    +
    +

    Rendered HTML

    +
    + +
    +
    +
    +
    +
    + AST JSON +
    +
    + +} diff --git a/test/memcached.uce b/site/test/memcached.uce similarity index 89% rename from test/memcached.uce rename to site/test/memcached.uce index db8465b..8376e6a 100644 --- a/test/memcached.uce +++ b/site/test/memcached.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -34,7 +34,7 @@ RENDER() ?>
    Params -
    params) ?>
    +
    } diff --git a/test/mysql.uce b/site/test/mysql.uce similarity index 93% rename from test/mysql.uce rename to site/test/mysql.uce index 8ca7043..70f7025 100644 --- a/test/mysql.uce +++ b/site/test/mysql.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -45,7 +45,7 @@ RENDER() ?>
    -
    params) ?>
    +
    } diff --git a/test/parse_time.uce b/site/test/parse_time.uce similarity index 77% rename from test/parse_time.uce rename to site/test/parse_time.uce index 088eaca..ecdfbf5 100644 --- a/test/parse_time.uce +++ b/site/test/parse_time.uce @@ -1,8 +1,8 @@ -RENDER() +RENDER(Request& context) { - String raw = first(context->post["raw"], "today"); + String raw = first(context.post["raw"], "today"); <> @@ -24,7 +24,7 @@ RENDER() print(date("%Y-%m-%d %H:%M", parse_time(raw))); ?> -
    params) ?>
    +
    } diff --git a/test/post-multipart.uce b/site/test/post-multipart.uce similarity index 59% rename from test/post-multipart.uce rename to site/test/post-multipart.uce index a640a6b..ebb8508 100644 --- a/test/post-multipart.uce +++ b/site/test/post-multipart.uce @@ -1,15 +1,15 @@ -void show_form() +void show_form(Request& context) { <>
    - "/> + "/>
    - +
    @@ -18,7 +18,7 @@ void show_form() } -RENDER() +RENDER(Request& context) { <> @@ -28,20 +28,20 @@ RENDER() Multipart-Encoded Form POST - +
    -
    post) ?>
    +
    -
    in ?>
    +
    -
    params) ?>
    +
    } diff --git a/test/post.uce b/site/test/post.uce similarity index 56% rename from test/post.uce rename to site/test/post.uce index a393828..3343a95 100644 --- a/test/post.uce +++ b/site/test/post.uce @@ -1,15 +1,15 @@ -EXPORT void show_form() +EXPORT void show_form(Request& context) { <>
    - "/> + "/>
    - +
    @@ -18,7 +18,7 @@ EXPORT void show_form() } -RENDER() +RENDER(Request& context) { <> @@ -28,16 +28,16 @@ RENDER() Form POST - + -
    post) ?>
    +
    -
    in ?>
    +
    -
    params) ?>
    +
    } diff --git a/test/random.uce b/site/test/random.uce similarity index 87% rename from test/random.uce rename to site/test/random.uce index f922630..0c13751 100644 --- a/test/random.uce +++ b/site/test/random.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -27,7 +27,7 @@ RENDER() ?> Draw Some Numbers
    - Seed random_seed ?> + Seed
    
     
     		Params
    -		
    params) ?>
    +
    } diff --git a/test/script-example1.usp b/site/test/script-example1.usp similarity index 100% rename from test/script-example1.usp rename to site/test/script-example1.usp diff --git a/test/script.h b/site/test/script.h similarity index 100% rename from test/script.h rename to site/test/script.h diff --git a/test/script.uce b/site/test/script.uce similarity index 93% rename from test/script.uce rename to site/test/script.uce index 2eda2ad..21db642 100644 --- a/test/script.uce +++ b/site/test/script.uce @@ -110,7 +110,7 @@ String to_string(std::vector t) } // " -RENDER() +RENDER(Request& context) { <> @@ -121,7 +121,7 @@ RENDER() - String script_src = first(context->post["code"], file_get_contents("script-example1.usp")); + String script_src = first(context.post["code"], file_get_contents("script-example1.usp")); <>

    Code

    @@ -137,7 +137,7 @@ RENDER() ?>
    Params -
    params) ?>
    +
    } diff --git a/test/session.uce b/site/test/session.uce similarity index 65% rename from test/session.uce rename to site/test/session.uce index fa46e62..487d538 100644 --- a/test/session.uce +++ b/site/test/session.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -12,8 +12,8 @@ RENDER() - String action = context->get["action"]; - if(context->cookies["uce-session"].length() > 0) + String action = context.get["action"]; + if(context.cookies["uce-session"].length() > 0) session_start(); if(action == "start") { @@ -22,8 +22,8 @@ RENDER() } else if(action == "store") { - context->session["stored-value"] = make_session_id(); - print("action: storing value "+context->session["stored-value"]); + context.session["stored-value"] = make_session_id(); + print("action: storing value "+context.session["stored-value"]); } else if(action == "destroy") { @@ -40,13 +40,13 @@ RENDER() Info
    cookies));
    +		print(var_dump(context.cookies));
     		print("SESSION:\n");
    -		print(var_dump(context->session));
    +		print(var_dump(context.session));
     
     		?>
    Params -
    params) ?>
    +
    } diff --git a/test/sharedunit.uce b/site/test/sharedunit.uce similarity index 78% rename from test/sharedunit.uce rename to site/test/sharedunit.uce index 08cf59c..1ae5648 100644 --- a/test/sharedunit.uce +++ b/site/test/sharedunit.uce @@ -1,5 +1,5 @@ -RENDER() +RENDER(Request& context) { auto p = compiler_load_shared_unit(context, "post.uce"); if(p) diff --git a/test/shell.uce b/site/test/shell.uce similarity index 87% rename from test/shell.uce rename to site/test/shell.uce index 16e8f0b..07929de 100644 --- a/test/shell.uce +++ b/site/test/shell.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -34,7 +34,7 @@ RENDER() ?> -
    params) ?>
    +
    } diff --git a/test/str_replace.uce b/site/test/str_replace.uce similarity index 88% rename from test/str_replace.uce rename to site/test/str_replace.uce index 138b0b5..9600bd5 100644 --- a/test/str_replace.uce +++ b/site/test/str_replace.uce @@ -5,7 +5,7 @@ void str_replace_test(String s, String sr, String rp) print(replace(s, sr, rp)); } -RENDER() +RENDER(Request& context) { <> @@ -27,7 +27,7 @@ RENDER()
    -
    params) ?>
    +
    } diff --git a/test/string.uce b/site/test/string.uce similarity index 77% rename from test/string.uce rename to site/test/string.uce index 80b00ef..99e8787 100644 --- a/test/string.uce +++ b/site/test/string.uce @@ -3,7 +3,7 @@ -RENDER() +RENDER(Request& context) { DTree t; @@ -21,7 +21,7 @@ RENDER() ?> Params -
    params) ?>
    +
    } diff --git a/test/style.css b/site/test/style.css similarity index 100% rename from test/style.css rename to site/test/style.css diff --git a/test/task-status.uce b/site/test/task-status.uce similarity index 63% rename from test/task-status.uce rename to site/test/task-status.uce index 4891132..153555c 100644 --- a/test/task-status.uce +++ b/site/test/task-status.uce @@ -1,7 +1,7 @@ -RENDER() +RENDER(Request& context) { - String task_name = first(context->get["task-name"], "example-task"); + String task_name = first(context.get["task-name"], "example-task"); print("Task Name: ", task_name, "\n"); print("Task ID: ", task_pid(task_name), "\n"); diff --git a/test/task.uce b/site/test/task.uce similarity index 88% rename from test/task.uce rename to site/test/task.uce index 55458a6..ce85595 100644 --- a/test/task.uce +++ b/site/test/task.uce @@ -1,9 +1,9 @@ -RENDER() +RENDER(Request& context) { DTree t; - String task_name = first(context->get["task-name"], "example-task"); + String task_name = first(context.get["task-name"], "example-task"); <> @@ -54,7 +54,7 @@ RENDER() ?> get["cmd"] == "Execute") + if(context.get["cmd"] == "Execute") { ?> Task Start @@ -74,7 +74,7 @@ RENDER() ?> Params -
    get) ?>
    +
    } diff --git a/test/task_repeat.uce b/site/test/task_repeat.uce similarity index 88% rename from test/task_repeat.uce rename to site/test/task_repeat.uce index 51aafbd..6d07496 100644 --- a/test/task_repeat.uce +++ b/site/test/task_repeat.uce @@ -1,9 +1,9 @@ -RENDER() +RENDER(Request& context) { DTree t; - String task_name = first(context->get["task-name"], "example-task"); + String task_name = first(context.get["task-name"], "example-task"); <> @@ -54,7 +54,7 @@ RENDER() ?> get["cmd"] == "Execute") + if(context.get["cmd"] == "Execute") { ?> Task Start @@ -74,7 +74,7 @@ RENDER() ?> Params -
    get) ?>
    +
    } diff --git a/test/test2/working-dir-test.uce b/site/test/test2/working-dir-test.uce similarity index 70% rename from test/test2/working-dir-test.uce rename to site/test/test2/working-dir-test.uce index c5b3a27..99bab3e 100644 --- a/test/test2/working-dir-test.uce +++ b/site/test/test2/working-dir-test.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { print("Sub-Invoke Working dir: ", get_cwd(), "\n"); } diff --git a/test/uri.uce b/site/test/uri.uce similarity index 51% rename from test/uri.uce rename to site/test/uri.uce index f46c890..c468d51 100644 --- a/test/uri.uce +++ b/site/test/uri.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -12,17 +12,17 @@ RENDER()
    - "/> + "/>
    - Encode: post["uri_encode"]) ?> + Encode:
    - Decode: post["uri_encode"])) ?> + Decode:
    -
    params) ?>
    +
    } diff --git a/test/utf8.uce b/site/test/utf8.uce similarity index 68% rename from test/utf8.uce rename to site/test/utf8.uce index 252ce55..b937daa 100644 --- a/test/utf8.uce +++ b/site/test/utf8.uce @@ -9,10 +9,10 @@ String string_to_hex(String s) } -RENDER() +RENDER(Request& context) { - String raw = first(context->post["raw"], "■👪▧▲🏳️‍🌈😂👋🏽😆😏😱🇦🇽Udø島リZ̸̢̧̡̧̗̰̪͉̤͖͉̪̝̦͎̮̑͜a̸̧̝̱̹̲̗̪̰̦͒̃͋̿̿̃̈͑̐͑͗̚̕̚͝͠l͚̜͕̠̣ģ̸̧̘̜͇͚͈͙̓̌̅͑͊͊͋̓́͌̈́̿̈́͗͘̚ͅͅȏ̶̗̤̳͎̫̥͕̣͔̥̙̜̰̂͌̍͊͂́̅̇̒̕̕ルイ社もなく"); + String raw = first(context.post["raw"], "■👪▧▲🏳️‍🌈😂👋🏽😆😏😱🇦🇽Udø島リZ̸̢̧̡̧̗̰̪͉̤͖͉̪̝̦͎̮̑͜a̸̧̝̱̹̲̗̪̰̦͒̃͋̿̿̃̈͑̐͑͗̚̕̚͝͠l͚̜͕̠̣ģ̸̧̘̜͇͚͈͙̓̌̅͑͊͊͋̓́͌̈́̿̈́͗͘̚ͅͅȏ̶̗̤̳͎̫̥͕̣͔̥̙̜̰̂͌̍͊͂́̅̇̒̕̕ルイ社もなく"); <> @@ -55,7 +55,7 @@ RENDER() } ?> -
    params) ?>
    +
    } diff --git a/test/websockets.ws.uce b/site/test/websockets.ws.uce similarity index 81% rename from test/websockets.ws.uce rename to site/test/websockets.ws.uce index e07aec0..039b740 100644 --- a/test/websockets.ws.uce +++ b/site/test/websockets.ws.uce @@ -8,21 +8,23 @@ String clean_chat_field(String raw, u32 max_length) return(value); } -DTree chat_event(String type, String body) +DTree chat_event(Request& context, String type, String body) { DTree event; event["type"] = type; event["body"] = body; event["connection_id"] = ws_connection_id(); - event["scope"] = first(context->params["DOCUMENT_URI"], ws_scope()); + event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope()); event["online"] = (f64)ws_connection_count(); event["at"] = gmdate("%H:%M:%S"); + event["name"] = context.connection["name"].to_string(); + event["message_count"] = context.connection["message_count"]; return(event); } -RENDER() +RENDER(Request& context) { - String ws_url = context->params["DOCUMENT_URI"]; + String ws_url = context.params["DOCUMENT_URI"]; u64 online = ws_connection_count(); <> @@ -145,13 +147,13 @@ Status: Connecting... } -WS() +WS(Request& context) { if(ws_is_binary()) { ws_send_to( ws_connection_id(), - json_encode(chat_event("notice", "Binary messages are not handled by this demo")) + json_encode(chat_event(context, "notice", "Binary messages are not handled by this demo")) ); return; } @@ -160,26 +162,34 @@ WS() String type = clean_chat_field(payload["type"].to_string(), 24); String name = clean_chat_field(payload["name"].to_string(), 32); String body = clean_chat_field(payload["body"].to_string(), 500); + u64 message_count = int_val(context.connection["message_count"].to_string()); if(name == "") - name = "guest"; + name = first(context.connection["name"].to_string(), "guest"); + + context.connection["name"] = name; if(type == "join") { - ws_send(json_encode(chat_event("join", name + " joined the room"))); + context.connection["joined_at"] = gmdate("%Y-%m-%d %H:%M:%S"); + context.connection["last_type"] = type; + ws_send(json_encode(chat_event(context, "join", name + " joined the room"))); return; } if(type == "message" && body != "") { - DTree event = chat_event("message", body); - event["name"] = name; + context.connection["last_type"] = type; + context.connection["last_body"] = body; + context.connection["message_count"] = (f64)(message_count + 1); + DTree event = chat_event(context, "message", body); ws_send(json_encode(event)); return; } if(type != "") { - ws_send_to(ws_connection_id(), json_encode(chat_event("notice", "Unknown message type: " + type))); + context.connection["last_type"] = type; + ws_send_to(ws_connection_id(), json_encode(chat_event(context, "notice", "Unknown message type: " + type))); } } diff --git a/test/working-dir.uce b/site/test/working-dir.uce similarity index 61% rename from test/working-dir.uce rename to site/test/working-dir.uce index f4fd296..e063fbc 100644 --- a/test/working-dir.uce +++ b/site/test/working-dir.uce @@ -1,6 +1,6 @@ -RENDER() +RENDER(Request& context) { <> @@ -11,10 +11,9 @@ RENDER()
    -
    params) ?>
    +
    } - diff --git a/src/fastcgi/src/fcgicc.cc b/src/fastcgi/src/fcgicc.cc index dbee3ba..ff1322f 100644 --- a/src/fastcgi/src/fcgicc.cc +++ b/src/fastcgi/src/fcgicc.cc @@ -38,6 +38,7 @@ #include #include // E* +#include #include // read, write, close, unlink #include // hton* #include // sockaddr_in, INADDR_* @@ -74,6 +75,16 @@ is_valid_close_code(u16 status_code) return(false); } +static void +set_socket_nonblocking(int socket_handle) +{ + int flags = fcntl(socket_handle, F_GETFL, 0); + if(flags == -1) + throw std::runtime_error("fcntl(F_GETFL) failed"); + if(fcntl(socket_handle, F_SETFL, flags | O_NONBLOCK) == -1) + throw std::runtime_error("fcntl(F_SETFL) failed"); +} + void FastCGIServer::shutdown() { @@ -145,6 +156,7 @@ FastCGIServer::listen(unsigned tcp_port) if (::listen(server_socket, 100)) throw std::runtime_error("listen() failed"); + set_socket_nonblocking(server_socket); server_sockets.push_back(server_socket); } catch (...) { close(server_socket); @@ -185,6 +197,7 @@ FastCGIServer::listen(const std::string& local_path) if (::listen(server_socket, 100)) throw std::runtime_error("listen() failed"); + set_socket_nonblocking(server_socket); server_sockets.push_back(server_socket); listen_unlink.push_back(local_path); @@ -208,8 +221,14 @@ FastCGIServer::send_output_buffer(Connection& con) int write_result = write(con.client_socket, con.output_buffer.data(), con.output_buffer.size()); - if (write_result == -1) + if(write_result == -1) + { + if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return(0); + if(errno == ECONNRESET || errno == EPIPE) + return(-1); throw std::runtime_error("write() failed"); + } con.output_buffer.erase(0, write_result); return write_result; } @@ -255,6 +274,8 @@ FastCGIServer::dispatch_websocket_message(Connection& connection, RequestID requ it->second->resources.is_websocket = true; it->second->resources.websocket_connection_id = connection.websocket_connection_id; it->second->resources.websocket_scope = connection.websocket_scope; + it->second->resources.websocket_connection_state = &connection.websocket_state; + it->second->connection.set_reference(&connection.websocket_state); it->second->resources.websocket_opcode = opcode; it->second->resources.websocket_is_binary = (opcode == 0x2); it->second->resources.websocket_is_text = (opcode == 0x1); @@ -282,125 +303,152 @@ FastCGIServer::process(int timeout_ms) for(auto con : client_sockets) { FD_SET(con.first, &fs_read); - if (!con.second->output_buffer.empty() || con.second->close_socket) + if(!con.second->output_buffer.empty() || con.second->close_socket) FD_SET(con.first, &fs_write); nfd = std::max(nfd, con.first); } - int select_result = select(nfd + 1, &fs_read, &fs_write, NULL, - timeout_ms < 0 ? NULL : &tv); - if (select_result == -1) - if (errno == EINTR) - return; - else + int select_result = select( + nfd + 1, + &fs_read, + &fs_write, + NULL, + timeout_ms < 0 ? NULL : &tv + ); + if(select_result == -1) + { + if(errno == EINTR) + return; throw std::runtime_error("select() failed"); + } for(auto socket_handle : server_sockets) - if (FD_ISSET(socket_handle, &fs_read)) { - int client_socket = accept(socket_handle, NULL, NULL); - if (client_socket == -1) - throw std::runtime_error("accept() failed"); - printf("Opening socket %i\n", client_socket); - client_sockets[client_socket] = new Connection(); - client_sockets[client_socket]->client_socket = client_socket; - client_sockets[client_socket]->server_socket = socket_handle; - client_sockets[client_socket]->type = server_socket_types[socket_handle]; - if(client_sockets[client_socket]->type == 'H') + if(!FD_ISSET(socket_handle, &fs_read)) + continue; + + for(;;) { - FastCGIRequest* new_request = new FastCGIRequest(); - new_request->resources.client_socket = client_socket; - new_request->resources.server_socket = socket_handle; - new_request->stats.time_init = microtime(); - client_sockets[client_socket]->requests[client_socket] = new_request; + int client_socket = accept(socket_handle, NULL, NULL); + if(client_socket == -1) + { + if(errno == EAGAIN || errno == EWOULDBLOCK) + break; + if(errno == EINTR) + continue; + throw std::runtime_error("accept() failed"); + } + + set_socket_nonblocking(client_socket); + printf("Opening socket %i\n", client_socket); + client_sockets[client_socket] = new Connection(); + client_sockets[client_socket]->client_socket = client_socket; + client_sockets[client_socket]->server_socket = socket_handle; + client_sockets[client_socket]->type = server_socket_types[socket_handle]; + if(client_sockets[client_socket]->type == 'H') + { + FastCGIRequest* new_request = new FastCGIRequest(); + new_request->resources.client_socket = client_socket; + new_request->resources.server_socket = socket_handle; + new_request->stats.time_init = microtime(); + client_sockets[client_socket]->requests[client_socket] = new_request; + } } } - for (std::map::iterator it = client_sockets.begin(); + for(std::map::iterator it = client_sockets.begin(); it != client_sockets.end();) { int read_socket = it->first; + Connection* connection = it->second; - if (FD_ISSET(read_socket, &fs_read)) + if(FD_ISSET(read_socket, &fs_read)) { int read_result = read(read_socket, buffer, sizeof(buffer)); - if (read_result == -1) - if (errno == ECONNRESET) + if(read_result == -1) + { + if(errno == ECONNRESET) goto close_socket; - else + if(errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) throw std::runtime_error("read() on socket failed"); - if (read_result == 0) + } + else if(read_result == 0) + { + if(connection->type == 'H' && connection->is_websocket) { - if(it->second->type == 'H' && it->second->is_websocket) - { - it->second->close_socket = true; - } - else if(it->second->type == 'H' && it->second->input_buffer != "") - { - process_http_request( - *client_sockets[it->second->client_socket]->requests[it->second->client_socket], - it->second->input_buffer); - if(it->second->close_socket || !it->second->output_buffer.empty()) - it->second->input_buffer = ""; - else - it->second->close_socket = true; - } + connection->close_socket = true; + } + else if(connection->type == 'H' && connection->input_buffer != "") + { + process_http_request( + *client_sockets[connection->client_socket]->requests[connection->client_socket], + connection->input_buffer + ); + if(connection->close_socket || !connection->output_buffer.empty()) + connection->input_buffer = ""; else - { - it->second->close_socket = true; + connection->close_socket = true; + } + else + { + connection->close_socket = true; } } else { - it->second->input_buffer.append(buffer, read_result); - if(it->second->type == 'H') + connection->input_buffer.append(buffer, read_result); + if(connection->type == 'H') + { + if(connection->is_websocket) { - if(it->second->is_websocket) - { - process_websocket_input(*it->second); - } - else - { - process_http_request( - *client_sockets[it->second->client_socket]->requests[it->second->client_socket], - it->second->input_buffer); - if(it->second->close_socket || !it->second->output_buffer.empty()) - it->second->input_buffer = ""; - } + process_websocket_input(*connection); } else { - read_fgci(*it->second); + process_http_request( + *client_sockets[connection->client_socket]->requests[connection->client_socket], + connection->input_buffer + ); + if(connection->close_socket || !connection->output_buffer.empty()) + connection->input_buffer = ""; + } + } + else + { + read_fgci(*connection); } } } - if (!it->second->output_buffer.empty() && - FD_ISSET(read_socket, &fs_write)) + if(!connection->output_buffer.empty() && FD_ISSET(read_socket, &fs_write)) { - if(it->second->type == 'F') - write_fgci(*it->second); - send_output_buffer(*it->second); + if(connection->type == 'F') + write_fgci(*connection); + if(send_output_buffer(*connection) == -1) + goto close_socket; } - if (it->second->close_socket && it->second->output_buffer.empty()) + if(connection->close_socket && connection->output_buffer.empty()) { close_socket: printf("Closing socket %i\n", it->first); int close_result = close(it->first); - if (close_result == -1 && errno != ECONNRESET) + if(close_result == -1 && errno != ECONNRESET) throw std::runtime_error("close() failed"); - Connection* connection = it->second; + Connection* doomed_connection = it->second; client_sockets.erase(it++); - delete connection; + delete doomed_connection; if(calls_until_termination != -1 && client_sockets.size() == 0) { calls_until_termination -= 1; if(calls_until_termination <= 0) exit(0); } - } else + } + else + { ++it; + } } } @@ -528,6 +576,8 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data) request.resources.is_websocket = true; request.resources.websocket_connection_id = connection->websocket_connection_id; request.resources.websocket_scope = connection->websocket_scope; + request.resources.websocket_connection_state = &connection->websocket_state; + request.connection.set_reference(&connection->websocket_state); connection->output_buffer += "HTTP/1.1 101 Switching Protocols\r\n" @@ -682,14 +732,15 @@ FastCGIServer::process_websocket_input(Connection& connection) } bool -FastCGIServer::websocket_send_to(String connection_id, String message) +FastCGIServer::websocket_send_to(String connection_id, String message, bool binary) { + u8 opcode = binary ? 0x2 : 0x1; for(auto& item : client_sockets) { Connection* connection = item.second; if(connection->is_websocket && connection->websocket_connection_id == connection_id) { - connection->output_buffer += ws_encode_frame(message); + connection->output_buffer += ws_encode_frame(message, opcode); return(true); } } @@ -697,9 +748,10 @@ FastCGIServer::websocket_send_to(String connection_id, String message) } u64 -FastCGIServer::websocket_broadcast(String scope, String message) +FastCGIServer::websocket_broadcast(String scope, String message, bool binary) { u64 sent = 0; + u8 opcode = binary ? 0x2 : 0x1; for(auto& item : client_sockets) { Connection* connection = item.second; @@ -707,7 +759,7 @@ FastCGIServer::websocket_broadcast(String scope, String message) continue; if(scope != "" && connection->websocket_scope != scope) continue; - connection->output_buffer += ws_encode_frame(message); + connection->output_buffer += ws_encode_frame(message, opcode); sent += 1; } return(sent); diff --git a/src/fastcgi/src/fcgicc.h b/src/fastcgi/src/fcgicc.h index c94a529..dcacdbb 100644 --- a/src/fastcgi/src/fcgicc.h +++ b/src/fastcgi/src/fcgicc.h @@ -72,6 +72,7 @@ public: bool is_websocket = false; String websocket_connection_id; String websocket_scope; + DTree websocket_state; String websocket_fragment_buffer; u8 websocket_fragment_opcode = 0; char type = 'F'; // F = FastCGI, H = HttpServer @@ -91,8 +92,8 @@ public: void fail_websocket_connection(Connection& connection, u16 status_code, String reason = ""); void close_websocket_connection(Connection& connection, u16 status_code = 1000, String reason = ""); void dispatch_websocket_message(Connection& connection, RequestID request_id, String payload, u8 opcode); - bool websocket_send_to(String connection_id, String message); - u64 websocket_broadcast(String scope, String message); + bool websocket_send_to(String connection_id, String message, bool binary = false); + u64 websocket_broadcast(String scope, String message, bool binary = false); StringList websocket_connection_ids(String scope = ""); bool websocket_close(String connection_id, u16 status_code = 1000, String reason = ""); static void request_write_fgci(Connection&, RequestID, FastCGIRequest&); diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index 0970d41..1c8395c 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -12,25 +12,36 @@ String process_text_literal(Request* context, SharedUnit* su, String content) bool inside_quote = false; String code_buffer = ""; bool is_field = false; + bool escape_field = false; for(u32 i = 0; i < content.length(); i++) { char c = content[i]; + char c1 = (i + 1 < content.length()) ? content[i + 1] : '\0'; + char c2 = (i + 2 < content.length()) ? content[i + 2] : '\0'; switch(mode) { case(0): - if(c == '<' && content[i+1] == '?') + if(c == '<' && c1 == '?') { code_buffer = ""; - if(content[i+2] == '=') + if(c2 == '=') { is_field = true; + escape_field = true; + i += 2; + } + else if(c2 == ':') + { + is_field = true; + escape_field = false; i += 2; } else { is_field = false; + escape_field = false; i += 1; } mode = 1; // code-parsing mode @@ -43,7 +54,7 @@ String process_text_literal(Request* context, SharedUnit* su, String content) case(1): if(inside_quote) { - if(quote_char == c && content[i-1] != '\\') + if(quote_char == c && (i == 0 || content[i-1] != '\\')) inside_quote = false; code_buffer.append(1, c); } @@ -55,19 +66,32 @@ String process_text_literal(Request* context, SharedUnit* su, String content) quote_char = c; code_buffer.append(1, c); } - else if(c == '?' && content[i+1] == '>') + else if(c == '?' && c1 == '>') { mode = 0; i += 1; if(is_field) { - pc.append( - HT_END + - "print(html_escape( " + - code_buffer + - " )); " + - HT_START - ); + if(escape_field) + { + pc.append( + HT_END + + "print(html_escape( " + + code_buffer + + " )); " + + HT_START + ); + } + else + { + pc.append( + HT_END + + "print( " + + code_buffer + + " ); " + + HT_START + ); + } } else { @@ -87,6 +111,57 @@ String process_text_literal(Request* context, SharedUnit* su, String content) return(HT_START + pc + HT_END); } +String preprocess_named_render_syntax(String content) +{ + String result = ""; + String current_line = ""; + + auto flush_line = [&]() { + if(current_line.length() == 0) + return; + + String line = current_line; + String line_break = ""; + if(line.length() > 0 && line.back() == '\n') + { + line_break = "\n"; + line.pop_back(); + } + + u32 indent_length = 0; + while(indent_length < line.length() && isspace(line[indent_length])) + indent_length += 1; + + String indent = line.substr(0, indent_length); + String trimmed = trim(line); + if(trimmed.rfind("RENDER:", 0) == 0) + { + String signature = trimmed.substr(7); + auto open_paren_pos = signature.find("("); + if(open_paren_pos != String::npos) + { + String render_name = trim(signature.substr(0, open_paren_pos)); + String render_signature = signature.substr(open_paren_pos); + if(render_name != "") + line = indent + "EXPORT void render_" + safe_name(render_name) + render_signature; + } + } + + result += line + line_break; + current_line = ""; + }; + + for(auto c : content) + { + current_line.append(1, c); + if(c == '\n') + flush_line(); + } + flush_line(); + + return(result); +} + String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content) { String pc = @@ -104,10 +179,12 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String for(u32 i = 0; i < source_length; i++) { char c = content[i]; + char c1 = (i + 1 < source_length) ? content[i + 1] : '\0'; + char c2 = (i + 2 < source_length) ? content[i + 2] : '\0'; current_line.append(1, c); if(mode == 1) { - if(c == '<' && (content[i+1] == '/') && (content[i+2] == '>')) + if(c == '<' && c1 == '/' && c2 == '>') { i += 2; pc.append(process_text_literal(context, su, html_buffer)); @@ -119,7 +196,7 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String html_buffer.append(1, c); } } - else if(!inside_quote && c == '<' && (content[i+1] == '>')) + else if(!inside_quote && c == '<' && c1 == '>') { mode = 1; token = ""; @@ -147,16 +224,20 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String pc.append("#include \"" + sub_su->bin_path + "/" + sub_su->pre_file_name + "\"\n"); } } - else if(current_line.substr(0, 6) == "EXPORT" && isspace(current_line[6])) + else { - current_line = ""; - auto end_declaration_pos = content.find("{", i); - if(end_declaration_pos != std::string::npos) + String trimmed_line = trim(current_line); + if(c == 10 && trimmed_line.length() > 7 && trimmed_line.substr(0, 6) == "EXPORT" && isspace(trimmed_line[6])) { - pc.append(1, '\n'); - String declaration = trim(content.substr(i, end_declaration_pos - i)); - su->api_declarations.push_back(declaration+";\n"); - //printf("declaration found: %s\n", declaration.c_str()); + current_line = ""; + auto end_declaration_pos = content.find("{", i); + if(end_declaration_pos != std::string::npos) + { + pc.append(1, '\n'); + String declaration = trim(content.substr(i, end_declaration_pos - i)); + su->api_declarations.push_back(declaration+";\n"); + //printf("declaration found: %s\n", declaration.c_str()); + } } } } @@ -168,11 +249,13 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String String preprocess_shared_unit(Request* context, SharedUnit* su) { + String content = file_get_contents(su->file_name); + content = preprocess_named_render_syntax(content); return( preprocess_shared_unit_char_wise( context, su, - file_get_contents(su->file_name) + content ) ); } @@ -223,9 +306,9 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) su->on_setup = (request_handler)dlsym(su->so_handle, "set_current_request"); if ((error = dlerror()) != NULL) printf("Error - %s in %s\n", error, su->file_name.c_str()); - su->on_render = (call_handler)dlsym(su->so_handle, "render"); + su->on_render = (request_ref_handler)dlsym(su->so_handle, "render"); dlerror(); - su->on_websocket = (call_handler)dlsym(su->so_handle, "websocket"); + su->on_websocket = (request_ref_handler)dlsym(su->so_handle, "websocket"); dlerror(); su->api_declarations = split(file_get_contents(su->api_file_name), "\n"); //else @@ -291,6 +374,11 @@ SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_opti { SharedUnit* su = context->server->units[file_name]; auto mod_time = file_mtime(file_name); + auto setup_template_time = file_mtime( + context->server->config["COMPILER_SYS_PATH"] + "/" + + context->server->config["SETUP_TEMPLATE"]); + if(setup_template_time > mod_time) + mod_time = setup_template_time; auto compiled_time = su ? file_mtime(su->so_name) : 0; bool do_recompile = false; if(su && (compiled_time < mod_time || mod_time == 0)) @@ -378,36 +466,141 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String } -void compiler_invoke(Request* context, String file_name, DTree& call_param) +String component_normalize_path(String name) +{ + name = trim(name); + if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce") + return(name); + return(name + ".uce"); +} + +void component_parse_target(String target, String& file_name, String& render_name) +{ + target = trim(target); + render_name = "render"; + auto render_split_pos = target.find(":"); + if(render_split_pos != String::npos) + { + render_name = trim(target.substr(render_split_pos + 1)); + target = trim(target.substr(0, render_split_pos)); + if(render_name == "") + render_name = "render"; + } + file_name = target; +} + +String component_resolve_path(String name) +{ + String file_name; + String render_name; + component_parse_target(name, file_name, render_name); + + if(file_name == "") + return(""); + + StringList candidates; + auto push_candidate = [&] (String candidate) { + if(candidate == "") + return; + candidates.push_back(candidate); + }; + + push_candidate(file_name); + push_candidate(component_normalize_path(file_name)); + + if(file_name.rfind("components/", 0) != 0) + { + push_candidate("components/" + file_name); + push_candidate(component_normalize_path("components/" + file_name)); + } + + std::map seen; + for(auto& candidate : candidates) + { + if(seen[candidate]) + continue; + seen[candidate] = true; + String resolved = candidate; + if(resolved[0] != '/') + resolved = expand_path(resolved, get_cwd()); + if(file_exists(resolved)) + return(resolved); + } + + return(""); +} + +String render_handler_symbol(String render_name) +{ + render_name = trim(render_name); + if(render_name == "" || render_name == "render") + return("render"); + return("render_" + safe_name(render_name)); +} + +request_ref_handler get_render_handler(SharedUnit* su, String render_name) +{ + String symbol = render_handler_symbol(render_name); + if(symbol == "render") + return(su->on_render); + + auto it = su->api_functions.find(symbol); + if(it != su->api_functions.end()) + return((request_ref_handler)it->second); + + auto handler = (request_ref_handler)dlsym(su->so_handle, symbol.c_str()); + dlerror(); + su->api_functions[symbol] = (void*)handler; + return(handler); +} + +bool compiler_invoke_render(Request* context, String file_name, String render_name, String* error_out = 0) +{ + auto su = compiler_load_shared_unit(context, file_name, "", false); + if(!su) + return(false); + + if(!su->on_setup) + { + if(error_out) + *error_out = "internal error: set_current_request() not defined in " + file_name; + return(false); + } + + auto handler = get_render_handler(su, render_name); + if(!handler) + { + if(error_out) + { + if(trim(render_name) == "" || trim(render_name) == "render") + *error_out = "no RENDER() entry point"; + else + *error_out = "no RENDER:" + render_name + "() entry point"; + } + return(false); + } + + String prev_wd = get_cwd(); + set_cwd(su->src_path); + su->on_setup(context); + handler(*context); + set_cwd(prev_wd); + return(true); +} + +void compiler_invoke(Request* context, String file_name) { printf("(i) compiler_invoke file %s\n", file_name.c_str()); - auto su = compiler_load_shared_unit(context, file_name, "", false); - if(su) + String error_message = ""; + if(!compiler_invoke_render(context, file_name, "render", &error_message) && error_message != "") { - if(!su->on_setup) - { - if(context->stats.invoke_count == 1) - context->header["Content-Type"] = "text/plain"; - print("internal error: set_current_request() not defined in", file_name, "\n"); - } - else if(!su->on_render) - { - if(context->stats.invoke_count == 1) - context->header["Content-Type"] = "text/plain"; - print("no RENDER() entry point"); - } - else - { - String prev_wd = get_cwd(); - set_cwd(su->src_path); - su->on_setup(context); - su->on_render(call_param); - set_cwd(prev_wd); - } + if(context->stats.invoke_count == 1) + context->header["Content-Type"] = "text/plain"; + print(error_message); } } -void compiler_invoke_websocket(Request* context, String file_name, DTree& call_param) +void compiler_invoke_websocket(Request* context, String file_name) { auto su = compiler_load_shared_unit(context, file_name, "", false); if(!su) @@ -428,20 +621,98 @@ void compiler_invoke_websocket(Request* context, String file_name, DTree& call_p String prev_wd = get_cwd(); set_cwd(su->src_path); su->on_setup(context); - su->on_websocket(call_param); + su->on_websocket(*context); set_cwd(prev_wd); } void render_file(String file_name) { //printf("(i) render_file(%s)\n", file_name.c_str()); - DTree call_param; - compiler_invoke(context, file_name, call_param); + compiler_invoke(context, file_name); } -void render_file(String file_name, DTree& call_param) +void render_file(String file_name, Request& context) { - compiler_invoke(context, file_name, call_param); + compiler_invoke(&context, file_name); +} + +String component_resolve(String name) +{ + return(component_resolve_path(name)); +} + +bool component_exists(String name) +{ + return(component_resolve(name) != ""); +} + +String component_error_banner(String message) +{ + return("
    " + html_escape(message) + "
    "); +} + +void render_component(String name) +{ + DTree props; + render_component(name, props, *context); +} + +void render_component(String name, Request& context) +{ + DTree props; + render_component(name, props, context); +} + +void render_component(String name, DTree props) +{ + render_component(name, props, *context); +} + +void render_component(String name, DTree props, Request& context) +{ + String file_name; + String render_name; + component_parse_target(name, file_name, render_name); + + String resolved_name = component_resolve_path(file_name); + if(resolved_name == "") + { + print(component_error_banner("component not found: " + file_name)); + return; + } + + DTree previous_call = context.call; + context.call = props; + + String error_message = ""; + if(!compiler_invoke_render(&context, resolved_name, render_name, &error_message) && error_message != "") + print(component_error_banner(error_message)); + + context.call = previous_call; +} + +String component(String name) +{ + DTree props; + return(component(name, props, *context)); +} + +String component(String name, Request& context) +{ + DTree props; + return(component(name, props, context)); +} + +String component(String name, DTree props) +{ + return(component(name, props, *context)); +} + +String component(String name, DTree props, Request& context) +{ + ob_start(); + render_component(name, props, context); + return(ob_get_close()); } SharedUnit* load_file(String file_name) diff --git a/src/lib/compiler.h b/src/lib/compiler.h index 152badf..5361e6c 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -1,5 +1,7 @@ -#define RENDER() extern "C" void render(DTree& call) -#define WS() extern "C" void websocket(DTree& call) +#pragma once + +#define RENDER(X) extern "C" void render(Request& context) +#define WS(X) extern "C" void websocket(Request& context) #define EXPORT extern "C" String process_html_literal(Request* context, SharedUnit* su, String content); @@ -8,13 +10,23 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name); void load_shared_unit(Request* context, SharedUnit* su, String file_name); void compile_shared_unit(Request* context, SharedUnit* su, String file_name); SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_optional = false); -void compiler_invoke(Request* context, String file_name, DTree& call_param); -void compiler_invoke_websocket(Request* context, String file_name, DTree& call_param); +void compiler_invoke(Request* context, String file_name); +void compiler_invoke_websocket(Request* context, String file_name); SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path = "", bool opt_so_optional = false); SharedUnit* load_file(String file_name); void render_file(String file_name); -void render_file(String file_name, DTree& call_param); +void render_file(String file_name, Request& context); DTree* call_file(String file_name, String function_name, DTree* call_param = 0); +String component_resolve(String name); +bool component_exists(String name); +void render_component(String name); +void render_component(String name, Request& context); +void render_component(String name, DTree props); +void render_component(String name, DTree props, Request& context); +String component(String name); +String component(String name, Request& context); +String component(String name, DTree props); +String component(String name, DTree props, Request& context); StringList precompile_jobs; diff --git a/src/lib/dtree.cpp b/src/lib/dtree.cpp index ab0ee48..168d9ba 100644 --- a/src/lib/dtree.cpp +++ b/src/lib/dtree.cpp @@ -1,10 +1,30 @@ +namespace { + +template +TreePtr dtree_resolve_reference(TreePtr tree) +{ + u32 depth = 0; + while(tree && tree->type == 'R' && depth < 16) + { + TreePtr target = reinterpret_cast(tree->_ptr); + if(target == 0 || target == tree) + break; + tree = target; + depth += 1; + } + return(tree); +} + +} + void DTree::each(std::function f) { - switch(type) + const DTree& target = deref(); + switch(target.type) { case('M'): - for (auto it = _map.begin(); it != _map.end(); ++it) + for (auto it = target._map.begin(); it != target._map.end(); ++it) { f(it->second, it->first); } @@ -17,43 +37,49 @@ void DTree::each(std::function f) bool DTree::is_array() { - return(type == 'M'); + return(deref().type == 'M'); } String DTree::to_string() { - switch(type) + const DTree& target = deref(); + switch(target.type) { case('S'): - return(_String); + return(target._String); break; case('F'): - return(std::to_string(_float)); + return(std::to_string(target._float)); break; case('B'): - return(_bool ? "(true)" : "(false)"); + return(target._bool ? "(true)" : "(false)"); break; case('M'): return(""); break; case('P'): - return(std::to_string((u64)_ptr)); + return(std::to_string((u64)target._ptr)); + break; + case('R'): + return(""); break; } + return(""); } String DTree::to_json() { - switch(type) + const DTree& target = deref(); + switch(target.type) { case('S'): - return(json_escape(_String)); + return(json_escape(target._String)); break; case('F'): - return(std::to_string(_float)); + return(std::to_string(target._float)); break; case('B'): - return(_bool ? "true" : "false"); + return(target._bool ? "true" : "false"); break; case('M'): return("\"(array)\""); @@ -61,12 +87,17 @@ String DTree::to_json() case('P'): return("\"(pointer)\""); break; + case('R'): + return("\"(reference)\""); + break; } + return("\"(unknown)\""); } String DTree::get_type_name() { - switch(type) + const DTree& target = deref(); + switch(target.type) { case('S'): return("String"); @@ -83,11 +114,62 @@ String DTree::get_type_name() case('P'): return("pointer"); break; + case('R'): + return("reference"); + break; } + return("unknown"); +} + +bool DTree::is_reference() +{ + return(type == 'R'); +} + +DTree* DTree::reference_target() +{ + if(type != 'R') + return(0); + DTree* target = dtree_resolve_reference(this); + if(target == 0 || target == this || target->type == 'R') + return(0); + return(target); +} + +const DTree* DTree::reference_target() const +{ + if(type != 'R') + return(0); + const DTree* target = dtree_resolve_reference(this); + if(target == 0 || target == this || target->type == 'R') + return(0); + return(target); +} + +DTree& DTree::deref() +{ + DTree* target = dtree_resolve_reference(this); + if(target == 0) + return(*this); + return(*target); +} + +const DTree& DTree::deref() const +{ + const DTree* target = dtree_resolve_reference(this); + if(target == 0) + return(*this); + return(*target); } void DTree::set_type(char t) { + DTree* target = reference_target(); + if(target) + { + target->set_type(t); + return; + } if(type != t) { type = t; @@ -103,30 +185,60 @@ void DTree::set_type(char t) void DTree::set(String s) { + DTree* target = reference_target(); + if(target) + { + target->set(s); + return; + } set_type('S'); _String = s; } void DTree::set(void* p) { + DTree* target = reference_target(); + if(target) + { + target->set(p); + return; + } set_type('P'); _ptr = p; } void DTree::set(f64 f) { + DTree* target = reference_target(); + if(target) + { + target->set(f); + return; + } set_type('F'); _float = f; } void DTree::set_bool(bool b) { + DTree* target = reference_target(); + if(target) + { + target->set_bool(b); + return; + } set_type('B'); _bool = b; } void DTree::set(DTree source) { + DTree* target = reference_target(); + if(target) + { + target->set(source); + return; + } set_type(source.type); switch(type) { @@ -145,11 +257,20 @@ void DTree::set(DTree source) case('P'): _ptr = source._ptr; break; + case('R'): + _ptr = source._ptr; + break; } } void DTree::set(StringMap source) { + DTree* target = reference_target(); + if(target) + { + target->set(source); + return; + } set_type('M'); for (auto it = source.begin(); it != source.end(); ++it) { @@ -157,13 +278,25 @@ void DTree::set(StringMap source) } } +void DTree::set_reference(DTree* target) +{ + type = 'R'; + _ptr = target; +} + DTree* DTree::key(String s) { + DTree* target = reference_target(); + if(target) + return(target->key(s)); set_type('M'); return(&_map[s]); } DTree& DTree::operator [] (String s) { + DTree* target = reference_target(); + if(target) + return((*target)[s]); set_type('M'); return(_map[s]); } @@ -176,6 +309,12 @@ void DTree::operator = (StringMap v) { set(v); } void DTree::push(DTree& child) { + DTree* target = reference_target(); + if(target) + { + target->push(child); + return; + } set_type('M'); _map[std::to_string(_array_index)] = child; _array_index += 1; @@ -183,6 +322,9 @@ void DTree::push(DTree& child) DTree DTree::pop() { + DTree* target = reference_target(); + if(target) + return(target->pop()); set_type('M'); auto last = _map.rbegin(); DTree result = last->second; @@ -192,12 +334,24 @@ DTree DTree::pop() void DTree::remove(String s) { + DTree* target = reference_target(); + if(target) + { + target->remove(s); + return; + } set_type('M'); _map.erase(s); } void DTree::clear() { + DTree* target = reference_target(); + if(target) + { + target->clear(); + return; + } set_type('M'); _map.clear(); } diff --git a/src/lib/dtree.h b/src/lib/dtree.h index 51df5f2..5ec68e0 100644 --- a/src/lib/dtree.h +++ b/src/lib/dtree.h @@ -1,3 +1,5 @@ +#pragma once + String json_escape(String s); struct DTree { @@ -16,6 +18,11 @@ struct DTree { String to_string(); String to_json(); String get_type_name(); + bool is_reference(); + DTree* reference_target(); + const DTree* reference_target() const; + DTree& deref(); + const DTree& deref() const; void set_type(char t); void set(String s); void set(void* p); @@ -23,6 +30,7 @@ struct DTree { void set_bool(bool b); void set(DTree source); void set(StringMap source); + void set_reference(DTree* target); DTree* key(String s); DTree& operator [] (String s); void operator = (String v); diff --git a/src/lib/functionlib.cpp b/src/lib/functionlib.cpp index 8c42175..9ac462e 100644 --- a/src/lib/functionlib.cpp +++ b/src/lib/functionlib.cpp @@ -77,9 +77,9 @@ String replace(String s, String search, String replace_with) String trim(String raw) { - u32 len = raw.length(); - u32 start_pos = 0; - u32 end_pos = len - 1; + s64 len = raw.length(); + s64 start_pos = 0; + s64 end_pos = len - 1; if(len == 0 || (len == 1 && isspace(raw[0]))) return(""); while(start_pos < len && isspace(raw[start_pos])) @@ -610,7 +610,13 @@ void ob_close() delete context->ob; context->ob_stack.pop_back(); if(context->ob_stack.size() == 0) + { ob_start(); + } + else + { + context->ob = context->ob_stack.back(); + } } String ob_get() diff --git a/src/lib/functionlib.h b/src/lib/functionlib.h index efbc7fb..0ee7ff9 100644 --- a/src/lib/functionlib.h +++ b/src/lib/functionlib.h @@ -1,4 +1,6 @@ +#pragma once + u8 char_to_u8(char input); u8 hex_to_u8(String src); u64 int_val(String s, u32 base = 10); diff --git a/src/lib/hash.h b/src/lib/hash.h index 2460e55..b670c2f 100644 --- a/src/lib/hash.h +++ b/src/lib/hash.h @@ -1,3 +1,5 @@ +#pragma once + /* ================ sha1.h ================ */ /* SHA-1 in C diff --git a/src/lib/markdown.cpp b/src/lib/markdown.cpp new file mode 100644 index 0000000..f54c7f6 --- /dev/null +++ b/src/lib/markdown.cpp @@ -0,0 +1,1378 @@ +DTree markdown_to_ast(String src, DTree options); +String markdown_to_html(String src, DTree options); + +bool markdown_has_key(DTree& tree, String key) +{ + return(tree.type == 'M' && tree._map.count(key) > 0); +} + +DTree markdown_get_value(DTree& tree, String key) +{ + if(markdown_has_key(tree, key)) + return(tree._map[key]); + return(DTree()); +} + +String markdown_get_string(DTree& tree, String key, String default_value = "") +{ + if(markdown_has_key(tree, key)) + return(tree._map[key].to_string()); + return(default_value); +} + +bool markdown_truthy(DTree value, bool default_value = false) +{ + switch(value.type) + { + case('B'): + return(value._bool); + case('F'): + return(value._float != 0); + case('S'): + { + String raw = to_lower(trim(value._String)); + if(raw == "true" || raw == "1" || raw == "yes" || raw == "on" || raw == "(true)") + return(true); + if(raw == "false" || raw == "0" || raw == "no" || raw == "off" || raw == "(false)") + return(false); + return(default_value); + } + case('M'): + return(!value._map.empty()); + default: + return(default_value); + } +} + +bool markdown_get_bool(DTree& tree, String key, bool default_value = false) +{ + if(markdown_has_key(tree, key)) + return(markdown_truthy(tree._map[key], default_value)); + return(default_value); +} + +bool markdown_gfm_enabled(DTree& options) +{ + return(markdown_get_bool(options, "gfm", true)); +} + +bool markdown_allow_html(DTree& options) +{ + return(markdown_get_bool(options, "allow_html", false)); +} + +String markdown_get_component_target(DTree& options, String hook) +{ + if(!markdown_has_key(options, "components")) + return(""); + DTree components = options._map["components"]; + if(!markdown_has_key(components, hook)) + return(""); + return(trim(components._map[hook].to_string())); +} + +DTree markdown_make_node(String type) +{ + DTree node; + node["type"] = type; + return(node); +} + +void markdown_push(DTree& parent, DTree child) +{ + parent.push(child); +} + +bool markdown_is_blank(String line) +{ + return(trim(line) == ""); +} + +s64 markdown_count_indent(String line) +{ + s64 result = 0; + while(result < (s64)line.length() && line[result] == ' ') + result += 1; + return(result); +} + +bool markdown_is_digits(String s) +{ + if(s == "") + return(false); + for(auto c : s) + { + if(!isdigit((unsigned char)c)) + return(false); + } + return(true); +} + +bool markdown_is_identifier(String s) +{ + if(s == "") + return(false); + for(auto c : s) + { + if(!(isalnum((unsigned char)c) || c == '_' || c == '-')) + return(false); + } + return(true); +} + +String markdown_strip_newlines(String s) +{ + s = replace(s, "\r\n", "\n"); + s = replace(s, "\r", "\n"); + return(s); +} + +String markdown_trim_quotes(String s) +{ + s = trim(s); + if(s.length() >= 2) + { + char first = s.front(); + char last = s.back(); + if((first == '"' && last == '"') || (first == '\'' && last == '\'')) + return(s.substr(1, s.length() - 2)); + } + return(s); +} + +String markdown_read_quoted(String raw, s64& i) +{ + String result = ""; + if(i >= (s64)raw.length()) + return(result); + char quote = raw[i]; + i += 1; + while(i < (s64)raw.length()) + { + char c = raw[i]; + if(c == '\\' && i + 1 < (s64)raw.length()) + { + result.append(1, raw[i + 1]); + i += 2; + continue; + } + if(c == quote) + { + i += 1; + return(result); + } + result.append(1, c); + i += 1; + } + return(result); +} + +DTree markdown_parse_attrs(String raw, String& argument_out) +{ + DTree attrs; + argument_out = ""; + s64 i = 0; + while(i < (s64)raw.length()) + { + while(i < (s64)raw.length() && isspace((unsigned char)raw[i])) + i += 1; + if(i >= (s64)raw.length()) + break; + + String token = ""; + if(raw[i] == '"' || raw[i] == '\'') + { + token = markdown_read_quoted(raw, i); + if(argument_out != "") + argument_out += " "; + argument_out += token; + continue; + } + + s64 token_start = i; + while(i < (s64)raw.length() && !isspace((unsigned char)raw[i])) + { + if(raw[i] == '=') + break; + i += 1; + } + token = raw.substr(token_start, i - token_start); + + if(i < (s64)raw.length() && raw[i] == '=' && markdown_is_identifier(token)) + { + i += 1; + String value = ""; + if(i < (s64)raw.length() && (raw[i] == '"' || raw[i] == '\'')) + value = markdown_read_quoted(raw, i); + else + { + s64 value_start = i; + while(i < (s64)raw.length() && !isspace((unsigned char)raw[i])) + i += 1; + value = raw.substr(value_start, i - value_start); + } + attrs[token] = markdown_trim_quotes(value); + } + else + { + if(argument_out != "") + argument_out += " "; + argument_out += token; + } + } + return(attrs); +} + +StringList markdown_sorted_keys(DTree tree) +{ + StringList keys; + if(tree.type != 'M') + return(keys); + for(auto& it : tree._map) + keys.push_back(it.first); + std::sort(keys.begin(), keys.end(), [] (String a, String b) { + bool a_numeric = markdown_is_digits(a); + bool b_numeric = markdown_is_digits(b); + if(a_numeric && b_numeric) + return(int_val(a) < int_val(b)); + if(a_numeric != b_numeric) + return(a_numeric); + return(a < b); + }); + return(keys); +} + +StringList markdown_split_pipe_row(String raw) +{ + StringList result; + String cell = ""; + bool escaped = false; + for(auto c : raw) + { + if(escaped) + { + cell.append(1, c); + escaped = false; + continue; + } + if(c == '\\') + { + escaped = true; + continue; + } + if(c == '|') + { + result.push_back(trim(cell)); + cell = ""; + } + else + { + cell.append(1, c); + } + } + result.push_back(trim(cell)); + if(result.size() > 0 && result.front() == "") + result.erase(result.begin()); + if(result.size() > 0 && result.back() == "") + result.pop_back(); + return(result); +} + +bool markdown_parse_table_separator(String raw, StringList& alignments) +{ + alignments.clear(); + StringList cells = markdown_split_pipe_row(raw); + if(cells.size() == 0) + return(false); + for(auto cell : cells) + { + cell = trim(cell); + if(cell == "") + return(false); + String align = ""; + bool left = cell.front() == ':'; + bool right = cell.back() == ':'; + String test = cell; + if(left) + test = test.substr(1); + if(right && test.length() > 0) + test = test.substr(0, test.length() - 1); + test = trim(test); + if(test.length() < 3) + return(false); + for(auto c : test) + { + if(c != '-') + return(false); + } + if(left && right) + align = "center"; + else if(left) + align = "left"; + else if(right) + align = "right"; + else + align = ""; + alignments.push_back(align); + } + return(true); +} + +bool markdown_parse_fence(String raw, char& fence_char, s64& fence_length, String& info) +{ + String trimmed = trim(raw); + if(trimmed.length() < 3) + return(false); + char c = trimmed[0]; + if(c != '`' && c != '~') + return(false); + s64 i = 0; + while(i < (s64)trimmed.length() && trimmed[i] == c) + i += 1; + if(i < 3) + return(false); + fence_char = c; + fence_length = i; + info = trim(trimmed.substr(i)); + return(true); +} + +bool markdown_is_hr(String raw) +{ + String trimmed = trim(raw); + if(trimmed.length() < 3) + return(false); + char marker = trimmed[0]; + if(marker != '-' && marker != '*' && marker != '_') + return(false); + s64 mark_count = 0; + for(auto c : trimmed) + { + if(c == marker) + mark_count += 1; + else if(!isspace((unsigned char)c)) + return(false); + } + return(mark_count >= 3); +} + +bool markdown_parse_list_marker(String raw, bool& ordered, s64& marker_length, s64& start_number, String& content) +{ + ordered = false; + marker_length = 0; + start_number = 1; + content = ""; + if(raw.length() < 2) + return(false); + char first = raw[0]; + if((first == '-' || first == '*' || first == '+') && raw.length() >= 2 && raw[1] == ' ') + { + ordered = false; + marker_length = 2; + content = raw.substr(2); + return(true); + } + + s64 i = 0; + while(i < (s64)raw.length() && isdigit((unsigned char)raw[i])) + i += 1; + if(i == 0 || i + 1 >= (s64)raw.length()) + return(false); + if((raw[i] == '.' || raw[i] == ')') && raw[i + 1] == ' ') + { + ordered = true; + start_number = int_val(raw.substr(0, i)); + marker_length = i + 2; + content = raw.substr(marker_length); + return(true); + } + return(false); +} + +bool markdown_parse_task_prefix(String& content, bool& checked) +{ + if(content.length() < 4 || content[0] != '[' || content[2] != ']' || content[3] != ' ') + return(false); + char marker = to_lower(String().append(1, content[1]))[0]; + if(marker != 'x' && marker != ' ') + return(false); + checked = (marker == 'x'); + content = content.substr(4); + return(true); +} + +bool markdown_parse_atx_heading(String raw, s64& level, String& content) +{ + String trimmed = trim(raw); + if(trimmed == "" || trimmed[0] != '#') + return(false); + level = 0; + while(level < (s64)trimmed.length() && trimmed[level] == '#') + level += 1; + if(level == 0 || level > 6) + return(false); + if(level >= (s64)trimmed.length() || !isspace((unsigned char)trimmed[level])) + return(false); + content = trim(trimmed.substr(level)); + while(content.length() > 0 && content.back() == '#') + { + String stripped = trim(content.substr(0, content.length() - 1)); + if(stripped != "") + content = stripped; + else + break; + } + return(true); +} + +bool markdown_parse_setext_heading(String current, String next, s64& level, String& content) +{ + current = trim(current); + next = trim(next); + if(current == "" || next.length() < 3) + return(false); + bool all_equals = true; + bool all_dashes = true; + for(auto c : next) + { + if(c != '=' && !isspace((unsigned char)c)) + all_equals = false; + if(c != '-' && !isspace((unsigned char)c)) + all_dashes = false; + } + if(!all_equals && !all_dashes) + return(false); + level = all_equals ? 1 : 2; + content = current; + return(true); +} + +bool markdown_parse_directive_open(String raw, String& name, String& argument, DTree& attrs) +{ + String trimmed = trim(raw); + if(trimmed.rfind(":::", 0) != 0 || trimmed == ":::") + return(false); + String payload = trim(trimmed.substr(3)); + StringList parts = split_space(payload); + if(parts.size() == 0) + return(false); + name = parts[0]; + if(!markdown_is_identifier(name)) + return(false); + String remainder = trim(payload.substr(name.length())); + attrs = markdown_parse_attrs(remainder, argument); + return(true); +} + +bool markdown_parse_autolink(String raw, s64 start, s64& end, String& href, String& text) +{ + String rest = raw.substr(start); + if(rest.rfind("https://", 0) != 0 && rest.rfind("http://", 0) != 0) + return(false); + end = start; + while(end < (s64)raw.length()) + { + char c = raw[end]; + if(isspace((unsigned char)c) || c == '<' || c == '>') + break; + end += 1; + } + if(end <= start) + return(false); + href = raw.substr(start, end - start); + while(href.length() > 0) + { + char last = href.back(); + if(last == '.' || last == ',' || last == '!' || last == '?' || last == ';' || last == ':') + { + href.pop_back(); + end -= 1; + } + else + { + break; + } + } + if(href == "") + return(false); + text = href; + return(true); +} + +bool markdown_parse_link_target(String raw, String& url, String& title) +{ + raw = trim(raw); + if(raw == "") + return(false); + if(raw.find(" ") == String::npos) + { + url = markdown_trim_quotes(raw); + title = ""; + return(true); + } + String work = raw; + url = markdown_trim_quotes(trim(nibble(work, " "))); + title = markdown_trim_quotes(trim(work)); + return(url != ""); +} + +DTree markdown_parse_inline(String raw, DTree options); + +void markdown_flush_text(String& buffer, DTree& children) +{ + if(buffer == "") + return; + DTree node = markdown_make_node("text"); + node["text"] = buffer; + markdown_push(children, node); + buffer = ""; +} + +DTree markdown_parse_inline(String raw, DTree options) +{ + DTree children; + String buffer = ""; + bool gfm = markdown_gfm_enabled(options); + bool allow_html = markdown_allow_html(options); + + for(s64 i = 0; i < (s64)raw.length(); i++) + { + char c = raw[i]; + char c1 = (i + 1 < (s64)raw.length()) ? raw[i + 1] : '\0'; + + if(c == '\\' && i + 1 < (s64)raw.length()) + { + buffer.append(1, raw[i + 1]); + i += 1; + continue; + } + + if(c == '`') + { + s64 tick_count = 1; + while(i + tick_count < (s64)raw.length() && raw[i + tick_count] == '`') + tick_count += 1; + String ticks(tick_count, '`'); + auto close_pos = raw.find(ticks, i + tick_count); + if(close_pos != String::npos) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("code"); + node["text"] = raw.substr(i + tick_count, close_pos - (i + tick_count)); + markdown_push(children, node); + i = close_pos + tick_count - 1; + continue; + } + } + + if(c == '!' && c1 == '[') + { + auto label_end = raw.find("]", i + 2); + if(label_end != String::npos && label_end + 1 < (s64)raw.length() && raw[label_end + 1] == '(') + { + auto target_end = raw.find(")", label_end + 2); + if(target_end != String::npos) + { + String url = ""; + String title = ""; + if(markdown_parse_link_target(raw.substr(label_end + 2, target_end - (label_end + 2)), url, title)) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("image"); + node["alt"] = raw.substr(i + 2, label_end - (i + 2)); + node["src"] = url; + node["title"] = title; + markdown_push(children, node); + i = target_end; + continue; + } + } + } + } + + if(c == '[') + { + auto label_end = raw.find("]", i + 1); + if(label_end != String::npos && label_end + 1 < (s64)raw.length() && raw[label_end + 1] == '(') + { + auto target_end = raw.find(")", label_end + 2); + if(target_end != String::npos) + { + String url = ""; + String title = ""; + if(markdown_parse_link_target(raw.substr(label_end + 2, target_end - (label_end + 2)), url, title)) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("link"); + node["href"] = url; + node["title"] = title; + node["children"] = markdown_parse_inline(raw.substr(i + 1, label_end - (i + 1)), options); + markdown_push(children, node); + i = target_end; + continue; + } + } + } + } + + if(gfm && c == '~' && c1 == '~') + { + auto close_pos = raw.find("~~", i + 2); + if(close_pos != String::npos) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("strike"); + node["children"] = markdown_parse_inline(raw.substr(i + 2, close_pos - (i + 2)), options); + markdown_push(children, node); + i = close_pos + 1; + continue; + } + } + + if((c == '*' && c1 == '*') || (c == '_' && c1 == '_')) + { + String delim = raw.substr(i, 2); + auto close_pos = raw.find(delim, i + 2); + if(close_pos != String::npos) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("strong"); + node["children"] = markdown_parse_inline(raw.substr(i + 2, close_pos - (i + 2)), options); + markdown_push(children, node); + i = close_pos + 1; + continue; + } + } + + if(c == '*' || c == '_') + { + String delim = raw.substr(i, 1); + auto close_pos = raw.find(delim, i + 1); + if(close_pos != String::npos) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("em"); + node["children"] = markdown_parse_inline(raw.substr(i + 1, close_pos - (i + 1)), options); + markdown_push(children, node); + i = close_pos; + continue; + } + } + + if(gfm) + { + s64 end = 0; + String href = ""; + String text = ""; + if(markdown_parse_autolink(raw, i, end, href, text)) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("link"); + node["href"] = href; + node["title"] = ""; + DTree link_children; + DTree text_node = markdown_make_node("text"); + text_node["text"] = text; + markdown_push(link_children, text_node); + node["children"] = link_children; + markdown_push(children, node); + i = end - 1; + continue; + } + } + + if(allow_html && c == '<') + { + auto close_pos = raw.find(">", i + 1); + if(close_pos != String::npos) + { + String html = raw.substr(i, close_pos - i + 1); + if(html.length() >= 3) + { + markdown_flush_text(buffer, children); + DTree node = markdown_make_node("raw_html"); + node["html"] = html; + markdown_push(children, node); + i = close_pos; + continue; + } + } + } + + buffer.append(1, c); + } + + markdown_flush_text(buffer, children); + return(children); +} + +DTree markdown_parse_blocks(StringList lines, DTree options); + +DTree markdown_parse_paragraph(StringList& lines, s64& idx, s64 base_indent, DTree& options) +{ + StringList paragraph_lines; + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + break; + if(markdown_count_indent(lines[idx]) < base_indent) + break; + if(paragraph_lines.size() > 0) + { + String current = lines[idx].substr(base_indent); + s64 heading_level = 0; + String heading_content = ""; + char fence_char = '\0'; + s64 fence_length = 0; + String fence_info = ""; + bool ordered = false; + s64 marker_length = 0; + s64 start_number = 1; + String list_content = ""; + String directive_name = ""; + String directive_argument = ""; + DTree directive_attrs; + if(markdown_parse_atx_heading(current, heading_level, heading_content) || + markdown_parse_fence(current, fence_char, fence_length, fence_info) || + markdown_is_hr(current) || + trim(current).rfind(">", 0) == 0 || + markdown_parse_list_marker(current, ordered, marker_length, start_number, list_content) || + markdown_parse_directive_open(current, directive_name, directive_argument, directive_attrs)) + break; + if(markdown_gfm_enabled(options) && idx + 1 < (s64)lines.size() && current.find("|") != String::npos && + markdown_count_indent(lines[idx + 1]) >= base_indent) + { + StringList alignments; + if(markdown_parse_table_separator(lines[idx + 1].substr(base_indent), alignments)) + break; + } + } + paragraph_lines.push_back(lines[idx].substr(base_indent)); + idx += 1; + } + + DTree node = markdown_make_node("paragraph"); + String text = join(paragraph_lines, "\n"); + node["text"] = text; + node["children"] = markdown_parse_inline(text, options); + return(node); +} + +DTree markdown_parse_list(StringList& lines, s64& idx, s64 base_indent, DTree& options) +{ + bool ordered = false; + s64 marker_length = 0; + s64 start_number = 1; + String content = ""; + String raw = lines[idx].substr(base_indent); + markdown_parse_list_marker(raw, ordered, marker_length, start_number, content); + + DTree node = markdown_make_node("list"); + node["ordered"].set_bool(ordered); + node["start"] = (f64)start_number; + + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + break; + if(markdown_count_indent(lines[idx]) < base_indent) + break; + + String line_raw = lines[idx].substr(base_indent); + bool item_ordered = false; + s64 item_marker_length = 0; + s64 item_start_number = 1; + String item_content = ""; + if(!markdown_parse_list_marker(line_raw, item_ordered, item_marker_length, item_start_number, item_content) || item_ordered != ordered) + break; + + DTree item = markdown_make_node("list_item"); + if(markdown_gfm_enabled(options)) + { + bool checked = false; + String task_content = item_content; + if(markdown_parse_task_prefix(task_content, checked)) + { + item["task"].set_bool(true); + item["checked"].set_bool(checked); + item_content = task_content; + } + } + + StringList child_lines; + child_lines.push_back(item_content); + idx += 1; + + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + { + child_lines.push_back(""); + idx += 1; + continue; + } + s64 indent = markdown_count_indent(lines[idx]); + if(indent < base_indent + 2) + break; + + String next_raw = lines[idx].substr(base_indent); + bool next_item_ordered = false; + s64 next_marker_length = 0; + s64 next_start_number = 1; + String next_content = ""; + if(indent == base_indent && + markdown_parse_list_marker(next_raw, next_item_ordered, next_marker_length, next_start_number, next_content) && + next_item_ordered == ordered) + break; + + s64 consume_indent = std::min((s64)lines[idx].length(), base_indent + 2); + child_lines.push_back(lines[idx].substr(consume_indent)); + idx += 1; + } + + DTree child_doc = markdown_parse_blocks(child_lines, options); + item["children"] = markdown_get_value(child_doc, "children"); + markdown_push(node["children"], item); + } + + return(node); +} + +DTree markdown_parse_blockquote(StringList& lines, s64& idx, s64 base_indent, DTree& options) +{ + StringList quote_lines; + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + { + quote_lines.push_back(""); + idx += 1; + continue; + } + if(markdown_count_indent(lines[idx]) < base_indent) + break; + String raw = lines[idx].substr(base_indent); + String trimmed = trim(raw); + if(trimmed.rfind(">", 0) != 0) + break; + s64 marker_pos = raw.find(">"); + String stripped = raw.substr(marker_pos + 1); + if(stripped.rfind(" ", 0) == 0) + stripped = stripped.substr(1); + quote_lines.push_back(stripped); + idx += 1; + } + + DTree node = markdown_make_node("blockquote"); + DTree child_doc = markdown_parse_blocks(quote_lines, options); + node["children"] = markdown_get_value(child_doc, "children"); + return(node); +} + +DTree markdown_parse_code_block(StringList& lines, s64& idx, s64 base_indent) +{ + char fence_char = '\0'; + s64 fence_length = 0; + String info = ""; + String raw = lines[idx].substr(base_indent); + markdown_parse_fence(raw, fence_char, fence_length, info); + String lang = trim(nibble(info, " ")); + String meta = trim(info); + idx += 1; + + StringList body; + while(idx < (s64)lines.size()) + { + String current = lines[idx]; + String current_raw = current; + if(markdown_count_indent(current) >= base_indent) + current_raw = current.substr(base_indent); + String trimmed = trim(current_raw); + s64 close_length = 0; + while(close_length < (s64)trimmed.length() && trimmed[close_length] == fence_char) + close_length += 1; + if(close_length >= fence_length) + { + idx += 1; + break; + } + body.push_back(current_raw); + idx += 1; + } + + DTree node = markdown_make_node("code_block"); + node["text"] = join(body, "\n"); + node["lang"] = lang; + node["meta"] = meta; + return(node); +} + +DTree markdown_parse_table(StringList& lines, s64& idx, s64 base_indent, DTree& options) +{ + DTree node = markdown_make_node("table"); + String header_line = lines[idx].substr(base_indent); + String separator_line = lines[idx + 1].substr(base_indent); + StringList alignments; + markdown_parse_table_separator(separator_line, alignments); + + StringList header_cells = markdown_split_pipe_row(header_line); + for(auto cell_text : header_cells) + { + DTree cell = markdown_make_node("table_cell"); + cell["text"] = cell_text; + cell["children"] = markdown_parse_inline(cell_text, options); + markdown_push(node["header"], cell); + } + + for(auto align : alignments) + { + DTree cell_align; + cell_align = align; + markdown_push(node["align"], cell_align); + } + + idx += 2; + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + break; + if(markdown_count_indent(lines[idx]) < base_indent) + break; + String raw = lines[idx].substr(base_indent); + if(raw.find("|") == String::npos) + break; + StringList cells = markdown_split_pipe_row(raw); + if(cells.size() == 0) + break; + DTree row; + for(auto cell_text : cells) + { + DTree cell = markdown_make_node("table_cell"); + cell["text"] = cell_text; + cell["children"] = markdown_parse_inline(cell_text, options); + markdown_push(row, cell); + } + markdown_push(node["rows"], row); + idx += 1; + } + + return(node); +} + +DTree markdown_parse_directive(StringList& lines, s64& idx, s64 base_indent, DTree& options) +{ + String name = ""; + String argument = ""; + DTree attrs; + markdown_parse_directive_open(lines[idx].substr(base_indent), name, argument, attrs); + + StringList body_lines; + s64 depth = 1; + idx += 1; + while(idx < (s64)lines.size()) + { + String current = lines[idx]; + String raw = current; + if(markdown_count_indent(current) >= base_indent) + raw = current.substr(base_indent); + String trimmed = trim(raw); + if(trimmed.rfind(":::", 0) == 0) + { + if(trimmed == ":::") + { + depth -= 1; + if(depth == 0) + { + idx += 1; + break; + } + body_lines.push_back(raw); + idx += 1; + continue; + } + String nested_name = ""; + String nested_argument = ""; + DTree nested_attrs; + if(markdown_parse_directive_open(raw, nested_name, nested_argument, nested_attrs)) + depth += 1; + } + body_lines.push_back(raw); + idx += 1; + } + + DTree node = markdown_make_node("directive"); + node["name"] = name; + node["argument"] = argument; + node["attrs"] = attrs; + node["body_source"] = join(body_lines, "\n"); + DTree child_doc = markdown_parse_blocks(body_lines, options); + node["children"] = markdown_get_value(child_doc, "children"); + return(node); +} + +DTree markdown_parse_raw_html_block(StringList& lines, s64& idx, s64 base_indent) +{ + StringList html_lines; + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + break; + if(markdown_count_indent(lines[idx]) < base_indent) + break; + String raw = lines[idx].substr(base_indent); + if(trim(raw).rfind("<", 0) != 0) + break; + html_lines.push_back(raw); + idx += 1; + } + DTree node = markdown_make_node("raw_html"); + node["html"] = join(html_lines, "\n"); + return(node); +} + +DTree markdown_parse_blocks(StringList lines, DTree options) +{ + DTree document = markdown_make_node("document"); + document["source"] = join(lines, "\n"); + s64 idx = 0; + + while(idx < (s64)lines.size()) + { + if(markdown_is_blank(lines[idx])) + { + idx += 1; + continue; + } + + s64 base_indent = markdown_count_indent(lines[idx]); + String raw = lines[idx].substr(base_indent); + String trimmed = trim(raw); + + s64 atx_level = 0; + String heading_content = ""; + if(markdown_parse_atx_heading(raw, atx_level, heading_content)) + { + DTree node = markdown_make_node("heading"); + node["level"] = (f64)atx_level; + node["text"] = heading_content; + node["children"] = markdown_parse_inline(heading_content, options); + markdown_push(document["children"], node); + idx += 1; + continue; + } + + if(idx + 1 < (s64)lines.size() && markdown_count_indent(lines[idx + 1]) >= base_indent) + { + s64 setext_level = 0; + String setext_content = ""; + if(markdown_parse_setext_heading(raw, lines[idx + 1].substr(base_indent), setext_level, setext_content)) + { + DTree node = markdown_make_node("heading"); + node["level"] = (f64)setext_level; + node["text"] = setext_content; + node["children"] = markdown_parse_inline(setext_content, options); + markdown_push(document["children"], node); + idx += 2; + continue; + } + } + + char fence_char = '\0'; + s64 fence_length = 0; + String fence_info = ""; + if(markdown_parse_fence(raw, fence_char, fence_length, fence_info)) + { + DTree node = markdown_parse_code_block(lines, idx, base_indent); + markdown_push(document["children"], node); + continue; + } + + if(markdown_is_hr(raw)) + { + DTree node = markdown_make_node("hr"); + markdown_push(document["children"], node); + idx += 1; + continue; + } + + String directive_name = ""; + String directive_argument = ""; + DTree directive_attrs; + if(markdown_parse_directive_open(raw, directive_name, directive_argument, directive_attrs)) + { + DTree node = markdown_parse_directive(lines, idx, base_indent, options); + markdown_push(document["children"], node); + continue; + } + + if(trimmed.rfind(">", 0) == 0) + { + DTree node = markdown_parse_blockquote(lines, idx, base_indent, options); + markdown_push(document["children"], node); + continue; + } + + bool ordered = false; + s64 marker_length = 0; + s64 start_number = 1; + String list_content = ""; + if(markdown_parse_list_marker(raw, ordered, marker_length, start_number, list_content)) + { + DTree node = markdown_parse_list(lines, idx, base_indent, options); + markdown_push(document["children"], node); + continue; + } + + if(markdown_gfm_enabled(options) && idx + 1 < (s64)lines.size() && raw.find("|") != String::npos && + markdown_count_indent(lines[idx + 1]) >= base_indent) + { + StringList alignments; + if(markdown_parse_table_separator(lines[idx + 1].substr(base_indent), alignments)) + { + DTree node = markdown_parse_table(lines, idx, base_indent, options); + markdown_push(document["children"], node); + continue; + } + } + + if(markdown_allow_html(options) && trimmed.rfind("<", 0) == 0) + { + DTree node = markdown_parse_raw_html_block(lines, idx, base_indent); + markdown_push(document["children"], node); + continue; + } + + DTree node = markdown_parse_paragraph(lines, idx, base_indent, options); + markdown_push(document["children"], node); + } + + return(document); +} + +String markdown_render_children(DTree children, DTree& options); +String markdown_render_node(DTree node, DTree& options); + +String markdown_render_cell_children(DTree cell, DTree& options) +{ + return(markdown_render_children(markdown_get_value(cell, "children"), options)); +} + +String markdown_render_with_component_hook(DTree node, DTree& options, String hook, String children_html, String default_html) +{ + String type = markdown_get_string(node, "type"); + String target = ""; + String exact_hook = ""; + if(type == "directive") + { + exact_hook = ":::" + markdown_get_string(node, "name"); + target = markdown_get_component_target(options, exact_hook); + } + if(target == "") + target = markdown_get_component_target(options, hook); + if(target == "" || !context) + return(default_html); + if(target != "" && exact_hook != "") + hook = exact_hook; + + DTree props; + props["hook"] = hook; + props["target"] = target; + props["default_html"] = default_html; + props["children_html"] = children_html; + props["node"] = node; + props["type"] = type; + props["name"] = markdown_get_string(node, "name"); + props["argument"] = markdown_get_string(node, "argument"); + props["text"] = markdown_get_string(node, "text"); + props["lang"] = markdown_get_string(node, "lang"); + props["href"] = markdown_get_string(node, "href"); + props["src"] = markdown_get_string(node, "src"); + props["title"] = markdown_get_string(node, "title"); + props["options"] = options; + return(component(target, props, *context)); +} + +String markdown_render_children(DTree children, DTree& options) +{ + String result = ""; + for(auto key : markdown_sorted_keys(children)) + result += markdown_render_node(children._map[key], options); + return(result); +} + +String markdown_render_node(DTree node, DTree& options) +{ + String type = markdown_get_string(node, "type"); + String hook = "node." + type; + String children_html = markdown_render_children(markdown_get_value(node, "children"), options); + String html = ""; + + if(type == "document") + { + html = children_html; + } + else if(type == "paragraph") + { + html = "

    " + children_html + "

    "; + } + else if(type == "heading") + { + s64 level = int_val(markdown_get_string(node, "level", "1")); + if(level < 1) level = 1; + if(level > 6) level = 6; + html = String("" + children_html + String(""; + } + else if(type == "text") + { + html = html_escape(markdown_get_string(node, "text")); + } + else if(type == "code") + { + html = "" + html_escape(markdown_get_string(node, "text")) + ""; + } + else if(type == "strong") + { + html = "" + children_html + ""; + } + else if(type == "em") + { + html = "" + children_html + ""; + } + else if(type == "strike") + { + html = "" + children_html + ""; + } + else if(type == "link") + { + String attrs = " href=\"" + html_escape(markdown_get_string(node, "href")) + "\""; + String title = markdown_get_string(node, "title"); + if(title != "") + attrs += " title=\"" + html_escape(title) + "\""; + html = "" + children_html + ""; + } + else if(type == "image") + { + String attrs = " src=\"" + html_escape(markdown_get_string(node, "src")) + "\""; + attrs += " alt=\"" + html_escape(markdown_get_string(node, "alt")) + "\""; + String title = markdown_get_string(node, "title"); + if(title != "") + attrs += " title=\"" + html_escape(title) + "\""; + html = ""; + } + else if(type == "raw_html") + { + html = markdown_get_string(node, "html"); + } + else if(type == "hr") + { + html = "
    "; + } + else if(type == "blockquote") + { + html = "
    " + children_html + "
    "; + } + else if(type == "list") + { + bool ordered = markdown_get_bool(node, "ordered", false); + String tag = ordered ? "ol" : "ul"; + String attrs = ""; + if(ordered) + { + s64 start = int_val(markdown_get_string(node, "start", "1")); + if(start > 1) + attrs = String(" start=\"") + start + "\""; + } + html = "<" + tag + attrs + ">" + children_html + ""; + } + else if(type == "list_item") + { + String attrs = ""; + if(markdown_get_bool(node, "task", false)) + attrs = " class=\"task-list-item\""; + html = ""; + if(markdown_get_bool(node, "task", false)) + { + html += ""; + } + else if(type == "code_block") + { + String lang = markdown_get_string(node, "lang"); + String class_attr = ""; + if(lang != "") + class_attr = " class=\"language-" + html_escape(lang) + "\""; + html = "
    " + html_escape(markdown_get_string(node, "text")) + "
    "; + } + else if(type == "table") + { + String header_html = ""; + StringList align_keys = markdown_sorted_keys(markdown_get_value(node, "align")); + StringList header_keys = markdown_sorted_keys(markdown_get_value(node, "header")); + for(s64 i = 0; i < (s64)header_keys.size(); i++) + { + String align = (i < (s64)align_keys.size()) ? node["align"]._map[align_keys[i]].to_string() : ""; + String align_attr = align != "" ? " align=\"" + html_escape(align) + "\"" : ""; + header_html += "" + markdown_render_cell_children(node["header"]._map[header_keys[i]], options) + ""; + } + + String body_html = ""; + for(auto row_key : markdown_sorted_keys(markdown_get_value(node, "rows"))) + { + DTree row = node["rows"]._map[row_key]; + String row_html = ""; + StringList row_keys = markdown_sorted_keys(row); + for(s64 i = 0; i < (s64)row_keys.size(); i++) + { + String align = (i < (s64)align_keys.size()) ? node["align"]._map[align_keys[i]].to_string() : ""; + String align_attr = align != "" ? " align=\"" + html_escape(align) + "\"" : ""; + row_html += "" + markdown_render_cell_children(row._map[row_keys[i]], options) + ""; + } + body_html += "" + row_html + ""; + } + + html = "" + header_html + "" + body_html + "
    "; + } + else if(type == "directive") + { + String name = markdown_get_string(node, "name"); + String title = markdown_get_string(node["attrs"], "title", markdown_get_string(node, "argument")); + String title_html = ""; + if(title != "") + title_html = "
    " + markdown_render_children(markdown_parse_inline(title, options), options) + "
    "; + html = "
    " + title_html + children_html + "
    "; + } + else + { + html = children_html; + } + + return(markdown_render_with_component_hook(node, options, hook, children_html, html)); +} + +DTree markdown_to_ast(String src) +{ + DTree options; + return(markdown_to_ast(src, options)); +} + +DTree markdown_to_ast(String src, DTree options) +{ + src = markdown_strip_newlines(src); + return(markdown_parse_blocks(split(src, "\n"), options)); +} + +String markdown_to_html(String src) +{ + DTree options; + return(markdown_to_html(src, options)); +} + +String markdown_to_html(String src, DTree options) +{ + DTree ast = markdown_to_ast(src, options); + return(markdown_render_node(ast, options)); +} diff --git a/src/lib/markdown.h b/src/lib/markdown.h new file mode 100644 index 0000000..80b9dfa --- /dev/null +++ b/src/lib/markdown.h @@ -0,0 +1,6 @@ +#pragma once + +DTree markdown_to_ast(String src); +DTree markdown_to_ast(String src, DTree options); +String markdown_to_html(String src); +String markdown_to_html(String src, DTree options); diff --git a/src/lib/mysql-connector.h b/src/lib/mysql-connector.h index 59c866b..832294c 100644 --- a/src/lib/mysql-connector.h +++ b/src/lib/mysql-connector.h @@ -1,3 +1,5 @@ +#pragma once + struct MySQLFieldInfo { String name; String table; diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 75abde6..c3922b0 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -10,6 +10,45 @@ #include #include "sys.h" +String capture_backtrace_string(u32 max_frames, u32 skip_frames) +{ + if(max_frames == 0) + return(""); + + std::vector frames(max_frames); + size_t size = backtrace(frames.data(), max_frames); + if(size == 0) + return(""); + + char** symbols = backtrace_symbols(frames.data(), size); + if(!symbols) + return(""); + + String trace; + for(size_t i = skip_frames; i < size; i++) + { + trace += symbols[i]; + trace += "\n"; + } + free(symbols); + return(trace); +} + +String signal_name(int sig) +{ + switch(sig) + { + case SIGABRT: return("SIGABRT"); + case SIGBUS: return("SIGBUS"); + case SIGFPE: return("SIGFPE"); + case SIGILL: return("SIGILL"); + case SIGINT: return("SIGINT"); + case SIGSEGV: return("SIGSEGV"); + case SIGTERM: return("SIGTERM"); + default: return(""); + } +} + String shell_exec(String cmd) { //printf("(i) shell_exec(%s)\n", cmd.c_str()); @@ -410,15 +449,12 @@ StringMap memcache_get_multiple(u64 connection, StringList keys) void on_segfault(int sig) { - void *array[10]; - size_t size; - - // get void*'s for all entries on the stack - size = backtrace(array, 10); - - // print out all the frames to stderr - fprintf(stderr, "SEG FAULT: %d:\n", sig); - backtrace_symbols_fd(array, size, STDERR_FILENO); + String trace = capture_backtrace_string(32, 1); + String sig_label = signal_name(sig); + if(sig_label != "") + fprintf(stderr, "SEG FAULT: %d (%s):\n%s", sig, sig_label.c_str(), trace.c_str()); + else + fprintf(stderr, "SEG FAULT: %d:\n%s", sig, trace.c_str()); exit(1); } diff --git a/src/lib/sys.h b/src/lib/sys.h index d3982de..dd853fc 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -1,3 +1,5 @@ +#pragma once + #include String shell_exec(String cmd); @@ -43,11 +45,13 @@ u8 ws_opcode(); bool ws_is_binary(); StringList ws_connections(String scope = ""); u64 ws_connection_count(String scope = ""); -bool ws_send(String message, String scope = ""); -u64 ws_broadcast(String message, String scope = ""); -bool ws_send_to(String connection_id, String message); +bool ws_send(String message, bool binary = false, String scope = ""); +bool ws_send_to(String connection_id, String message, bool binary = false); bool ws_close(String connection_id = ""); +String capture_backtrace_string(u32 max_frames = 32, u32 skip_frames = 0); +String signal_name(int sig); + String memcache_escape_key(String key); StringList memcache_escape_keys(StringList keys); u64 memcache_connect(String host = "127.0.0.1", short port = 11211); diff --git a/src/lib/types.h b/src/lib/types.h index 04aeab2..4c49b82 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -52,7 +54,7 @@ typedef std::ostringstream ByteStream; struct Request; struct DTree; -typedef void (*call_handler)(DTree& call_param); +typedef void (*request_ref_handler)(Request& request); typedef DTree* (*dtree_call_handler)(DTree* call_param); typedef void (*request_handler)(Request* request); @@ -77,8 +79,8 @@ struct SharedUnit { void* so_handle; request_handler on_setup; - call_handler on_render; - call_handler on_websocket; + request_ref_handler on_render; + request_ref_handler on_websocket; String compiler_messages; time_t last_compiled; @@ -113,7 +115,7 @@ String nibble(String div, String& haystack); #include "dtree.h" -void compiler_invoke(Request* context, String file_name, DTree& call_param); +void compiler_invoke(Request* context, String file_name); struct Request { @@ -126,6 +128,8 @@ struct Request { StringMap session; DTree var; + DTree call; + DTree connection; String session_id = ""; String session_name = ""; @@ -148,7 +152,7 @@ struct Request { struct Flags { bool log_request = true; bool is_finished = false; - int status; + int status = 0; bool output_closed = false; bool params_closed = false; bool input_closed = false; @@ -173,6 +177,7 @@ struct Request { bool is_websocket = false; String websocket_connection_id = ""; String websocket_scope = ""; + DTree* websocket_connection_state = 0; u8 websocket_opcode = 0; bool websocket_is_binary = false; bool websocket_is_text = false; diff --git a/src/lib/uce_lib.cpp b/src/lib/uce_lib.cpp index 6e27be3..b5836fb 100644 --- a/src/lib/uce_lib.cpp +++ b/src/lib/uce_lib.cpp @@ -7,4 +7,5 @@ #include "sys.cpp" #include "uri.cpp" #include "compiler.cpp" +#include "markdown.cpp" #include "mysql-connector.cpp" diff --git a/src/lib/uce_lib.h b/src/lib/uce_lib.h index 5ca6367..37ab592 100644 --- a/src/lib/uce_lib.h +++ b/src/lib/uce_lib.h @@ -1,10 +1,11 @@ +#pragma once + #include "types.h" #include "hash.h" #include "functionlib.h" #include "sys.h" #include "uri.h" #include "compiler.h" +#include "markdown.h" #include "mysql-connector.h" - - diff --git a/src/lib/unit.h b/src/lib/unit.h index af4d7ce..602d88a 100644 --- a/src/lib/unit.h +++ b/src/lib/unit.h @@ -1,3 +1,4 @@ +#pragma once + #include "uce_lib.h" - diff --git a/src/lib/uri.h b/src/lib/uri.h index a0913e9..12e04fe 100644 --- a/src/lib/uri.h +++ b/src/lib/uri.h @@ -1,4 +1,6 @@ +#pragma once + String var_dump(URI uri, String prefix = "", String postfix = "\n"); String uri_decode(String q); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 96e593d..01e90f1 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -1,4 +1,5 @@ #include "lib/uce_lib.cpp" +#include ServerState server_state; @@ -7,6 +8,11 @@ ServerState server_state; FastCGIServer server; pid_t http_worker_pid = 0; bool worker_accepts_http = false; +static sigjmp_buf request_fault_jmp; +static volatile sig_atomic_t request_fault_active = 0; +static volatile sig_atomic_t request_fault_signal = 0; +static Request* request_fault_request = 0; +static String request_fault_trace = ""; Request* set_active_request(Request& request) { @@ -20,6 +26,98 @@ void restore_active_request(Request* previous_context) context = previous_context; } +String request_status_line(Request& request, int status_code, String reason) +{ + String status = std::to_string(status_code) + " " + reason; + if(request.params["GATEWAY_INTERFACE"] != "") + return("Status: " + status); + return("HTTP/1.1 " + status); +} + +void clear_request_output(Request& request) +{ + for(auto* stream : request.ob_stack) + delete stream; + request.ob_stack.clear(); + request.ob_start(); +} + +void render_request_failure(Request& request, String title, String details, String trace, int status_code = 500) +{ + request.response_code = request_status_line(request, status_code, "Internal Server Error"); + request.header.clear(); + request.set_cookies.clear(); + request.header["Content-Type"] = "text/plain; charset=utf-8"; + request.err.clear(); + + Request* previous_context = set_active_request(request); + clear_request_output(request); + + print("UCE runtime error\n"); + print("Request: ", first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]), "\n"); + print("Script: ", request.params["SCRIPT_FILENAME"], "\n"); + print("Error: ", title, "\n"); + if(details != "") + print("Details: ", details, "\n"); + if(request_fault_signal != 0) + { + String sig_label = signal_name((int)request_fault_signal); + print("Signal: ", (s64)request_fault_signal); + if(sig_label != "") + print(" (", sig_label, ")"); + print("\n"); + } + if(trace != "") + print("\nTrace:\n", trace); + + request.err += "UCE runtime error\n"; + request.err += "Request: " + first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]) + "\n"; + request.err += "Script: " + request.params["SCRIPT_FILENAME"] + "\n"; + request.err += "Error: " + title + "\n"; + if(details != "") + request.err += "Details: " + details + "\n"; + if(request_fault_signal != 0) + { + String sig_label = signal_name((int)request_fault_signal); + request.err += "Signal: " + std::to_string((int)request_fault_signal); + if(sig_label != "") + request.err += " (" + sig_label + ")"; + request.err += "\n"; + } + if(trace != "") + request.err += "\nTrace:\n" + trace; + + request.flags.status = status_code; + restore_active_request(previous_context); +} + +void on_request_fault_signal(int sig) +{ + request_fault_signal = sig; + request_fault_trace = capture_backtrace_string(32, 1); + if(request_fault_active && request_fault_request) + siglongjmp(request_fault_jmp, 1); + on_segfault(sig); +} + +void install_request_fault_handlers() +{ + signal(SIGSEGV, on_request_fault_signal); + signal(SIGABRT, on_request_fault_signal); + signal(SIGBUS, on_request_fault_signal); + signal(SIGILL, on_request_fault_signal); + signal(SIGFPE, on_request_fault_signal); +} + +void restore_request_fault_handlers() +{ + signal(SIGSEGV, on_segfault); + signal(SIGABRT, on_segfault); + signal(SIGBUS, on_segfault); + signal(SIGILL, on_segfault); + signal(SIGFPE, on_segfault); +} + String current_ws_scope() { if(!context) @@ -43,7 +141,7 @@ String ws_message() { if(!context) return(""); - return(context->var["ws"]["message"].to_string()); + return(context->call["message"].to_string()); } String ws_connection_id() @@ -82,19 +180,14 @@ u64 ws_connection_count(String scope) return(ws_connections(scope).size()); } -bool ws_send(String message, String scope) +bool ws_send(String message, bool binary, String scope) { - return(server.websocket_broadcast(normalize_ws_scope(scope), message) > 0); + return(server.websocket_broadcast(normalize_ws_scope(scope), message, binary) > 0); } -u64 ws_broadcast(String message, String scope) +bool ws_send_to(String connection_id, String message, bool binary) { - return(server.websocket_broadcast(normalize_ws_scope(scope), message)); -} - -bool ws_send_to(String connection_id, String message) -{ - return(server.websocket_send_to(connection_id, message)); + return(server.websocket_send_to(connection_id, message, binary)); } bool ws_close(String connection_id) @@ -147,51 +240,89 @@ int handle_complete(FastCGIRequest& request) { //request.stats.mem_high = 0; request.header["Content-Type"] = context->server->config["CONTENT_TYPE"]; request.get = parse_query(request.params["QUERY_STRING"]); - request.random_index = 0; - request.random_seed = gen_noise64(*reinterpret_cast(&request.stats.time_start)); + request.random_index = 0; + request.random_seed = gen_noise64(*reinterpret_cast(&request.stats.time_start)); request.ob_start(); + request_fault_request = &request; + request_fault_active = 1; + request_fault_signal = 0; + request_fault_trace = ""; + install_request_fault_handlers(); - if(request.params["HTTP_COOKIE"].length() > 0) - request.cookies = parse_cookies(request.params["HTTP_COOKIE"]); + String failure_title = ""; + String failure_details = ""; + String failure_trace = ""; - String ct_info = request.params["CONTENT_TYPE"]; - String ct_type = nibble(";", ct_info); - - if(request.params["REQUEST_METHOD"] == "POST") + if(sigsetjmp(request_fault_jmp, 1) != 0) { - if(ct_type == "multipart/form-data") + failure_title = "fatal signal during request"; + failure_details = "worker recovered before closing the upstream connection"; + failure_trace = request_fault_trace; + } + else + { + try { - nibble("boundary=", ct_info); - request.post = parse_multipart(request.in, String("--")+ct_info, request.uploaded_files); + if(request.params["HTTP_COOKIE"].length() > 0) + request.cookies = parse_cookies(request.params["HTTP_COOKIE"]); + + String ct_info = request.params["CONTENT_TYPE"]; + String ct_type = nibble(";", ct_info); + + if(request.params["REQUEST_METHOD"] == "POST") + { + if(ct_type == "multipart/form-data") + { + nibble("boundary=", ct_info); + request.post = parse_multipart(request.in, String("--")+ct_info, request.uploaded_files); + } + else + { + request.post = parse_query(request.in); + } + } + + request.call = DTree(); + compiler_invoke(&request, request.params["SCRIPT_FILENAME"]); } - else + catch(const std::exception& e) { - request.post = parse_query(request.in); + failure_title = "uncaught exception during request"; + failure_details = e.what(); + failure_trace = capture_backtrace_string(32, 1); + } + catch(...) + { + failure_title = "unknown uncaught exception during request"; + failure_trace = capture_backtrace_string(32, 1); } } - DTree call_param; - compiler_invoke(&request, request.params["SCRIPT_FILENAME"], call_param); + request_fault_active = 0; + request_fault_request = 0; + restore_request_fault_handlers(); + + if(failure_title != "") + render_request_failure(request, failure_title, failure_details, failure_trace, 500); for( auto &f : request.uploaded_files) { unlink(f.tmp_name); } - if(request.session_id.length() > 0) + if(failure_title == "" && request.session_id.length() > 0) save_session_data(request.session_id, request.session); cleanup_mysql_connections(); restore_active_request(previous_context); - return 0; + return request.flags.status; } int handle_websocket_message(FastCGIRequest& request, const String& message, u8 opcode) { Request event_request; ByteStream ws_output; - DTree call_param; Request* previous_context = set_active_request(event_request); server_state.request_count += 1; @@ -200,6 +331,8 @@ int handle_websocket_message(FastCGIRequest& request, const String& message, u8 event_request.params["REQUEST_METHOD"] = "WEBSOCKET"; event_request.get = parse_query(event_request.params["QUERY_STRING"]); event_request.resources = request.resources; + if(event_request.resources.websocket_connection_state) + event_request.connection.set_reference(event_request.resources.websocket_connection_state); event_request.stats.time_init = microtime(); event_request.stats.time_start = event_request.stats.time_init; event_request.random_index = 0; @@ -224,13 +357,13 @@ int handle_websocket_message(FastCGIRequest& request, const String& message, u8 request.params["REQUEST_URI"] ); - call_param["message"] = message; - call_param["connection_id"] = request.resources.websocket_connection_id; - call_param["scope"] = request.resources.websocket_scope; - call_param["opcode"] = (f64)opcode; - call_param["document_uri"] = event_request.var["ws"]["document_uri"].to_string(); + event_request.call["message"] = message; + event_request.call["connection_id"] = request.resources.websocket_connection_id; + event_request.call["scope"] = request.resources.websocket_scope; + event_request.call["opcode"] = (f64)opcode; + event_request.call["document_uri"] = event_request.var["ws"]["document_uri"].to_string(); - compiler_invoke_websocket(&event_request, request.params["SCRIPT_FILENAME"], call_param); + compiler_invoke_websocket(&event_request, request.params["SCRIPT_FILENAME"]); if(event_request.session_id.length() > 0) save_session_data(event_request.session_id, event_request.session); @@ -268,6 +401,19 @@ void listen_for_connections() } signal(SIGSEGV, on_segfault); + signal(SIGABRT, on_segfault); + signal(SIGBUS, on_segfault); + signal(SIGILL, on_segfault); + signal(SIGFPE, on_segfault); + signal(SIGPIPE, SIG_IGN); + if(worker_accepts_http) + { + // Keep the dedicated HTTP/WebSocket worker alive. If it ages out like a + // normal FastCGI worker, nginx can connect to the shared listening socket + // while no child is actively accepting, which makes `.ws.uce` page loads + // appear to hang until the parent respawns a replacement worker. + server.calls_until_termination = -1; + } if(!worker_accepts_http) server.close_http_listeners(); server.on_request = &handle_request; @@ -311,6 +457,7 @@ void init_base_process() signal(SIGCHLD, on_child_exit); signal(SIGINT, on_terminate); + signal(SIGPIPE, SIG_IGN); srand(time()); }