some cleanup

This commit is contained in:
Udo 2026-04-28 12:10:07 +00:00
parent 223cf4c6e1
commit cd445f3c9b
224 changed files with 1558 additions and 38907 deletions

View File

@ -11,7 +11,7 @@ UCE is a PHP-inspired server-side runtime that lets you build web pages and hand
- `.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`
- sub-rendering and components pass structured data through `context.props`
- 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
@ -61,8 +61,10 @@ Useful related runtime patterns:
- `unit_render(String file_name)` or `unit_render(String file_name, Request& context)` to invoke another page
- `context.cfg` for request-local structured configuration
- `context.call` for invocation or message-local structured input
- `context.props` for invocation-local structured input such as component props
- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls
- `context.in` for the current request body, including the current WebSocket message payload inside `WS(Request& context)`
- `context.params["WS_..."]` for direct WebSocket message metadata on the request parameter map
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state
- `context.set_status(code[, reason])` to set the HTTP response status
@ -81,7 +83,7 @@ Named component handlers are also supported:
COMPONENT:BODY(Request& context)
{
<>
<p><?= context.call["body"].to_string() ?></p>
<p><?= context.props["body"].to_string() ?></p>
</>
}
```
@ -117,7 +119,7 @@ UCE includes a native component layer built on top of ordinary `.uce` files:
- `component_exists(name)`
- `component_resolve(name)`
Component props are passed through `context.call`.
Component props are passed through `context.props`.
Component names resolve:
@ -158,7 +160,9 @@ By default, the WebSocket scope is the current page file, so `ws_send()` queues
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 inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
`ws_message()` may still be used when you want the payload through a helper API. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame.

View File

@ -3,7 +3,7 @@ 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().";
card_props["body"] = "This card body comes from context.props and is rendered through component().";
DTree named_props;
named_props["title"] = "Named Render Example";

View File

@ -3,8 +3,8 @@ COMPONENT(Request& context)
{
<>
<section style="border:1px solid #ccc;padding:1em;margin:1em 0;">
<? component_render(":TITLE", context.call, context); ?>
<? component_render(":BODY", context.call, context); ?>
<? component_render(":TITLE", context.props, context); ?>
<? component_render(":BODY", context.props, context); ?>
</section>
</>
}
@ -12,13 +12,13 @@ COMPONENT(Request& context)
COMPONENT:TITLE(Request& context)
{
<>
<h3><?= first(context.call["title"].to_string(), "Component Title") ?></h3>
<h3><?= first(context.props["title"].to_string(), "Component Title") ?></h3>
</>
}
COMPONENT:BODY(Request& context)
{
<>
<p><?= first(context.call["body"].to_string(), "Component Body") ?></p>
<p><?= first(context.props["body"].to_string(), "Component Body") ?></p>
</>
}

View File

@ -1,10 +1,10 @@
COMPONENT(Request& context)
{
String lang = first(context.call["lang"].to_string(), "plain");
String lang = first(context.props["lang"].to_string(), "plain");
<>
<section>
<p><strong>Code block:</strong> <?= lang ?></p>
<div><?: context.call["default_html"].to_string() ?></div>
<div><?: context.props["default_html"].to_string() ?></div>
</section>
</>
}

View File

@ -1,15 +1,15 @@
COMPONENT(Request& context)
{
String title = first(
context.call["node"]["attrs"]["title"].to_string(),
context.call["argument"].to_string(),
context.props["node"]["attrs"]["title"].to_string(),
context.props["argument"].to_string(),
"Notice"
);
<>
<aside>
<p><strong><?= title ?></strong></p>
<div><?: context.call["children_html"].to_string() ?></div>
<div><?: context.props["children_html"].to_string() ?></div>
</aside>
</>
}

View File

@ -27,8 +27,6 @@ RENDER(Request& context)
<a class="docs-link" href="../doc/index.uce">API Docs &rarr;</a>
</h1>
<div class="test-grid">
<div class="grid-heading">Coverage</div>
<? render_card("../tests/index.uce", "Site Tests", "Grouped coverage pages for broad runtime and API validation"); ?>
<div class="grid-heading">Basics</div>
<? render_card("hello.uce", "Hello World", "Basic output and server time"); ?>

View File

@ -13,9 +13,9 @@ 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["online"] = (f64)ws_connection_count();
event["connection_id"] = context.params["WS_CONNECTION_ID"];
event["scope"] = first(context.params["WS_DOCUMENT_URI"], context.params["DOCUMENT_URI"]);
event["online"] = float_val(first(context.params["WS_CONNECTION_COUNT"], "0"));
event["at"] = time_format_utc("%H:%M:%S");
event["name"] = context.connection["name"].to_string();
event["message_count"] = context.connection["message_count"];
@ -37,6 +37,7 @@ RENDER(Request& context)
<pre>Page scope: <?= ws_url ?>
Connected right now: <span id="online-count"><?= online ?></span>
Status: <span id="status">Connecting...</span></pre>
<p>This demo uses <code>context.in</code> for the current payload, <code>context.params["WS_..."]</code> for message metadata, and <code>context.connection</code> for per-client state.</p>
<form id="chat-form" action="?" method="post">
<div>
@ -152,13 +153,13 @@ WS(Request& context)
if(ws_is_binary())
{
ws_send_to(
ws_connection_id(),
context.params["WS_CONNECTION_ID"],
json_encode(chat_event(context, "notice", "Binary messages are not handled by this demo"))
);
return;
}
DTree payload = json_decode(ws_message());
DTree payload = json_decode(context.in);
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);
@ -190,6 +191,6 @@ WS(Request& context)
if(type != "")
{
context.connection["last_type"] = type;
ws_send_to(ws_connection_id(), json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
ws_send_to(context.params["WS_CONNECTION_ID"], json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
}
}

View File

@ -28,9 +28,8 @@ Map-shaped `DTree` values can also represent list-like data when their keys are
You will encounter `DTree` throughout the runtime, especially in:
- `context.var`
- `context.cfg`
- `context.call`
- `context.props`
- `context.connection`
- `json_decode()` results
- `unit_call()` return values
@ -107,7 +106,7 @@ For non-map values, `each()` still invokes the callback once:
- `key` is an empty string
```cpp
context.var["items"].each([&](DTree item, String key) {
context.connection["items"].each([&](DTree item, String key) {
print(key, ": ", item.to_string(), "\n");
});
```
@ -119,7 +118,7 @@ String theme = context.cfg.get_by_path("theme/key").to_string();
u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64();
bool dark_mode = context.call["dark_mode"].to_bool();
bool dark_mode = context.props["dark_mode"].to_bool();
if(DTree* user = payload.key("user")) {
print(user->to_json());

View File

@ -8,7 +8,7 @@ Request& context;
>types
:content
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.var`, and `context.call`.
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.props`, and `context.connection`.
## Core Fields
@ -20,9 +20,8 @@ Request& context;
- `StringMap session`: current session data
- `String session_id`: session cookie ID
- `String session_name`: session cookie name
- `DTree var`: user-defined request-local data
- `DTree cfg`: request-local configuration tree
- `DTree call`: invocation or message-local structured data
- `DTree call`: invocation-local structured data for helpers such as component and unit calls
- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket
- `std::vector<UploadedFile> uploaded_files`: files uploaded in the current request
- `StringMap header`: headers to send back to the browser
@ -45,7 +44,9 @@ Request& context;
## Common Usage Notes
- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads.
- `context.call` carries invocation data for component calls, unit calls, and WebSocket messages.
- `context.props` carries invocation data for component calls and unit calls.
- `context.in` carries the current request body, and for `WS(Request& context)` it is the current WebSocket message payload.
- `context.params["WS_..."]` exposes the current WebSocket message metadata directly on the request parameter map.
- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection.
`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context.

View File

@ -25,7 +25,7 @@ This keeps page rendering and component rendering separate:
A file intended only for component reuse can define `COMPONENT()` without defining `RENDER()`.
Inside component handlers, props arrive through `context.call`.
Inside component handlers, props arrive through `context.props`.
When you call `component(":NAME", props, context)` or `component_render(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again.
@ -34,12 +34,12 @@ When you call `component(":NAME", props, context)` or `component_render(":NAME",
```cpp
COMPONENT(Request& context)
{
<><section><?: component(":BODY", context.call, context) ?></section></>
<><section><?: component(":BODY", context.props, context) ?></section></>
}
COMPONENT:BODY(Request& context)
{
<><p><?= context.call["body"] ?></p></>
<><p><?= context.props["body"] ?></p></>
}
```

View File

@ -18,9 +18,9 @@ The default page entrypoint is always the plain `RENDER(Request& context)` handl
Reusable component handlers now live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`.
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree.
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.props` tree.
For a normal direct page request, `context.call` starts empty.
For a normal direct page request, `context.props` starts empty.
If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`.

View File

@ -12,7 +12,7 @@ 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.
UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. The current payload is available directly on `context.in`, message metadata is mirrored into `context.params["WS_..."]`, and connection-local state lives on `context.connection`.
## Connection State
@ -20,13 +20,21 @@ UCE reassembles fragmented messages before calling `WS(Request& context)`. Text
## Message Data
The current message data is available in `context.call`:
The current message payload is available as:
- `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
- `context.in`: current WebSocket payload
- `context.params["WS_MESSAGE"]`: same payload mirrored into the parameter map
The current message metadata is available as:
- `context.params["WS_CONNECTION_ID"]`: sender connection ID
- `context.params["WS_SCOPE"]`: current endpoint scope
- `context.params["WS_CONNECTION_COUNT"]`: number of currently connected clients in that scope
- `context.params["WS_OPCODE"]`: WebSocket opcode of the current message
- `context.params["WS_MESSAGE_TYPE"]`: `TEXT` or `BINARY`
- `context.params["WS_DOCUMENT_URI"]`: request URI of the current endpoint
Helper wrappers such as `ws_message()`, `ws_connection_id()`, `ws_scope()`, `ws_connection_count()`, `ws_opcode()`, and `ws_is_binary()` are still available when that reads better for the handler.
## Related Concepts

View File

@ -88,7 +88,7 @@ Literal output with trusted unescaped markup:
```cpp
RENDER(Request& context)
{
<><div class="panel"><?: component("components/card", context.call, context) ?></div></>
<><div class="panel"><?: component("components/card", context.props, context) ?></div></>
}
```
@ -96,7 +96,7 @@ Roughly becomes:
```cpp
print(R"(<div class="panel">)");
print(component("components/card", context.call, context));
print(component("components/card", context.props, context));
print(R"(</div>)");
```

View File

@ -9,7 +9,7 @@ Renders another `.uce` file as a component and returns the captured output as a
`component()` resolves the target file relative to the current page and also tries the `components/` prefix automatically, mirroring the shorthand used by the starter example project.
Component props are passed in `context.call`.
Component props are passed in `context.props`.
Because `<?= ... ?>` HTML-escapes its value, embed component markup with `<?: component(...) ?>`, `print(component(...))`, or use `component_render(...)` for direct output.

View File

@ -9,7 +9,7 @@ Renders another `.uce` file as a component and writes the result directly to the
This is the direct-output counterpart to `component()`.
Component props are passed through `context.call`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
Component props are passed through `context.props`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file.

View File

@ -70,22 +70,22 @@ 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.
When a markdown hook component is called, its props arrive in `context.call`.
When a markdown hook component is called, its props arrive in `context.props`.
Useful fields include:
- `context.call["hook"]` for the matched hook key such as `:::warning` or `node.code_block`
- `context.call["target"]` for the resolved component target name
- `context.call["default_html"]` for the renderer output without the hook
- `context.call["children_html"]` for already-rendered child HTML
- `context.call["node"]` for the full AST node
- `context.call["type"]` for the node type
- `context.call["name"]` for the directive name when applicable
- `context.call["argument"]` for directive remainder after the name
- `context.call["text"]` for source text used by nodes such as `code_block`
- `context.call["lang"]` for fenced code language
- `context.call["href"]`, `context.call["src"]`, and `context.call["title"]`
- `context.call["options"]` for the full markdown options tree
- `context.props["hook"]` for the matched hook key such as `:::warning` or `node.code_block`
- `context.props["target"]` for the resolved component target name
- `context.props["default_html"]` for the renderer output without the hook
- `context.props["children_html"]` for already-rendered child HTML
- `context.props["node"]` for the full AST node
- `context.props["type"]` for the node type
- `context.props["name"]` for the directive name when applicable
- `context.props["argument"]` for directive remainder after the name
- `context.props["text"]` for source text used by nodes such as `code_block`
- `context.props["lang"]` for fenced code language
- `context.props["href"]`, `context.props["src"]`, and `context.props["title"]`
- `context.props["options"]` for the full markdown options tree
Directive blocks use this form:

View File

@ -2,11 +2,11 @@
COMPONENT(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));
String title = first(context.props["title"].to_string(), "Sign in with OAuth");
String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method");
String callback_url = first(context.props["callback_url"].to_string(), starter_link("auth/callback", context));
DTree services = context.call["services"];
DTree services = context.props["services"];
if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "")
{
services["google"]["name"] = "Google";
@ -41,7 +41,7 @@ COMPONENT(Request& context)
<label><?= subtitle ?></label>
</div>
<? services.each([&](DTree service, String service_key) {
String client_id = context.call[service_key + "_client_id"].to_string();
String client_id = context.props[service_key + "_client_id"].to_string();
String handler_name = ascii_safe_name(service_key);
String service_name = service["name"].to_string();
String scope = service["scope"].to_string();

View File

@ -76,8 +76,8 @@ DTree data_format_value(DTree value, DTree row, DTree column)
COMPONENT:SUMMARY_METRICS(Request& context)
{
String title = context.call["title"].to_string();
String subtitle = context.call["subtitle"].to_string();
String title = context.props["title"].to_string();
String subtitle = context.props["subtitle"].to_string();
<>
<div class="dashboard-panel">
@ -88,7 +88,7 @@ COMPONENT:SUMMARY_METRICS(Request& context)
</div>
<? } ?>
<div class="dashboard-stat-grid">
<? context.call["items"].each([&](DTree item, String key) {
<? context.props["items"].each([&](DTree item, String key) {
String tone = first(item["tone"].to_string(), "info");
String href = item["href"].to_string();
String tag = href != "" ? "a" : "div";
@ -108,16 +108,16 @@ COMPONENT: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 chart_id = first(context.props["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");
String title = context.props["title"].to_string();
String subtitle = context.props["subtitle"].to_string();
s32 height = std::max(180, (s32)int_val(first(context.props["height"].to_string(), "320")));
String x_axis_label = first(context.props["x_axis_label"].to_string(), "Time");
String y_axis_left_label = first(context.props["y_axis_left_label"].to_string(), "Value");
String y_axis_right_label = context.props["y_axis_right_label"].to_string();
String y_axis_left_format = first(context.props["y_axis_left_format"].to_string(), "number");
String y_axis_right_format = first(context.props["y_axis_right_format"].to_string(), "number");
<>
<div class="dashboard-panel">
@ -139,7 +139,7 @@ COMPONENT:TIMESERIES_CHART(Request& context)
yAxisLeftFormat: <?: json_encode(y_axis_left_format) ?>,
yAxisRightFormat: <?: json_encode(y_axis_right_format) ?>
});
chart.setData(<?: json_encode(context.call["series"]) ?>, <?: json_encode(context.call["x_labels"]) ?>);
chart.setData(<?: json_encode(context.props["series"]) ?>, <?: json_encode(context.props["x_labels"]) ?>);
window[<?: json_encode("chart_" + chart_id) ?>] = chart;
}());
</script>
@ -151,17 +151,17 @@ COMPONENT: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");
String table_id = first(context.props["id"].to_string(), "sortable-table-" + std::to_string((u64)time()));
String title = context.props["title"].to_string();
String subtitle = context.props["subtitle"].to_string();
String empty_label = first(context.props["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["storageKey"] = first(context.props["storage_key"].to_string(), "starter.sort." + table_id);
if(context.props["sort"]["column"].to_string() != "")
{
init_options["initialSort"]["column"] = context.call["sort"]["column"];
init_options["initialSort"]["direction"] = first(context.call["sort"]["direction"].to_string(), "asc");
init_options["initialSort"]["column"] = context.props["sort"]["column"];
init_options["initialSort"]["direction"] = first(context.props["sort"]["direction"].to_string(), "asc");
}
<>
@ -176,7 +176,7 @@ COMPONENT:SORTABLE_TABLE(Request& context)
<table id="<?= table_id ?>" class="u-sortable-table">
<thead>
<tr>
<? context.call["columns"].each([&](DTree column, String key) {
<? context.props["columns"].each([&](DTree column, String key) {
String align = first(column["align"].to_string(), "left");
bool sortable = column["sortable"].to_string() == "" || column["sortable"].to_string() == "1";
?><th scope="col" class="align-<?= align ?>"<?= sortable ? "" : " data-sortable=\"false\"" ?>><?= first(column["label"].to_string(), column["key"].to_string(), "Column") ?></th><?
@ -184,14 +184,14 @@ COMPONENT:SORTABLE_TABLE(Request& context)
</tr>
</thead>
<tbody>
<? if(context.call["rows"].get_type_name() != "array" || context.call["rows"]["0"].get_type_name() != "array") { ?>
<tr data-empty-row="1"><td colspan="<?= context.call["columns"]._map.size() == 0 ? "1" : std::to_string(context.call["columns"]._map.size()) ?>" class="muted"><?= empty_label ?></td></tr>
<? if(context.props["rows"].get_type_name() != "array" || context.props["rows"]["0"].get_type_name() != "array") { ?>
<tr data-empty-row="1"><td colspan="<?= context.props["columns"]._map.size() == 0 ? "1" : std::to_string(context.props["columns"]._map.size()) ?>" class="muted"><?= empty_label ?></td></tr>
<? } ?>
<? context.call["rows"].each([&](DTree row, String key) {
<? context.props["rows"].each([&](DTree row, String key) {
if(row.get_type_name() != "array")
return;
?><tr><?
context.call["columns"].each([&](DTree column, String ckey) {
context.props["columns"].each([&](DTree column, String ckey) {
String col_key = column["key"].to_string();
DTree formatted = data_format_value(row[col_key], row, column);
?><td class="align-<?= first(column["align"].to_string(), "left") ?>" data-sort-value="<?= formatted["sort"].to_string() ?>"><?= formatted["display"].to_string() ?></td><?
@ -213,21 +213,21 @@ COMPONENT:SORTABLE_TABLE(Request& context)
COMPONENT:DATA_TABLE(Request& context)
{
String table_id = first(context.call["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
String table_id = first(context.props["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"];
DTree columns = context.props["columns"];
if(columns.get_type_name() != "array")
columns.set_array();
bool has_explicit_columns = columns.is_list() &&
columns._map.size() > 0 &&
columns._map.find("0") != columns._map.end() &&
columns["0"]["field"].to_string() != "";
if(!has_explicit_columns && context.call["items"]["0"].get_type_name() == "array")
if(!has_explicit_columns && context.props["items"]["0"].get_type_name() == "array")
{
context.call["items"]["0"].each([&](DTree value, String key) {
context.props["items"]["0"].each([&](DTree value, String key) {
DTree col;
col["field"] = key;
col["headerName"] = key;
@ -248,7 +248,7 @@ COMPONENT:DATA_TABLE(Request& context)
columns = normalized_columns;
}
DTree row_data = context.call["items"];
DTree row_data = context.props["items"];
if(row_data.get_type_name() != "array")
row_data.set_array();
if(!row_data.is_list())
@ -264,7 +264,7 @@ COMPONENT:DATA_TABLE(Request& context)
DTree grid_options;
grid_options["pagination"].set_bool(true);
grid_options["paginationPageSize"] = first(context.call["options"]["paginationPageSize"].to_string(), "20");
grid_options["paginationPageSize"] = first(context.props["options"]["paginationPageSize"].to_string(), "20");
grid_options["paginationPageSizeSelector"].set_bool(false);
grid_options["suppressMenuHide"].set_bool(true);
grid_options["animateRows"].set_bool(true);
@ -275,7 +275,7 @@ COMPONENT:DATA_TABLE(Request& context)
<div class="ag-grid-container" style="margin: 1rem 0;">
<div class="ag-grid-toolbar" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:0.75rem;background:var(--surface-elevated);border:1px solid var(--border);border-radius:0.5rem 0.5rem 0 0;border-bottom:none;">
<div style="display:flex;gap:1rem;align-items:center;flex:1;">
<span style="color: var(--text-secondary); font-size: 0.875rem; font-weight: 500;"><?= std::to_string(context.call["items"]._map.size()) ?> rows</span>
<span style="color: var(--text-secondary); font-size: 0.875rem; font-weight: 500;"><?= std::to_string(context.props["items"]._map.size()) ?> rows</span>
<input type="text" id="<?= table_id ?>-search" placeholder="Search all columns..." style="padding:0.5rem 0.75rem;border:1px solid var(--border);border-radius:0.375rem;background:var(--surface);color:var(--text-primary);font-size:0.875rem;width:250px;" />
</div>
<div style="display:flex;gap:0.5rem;">
@ -283,7 +283,7 @@ COMPONENT:DATA_TABLE(Request& context)
<button id="<?= table_id ?>-clear-filters" class="btn btn-outline">Clear Filters</button>
</div>
</div>
<div id="<?= table_id ?>" class="ag-theme-alpine-dark" style="height: <?= first(context.call["height"].to_string(), "400px") ?>; width: 100%; border: 1px solid var(--border); border-radius: 0 0 0.5rem 0.5rem; overflow: hidden;"></div>
<div id="<?= table_id ?>" class="ag-theme-alpine-dark" style="height: <?= first(context.props["height"].to_string(), "400px") ?>; width: 100%; border: 1px solid var(--border); border-radius: 0 0 0.5rem 0.5rem; overflow: hidden;"></div>
</div>
<script>
(function () {

View File

@ -44,10 +44,10 @@ void marketing_default_features(DTree& features)
COMPONENT: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(), "#");
String title = first(context.props["title"].to_string(), "Welcome to the Present");
String subtitle = first(context.props["subtitle"].to_string(), "Experience no-quite-modern web development with our cutting-edge framework");
String cta_text = first(context.props["cta_text"].to_string(), "Get Started");
String cta_link = first(context.props["cta_link"].to_string(), "#");
<>
<div class="hero-section">
@ -194,7 +194,7 @@ COMPONENT:HERO_SECTION(Request& context)
COMPONENT:FEATURES_GRID(Request& context)
{
DTree features = context.call["features"];
DTree features = context.props["features"];
marketing_default_features(features);
<>
@ -288,7 +288,7 @@ COMPONENT:FEATURES_GRID(Request& context)
COMPONENT:STATS_SECTION(Request& context)
{
DTree stats = context.call["stats"];
DTree stats = context.props["stats"];
if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "")
{
DTree item;
@ -364,7 +364,7 @@ COMPONENT:STATS_SECTION(Request& context)
COMPONENT:BRANDS_SHOWCASE(Request& context)
{
DTree logos = context.call["logos"];
DTree logos = context.props["logos"];
if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "")
{
DTree item;
@ -376,7 +376,7 @@ COMPONENT:BRANDS_SHOWCASE(Request& context)
item.clear();
}
}
String title = first(context.call["title"].to_string(), "Trusted by Leading Companies");
String title = first(context.props["title"].to_string(), "Trusted by Leading Companies");
<>
<div class="brands-section">
@ -410,7 +410,7 @@ COMPONENT:BRANDS_SHOWCASE(Request& context)
COMPONENT:TESTIMONIALS(Request& context)
{
DTree testimonials = context.call["testimonials"];
DTree testimonials = context.props["testimonials"];
if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "")
{
DTree item;
@ -473,7 +473,7 @@ COMPONENT:TESTIMONIALS(Request& context)
COMPONENT:PRICING_TABLE(Request& context)
{
DTree plans = context.call["plans"];
DTree plans = context.props["plans"];
if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "")
{
DTree plan;
@ -546,12 +546,12 @@ COMPONENT:PRICING_TABLE(Request& context)
COMPONENT: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(), "#");
String title = first(context.props["title"].to_string(), "Ready to Get Started?");
String subtitle = first(context.props["subtitle"].to_string(), "Join thousands of developers building amazing applications with our framework.");
String cta_text = first(context.props["cta_text"].to_string(), "Start Building Now");
String cta_link = first(context.props["cta_link"].to_string(), "#");
String secondary_text = first(context.props["secondary_text"].to_string(), "View Documentation");
String secondary_link = first(context.props["secondary_link"].to_string(), "#");
<>
<div class="cta-section">

View File

@ -5,11 +5,11 @@ COMPONENT(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();
DTree scale = context.call["scale"];
String gauge_id = first(context.props["id"].to_string(), "arcgauge-" + std::to_string((u64)time()));
String title = context.props["title"].to_string();
String subtitle = context.props["subtitle"].to_string();
String style = context.props["style"].to_string();
DTree scale = context.props["scale"];
<>
<div class="arcgauge-set" id="<?= gauge_attr_id(gauge_id) ?>" style="<?= style ?>">
@ -20,7 +20,7 @@ COMPONENT(Request& context)
</div>
<? } ?>
<div class="arcgauge-grid">
<? context.call["items"].each([&](DTree item, String item_id_raw) {
<? context.props["items"].each([&](DTree item, String item_id_raw) {
DTree merged = scale;
item.each([&](DTree value, String key) { merged[key] = value; });
String item_id = gauge_attr_id(item_id_raw);
@ -62,7 +62,7 @@ COMPONENT(Request& context)
}); ?>
</div>
</div>
<? if(context.call["listen"].to_string() != "") { ?>
<? if(context.props["listen"].to_string() != "") { ?>
<script>
window.ArcgaugeComponents = window.ArcgaugeComponents || {
start_listen : function(prop) {
@ -84,7 +84,7 @@ COMPONENT(Request& context)
});
}
};
ArcgaugeComponents.start_listen(<?: json_encode(context.call, '\'') ?>);
ArcgaugeComponents.start_listen(<?: json_encode(context.props, '\'') ?>);
</script>
<? } ?>
</>

View File

@ -5,15 +5,15 @@ COMPONENT(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(), "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();
f64 size = float_val(first(context.call["size"].to_string(), "200"));
DTree scale = context.call["scale"];
String gauge_id = first(context.props["id"].to_string(), "needlegauge-" + std::to_string((u64)time()));
String style = context.props["style"].to_string();
String title = context.props["title"].to_string();
String subtitle = context.props["subtitle"].to_string();
f64 size = float_val(first(context.props["size"].to_string(), "200"));
DTree scale = context.props["scale"];
f64 scale_angle_start = float_val(first(scale["angle_start"].to_string(), std::to_string(-gauge_pi())));
f64 scale_angle_end = float_val(first(scale["angle_end"].to_string(), "0"));
f64 img_height = float_val(first(context.call["img_height"].to_string(), std::to_string(size * std::max(cos(scale_angle_start), cos(scale_angle_end)))));
f64 img_height = float_val(first(context.props["img_height"].to_string(), std::to_string(size * std::max(cos(scale_angle_start), cos(scale_angle_end)))));
<>
<section class="gauge-set needlegauge-set" id="<?= gauge_attr_id(gauge_id) ?>" style="<?= style ?>">
@ -24,7 +24,7 @@ COMPONENT(Request& context)
</div>
<? } ?>
<div class="needlegauge-grid">
<? context.call["items"].each([&](DTree item, String item_id_raw) {
<? context.props["items"].each([&](DTree item, String item_id_raw) {
DTree merged = scale;
item.each([&](DTree value, String key) { merged[key] = value; });
String item_id = gauge_attr_id(item_id_raw);
@ -117,7 +117,7 @@ COMPONENT(Request& context)
}); ?>
</div>
</section>
<? if(context.call["listen"].to_string() != "") { ?>
<? if(context.props["listen"].to_string() != "") { ?>
<script>
window.NeedlegaugeComponents = window.NeedlegaugeComponents || {
start_listen : function(prop) {
@ -137,7 +137,7 @@ COMPONENT(Request& context)
});
}
};
NeedlegaugeComponents.start_listen(<?: json_encode(context.call, '\'') ?>);
NeedlegaugeComponents.start_listen(<?: json_encode(context.props, '\'') ?>);
</script>
<? } ?>
</>

View File

@ -5,18 +5,18 @@ COMPONENT(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 style = context.call["style"].to_string();
String item_style = context.call["item-style"].to_string();
String label_style = context.call["label-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();
String layout = first(context.call["layout"].to_string(), "horizontal");
String bar_color_default = context.call["bar-color"].to_string();
DTree scale = context.call["scale"];
DTree markers = context.call["markers"];
String gauge_id = first(context.props["id"].to_string(), "progressbar-" + std::to_string((u64)time()));
String style = context.props["style"].to_string();
String item_style = context.props["item-style"].to_string();
String label_style = context.props["label-style"].to_string();
String value_style = context.props["value-style"].to_string();
String bar_style = context.props["bar-style"].to_string();
String title = context.props["title"].to_string();
String subtitle = context.props["subtitle"].to_string();
String layout = first(context.props["layout"].to_string(), "horizontal");
String bar_color_default = context.props["bar-color"].to_string();
DTree scale = context.props["scale"];
DTree markers = context.props["markers"];
StringList default_palette;
default_palette.push_back("var(--success, #10b981)");
@ -35,7 +35,7 @@ COMPONENT(Request& context)
<? } ?>
<? if(layout == "horizontal") { ?>
<div class="progressbar-grid progressbar-grid-horizontal">
<? context.call["items"].each([&](DTree bar, String bar_id) {
<? context.props["items"].each([&](DTree bar, String bar_id) {
DTree merged = scale;
bar.each([&](DTree item, String key) { merged[key] = item; });
String item_id = gauge_attr_id(bar_id);
@ -86,8 +86,8 @@ COMPONENT(Request& context)
}); ?>
</div>
<? } else { ?>
<div class="progressbar-grid progressbar-grid-vertical" style="--progressbar-height: <?= first(context.call["height"].to_string(), "240") ?>px;">
<? context.call["items"].each([&](DTree bar, String bar_id) {
<div class="progressbar-grid progressbar-grid-vertical" style="--progressbar-height: <?= first(context.props["height"].to_string(), "240") ?>px;">
<? context.props["items"].each([&](DTree bar, String bar_id) {
DTree merged = scale;
bar.each([&](DTree item, String key) { merged[key] = item; });
String item_id = gauge_attr_id(bar_id);
@ -137,7 +137,7 @@ COMPONENT(Request& context)
</div>
<? } ?>
</section>
<? if(context.call["listen"].to_string() != "") { ?>
<? if(context.props["listen"].to_string() != "") { ?>
<script>
window.ProgressbarComponents = window.ProgressbarComponents || {
start_listen : function(prop) {
@ -161,7 +161,7 @@ COMPONENT(Request& context)
});
}
};
ProgressbarComponents.start_listen(<?: json_encode(context.call, '\'') ?>);
ProgressbarComponents.start_listen(<?: json_encode(context.props, '\'') ?>);
</script>
<? } ?>
</>

View File

@ -7,9 +7,9 @@ COMPONENT(Request& context)
DTree user = users.current();
bool signed_in = user["email"].to_string() != "";
String theme_key = context.cfg.get_by_path("theme/key").to_string();
String wrapper_class = first(context.call["wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu");
String links_class = first(context.call["links_wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-links" : "");
String name_class = first(context.call["name_class"].to_string(), theme_key == "localfirst" ? "admin-account-name" : "account-name");
String wrapper_class = first(context.props["wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu");
String links_class = first(context.props["links_wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-links" : "");
String name_class = first(context.props["name_class"].to_string(), theme_key == "localfirst" ? "admin-account-name" : "account-name");
<>
<div class="<?= wrapper_class ?>">

View File

@ -8,7 +8,7 @@ COMPONENT(Request& context)
return;
String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string());
String inner_class = first(context.call["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "");
String inner_class = first(context.props["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "");
<>
<footer>

View File

@ -6,7 +6,7 @@ COMPONENT(Request& context)
if(starter_page_embed_mode(context))
return;
String cookie_value = context.call["cookie_consent"].to_string();
String cookie_value = context.props["cookie_consent"].to_string();
bool cookie_consent = !(cookie_value == "0" || cookie_value == "false" || cookie_value == "FALSE" || cookie_value == "no" || cookie_value == "NO");
<>

View File

@ -3,18 +3,18 @@
COMPONENT(Request& context)
{
starter_boot(context);
if(context.call["main_html"].to_string() != "")
context.var["starter"]["fragments"]["main"] = context.call["main_html"];
if(context.call["json"].get_type_name() == "array")
context.var["starter"]["json"] = context.call["json"];
if(context.call["page_type"].to_string() != "")
context.var["starter"]["page_type"] = context.call["page_type"];
if(context.call["embed_mode"].to_string() != "")
context.var["starter"]["embed_mode"] = context.call["embed_mode"];
if(context.props["main_html"].to_string() != "")
context.var["starter"]["fragments"]["main"] = context.props["main_html"];
if(context.props["json"].get_type_name() == "array")
context.var["starter"]["json"] = context.props["json"];
if(context.props["page_type"].to_string() != "")
context.var["starter"]["page_type"] = context.props["page_type"];
if(context.props["embed_mode"].to_string() != "")
context.var["starter"]["embed_mode"] = context.props["embed_mode"];
starter_render_page(context);
}
COMPONENT:HEAD(Request& context)
{
print(component("head", context.call, context));
print(component("head", context.props, context));
}

View File

@ -3,17 +3,17 @@
COMPONENT(Request& context)
{
starter_boot(context);
String nav_class = context.call["nav_class"].to_string();
String nav_class = context.props["nav_class"].to_string();
if(nav_class == "" && (context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming"))
nav_class = "nav-shell";
DTree account_props;
if(context.call["account_wrapper_class"].to_string() != "")
account_props["wrapper_class"] = context.call["account_wrapper_class"];
if(context.call["links_wrapper_class"].to_string() != "")
account_props["links_wrapper_class"] = context.call["links_wrapper_class"];
if(context.call["name_class"].to_string() != "")
account_props["name_class"] = context.call["name_class"];
if(context.props["account_wrapper_class"].to_string() != "")
account_props["wrapper_class"] = context.props["account_wrapper_class"];
if(context.props["links_wrapper_class"].to_string() != "")
account_props["links_wrapper_class"] = context.props["links_wrapper_class"];
if(context.props["name_class"].to_string() != "")
account_props["name_class"] = context.props["name_class"];
<>
<nav<?: nav_class != "" ? " class=\"" + html_escape(nav_class) + "\"" : "" ?>>

View File

@ -4,14 +4,14 @@ COMPONENT: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();
String id = context.props["id"].to_string();
String overlay_id = context.props["overlay_id"].to_string();
String class_name = context.props["class"].to_string();
<>
<div<?: id != "" ? " id=\"" + html_escape(id) + "\"" : "" ?> class="ws-app<?= class_name != "" ? " " + html_escape(class_name) : "" ?>">
<?: context.call["sidebar_html"].to_string() ?>
<?: context.props["sidebar_html"].to_string() ?>
<div<?: overlay_id != "" ? " id=\"" + html_escape(overlay_id) + "\"" : "" ?> class="ws-sidebar-overlay"></div>
<main class="ws-main"><?: context.call["main_html"].to_string() ?></main>
<main class="ws-main"><?: context.props["main_html"].to_string() ?></main>
</div>
</>
}
@ -19,11 +19,11 @@ COMPONENT:APP_FRAME(Request& context)
COMPONENT:EMPTY_STATE(Request& context)
{
<>
<div class="ws-empty-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<div class="ws-empty-icon"><i class="<?= first(context.call["icon_class"].to_string(), "fas fa-layer-group") ?>"></i></div>
<h2><?= context.call["title"].to_string() ?></h2>
<p><?= context.call["text"].to_string() ?></p>
<?: context.call["action_html"].to_string() ?>
<div class="ws-empty-state<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<div class="ws-empty-icon"><i class="<?= first(context.props["icon_class"].to_string(), "fas fa-layer-group") ?>"></i></div>
<h2><?= context.props["title"].to_string() ?></h2>
<p><?= context.props["text"].to_string() ?></p>
<?: context.props["action_html"].to_string() ?>
</div>
</>
}
@ -31,18 +31,18 @@ COMPONENT:EMPTY_STATE(Request& context)
COMPONENT:LIST_STATE(Request& context)
{
<>
<div class="ws-list-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><? if(context.call["icon_class"].to_string() != "") { ?><i class="<?= context.call["icon_class"].to_string() ?>"></i><? } ?><span><?= context.call["text"].to_string() ?></span></div>
<div class="ws-list-state<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>"><? if(context.props["icon_class"].to_string() != "") { ?><i class="<?= context.props["icon_class"].to_string() ?>"></i><? } ?><span><?= context.props["text"].to_string() ?></span></div>
</>
}
COMPONENT:MOBILE_BAR(Request& context)
{
<>
<div class="ws-mobile-bar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<button<?: context.call["button_id"].to_string() != "" ? " id=\"" + html_escape(context.call["button_id"].to_string()) + "\"" : "" ?> class="ws-mobile-toggle" title="Toggle sidebar" type="button">
<i class="<?= first(context.call["icon_class"].to_string(), "fas fa-bars") ?>"></i>
<div class="ws-mobile-bar<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<button<?: context.props["button_id"].to_string() != "" ? " id=\"" + html_escape(context.props["button_id"].to_string()) + "\"" : "" ?> class="ws-mobile-toggle" title="Toggle sidebar" type="button">
<i class="<?= first(context.props["icon_class"].to_string(), "fas fa-bars") ?>"></i>
</button>
<span class="ws-mobile-title"><?= context.call["title"].to_string() ?></span>
<span class="ws-mobile-title"><?= context.props["title"].to_string() ?></span>
</div>
</>
}
@ -50,12 +50,12 @@ COMPONENT:MOBILE_BAR(Request& context)
COMPONENT:PANEL_HEADER(Request& context)
{
<>
<div class="ws-panel-header<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<div class="ws-panel-header<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<div class="ws-panel-title-group">
<h2><?= context.call["title"].to_string() ?></h2>
<? if(context.call["subtitle"].to_string() != "") { ?><p class="ws-subtitle"><?= context.call["subtitle"].to_string() ?></p><? } ?>
<h2><?= context.props["title"].to_string() ?></h2>
<? if(context.props["subtitle"].to_string() != "") { ?><p class="ws-subtitle"><?= context.props["subtitle"].to_string() ?></p><? } ?>
</div>
<? if(context.call["actions_html"].to_string() != "") { ?><div class="ws-header-actions"><?: context.call["actions_html"].to_string() ?></div><? } ?>
<? if(context.props["actions_html"].to_string() != "") { ?><div class="ws-header-actions"><?: context.props["actions_html"].to_string() ?></div><? } ?>
</div>
</>
}
@ -63,9 +63,9 @@ COMPONENT:PANEL_HEADER(Request& context)
COMPONENT:PANEL(Request& context)
{
<>
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-panel<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["header_html"].to_string() ?>
<?: context.call["body_html"].to_string() ?>
<section<?: context.props["id"].to_string() != "" ? " id=\"" + html_escape(context.props["id"].to_string()) + "\"" : "" ?> class="ws-panel<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<?: context.props["header_html"].to_string() ?>
<?: context.props["body_html"].to_string() ?>
</section>
</>
}
@ -73,9 +73,9 @@ COMPONENT:PANEL(Request& context)
COMPONENT:SECTION_HEAD(Request& context)
{
<>
<div class="ws-section-head<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<h3><?= context.call["title"].to_string() ?></h3>
<? if(context.call["actions_html"].to_string() != "") { ?><div class="ws-header-actions"><?: context.call["actions_html"].to_string() ?></div><? } ?>
<div class="ws-section-head<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<h3><?= context.props["title"].to_string() ?></h3>
<? if(context.props["actions_html"].to_string() != "") { ?><div class="ws-header-actions"><?: context.props["actions_html"].to_string() ?></div><? } ?>
</div>
</>
}
@ -83,9 +83,9 @@ COMPONENT:SECTION_HEAD(Request& context)
COMPONENT:SECTION(Request& context)
{
<>
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-section<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["header_html"].to_string() ?>
<?: context.call["body_html"].to_string() ?>
<section<?: context.props["id"].to_string() != "" ? " id=\"" + html_escape(context.props["id"].to_string()) + "\"" : "" ?> class="ws-section<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<?: context.props["header_html"].to_string() ?>
<?: context.props["body_html"].to_string() ?>
</section>
</>
}
@ -93,9 +93,9 @@ COMPONENT:SECTION(Request& context)
COMPONENT:SIDEBAR_SHELL(Request& context)
{
<>
<aside<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-sidebar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["top_html"].to_string() ?>
<?: context.call["body_html"].to_string() ?>
<aside<?: context.props["id"].to_string() != "" ? " id=\"" + html_escape(context.props["id"].to_string()) + "\"" : "" ?> class="ws-sidebar<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<?: context.props["top_html"].to_string() ?>
<?: context.props["body_html"].to_string() ?>
</aside>
</>
}
@ -103,11 +103,11 @@ COMPONENT:SIDEBAR_SHELL(Request& context)
COMPONENT:SIDEBAR_TOOLBAR(Request& context)
{
<>
<div class="ws-sidebar-top<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["action_html"].to_string() ?>
<div class="ws-sidebar-top<?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>">
<?: context.props["action_html"].to_string() ?>
<div class="ws-search-wrap">
<i class="fas fa-search ws-search-icon"></i>
<input type="search"<?: context.call["search_input_id"].to_string() != "" ? " id=\"" + html_escape(context.call["search_input_id"].to_string()) + "\"" : "" ?> name="<?= first(context.call["search_input_name"].to_string(), "search") ?>" class="ws-search-input" placeholder="<?= first(context.call["search_placeholder"].to_string(), "Search...") ?>" autocomplete="off">
<input type="search"<?: context.props["search_input_id"].to_string() != "" ? " id=\"" + html_escape(context.props["search_input_id"].to_string()) + "\"" : "" ?> name="<?= first(context.props["search_input_name"].to_string(), "search") ?>" class="ws-search-input" placeholder="<?= first(context.props["search_placeholder"].to_string(), "Search...") ?>" autocomplete="off">
</div>
</div>
</>
@ -115,8 +115,8 @@ COMPONENT:SIDEBAR_TOOLBAR(Request& context)
COMPONENT:STATUS_PILL(Request& context)
{
String variant = to_lower(first(context.call["variant"].to_string(), "neutral"));
String variant = to_lower(first(context.props["variant"].to_string(), "neutral"));
<>
<span class="ws-status-pill ws-status-pill-<?= variant ?><?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><?= first(context.call["label"].to_string(), context.call["text"].to_string()) ?></span>
<span class="ws-status-pill ws-status-pill-<?= variant ?><?= context.props["class"].to_string() != "" ? " " + html_escape(context.props["class"].to_string()) : "" ?>"><?= first(context.props["label"].to_string(), context.props["text"].to_string()) ?></span>
</>
}

View File

@ -206,7 +206,7 @@ String app_html_class(Request& context)
String app_page_main_html(Request& context)
{
String main_html = context.call["main_html"].to_string();
String main_html = context.props["main_html"].to_string();
if(main_html == "")
main_html = context.var["app"]["fragments"]["main"].to_string();
return(main_html);
@ -236,9 +236,9 @@ bool app_request_embed_mode(Request& context)
bool app_page_embed_mode(Request& context)
{
String embed_value = context.call["embed_mode"].to_string();
String embed_value = context.props["embed_mode"].to_string();
if(embed_value != "")
return(app_bool_value(context.call["embed_mode"]));
return(app_bool_value(context.props["embed_mode"]));
return(app_request_embed_mode(context));
}

View File

@ -5,8 +5,8 @@ COMPONENT(Request& context)
starter_boot(context);
context.header["Content-Type"] = "application/json";
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
if(context.call["json"].get_type_name() == "array")
print(json_encode(context.call["json"]));
if(context.props["json"].get_type_name() == "array")
print(json_encode(context.props["json"]));
else if(context.var["starter"]["json"].get_type_name() == "array")
print(json_encode(context.var["starter"]["json"]));
else

View File

@ -20,7 +20,7 @@ RENDER(Request& context)
</div>
<pre class="code-block"><code>COMPONENT:HERO_SECTION(Request&amp; context)
{
String title = first(context.call["title"].to_string(), "Welcome");
String title = first(context.props["title"].to_string(), "Welcome");
&lt;&gt;
&lt;div class="hero-section"&gt;...&lt;/div&gt;
&lt;/&gt;

View File

@ -0,0 +1,611 @@
bool code_is_space(char ch)
{
return(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r');
}
bool code_is_digit(char ch)
{
return(ch >= '0' && ch <= '9');
}
bool code_is_alpha(char ch)
{
return((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'));
}
bool code_is_ident_start(char ch)
{
return(code_is_alpha(ch) || ch == '_');
}
bool code_is_ident_char(char ch)
{
return(code_is_ident_start(ch) || code_is_digit(ch));
}
bool markup_name_char(char ch)
{
return(code_is_ident_char(ch) || ch == '-' || ch == ':' || ch == '@');
}
bool code_is_punct(char ch)
{
return(ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == '(' || ch == ')' || ch == ',' || ch == ';');
}
bool code_is_operator(char ch)
{
return(ch == '=' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '%' || ch == '!' || ch == '<' || ch == '>' || ch == '&' || ch == '|' || ch == '.' || ch == ':' || ch == '?' || ch == '^');
}
bool code_has_prefix(String src, u64 i, String prefix)
{
if(i + prefix.length() > src.length())
return(false);
return(substr(src, i, prefix.length()) == prefix);
}
String code_wrap(String cls, String text)
{
if(text == "")
return("");
return("<span class=\"tok tok-" + cls + "\">" + html_escape(text) + "</span>");
}
String uce_markup_open_token()
{
return("<" ">");
}
String uce_markup_close_token()
{
return("</" ">");
}
String uce_code_open_token()
{
return("<" "?");
}
String uce_code_expr_token()
{
return("<" "?=");
}
String uce_code_raw_token()
{
return("<" "?:");
}
String uce_code_close_token()
{
return("?" ">");
}
bool cpp_keyword(String word)
{
String lower = to_lower(word);
return(
word == "RENDER" ||
word == "COMPONENT" ||
word == "WS" ||
lower == "if" ||
lower == "else" ||
lower == "for" ||
lower == "while" ||
lower == "return" ||
lower == "const" ||
lower == "auto" ||
lower == "void" ||
lower == "bool" ||
lower == "true" ||
lower == "false"
);
}
bool cpp_type(String word)
{
return(
word == "String" ||
word == "DTree" ||
word == "Request" ||
word == "StringList" ||
word == "StringMap" ||
word == "u64" ||
word == "u32" ||
word == "s64" ||
word == "f64"
);
}
bool cpp_builtin_call(String word)
{
return(
word == "component" ||
word == "component_render" ||
word == "component_exists" ||
word == "component_resolve" ||
word == "markdown_to_ast" ||
word == "markdown_to_html" ||
word == "json_decode" ||
word == "json_encode" ||
word == "time_format_utc" ||
word == "first" ||
word == "contains" ||
word == "ws_send" ||
word == "ws_send_to" ||
word == "ws_message" ||
word == "ws_connection_id"
);
}
bool cpp_context_field(String word)
{
return(
word == "get" ||
word == "post" ||
word == "session" ||
word == "call" ||
word == "connection" ||
word == "params" ||
word == "cookies" ||
word == "header" ||
word == "cfg"
);
}
bool nginx_keyword(String word)
{
return(
word == "location" ||
word == "include" ||
word == "fastcgi_param" ||
word == "fastcgi_pass" ||
word == "proxy_pass" ||
word == "proxy_set_header" ||
word == "proxy_http_version" ||
word == "error_page" ||
word == "if" ||
word == "return" ||
word == "root" ||
word == "index" ||
word == "server" ||
word == "listen" ||
word == "server_name" ||
word == "map"
);
}
String highlight_cpp_fragment(String code)
{
String out;
for(u64 i = 0; i < code.length(); )
{
if(code_has_prefix(code, i, "//"))
{
u64 start = i;
while(i < code.length() && code[i] != '\n')
i += 1;
out += code_wrap("comment", substr(code, start, i - start));
continue;
}
if(code_has_prefix(code, i, "/*"))
{
u64 start = i;
i += 2;
while(i < code.length() && !code_has_prefix(code, i, "*/"))
i += 1;
if(code_has_prefix(code, i, "*/"))
i += 2;
out += code_wrap("comment", substr(code, start, i - start));
continue;
}
char ch = code[i];
if(ch == '"' || ch == '\'' || ch == '`')
{
char quote = ch;
u64 start = i;
i += 1;
while(i < code.length())
{
if(code[i] == '\\' && i + 1 < code.length())
{
i += 2;
continue;
}
if(code[i] == quote)
{
i += 1;
break;
}
i += 1;
}
out += code_wrap("string", substr(code, start, i - start));
continue;
}
if(code_is_space(ch))
{
out += substr(code, i, 1);
i += 1;
continue;
}
if(code_is_digit(ch))
{
u64 start = i;
i += 1;
while(i < code.length())
{
char next = code[i];
if(code_is_digit(next) || next == '.' || next == 'x' || next == 'X' || next == '_' || (next >= 'a' && next <= 'f') || (next >= 'A' && next <= 'F'))
i += 1;
else
break;
}
out += code_wrap("number", substr(code, start, i - start));
continue;
}
if(code_is_ident_start(ch))
{
u64 start = i;
i += 1;
while(i < code.length() && code_is_ident_char(code[i]))
i += 1;
String word = substr(code, start, i - start);
u64 j = i;
while(j < code.length() && (code[j] == ' ' || code[j] == '\t'))
j += 1;
if(cpp_keyword(word))
out += code_wrap("keyword", word);
else if(cpp_type(word))
out += code_wrap("type", word);
else if(cpp_context_field(word))
out += code_wrap("field", word);
else if(word == "context" || word == "props" || word == "payload" || word == "ws")
out += code_wrap("variable", word);
else if(cpp_builtin_call(word))
out += code_wrap("builtin", word);
else if(j < code.length() && code[j] == '(')
out += code_wrap("call", word);
else
out += code_wrap("ident", word);
continue;
}
if(code_is_punct(ch))
{
out += code_wrap("punct", substr(code, i, 1));
i += 1;
continue;
}
if(code_is_operator(ch))
{
out += code_wrap("operator", substr(code, i, 1));
i += 1;
continue;
}
out += html_escape(substr(code, i, 1));
i += 1;
}
return(out);
}
String highlight_markup_tag(String tag)
{
String out;
u64 i = 0;
if(code_has_prefix(tag, i, "</"))
{
out += code_wrap("delimiter", "</");
i += 2;
}
else
{
out += code_wrap("delimiter", "<");
i += 1;
}
u64 name_start = i;
while(i < tag.length() && markup_name_char(tag[i]))
i += 1;
if(i > name_start)
out += code_wrap("markup-tag", substr(tag, name_start, i - name_start));
while(i < tag.length())
{
if(code_is_space(tag[i]))
{
out += substr(tag, i, 1);
i += 1;
continue;
}
if(code_has_prefix(tag, i, "/>"))
{
out += code_wrap("delimiter", "/>" );
i += 2;
break;
}
if(code_has_prefix(tag, i, ">"))
{
out += code_wrap("delimiter", ">" );
i += 1;
break;
}
u64 attr_start = i;
while(i < tag.length() && markup_name_char(tag[i]))
i += 1;
if(i > attr_start)
out += code_wrap("markup-attr", substr(tag, attr_start, i - attr_start));
while(i < tag.length() && code_is_space(tag[i]))
{
out += substr(tag, i, 1);
i += 1;
}
if(i < tag.length() && tag[i] == '=')
{
out += code_wrap("operator", "=");
i += 1;
while(i < tag.length() && code_is_space(tag[i]))
{
out += substr(tag, i, 1);
i += 1;
}
if(i < tag.length() && (tag[i] == '"' || tag[i] == '\''))
{
char quote = tag[i];
u64 value_start = i;
i += 1;
while(i < tag.length() && tag[i] != quote)
i += 1;
if(i < tag.length())
i += 1;
out += code_wrap("string", substr(tag, value_start, i - value_start));
}
else
{
u64 value_start = i;
while(i < tag.length() && !code_is_space(tag[i]) && tag[i] != '>')
i += 1;
out += code_wrap("string", substr(tag, value_start, i - value_start));
}
}
}
return(out);
}
String highlight_uce_code(String code)
{
String out;
bool in_markup = false;
String markup_open = uce_markup_open_token();
String markup_close = uce_markup_close_token();
String code_open = uce_code_open_token();
String code_expr = uce_code_expr_token();
String code_raw = uce_code_raw_token();
String code_close = uce_code_close_token();
for(u64 i = 0; i < code.length(); )
{
if(!in_markup)
{
s64 next_markup = strpos(code, markup_open, i);
if(next_markup < 0)
{
out += highlight_cpp_fragment(substr(code, i));
break;
}
if((u64)next_markup > i)
out += highlight_cpp_fragment(substr(code, i, (u64)next_markup - i));
out += code_wrap("delimiter", markup_open);
i = (u64)next_markup + 2;
in_markup = true;
continue;
}
if(code_has_prefix(code, i, markup_close))
{
out += code_wrap("delimiter", markup_close);
i += 3;
in_markup = false;
continue;
}
if(code_has_prefix(code, i, code_expr) || code_has_prefix(code, i, code_raw) || code_has_prefix(code, i, code_open))
{
String opener = code_has_prefix(code, i, code_expr) ? code_expr : (code_has_prefix(code, i, code_raw) ? code_raw : code_open);
out += code_wrap("delimiter", opener);
i += opener.length();
u64 island_start = i;
while(i < code.length() && !code_has_prefix(code, i, code_close))
i += 1;
out += highlight_cpp_fragment(substr(code, island_start, i - island_start));
if(code_has_prefix(code, i, code_close))
{
out += code_wrap("delimiter", code_close);
i += 2;
}
continue;
}
if(code_has_prefix(code, i, "<"))
{
u64 tag_start = i;
char quote = 0;
i += 1;
while(i < code.length())
{
if(quote != 0)
{
if(code[i] == quote)
quote = 0;
i += 1;
continue;
}
if(code[i] == '"' || code[i] == '\'')
{
quote = code[i];
i += 1;
continue;
}
if(code[i] == '>')
{
i += 1;
break;
}
i += 1;
}
out += highlight_markup_tag(substr(code, tag_start, i - tag_start));
continue;
}
u64 text_start = i;
while(i < code.length() && code[i] != '<')
i += 1;
out += code_wrap("markup-text", substr(code, text_start, i - text_start));
}
return(out);
}
String highlight_nginx_code(String code)
{
String out;
for(u64 i = 0; i < code.length(); )
{
if(code[i] == '#')
{
u64 start = i;
while(i < code.length() && code[i] != '\n')
i += 1;
out += code_wrap("comment", substr(code, start, i - start));
continue;
}
if(code[i] == '"' || code[i] == '\'')
{
char quote = code[i];
u64 start = i;
i += 1;
while(i < code.length() && code[i] != quote)
i += 1;
if(i < code.length())
i += 1;
out += code_wrap("string", substr(code, start, i - start));
continue;
}
if(code_is_space(code[i]))
{
out += substr(code, i, 1);
i += 1;
continue;
}
if(code[i] == '$' || code[i] == '@')
{
u64 start = i;
i += 1;
while(i < code.length() && markup_name_char(code[i]))
i += 1;
out += code_wrap("variable", substr(code, start, i - start));
continue;
}
if(code_is_digit(code[i]))
{
u64 start = i;
i += 1;
while(i < code.length() && (code_is_digit(code[i]) || code[i] == '.'))
i += 1;
out += code_wrap("number", substr(code, start, i - start));
continue;
}
if(markup_name_char(code[i]))
{
u64 start = i;
i += 1;
while(i < code.length() && markup_name_char(code[i]))
i += 1;
String word = substr(code, start, i - start);
if(nginx_keyword(word))
out += code_wrap("keyword", word);
else
out += code_wrap("ident", word);
continue;
}
if(code_is_punct(code[i]))
{
out += code_wrap("punct", substr(code, i, 1));
i += 1;
continue;
}
if(code_is_operator(code[i]))
{
out += code_wrap("operator", substr(code, i, 1));
i += 1;
continue;
}
out += html_escape(substr(code, i, 1));
i += 1;
}
return(out);
}
String highlight_code(String code)
{
if(contains(code, "fastcgi_pass") || contains(code, "proxy_pass"))
return(highlight_nginx_code(code));
return(highlight_uce_code(code));
}
String highlight_code(String code, String language)
{
if(language == "nginx")
return(highlight_nginx_code(code));
if(language == "uce")
return(highlight_uce_code(code));
return(highlight_code(code));
}
String code_language_label(String language)
{
if(language == "nginx")
return("nginx");
return("uce");
}
COMPONENT(Request& props)
{
String eyebrow = props.props["eyebrow"].to_string();
String title = props.props["title"].to_string();
String summary = props.props["summary"].to_string();
String code = props.props["code"].to_string();
String note = props.props["note"].to_string();
String language = first(props.props["language"].to_string(), "uce");
<><article class="code-card">
<div class="eyebrow"><?= eyebrow ?></div>
<h3><?= title ?></h3>
<p><?= summary ?></p>
<div class="code-toolbar">
<span class="code-lang"><?= code_language_label(language) ?></span>
</div>
<pre class="syntax-block"><code class="syntax-code"><?: highlight_code(code, language) ?></code></pre>
<? if(note != "") { ?><div class="code-note"><?= note ?></div><? } ?>
</article></>
}

View File

@ -29,609 +29,16 @@ void render_link_card(String href, String title, String body, String meta)
</a></>
}
bool code_is_space(char ch)
void render_code_example(Request& context, String eyebrow, String title, String summary, String code, String note, String language = "uce")
{
return(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r');
}
bool code_is_digit(char ch)
{
return(ch >= '0' && ch <= '9');
}
bool code_is_alpha(char ch)
{
return((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'));
}
bool code_is_ident_start(char ch)
{
return(code_is_alpha(ch) || ch == '_');
}
bool code_is_ident_char(char ch)
{
return(code_is_ident_start(ch) || code_is_digit(ch));
}
bool markup_name_char(char ch)
{
return(code_is_ident_char(ch) || ch == '-' || ch == ':' || ch == '@');
}
bool code_is_punct(char ch)
{
return(ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == '(' || ch == ')' || ch == ',' || ch == ';');
}
bool code_is_operator(char ch)
{
return(ch == '=' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '%' || ch == '!' || ch == '<' || ch == '>' || ch == '&' || ch == '|' || ch == '.' || ch == ':' || ch == '?' || ch == '^');
}
bool code_has_prefix(String src, u64 i, String prefix)
{
if(i + prefix.length() > src.length())
return(false);
return(substr(src, i, prefix.length()) == prefix);
}
String code_wrap(String cls, String text)
{
if(text == "")
return("");
return("<span class=\"tok tok-" + cls + "\">" + html_escape(text) + "</span>");
}
String uce_markup_open_token()
{
return("<" ">");
}
String uce_markup_close_token()
{
return("</" ">");
}
String uce_code_open_token()
{
return("<" "?");
}
String uce_code_expr_token()
{
return("<" "?=");
}
String uce_code_raw_token()
{
return("<" "?:");
}
String uce_code_close_token()
{
return("?" ">");
}
bool cpp_keyword(String word)
{
String lower = to_lower(word);
return(
word == "RENDER" ||
word == "COMPONENT" ||
word == "WS" ||
lower == "if" ||
lower == "else" ||
lower == "for" ||
lower == "while" ||
lower == "return" ||
lower == "const" ||
lower == "auto" ||
lower == "void" ||
lower == "bool" ||
lower == "true" ||
lower == "false"
);
}
bool cpp_type(String word)
{
return(
word == "String" ||
word == "DTree" ||
word == "Request" ||
word == "StringList" ||
word == "StringMap" ||
word == "u64" ||
word == "u32" ||
word == "s64" ||
word == "f64"
);
}
bool cpp_builtin_call(String word)
{
return(
word == "component" ||
word == "component_render" ||
word == "component_exists" ||
word == "component_resolve" ||
word == "markdown_to_ast" ||
word == "markdown_to_html" ||
word == "json_decode" ||
word == "json_encode" ||
word == "time_format_utc" ||
word == "first" ||
word == "contains" ||
word == "ws_send" ||
word == "ws_send_to" ||
word == "ws_message" ||
word == "ws_connection_id"
);
}
bool cpp_context_field(String word)
{
return(
word == "get" ||
word == "post" ||
word == "session" ||
word == "call" ||
word == "connection" ||
word == "params" ||
word == "cookies" ||
word == "header" ||
word == "cfg"
);
}
bool nginx_keyword(String word)
{
return(
word == "location" ||
word == "include" ||
word == "fastcgi_param" ||
word == "fastcgi_pass" ||
word == "proxy_pass" ||
word == "proxy_set_header" ||
word == "proxy_http_version" ||
word == "error_page" ||
word == "if" ||
word == "return" ||
word == "root" ||
word == "index" ||
word == "server" ||
word == "listen" ||
word == "server_name" ||
word == "map"
);
}
String highlight_cpp_fragment(String code)
{
String out;
for(u64 i = 0; i < code.length(); )
{
if(code_has_prefix(code, i, "//"))
{
u64 start = i;
while(i < code.length() && code[i] != '\n')
i += 1;
out += code_wrap("comment", substr(code, start, i - start));
continue;
}
if(code_has_prefix(code, i, "/*"))
{
u64 start = i;
i += 2;
while(i < code.length() && !code_has_prefix(code, i, "*/"))
i += 1;
if(code_has_prefix(code, i, "*/"))
i += 2;
out += code_wrap("comment", substr(code, start, i - start));
continue;
}
char ch = code[i];
if(ch == '"' || ch == '\'' || ch == '`')
{
char quote = ch;
u64 start = i;
i += 1;
while(i < code.length())
{
if(code[i] == '\\' && i + 1 < code.length())
{
i += 2;
continue;
}
if(code[i] == quote)
{
i += 1;
break;
}
i += 1;
}
out += code_wrap("string", substr(code, start, i - start));
continue;
}
if(code_is_space(ch))
{
out += substr(code, i, 1);
i += 1;
continue;
}
if(code_is_digit(ch))
{
u64 start = i;
i += 1;
while(i < code.length())
{
char next = code[i];
if(code_is_digit(next) || next == '.' || next == 'x' || next == 'X' || next == '_' || (next >= 'a' && next <= 'f') || (next >= 'A' && next <= 'F'))
i += 1;
else
break;
}
out += code_wrap("number", substr(code, start, i - start));
continue;
}
if(code_is_ident_start(ch))
{
u64 start = i;
i += 1;
while(i < code.length() && code_is_ident_char(code[i]))
i += 1;
String word = substr(code, start, i - start);
u64 j = i;
while(j < code.length() && (code[j] == ' ' || code[j] == '\t'))
j += 1;
if(cpp_keyword(word))
out += code_wrap("keyword", word);
else if(cpp_type(word))
out += code_wrap("type", word);
else if(cpp_context_field(word))
out += code_wrap("field", word);
else if(word == "context" || word == "props" || word == "payload" || word == "ws")
out += code_wrap("variable", word);
else if(cpp_builtin_call(word))
out += code_wrap("builtin", word);
else if(j < code.length() && code[j] == '(')
out += code_wrap("call", word);
else
out += code_wrap("ident", word);
continue;
}
if(code_is_punct(ch))
{
out += code_wrap("punct", substr(code, i, 1));
i += 1;
continue;
}
if(code_is_operator(ch))
{
out += code_wrap("operator", substr(code, i, 1));
i += 1;
continue;
}
out += html_escape(substr(code, i, 1));
i += 1;
}
return(out);
}
String highlight_markup_tag(String tag)
{
String out;
u64 i = 0;
if(code_has_prefix(tag, i, "</"))
{
out += code_wrap("delimiter", "</");
i += 2;
}
else
{
out += code_wrap("delimiter", "<");
i += 1;
}
u64 name_start = i;
while(i < tag.length() && markup_name_char(tag[i]))
i += 1;
if(i > name_start)
out += code_wrap("markup-tag", substr(tag, name_start, i - name_start));
while(i < tag.length())
{
if(code_is_space(tag[i]))
{
out += substr(tag, i, 1);
i += 1;
continue;
}
if(code_has_prefix(tag, i, "/>"))
{
out += code_wrap("delimiter", "/>" );
i += 2;
break;
}
if(code_has_prefix(tag, i, ">"))
{
out += code_wrap("delimiter", ">" );
i += 1;
break;
}
u64 attr_start = i;
while(i < tag.length() && markup_name_char(tag[i]))
i += 1;
if(i > attr_start)
out += code_wrap("markup-attr", substr(tag, attr_start, i - attr_start));
while(i < tag.length() && code_is_space(tag[i]))
{
out += substr(tag, i, 1);
i += 1;
}
if(i < tag.length() && tag[i] == '=')
{
out += code_wrap("operator", "=");
i += 1;
while(i < tag.length() && code_is_space(tag[i]))
{
out += substr(tag, i, 1);
i += 1;
}
if(i < tag.length() && (tag[i] == '"' || tag[i] == '\''))
{
char quote = tag[i];
u64 value_start = i;
i += 1;
while(i < tag.length() && tag[i] != quote)
i += 1;
if(i < tag.length())
i += 1;
out += code_wrap("string", substr(tag, value_start, i - value_start));
}
else
{
u64 value_start = i;
while(i < tag.length() && !code_is_space(tag[i]) && tag[i] != '>')
i += 1;
out += code_wrap("string", substr(tag, value_start, i - value_start));
}
}
}
return(out);
}
String highlight_uce_code(String code)
{
String out;
bool in_markup = false;
String markup_open = uce_markup_open_token();
String markup_close = uce_markup_close_token();
String code_open = uce_code_open_token();
String code_expr = uce_code_expr_token();
String code_raw = uce_code_raw_token();
String code_close = uce_code_close_token();
for(u64 i = 0; i < code.length(); )
{
if(!in_markup)
{
s64 next_markup = strpos(code, markup_open, i);
if(next_markup < 0)
{
out += highlight_cpp_fragment(substr(code, i));
break;
}
if((u64)next_markup > i)
out += highlight_cpp_fragment(substr(code, i, (u64)next_markup - i));
out += code_wrap("delimiter", markup_open);
i = (u64)next_markup + 2;
in_markup = true;
continue;
}
if(code_has_prefix(code, i, markup_close))
{
out += code_wrap("delimiter", markup_close);
i += 3;
in_markup = false;
continue;
}
if(code_has_prefix(code, i, code_expr) || code_has_prefix(code, i, code_raw) || code_has_prefix(code, i, code_open))
{
String opener = code_has_prefix(code, i, code_expr) ? code_expr : (code_has_prefix(code, i, code_raw) ? code_raw : code_open);
out += code_wrap("delimiter", opener);
i += opener.length();
u64 island_start = i;
while(i < code.length() && !code_has_prefix(code, i, code_close))
i += 1;
out += highlight_cpp_fragment(substr(code, island_start, i - island_start));
if(code_has_prefix(code, i, code_close))
{
out += code_wrap("delimiter", code_close);
i += 2;
}
continue;
}
if(code_has_prefix(code, i, "<"))
{
u64 tag_start = i;
char quote = 0;
i += 1;
while(i < code.length())
{
if(quote != 0)
{
if(code[i] == quote)
quote = 0;
i += 1;
continue;
}
if(code[i] == '"' || code[i] == '\'')
{
quote = code[i];
i += 1;
continue;
}
if(code[i] == '>')
{
i += 1;
break;
}
i += 1;
}
out += highlight_markup_tag(substr(code, tag_start, i - tag_start));
continue;
}
u64 text_start = i;
while(i < code.length() && code[i] != '<')
i += 1;
out += code_wrap("markup-text", substr(code, text_start, i - text_start));
}
return(out);
}
String highlight_nginx_code(String code)
{
String out;
for(u64 i = 0; i < code.length(); )
{
if(code[i] == '#')
{
u64 start = i;
while(i < code.length() && code[i] != '\n')
i += 1;
out += code_wrap("comment", substr(code, start, i - start));
continue;
}
if(code[i] == '"' || code[i] == '\'')
{
char quote = code[i];
u64 start = i;
i += 1;
while(i < code.length() && code[i] != quote)
i += 1;
if(i < code.length())
i += 1;
out += code_wrap("string", substr(code, start, i - start));
continue;
}
if(code_is_space(code[i]))
{
out += substr(code, i, 1);
i += 1;
continue;
}
if(code[i] == '$' || code[i] == '@')
{
u64 start = i;
i += 1;
while(i < code.length() && markup_name_char(code[i]))
i += 1;
out += code_wrap("variable", substr(code, start, i - start));
continue;
}
if(code_is_digit(code[i]))
{
u64 start = i;
i += 1;
while(i < code.length() && (code_is_digit(code[i]) || code[i] == '.'))
i += 1;
out += code_wrap("number", substr(code, start, i - start));
continue;
}
if(markup_name_char(code[i]))
{
u64 start = i;
i += 1;
while(i < code.length() && markup_name_char(code[i]))
i += 1;
String word = substr(code, start, i - start);
if(nginx_keyword(word))
out += code_wrap("keyword", word);
else
out += code_wrap("ident", word);
continue;
}
if(code_is_punct(code[i]))
{
out += code_wrap("punct", substr(code, i, 1));
i += 1;
continue;
}
if(code_is_operator(code[i]))
{
out += code_wrap("operator", substr(code, i, 1));
i += 1;
continue;
}
out += html_escape(substr(code, i, 1));
i += 1;
}
return(out);
}
String highlight_code(String code)
{
if(contains(code, "fastcgi_pass") || contains(code, "proxy_pass"))
return(highlight_nginx_code(code));
return(highlight_uce_code(code));
}
String highlight_code(String code, String language)
{
if(language == "nginx")
return(highlight_nginx_code(code));
if(language == "uce")
return(highlight_uce_code(code));
return(highlight_code(code));
}
String code_language_label(String language)
{
if(language == "nginx")
return("nginx");
return("uce");
}
void render_code_example(String eyebrow, String title, String summary, String code, String note, String language = "uce")
{
<><article class="code-card">
<div class="eyebrow"><?= eyebrow ?></div>
<h3><?= title ?></h3>
<p><?= summary ?></p>
<div class="code-toolbar">
<span class="code-lang"><?= code_language_label(language) ?></span>
</div>
<pre class="syntax-block"><code class="syntax-code"><?: highlight_code(code, language) ?></code></pre>
<? if(note != "") { ?><div class="code-note"><?= note ?></div><? } ?>
</article></>
DTree props;
props["eyebrow"] = eyebrow;
props["title"] = title;
props["summary"] = summary;
props["code"] = code;
props["note"] = note;
props["language"] = language;
<><?: component("components/code_example", props, context) ?></>
}
RENDER(Request& context)
@ -670,8 +77,8 @@ RENDER(Request& context)
"{\n"
"\t<>\n"
"\t\t<article class=\"card\">\n"
"\t\t\t<h3><?= context.call[\"title\"] ?></h3>\n"
"\t\t\t<p><?= context.call[\"body\"] ?></p>\n"
"\t\t\t<h3><?= context.props[\"title\"] ?></h3>\n"
"\t\t\t<p><?= context.props[\"body\"] ?></p>\n"
"\t\t</article>\n"
"\t</>\n"
"}\n\n"
@ -708,8 +115,10 @@ RENDER(Request& context)
"}\n\n"
"WS(Request& context)\n"
"{\n"
"\tDTree payload = json_decode(ws_message());\n"
"\tDTree payload = json_decode(context.in);\n"
"\tString connection_id = context.params[\"WS_CONNECTION_ID\"];\n"
"\tcontext.connection[\"name\"] = payload[\"name\"];\n"
"\tpayload[\"connection_id\"] = connection_id;\n"
"\tws_send(json_encode(payload));\n"
"}\n";
@ -802,7 +211,7 @@ RENDER(Request& context)
</div>
<div class="feature-grid">
<? render_feature_card("templating", "Inline markup without framework tax", "UCE templates live directly inside handler code with escaped and raw output modes, so rendering stays local to the request logic that decides it."); ?>
<? render_feature_card("composition", "Components and sub-rendering", "Components are just .uce files. Props flow through context.call. Full pages can call other units without inventing a second application runtime."); ?>
<? render_feature_card("composition", "Components and sub-rendering", "Components are just .uce files. Props flow through context.props. Full pages can call other units without inventing a second application runtime."); ?>
<? render_feature_card("transport", "FastCGI for pages, HTTP for sockets", "Normal page renders stay on the FastCGI socket. Real WebSocket upgrades on .ws.uce endpoints are proxied to the built-in HTTP listener."); ?>
<? render_feature_card("operations", "nginx in front, systemd behind", "Static files stay static, nginx does the edge work, and the runtime handles compilation, request execution, and socket fan-out."); ?>
</div>
@ -839,12 +248,12 @@ RENDER(Request& context)
</p>
</div>
<div class="code-grid">
<? render_code_example("templating", "A page with request data", "Drop from C++ into markup, escape output by default, and keep request handling next to the HTML it controls.", snippet_minimal, "See the live demos for forms, headers, sessions, JSON, markdown, and more."); ?>
<? render_code_example("forms", "POST + session flash", "Old-school dynamic websites still matter. UCE keeps form handling and page rendering in one file when that is the simplest thing.", snippet_forms, "Request data lives on context.get, context.post, context.cookies, and context.session."); ?>
<? render_code_example("components", "Reusable UI without a second framework", "Components are ordinary .uce files. Pass props through context.call and decide when to render or return markup.", snippet_components, "Named handlers and component_render() are also available for direct output flows."); ?>
<? render_code_example("content", "Markdown when you want it", "UCE also ships a markdown module, so content-heavy pages do not need an external rendering stack.", snippet_markdown, "The runtime supports markdown_to_ast() and markdown_to_html() for content pipelines and component hooks."); ?>
<? render_code_example("realtime", "WebSockets with broker-owned state", "A .ws.uce page can render HTML and also accept live socket messages. Connection-local state lives on context.connection.", snippet_websocket, "The published demo includes a full chat example with reconnect logic and per-connection metadata."); ?>
<? render_code_example("deploy", "nginx wiring", "Use FastCGI for ordinary requests and only send actual websocket upgrades to the runtime's built-in HTTP listener.", snippet_nginx, "Match .ws.uce before the generic .uce location so normal page loads and upgrades split correctly.", "nginx"); ?>
<? render_code_example(context, "templating", "A page with request data", "Drop from C++ into markup, escape output by default, and keep request handling next to the HTML it controls.", snippet_minimal, "See the live demos for forms, headers, sessions, JSON, markdown, and more."); ?>
<? render_code_example(context, "forms", "POST + session flash", "Old-school dynamic websites still matter. UCE keeps form handling and page rendering in one file when that is the simplest thing.", snippet_forms, "Request data lives on context.get, context.post, context.cookies, and context.session."); ?>
<? render_code_example(context, "components", "Reusable UI without a second framework", "Components are ordinary .uce files. Pass props through context.props and decide when to render or return markup.", snippet_components, "Named handlers and component_render() are also available for direct output flows."); ?>
<? render_code_example(context, "content", "Markdown when you want it", "UCE also ships a markdown module, so content-heavy pages do not need an external rendering stack.", snippet_markdown, "The runtime supports markdown_to_ast() and markdown_to_html() for content pipelines and component hooks."); ?>
<? render_code_example(context, "realtime", "WebSockets with broker-owned state", "A .ws.uce page can render HTML and also accept live socket messages. Connection-local state lives on context.connection.", snippet_websocket, "The published demo includes a full chat example with reconnect logic and per-connection metadata."); ?>
<? render_code_example(context, "deploy", "nginx wiring", "Use FastCGI for ordinary requests and only send actual websocket upgrades to the runtime's built-in HTTP listener.", snippet_nginx, "Match .ws.uce before the generic .uce location so normal page loads and upgrades split correctly.", "nginx"); ?>
</div>
</section>

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,8 @@ COMPONENT(Request& context)
starter_boot(context);
context.header["Content-Type"] = "application/json";
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
if(context.call["json"].get_type_name() == "array")
print(json_encode(context.call["json"]));
else if(context.var["starter"]["json"].get_type_name() == "array")
print(json_encode(context.var["starter"]["json"]));
if(context.props["json"].get_type_name() == "array")
print(json_encode(context.props["json"]));
else
print(starter_page_main_html(context));
}

View File

@ -1,516 +0,0 @@
:root {
/* Dark theme color palette */
--bg-color: #0f172a;
--bg-secondary: #1e293b;
--bg-gradient: linear-gradient(135deg, #1e3a8a 0%, #3730a3 100%);
--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;
/* Dark theme specific variables */
--glass-bg: rgba(30, 41, 59, 0.8);
--glass-border: rgba(148, 163, 184, 0.1);
--overlay: rgba(0, 0, 0, 0.6);
--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;
}
* {
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-light);
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.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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,31 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
</body>
</html>
</>
}

View File

@ -1,540 +0,0 @@
: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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,28 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
</body>
</html>
</>
}

View File

@ -1,382 +0,0 @@
@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; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View File

@ -1,54 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String current_path = context.var["starter"]["route"]["l_path"].to_string();
DTree global_props;
global_props["cookie_consent"].set_bool(false);
DTree account_props;
account_props["wrapper_class"] = "admin-account-card";
account_props["links_wrapper_class"] = "admin-account-links";
account_props["name_class"] = "admin-account-name";
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="admin-page localfirst-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
<?: component("../../components/theme/global_controls", global_props, context) ?>
<div class="admin-shell">
<nav class="admin-nav">
<div class="admin-nav-header">
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
</a>
</div>
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
bool active = menu_key != "" && (current_path == menu_key || current_path.rfind(menu_key + "/", 0) == 0);
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
}); ?>
</nav>
<div class="admin-main">
<? if(!embed_mode) { ?>
<div class="admin-toolbar">
<?: component("../../components/theme/account_links", account_props, context) ?>
</div>
<? } ?>
<div id="content" class="admin-content">
<?: main_html ?>
</div>
</div>
</div>
</body>
</html>
</>
}

View File

@ -1,313 +0,0 @@
: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; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,31 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="portal-theme portal-dark-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
</body>
</html>
</>
}

View File

@ -1,392 +0,0 @@
@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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,29 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
footer_props["inner_class"] = "footer-inner";
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="portal-theme portal-light-theme<?= embed_mode ? " embed-mode" : "" ?>">
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
</body>
</html>
</>
}

View File

@ -1,523 +0,0 @@
/*
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; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 B

View File

@ -1,32 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<div class="retro-stars"></div>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
</body>
</html>
</>
}

View File

@ -1,29 +0,0 @@
# 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 theme templates and 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()`.
Theme rendering is split the same way as the PHP starter:
- `themes/common/page.blank.uce`
- `themes/common/page.json.uce`
- `themes/<theme>/page.html.uce`
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes/<theme>/page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
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.

View File

@ -1,100 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(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";
}
<>
<form class="card">
<div>
<h2><?= title ?></h2>
<label><?= subtitle ?></label>
</div>
<? services.each([&](DTree service, String service_key) {
String client_id = context.call[service_key + "_client_id"].to_string();
String handler_name = ascii_safe_name(service_key);
String service_name = service["name"].to_string();
String scope = service["scope"].to_string();
String auth_url = service["auth_url"].to_string();
String store_url = starter_link("auth/store-oauth-session", context);
?><div data-service="<?= service_key ?>">
<button type="button" class="btn" onclick='initiateOAuth_<?= handler_name ?>()'>
<i class="<?= service["icon"].to_string() ?>" style="color: <?= service["color"].to_string() ?>"></i>
Continue with <?= service_name ?>
</button>
<div class="loading" style="display: none;"><span>Connecting...</span></div>
<label>Secure authentication via <?= service_name ?></label>
<script>
window.initiateOAuth_<?= handler_name ?> = function () {
const wrapper = document.querySelector('[data-service="<?= service_key ?>"]');
if (!wrapper) return;
const button = wrapper.querySelector('.btn');
const loading = wrapper.querySelector('.loading');
if (button) button.style.display = 'none';
if (loading) loading.style.display = 'block';
const clientId = <?: json_encode(client_id) ?>;
if (!clientId || clientId.indexOf('YOUR_') === 0) {
alert('<?= service_name ?> OAuth is not configured yet.');
if (button) button.style.display = '';
if (loading) loading.style.display = 'none';
return;
}
const state = btoa(Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).replace(/[^a-zA-Z0-9]/g, '');
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: <?: json_encode(callback_url) ?>,
scope: <?: json_encode(scope) ?>,
response_type: 'code',
state: state
});
sessionStorage.setItem('oauth_state', state);
sessionStorage.setItem('oauth_service', <?: json_encode(service_key) ?>);
fetch(<?: json_encode(store_url) ?>, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({oauth_service: <?: json_encode(service_key) ?>, oauth_state: state})
}).catch(console.error).finally(() => {
window.location.href = <?: json_encode(auth_url) ?> + '?' + params.toString();
});
};
</script>
</div><?
}); ?>
<div class="banner" id="oauth-status" style="display: none;"></div>
</form>
</>
}

View File

@ -1,114 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
<>
<div id="cookie-consent" style="
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--surface);
border-top: 1px solid var(--border);
box-shadow: var(--shadow-xl);
z-index: 9998;
padding: 1.5rem;
transform: translateY(100%);
transition: transform 0.3s ease;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
" aria-live="polite">
<div style="
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
">
<div style="flex: 1; min-width: 300px;">
<div style="
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke='var(--primary)' stroke-width="2" style="flex-shrink: 0;">
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2M21 9V7L15 1L13 3L15 5H3V21A2 2 0 0 0 5 23H19A2 2 0 0 0 21 21V11L19 13V20H5V7H21M9 11V13H7V11H9M13 11V13H11V11H13M17 11V13H15V11H17M9 15V17H7V15H9M13 15V17H11V15H13M17 15V17H15V15H17Z"/>
</svg>
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: var(--text-primary);">Cookie Notice</h3>
</div>
<p style="margin: 0; color: var(--text-secondary); font-size: 0.9rem; line-height: 1.5;">
We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic.
<a href="#" id="cookie-policy-link" style="color: var(--primary); text-decoration: none;">Learn more</a>
</p>
</div>
<div style="display: flex; gap: 0.75rem; flex-shrink: 0; align-items: center;">
<button id="cookie-reject" style="padding: 0.75rem 1.5rem; border: 1px solid var(--border); background: transparent; color: var(--text-secondary); border-radius: 0.5rem; cursor: pointer;">Reject All</button>
<button id="cookie-accept" style="padding: 0.75rem 1.5rem; border: 1px solid var(--primary); background: var(--primary); color: white; border-radius: 0.5rem; cursor: pointer;">Accept All</button>
</div>
</div>
</div>
<script>
(function() {
let cookieConsent = document.getElementById('cookie-consent');
let acceptButton = document.getElementById('cookie-accept');
let rejectButton = document.getElementById('cookie-reject');
let policyLink = document.getElementById('cookie-policy-link');
if (!cookieConsent) return;
let COOKIE_NAME = 'cookie_consent';
let COOKIE_EXPIRY_DAYS = 365;
function setCookie(name, value, days) {
let expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = name + '=' + value + ';expires=' + expires.toUTCString() + ';path=/;SameSite=Lax';
}
function getCookie(name) {
let nameEQ = name + '=';
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function hideBanner() {
cookieConsent.style.transform = 'translateY(100%)';
setTimeout(() => {
cookieConsent.style.display = 'none';
}, 300);
}
let existingConsent = getCookie(COOKIE_NAME);
if (!existingConsent) {
setTimeout(() => {
cookieConsent.style.transform = 'translateY(0)';
}, 1);
}
acceptButton.addEventListener('click', function() {
setCookie(COOKIE_NAME, 'accepted', COOKIE_EXPIRY_DAYS);
hideBanner();
});
rejectButton.addEventListener('click', function() {
setCookie(COOKIE_NAME, 'rejected', COOKIE_EXPIRY_DAYS);
hideBanner();
});
policyLink.addEventListener('click', function(e) {
e.preventDefault();
window.open('https://example.com/privacy-policy', '_blank');
});
}());
</script>
</>
}

View File

@ -1,308 +0,0 @@
#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);
}
COMPONENT:SUMMARY_METRICS(Request& context)
{
String title = context.call["title"].to_string();
String subtitle = context.call["subtitle"].to_string();
<>
<div class="dashboard-panel">
<? if(title != "" || subtitle != "") { ?>
<div class="dashboard-panel-header">
<? if(title != "") { ?><h2><?= title ?></h2><? } ?>
<? if(subtitle != "") { ?><p><?= subtitle ?></p><? } ?>
</div>
<? } ?>
<div class="dashboard-stat-grid">
<? context.call["items"].each([&](DTree item, String key) {
String tone = first(item["tone"].to_string(), "info");
String href = item["href"].to_string();
String tag = href != "" ? "a" : "div";
?><<?= tag ?> class="dashboard-stat-card tone-<?= tone ?>"<?= href != "" ? " href=\"" + html_escape(href) + "\"" : "" ?>>
<div class="dashboard-stat-label"><?= first(item["label"].to_string(), "Metric") ?></div>
<div class="dashboard-stat-value"><?= first(item["value"].to_string(), "--") ?></div>
<? if(item["meta"].to_string() != "") { ?><div class="dashboard-stat-meta"><?= item["meta"].to_string() ?></div><? } ?>
</<?= tag ?>><?
}); ?>
</div>
</div>
</>
}
COMPONENT: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");
<>
<div class="dashboard-panel">
<? if(title != "" || subtitle != "") { ?>
<div class="dashboard-panel-header">
<? if(title != "") { ?><h2><?= title ?></h2><? } ?>
<? if(subtitle != "") { ?><p><?= subtitle ?></p><? } ?>
</div>
<? } ?>
<canvas id="<?= canvas_id ?>" class="dashboard-chart-canvas" style="height: <?= std::to_string(height) ?>px"></canvas>
</div>
<script>
(function () {
if (typeof UTimeSeriesChart === 'undefined') return;
var chart = new UTimeSeriesChart(<?: json_encode(canvas_id) ?>, {
xAxisLabel: <?: json_encode(x_axis_label) ?>,
yAxisLeftLabel: <?: json_encode(y_axis_left_label) ?>,
yAxisRightLabel: <?: json_encode(y_axis_right_label) ?>,
yAxisLeftFormat: <?: json_encode(y_axis_left_format) ?>,
yAxisRightFormat: <?: json_encode(y_axis_right_format) ?>
});
chart.setData(<?: json_encode(context.call["series"]) ?>, <?: json_encode(context.call["x_labels"]) ?>);
window[<?: json_encode("chart_" + chart_id) ?>] = chart;
}());
</script>
</>
}
COMPONENT: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");
}
<>
<div class="dashboard-panel">
<? if(title != "" || subtitle != "") { ?>
<div class="dashboard-panel-header">
<? if(title != "") { ?><h2><?= title ?></h2><? } ?>
<? if(subtitle != "") { ?><p><?= subtitle ?></p><? } ?>
</div>
<? } ?>
<div class="dashboard-table-wrap">
<table id="<?= table_id ?>" class="u-sortable-table">
<thead>
<tr>
<? context.call["columns"].each([&](DTree column, String key) {
String align = first(column["align"].to_string(), "left");
bool sortable = column["sortable"].to_string() == "" || column["sortable"].to_string() == "1";
?><th scope="col" class="align-<?= align ?>"<?= sortable ? "" : " data-sortable=\"false\"" ?>><?= first(column["label"].to_string(), column["key"].to_string(), "Column") ?></th><?
}); ?>
</tr>
</thead>
<tbody>
<? if(context.call["rows"].get_type_name() != "array" || context.call["rows"]["0"].get_type_name() != "array") { ?>
<tr data-empty-row="1"><td colspan="<?= context.call["columns"]._map.size() == 0 ? "1" : std::to_string(context.call["columns"]._map.size()) ?>" class="muted"><?= empty_label ?></td></tr>
<? } ?>
<? context.call["rows"].each([&](DTree row, String key) {
if(row.get_type_name() != "array")
return;
?><tr><?
context.call["columns"].each([&](DTree column, String ckey) {
String col_key = column["key"].to_string();
DTree formatted = data_format_value(row[col_key], row, column);
?><td class="align-<?= first(column["align"].to_string(), "left") ?>" data-sort-value="<?= formatted["sort"].to_string() ?>"><?= formatted["display"].to_string() ?></td><?
});
?></tr><?
}); ?>
</tbody>
</table>
</div>
</div>
<script>
(function () {
if (typeof USortableTable === 'undefined') return;
USortableTable.init(<?: json_encode(table_id) ?>, <?: json_encode(init_options) ?>);
}());
</script>
</>
}
COMPONENT: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.set_array();
bool has_explicit_columns = columns.is_list() &&
columns._map.size() > 0 &&
columns._map.find("0") != columns._map.end() &&
columns["0"]["field"].to_string() != "";
if(!has_explicit_columns && 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);
});
}
if(!columns.is_list())
{
DTree normalized_columns;
normalized_columns.set_array();
columns.each([&](DTree column, String key) {
if(column.get_type_name() == "array")
normalized_columns.push(column);
});
columns = normalized_columns;
}
DTree row_data = context.call["items"];
if(row_data.get_type_name() != "array")
row_data.set_array();
if(!row_data.is_list())
{
DTree normalized_rows;
normalized_rows.set_array();
row_data.each([&](DTree row, String key) {
if(row.get_type_name() == "array")
normalized_rows.push(row);
});
row_data = normalized_rows;
}
DTree grid_options;
grid_options["pagination"].set_bool(true);
grid_options["paginationPageSize"] = first(context.call["options"]["paginationPageSize"].to_string(), "20");
grid_options["paginationPageSizeSelector"].set_bool(false);
grid_options["suppressMenuHide"].set_bool(true);
grid_options["animateRows"].set_bool(true);
grid_options["columnDefs"] = columns;
grid_options["rowData"] = row_data;
<>
<div class="ag-grid-container" style="margin: 1rem 0;">
<div class="ag-grid-toolbar" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding:0.75rem;background:var(--surface-elevated);border:1px solid var(--border);border-radius:0.5rem 0.5rem 0 0;border-bottom:none;">
<div style="display:flex;gap:1rem;align-items:center;flex:1;">
<span style="color: var(--text-secondary); font-size: 0.875rem; font-weight: 500;"><?= std::to_string(context.call["items"]._map.size()) ?> rows</span>
<input type="text" id="<?= table_id ?>-search" placeholder="Search all columns..." style="padding:0.5rem 0.75rem;border:1px solid var(--border);border-radius:0.375rem;background:var(--surface);color:var(--text-primary);font-size:0.875rem;width:250px;" />
</div>
<div style="display:flex;gap:0.5rem;">
<button id="<?= table_id ?>-export-csv" class="btn btn-outline">Export CSV</button>
<button id="<?= table_id ?>-clear-filters" class="btn btn-outline">Clear Filters</button>
</div>
</div>
<div id="<?= table_id ?>" class="ag-theme-alpine-dark" style="height: <?= first(context.call["height"].to_string(), "400px") ?>; width: 100%; border: 1px solid var(--border); border-radius: 0 0 0.5rem 0.5rem; overflow: hidden;"></div>
</div>
<script>
(function () {
if (typeof agGrid === 'undefined') return;
const gridOptions = <?: json_encode(grid_options) ?>;
const gridElement = document.getElementById(<?: json_encode(table_id) ?>);
if (!gridElement) return;
const gridApi = agGrid.createGrid(gridElement, gridOptions);
document.getElementById(<?: json_encode(table_id + "-search") ?>)?.addEventListener('input', function () {
gridApi.setQuickFilter(this.value);
});
document.getElementById(<?: json_encode(table_id + "-clear-filters") ?>)?.addEventListener('click', function () {
gridApi.setFilterModel(null);
gridApi.setQuickFilter('');
});
document.getElementById(<?: json_encode(table_id + "-export-csv") ?>)?.addEventListener('click', function () {
gridApi.exportDataAsCsv();
});
}());
</script>
</>
}

View File

@ -1,604 +0,0 @@
#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);
}
COMPONENT: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(), "#");
<>
<div class="hero-section">
<div class="hero-content">
<h1 class="hero-title"><?= title ?></h1>
<p class="hero-subtitle"><?= subtitle ?></p>
<div class="hero-actions">
<a href="<?= cta_link ?>" class="btn btn-large hero-cta"><?= cta_text ?></a>
<a href="#features" class="btn btn-outline btn-large">Learn More</a>
</div>
</div>
<div class="hero-visual">
<div class="floating-card">
<div class="card-header"></div>
<div class="card-content">
<div class="line"></div>
<div class="line short"></div>
<div class="line"></div>
</div>
</div>
<div class="floating-elements">
<div class="element element-1"></div>
<div class="element element-2"></div>
<div class="element element-3"></div>
</div>
</div>
</div>
<style>
.hero-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
min-height: 70vh;
padding: 4rem 2rem;
background: var(--bg-gradient);
color: var(--text-primary);
border-radius: var(--radius-xl);
margin-bottom: 4rem;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
inset: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox=\"0 0 100 100\"><defs><pattern id=\"grain\" width=\"100\" height=\"100\" patternUnits=\"userSpaceOnUse\"><circle cx=\"25\" cy=\"25\" r=\"1\" fill=\"white\" opacity=\"0.1\"/><circle cx=\"75\" cy=\"75\" r=\"1\" fill=\"white\" opacity=\"0.1\"/><circle cx=\"50\" cy=\"10\" r=\"0.5\" fill=\"white\" opacity=\"0.1\"/><circle cx=\"10\" cy=\"90\" r=\"0.5\" fill=\"white\" opacity=\"0.1\"/></pattern></defs><rect width=\"100%\" height=\"100%\" fill=\"url(%23grain)\"/></svg>');
}
.hero-content { position: relative; z-index: 2; }
.hero-title {
font-size: 3.5rem;
font-weight: 800;
margin-bottom: 1.5rem;
line-height: 1.1;
background: none;
color: var(--text-primary);
}
.hero-subtitle {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.9;
line-height: 1.6;
}
.hero-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.hero-cta {
background: var(--surface);
color: var(--primary);
border: none;
}
.hero-visual {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.floating-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 2rem;
width: 280px;
animation: float 6s ease-in-out infinite;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.card-header {
height: 60px;
background: linear-gradient(90deg, rgba(255,255,255,0.3), rgba(255,255,255,0.1));
border-radius: 8px;
margin-bottom: 1rem;
}
.card-content .line {
height: 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 6px;
margin-bottom: 8px;
}
.card-content .line.short { width: 60%; }
.floating-elements { position: absolute; width: 100%; height: 100%; }
.element {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
}
.element-1 { width: 60px; height: 60px; top: 20%; left: 10%; animation: float 4s ease-in-out infinite; }
.element-2 { width: 40px; height: 40px; top: 60%; right: 15%; animation: float 5s ease-in-out infinite reverse; }
.element-3 { width: 80px; height: 80px; bottom: 20%; left: 20%; animation: float 7s ease-in-out infinite; }
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(10deg); }
}
@media (max-width: 768px) {
.hero-section { grid-template-columns: 1fr; text-align: center; padding: 3rem 1rem; gap: 2rem; }
.hero-title { font-size: 2.5rem; }
.hero-subtitle { font-size: 1.1rem; }
.hero-visual { height: 300px; }
.floating-card { width: 240px; padding: 1.5rem; }
}
</style>
<script>
$.ready(() => {
const heroTitle = document.querySelector('.hero-title');
if (!heroTitle || heroTitle.dataset.typed === '1') return;
heroTitle.dataset.typed = '1';
const text = heroTitle.textContent;
heroTitle.textContent = '';
let i = 0;
const typeWriter = () => {
heroTitle.textContent += text.charAt(i);
i += 1;
if (i < text.length) setTimeout(typeWriter, 24);
};
typeWriter();
});
</script>
</>
}
COMPONENT:FEATURES_GRID(Request& context)
{
DTree features = context.call["features"];
marketing_default_features(features);
<>
<div class="features-section" id="features">
<div class="features-header">
<h2>Why Choose Our Framework?</h2>
<p>Discover the powerful features that make development a breeze</p>
</div>
<div class="features-grid">
<? features.each([&](DTree feature, String key) {
?><div class="feature-card">
<div class="feature-icon"><?: feature["icon"].to_string() ?></div>
<h3 class="feature-title"><?= feature["title"].to_string() ?></h3>
<p class="feature-description"><?= feature["description"].to_string() ?></p>
<div class="feature-overlay"></div>
</div><?
}); ?>
</div>
</div>
<style>
.features-section { padding: 4rem 0; background: var(--bg-color); }
.features-header { text-align: center; margin-bottom: 4rem; }
.features-header 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;
}
.features-header p { font-size: 1.25rem; color: var(--text-secondary); max-width: 600px; margin: 0 auto; }
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.feature-card {
background: var(--surface);
padding: 2.5rem 2rem;
border-radius: var(--radius-xl);
text-align: center;
position: relative;
overflow: hidden;
border: 1px solid var(--border);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.feature-card:hover { transform: translateY(-8px) scale(1.02); box-shadow: var(--shadow-xl); border-color: var(--primary); }
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.feature-card:hover::before { transform: scaleX(1); }
.feature-icon {
font-size: 3rem;
margin-bottom: 1.5rem;
display: inline-block;
padding: 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
border-radius: 50%;
color: white;
box-shadow: var(--shadow-lg);
}
.feature-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: var(--text-primary); }
.feature-description { color: var(--text-secondary); line-height: 1.6; }
.feature-overlay {
position: absolute;
inset: 0;
background: linear-gradient(135deg, var(--primary), var(--secondary));
opacity: 0;
transition: opacity 0.3s ease;
}
.feature-card:hover .feature-overlay { opacity: 0.05; }
@media (max-width: 768px) {
.features-grid { grid-template-columns: 1fr; gap: 1.5rem; padding: 0 0.5rem; }
.features-header h2 { font-size: 2rem; }
.feature-card { padding: 2rem 1.5rem; }
}
</style>
</>
}
COMPONENT: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);
}
<>
<div class="stats-section">
<div class="stats-container">
<div class="stats-header">
<h2>Trusted by Developers Worldwide</h2>
<p>Join thousands of developers who have chosen our framework</p>
</div>
<div class="stats-grid">
<? stats.each([&](DTree stat, String key) {
?><div class="stat-item">
<div class="stat-number"><?= stat["number"].to_string() ?></div>
<div class="stat-label"><?= stat["label"].to_string() ?></div>
<div class="stat-bar"><div class="stat-fill"></div></div>
</div><?
}); ?>
</div>
</div>
</div>
<style>
.stats-section {
background: linear-gradient(135deg, var(--surface) 0%, var(--surface-elevated) 100%);
color: var(--text-primary);
padding: 4rem 0;
position: relative;
overflow: hidden;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.stats-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; position: relative; z-index: 2; }
.stats-header { text-align: center; margin-bottom: 4rem; }
.stats-header h2 { font-size: 2.5rem; margin-bottom: 1rem; font-weight: 700; }
.stats-header p { font-size: 1.25rem; opacity: 0.8; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 3rem; }
.stat-item {
text-align: center;
padding: 2rem;
background: var(--glass-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
}
.stat-number {
font-size: 3rem;
font-weight: 800;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label { font-size: 1.1rem; color: var(--text-secondary); margin-bottom: 1rem; font-weight: 500; }
.stat-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.stat-fill { height: 100%; background: linear-gradient(90deg, var(--primary), var(--accent)); width: 100%; }
@media (max-width: 768px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
.stats-header h2 { font-size: 2rem; }
}
@media (max-width: 480px) {
.stats-grid { grid-template-columns: 1fr; }
}
</style>
</>
}
COMPONENT: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");
<>
<div class="brands-section">
<div class="brands-container">
<h3 class="brands-title"><?= title ?></h3>
<div class="brands-grid">
<? logos.each([&](DTree logo, String key) {
?><div class="brand-item">
<img src="<?= logo["url"].to_string() ?>" alt="<?= logo["name"].to_string() ?>" />
</div><?
}); ?>
</div>
</div>
</div>
<style>
.brands-section { padding: 3rem 0; background: var(--surface); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.brands-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; text-align: center; }
.brands-title { font-size: 1.125rem; color: var(--text-secondary); margin-bottom: 2rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
.brands-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 2rem; align-items: center; }
.brand-item { display: flex; align-items: center; justify-content: center; padding: 1rem; border-radius: var(--radius); transition: all 0.3s ease; }
.brand-item:hover { transform: scale(1.05); background: var(--surface-hover); }
.brand-item img { max-width: 100%; height: auto; opacity: 0.6; transition: opacity 0.3s ease; filter: grayscale(100%); }
.brand-item:hover img { opacity: 1; filter: grayscale(0%); }
@media (max-width: 768px) {
.brands-grid { grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.brands-section { padding: 2rem 0; }
}
</style>
</>
}
COMPONENT: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);
}
<>
<div class="testimonials-section">
<div class="testimonials-container">
<div class="testimonials-header">
<h2>What Developers Say</h2>
<p>Don't just take our word for it - hear from the community</p>
</div>
<div class="testimonials-grid">
<? testimonials.each([&](DTree testimonial, String key) {
?><div class="testimonial-card">
<div class="testimonial-content">
<div class="quote-icon">"</div>
<p><?= testimonial["content"].to_string() ?></p>
<div class="stars"><? for(s32 i = 0; i < int_val(first(testimonial["rating"].to_string(), "5")); i++) { ?><span class="star">★</span><? } ?></div>
</div>
<div class="testimonial-author">
<img src="<?= testimonial["avatar"].to_string() ?>" alt="<?= testimonial["name"].to_string() ?>" class="avatar">
<div class="author-info">
<div class="author-name"><?= testimonial["name"].to_string() ?></div>
<div class="author-role"><?= testimonial["role"].to_string() ?></div>
</div>
</div>
</div><?
}); ?>
</div>
</div>
</div>
<style>
.testimonials-section { padding: 4rem 0; background: var(--bg-color); }
.testimonials-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
.testimonials-header { text-align: center; margin-bottom: 4rem; }
.testimonials-header 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; }
.testimonials-header p { font-size: 1.25rem; color: var(--text-secondary); }
.testimonials-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 2rem; }
.testimonial-card { background: var(--surface); border-radius: var(--radius-xl); padding: 2.5rem 2rem; border: 1px solid var(--border); box-shadow: var(--shadow-md); }
.quote-icon { font-size: 4rem; color: var(--primary); opacity: 0.3; line-height: 1; margin-bottom: 1rem; font-family: serif; }
.testimonial-content p { font-size: 1.1rem; line-height: 1.7; color: var(--text-primary); margin-bottom: 1.5rem; }
.stars { display: flex; gap: 0.25rem; }
.star { color: #fbbf24; font-size: 1.25rem; }
.testimonial-author { display: flex; align-items: center; gap: 1rem; }
.avatar { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--border); }
.author-name { font-weight: 600; color: var(--text-primary); margin-bottom: 0.25rem; }
.author-role { color: var(--text-secondary); font-size: 0.9rem; }
@media (max-width: 768px) {
.testimonials-grid { grid-template-columns: 1fr; gap: 1.5rem; }
.testimonial-card { padding: 2rem 1.5rem; }
.testimonials-header h2 { font-size: 2rem; }
}
</style>
</>
}
COMPONENT: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);
}
<>
<div class="pricing-section">
<div class="pricing-container">
<div class="pricing-header">
<h2>Simple Pricing</h2>
<p>Choose the plan that fits your product stage.</p>
</div>
<div class="pricing-grid">
<? plans.each([&](DTree plan, String key) {
bool popular = plan["popular"].to_string() != "";
?><div class="pricing-card<?= popular ? " popular" : "" ?>">
<? if(popular) { ?><div class="popular-badge">Most Popular</div><? } ?>
<div class="plan-header">
<div class="plan-name"><?= plan["name"].to_string() ?></div>
<div class="plan-price">
<span class="price"><?= plan["price"].to_string() ?></span>
<span class="period"><?= plan["period"].to_string() ?></span>
</div>
<p class="plan-description"><?= plan["description"].to_string() ?></p>
</div>
<ul class="plan-features">
<? plan["features"].each([&](DTree feature, String feature_key) { ?><li><?= feature.to_string() ?></li><? }); ?>
</ul>
<div class="plan-footer">
<button class="btn <?= popular ? "btn-primary" : "btn-outline" ?> btn-large plan-cta"><?= first(plan["cta"].to_string(), "Choose Plan") ?></button>
</div>
</div><?
}); ?>
</div>
<div class="pricing-footer">
<p>All plans include a 30-day money-back guarantee. No setup fees.</p>
</div>
</div>
</div>
<style>
.pricing-section { padding: 5rem 0; background: var(--bg-color); }
.pricing-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
.pricing-header { text-align: center; margin-bottom: 4rem; }
.pricing-header 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; }
.pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-bottom: 3rem; }
.pricing-card { background: var(--surface); border: 2px solid var(--border); border-radius: var(--radius-xl); padding: 2.5rem 2rem; position: relative; }
.pricing-card.popular { border-color: var(--primary); box-shadow: var(--shadow-lg); transform: scale(1.05); }
.popular-badge { position: absolute; top: -1rem; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; padding: 0.5rem 1.5rem; border-radius: 50px; font-size: 0.875rem; font-weight: 600; }
.plan-header { text-align: center; margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid var(--border); }
.plan-name { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: var(--text-primary); }
.price { font-size: 3rem; font-weight: 800; color: var(--primary); }
.period { font-size: 1.125rem; color: var(--text-secondary); margin-left: 0.25rem; }
.plan-description { color: var(--text-secondary); line-height: 1.6; }
.plan-features { list-style: none; padding: 0; margin: 0 0 2rem; display: grid; gap: 0.75rem; }
.plan-features li { color: var(--text-primary); }
.plan-footer { text-align: center; }
.pricing-footer { text-align: center; color: var(--text-secondary); }
@media (max-width: 768px) {
.pricing-grid { grid-template-columns: 1fr; }
.pricing-card.popular { transform: none; }
}
</style>
</>
}
COMPONENT: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(), "#");
<>
<div class="cta-section">
<div class="cta-container">
<div class="cta-content">
<h2 class="cta-title"><?= title ?></h2>
<p class="cta-subtitle"><?= subtitle ?></p>
<div class="cta-actions">
<a href="<?= cta_link ?>" class="btn btn-large cta-primary"><?= cta_text ?></a>
<a href="<?= secondary_link ?>" class="btn btn-outline btn-large cta-secondary"><?= secondary_text ?></a>
</div>
</div>
<div class="cta-visual">
<div class="floating-shapes">
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<div class="shape shape-3"></div>
<div class="shape shape-4"></div>
</div>
</div>
</div>
</div>
<style>
.cta-section { background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); color: white; padding: 5rem 0; position: relative; overflow: hidden; }
.cta-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; display: grid; grid-template-columns: 2fr 1fr; gap: 4rem; align-items: center; position: relative; z-index: 2; }
.cta-title { font-size: 3rem; font-weight: 800; margin-bottom: 1.5rem; line-height: 1.1; }
.cta-subtitle { font-size: 1.25rem; margin-bottom: 2.5rem; opacity: 0.9; line-height: 1.6; }
.cta-actions { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.cta-primary { background: white; color: var(--primary); border: 2px solid white; }
.cta-secondary { border-color: rgba(255, 255, 255, 0.5); color: white; }
.cta-visual { position: relative; height: 300px; display: flex; align-items: center; justify-content: center; }
.floating-shapes { position: relative; width: 200px; height: 200px; }
.shape { position: absolute; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(5px); border: 1px solid rgba(255, 255, 255, 0.2); }
.shape-1 { width: 80px; height: 80px; border-radius: 20px; top: 0; left: 0; }
.shape-2 { width: 60px; height: 60px; border-radius: 50%; top: 20px; right: 0; }
.shape-3 { width: 100px; height: 40px; border-radius: 20px; bottom: 40px; left: 20px; }
.shape-4 { width: 50px; height: 50px; border-radius: 10px; bottom: 0; right: 30px; transform: rotate(45deg); }
@media (max-width: 968px) {
.cta-container { grid-template-columns: 1fr; text-align: center; gap: 2rem; }
.cta-title { font-size: 2.5rem; }
}
@media (max-width: 768px) {
.cta-section { padding: 3rem 0; }
.cta-title { font-size: 2rem; }
.cta-subtitle { font-size: 1.1rem; }
.cta-actions { justify-content: center; }
}
</style>
</>
}

View File

@ -1,153 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
String current_theme = context.cfg.get_by_path("theme/key").to_string();
String current_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme);
String route_path = context.var["starter"]["route"]["l_path"].to_string();
<>
<div id="theme-switcher" style="position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 9999; font-family: inherit;">
<style>
#theme-switcher .theme-launcher {
min-width: 56px;
height: 56px;
padding: 0 1rem;
border-radius: 999px;
background: var(--surface, #fff);
border: 1px solid var(--border, rgba(0,0,0,0.15));
box-shadow: var(--shadow-lg, 0 14px 32px rgba(0,0,0,0.18));
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.65rem;
color: var(--text-primary, #111827);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
font: inherit;
font-size: 0.9rem;
font-weight: 700;
}
#theme-switcher .theme-launcher:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-xl, 0 22px 42px rgba(0,0,0,0.22));
border-color: var(--primary, #2563eb);
background: var(--surface-elevated, var(--surface, #fff));
}
#theme-switcher .theme-launcher-label {
display: none;
white-space: nowrap;
}
#theme-switcher .theme-menu {
position: absolute;
right: 0;
bottom: 68px;
width: min(280px, calc(100vw - 2rem));
padding: 0.6rem;
border-radius: 14px;
border: 1px solid var(--border, rgba(0,0,0,0.15));
background: color-mix(in srgb, var(--surface, #fff) 92%, transparent 8%);
box-shadow: var(--shadow-xl, 0 22px 42px rgba(0,0,0,0.22));
}
#theme-switcher .theme-menu[hidden] {
display: none;
}
#theme-switcher .theme-menu-title {
margin: 0 0 0.45rem;
padding: 0.1rem 0.25rem 0.35rem;
color: var(--text-secondary, #6b7280);
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
#theme-switcher .theme-menu-list {
display: grid;
gap: 0.35rem;
}
#theme-switcher .theme-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.7rem 0.8rem;
border-radius: 10px;
border: 1px solid transparent;
color: var(--text-primary, #111827);
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
}
#theme-switcher .theme-option:hover {
background: color-mix(in srgb, var(--primary, #2563eb) 10%, transparent 90%);
border-color: color-mix(in srgb, var(--primary, #2563eb) 22%, transparent 78%);
text-decoration: none;
}
#theme-switcher .theme-option.is-active {
background: color-mix(in srgb, var(--primary, #2563eb) 16%, transparent 84%);
border-color: color-mix(in srgb, var(--primary, #2563eb) 28%, transparent 72%);
}
#theme-switcher .theme-option small {
color: var(--text-secondary, #6b7280);
font-size: 0.76rem;
font-weight: 600;
}
@media (min-width: 860px) {
#theme-switcher .theme-launcher-label {
display: inline;
}
}
</style>
<button id="theme-toggle" class="theme-launcher" aria-haspopup="true" aria-expanded="false" aria-label="Switch theme" title="Switch Theme" type="button">
<i class="fas fa-palette" aria-hidden="true"></i>
<span class="theme-launcher-label"><?= current_label ?></span>
</button>
<div id="theme-menu" class="theme-menu" hidden>
<div class="theme-menu-title">Available Themes</div>
<div class="theme-menu-list">
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String href = starter_link(route_path == "index" ? "" : route_path, params, context);
?><a class="theme-option<?= theme_key == current_theme ? " is-active" : "" ?>" href="<?= href ?>">
<span><?= theme_info["label"].to_string() ?></span>
<? if(theme_key == current_theme) { ?><small>Active</small><? } ?>
</a><?
}); ?>
</div>
</div>
</div>
<script>
(function() {
const themeToggle = document.getElementById('theme-toggle');
const themeMenu = document.getElementById('theme-menu');
if (!themeToggle || !themeMenu) return;
themeToggle.addEventListener('click', function() {
const isOpen = !themeMenu.hasAttribute('hidden');
themeMenu.toggleAttribute('hidden', isOpen);
themeToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
document.addEventListener('click', function(event) {
if (!themeMenu.hasAttribute('hidden') && !document.getElementById('theme-switcher').contains(event.target)) {
themeMenu.setAttribute('hidden', 'hidden');
themeToggle.setAttribute('aria-expanded', 'false');
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && !themeMenu.hasAttribute('hidden')) {
themeMenu.setAttribute('hidden', 'hidden');
themeToggle.setAttribute('aria-expanded', 'false');
}
});
}());
</script>
</>
}

View File

@ -1,91 +0,0 @@
#load "helpers.uce"
COMPONENT(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();
DTree scale = context.call["scale"];
<>
<div class="arcgauge-set" id="<?= gauge_attr_id(gauge_id) ?>" style="<?= style ?>">
<? if(title != "") { ?>
<div class="arcgauge-set-header">
<h3><?= title ?></h3>
<? if(subtitle != "") { ?><p><?= subtitle ?></p><? } ?>
</div>
<? } ?>
<div class="arcgauge-grid">
<? context.call["items"].each([&](DTree item, String item_id_raw) {
DTree merged = scale;
item.each([&](DTree value, String key) { merged[key] = value; });
String item_id = gauge_attr_id(item_id_raw);
f64 value = float_val(first(merged["value"].to_string(), "0"));
f64 max_value = float_val(first(merged["max"].to_string(), "100"));
s32 precision = (s32)int_val(first(merged["precision"].to_string(), "1"));
f64 pct = max_value > 0.0 ? gauge_clamp_value((value / max_value) * 100.0, 0.0, 100.0) : 0.0;
f64 arc_length = round((pct / 100.0) * 157.08 * 10.0) / 10.0;
String resolved_color = "";
if(merged["color"].get_type_name() == "array")
resolved_color = gauge_range_color(merged["color"], value);
else
resolved_color = merged["color"].to_string();
if(resolved_color == "")
{
if(pct >= 85.0)
resolved_color = "var(--error, #ef4444)";
else if(pct >= 60.0)
resolved_color = "var(--warning, #f59e0b)";
else
resolved_color = "var(--success, #10b981)";
}
String display_value = gauge_format_number(value, precision) + merged["unit"].to_string();
String watermark_prefix = merged["watermark_prefix"].to_string();
?><section class="arcgauge-card">
<div class="arcgauge-label"><?= first(merged["label"].to_string(), item_id_raw) ?></div>
<svg class="arcgauge-svg" viewBox="0 0 120 68" aria-hidden="true">
<path class="arcgauge-track" d="M 10 60 A 50 50 0 0 1 110 60" fill="none" stroke-width="5" stroke-linecap="round"/>
<path id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-arc" class="arcgauge-arc-dyn" d="M 10 60 A 50 50 0 0 1 110 60" fill="none" stroke="<?= resolved_color ?>" stroke-width="5" stroke-linecap="round" stroke-dasharray="<?= std::to_string(arc_length) ?> 157.08"/>
<? if(watermark_prefix != "") { ?>
<line id="<?= gauge_attr_id(watermark_prefix) ?>WmLo" class="arcgauge-watermark arcgauge-watermark-lo" x1="0" y1="0" x2="0" y2="0" opacity="0"/>
<line id="<?= gauge_attr_id(watermark_prefix) ?>WmHi" class="arcgauge-watermark arcgauge-watermark-hi" x1="0" y1="0" x2="0" y2="0" opacity="0"/>
<? } ?>
<text id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-text" class="arcgauge-value" x="60" y="47" text-anchor="middle"><?= display_value ?></text>
<text class="arcgauge-caption" x="60" y="62" text-anchor="middle"><?= first(merged["caption"].to_string(), "") ?></text>
</svg>
<div class="arcgauge-meta" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-meta"><?= first(merged["meta"].to_string(), "--") ?></div>
</section><?
}); ?>
</div>
</div>
<? if(context.call["listen"].to_string() != "") { ?>
<script>
window.ArcgaugeComponents = window.ArcgaugeComponents || {
start_listen : function(prop) {
$.events.on('value-broadcast', function(data) {
if(!prop.items || !prop.items[data.name]) return;
const item = Object.assign({}, prop.scale || {}, prop.items[data.name]);
GaugeComponents.updateArcGauge({
arcId: prop.id + '-' + data.name + '-arc',
textId: prop.id + '-' + data.name + '-text',
metaId: prop.id + '-' + data.name + '-meta',
value: Number(data.value),
max: Number(item.max || 100),
suffix: item.unit || '',
precision: item.precision,
watermarkPrefix: item.watermark_prefix || data.name,
color: item.color,
meta: data.meta != null ? data.meta : null
});
});
}
};
ArcgaugeComponents.start_listen(<?: json_encode(context.call, '\'') ?>);
</script>
<? } ?>
</>
}

View File

@ -1,95 +0,0 @@
.horizontal .progressbar-item {
display: flex;
min-width: 100%;
padding: 5px;
}
.progressbar-container.vertical {
display: flex;
}
.vertical .progressbar-item {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 5px;
text-align: center;
}
.progressbar-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.horizontal .progressbar-label, .horizontal .progressbar-value {
flex: 0 0 auto;
margin-right: 10px;
}
.horizontal .progressbar-label {
min-width: 50px;
}
.vertical .progressbar-label {
text-align: center;
}
.horizontal .progressbar-value {
min-width: 50px;
text-align: right;
}
.progressbar-background {
flex: 1;
background: var(--bg-color);
padding: 4px;
display: flex;
}
.vertical .progressbar-background {
flex-direction: column;
justify-content: flex-end;
}
.progressbar-bar {
transition: all 0.3s ease;
}
.progressbar-marker {
position: absolute;
z-index: 10;
opacity: 0.5;
background: var(--text-color, #333);
}
.progressbar-marker:hover {
opacity: 1;
}
.horizontal .progressbar-marker {
border-width: 0 2px 0 2px;
height: 100%;
top: 0;
min-width: 4px;
}
.vertical .progressbar-marker {
border-width: 2px 0 2px 0;
width: 100%;
left: 0;
min-height: 4px;
}
.progressbar-background {
position: relative;
}
.needlegauge-svg .needle {
transition: all 0.3s ease;
}
.needlegauge-item {
display: inline-block;
}

View File

@ -1,121 +0,0 @@
// common code for gauges components
window.GaugeComponents = window.GaugeComponents || {};
Object.assign(window.GaugeComponents, { // as a namespace
// utility functions
clampValue: function(value, min, max) {
return Math.min(max, Math.max(min, value));
},
pickEntryFromRange: function(ranges, value) {
if (!Array.isArray(ranges)) return null;
for (const entry of ranges) {
if (value >= 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);
}
}
});

View File

@ -1,71 +0,0 @@
#load "../../lib/app.uce"
#include <math.h>
f64 gauge_clamp_value(f64 value, f64 min_value, f64 max_value)
{
return(std::max(min_value, std::min(max_value, value)));
}
f64 gauge_pi()
{
return(3.14159265358979323846);
}
String gauge_string_attr(String value)
{
return(html_escape(value));
}
String gauge_attr_id(String value)
{
return(ascii_safe_name(value));
}
String gauge_range_color(DTree ranges, f64 value)
{
String matched = "";
ranges.each([&](DTree range, String key) {
f64 from_value = float_val(first(range["from"].to_string(), "-999999999"));
f64 to_value = float_val(first(range["to"].to_string(), "999999999"));
if(value >= from_value && value <= to_value && matched == "")
matched = range["color"].to_string();
});
return(matched);
}
String gauge_arc_path(f64 cx, f64 cy, f64 radius, f64 start_angle, f64 end_angle)
{
f64 start_x = cx + cos(start_angle) * radius;
f64 start_y = cy + sin(start_angle) * radius;
f64 end_x = cx + cos(end_angle) * radius;
f64 end_y = cy + sin(end_angle) * radius;
s32 large_arc = fabs(end_angle - start_angle) > gauge_pi() ? 1 : 0;
s32 sweep = end_angle >= start_angle ? 1 : 0;
return(
"M " + std::to_string(start_x) + " " + std::to_string(start_y) +
" A " + std::to_string(radius) + " " + std::to_string(radius) +
" 0 " + std::to_string(large_arc) + " " + std::to_string(sweep) +
" " + std::to_string(end_x) + " " + std::to_string(end_y)
);
}
String gauge_circle_segment_svg(f64 cx, f64 cy, f64 radius, f64 start_angle, f64 end_angle, String stroke, f64 stroke_width, String fill, String style)
{
String path_d = gauge_arc_path(cx, cy, radius, start_angle, end_angle);
return(
"<path d=\"" + gauge_string_attr(path_d) +
"\" fill=\"" + gauge_string_attr(fill) +
"\" stroke=\"" + gauge_string_attr(stroke) +
"\" stroke-width=\"" + gauge_string_attr(std::to_string(stroke_width)) +
"\" stroke-linecap=\"round\"" +
(style != "" ? " style=\"" + gauge_string_attr(style) + "\"" : "") +
"/>"
);
}
String gauge_format_number(f64 value, s32 precision)
{
char buf[64];
snprintf(buf, sizeof(buf), ("%." + std::to_string(std::max(0, precision)) + "f").c_str(), value);
return(String(buf));
}

View File

@ -1,144 +0,0 @@
#load "helpers.uce"
COMPONENT(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(), "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();
f64 size = float_val(first(context.call["size"].to_string(), "200"));
DTree scale = context.call["scale"];
f64 scale_angle_start = float_val(first(scale["angle_start"].to_string(), std::to_string(-gauge_pi())));
f64 scale_angle_end = float_val(first(scale["angle_end"].to_string(), "0"));
f64 img_height = float_val(first(context.call["img_height"].to_string(), std::to_string(size * std::max(cos(scale_angle_start), cos(scale_angle_end)))));
<>
<section class="gauge-set needlegauge-set" id="<?= gauge_attr_id(gauge_id) ?>" style="<?= style ?>">
<? if(title != "") { ?>
<div class="gauge-set-header">
<h3><?= title ?></h3>
<? if(subtitle != "") { ?><p><?= subtitle ?></p><? } ?>
</div>
<? } ?>
<div class="needlegauge-grid">
<? context.call["items"].each([&](DTree item, String item_id_raw) {
DTree merged = scale;
item.each([&](DTree value, String key) { merged[key] = value; });
String item_id = gauge_attr_id(item_id_raw);
String label = first(merged["label"].to_string(), item_id_raw);
String tooltip = merged["tooltip"].to_string();
String unit = merged["unit"].to_string();
f64 min_value = float_val(first(merged["min"].to_string(), "0"));
f64 max_value = float_val(first(merged["max"].to_string(), "100"));
f64 value = float_val(first(merged["value"].to_string(), "0"));
f64 vrange = max_value - min_value;
if(vrange == 0)
vrange = 1;
f64 angle_start = float_val(first(merged["angle_start"].to_string(), std::to_string(-gauge_pi())));
f64 angle_end = float_val(first(merged["angle_end"].to_string(), "0"));
f64 pct_value = gauge_clamp_value((value - min_value) / vrange, 0.0, 1.0);
f64 needle_angle = -gauge_pi() + angle_start + (pct_value * (angle_end - angle_start));
String needle_color = first(merged["color"].to_string(), "#888888");
if(merged["color"].get_type_name() == "array")
needle_color = first(gauge_range_color(merged["color"], value), "#888888");
String tick_color = first(merged["tick-color"].to_string(), "#888888");
f64 tick_interval = float_val(first(merged["ticks-every"].to_string(), std::to_string(vrange / 20.0)));
if(tick_interval == 0)
tick_interval = std::max(1.0, vrange / 20.0);
f64 label_interval = float_val(first(merged["value-labels-every"].to_string(), std::to_string(vrange / 4.0)));
if(label_interval == 0)
label_interval = std::max(1.0, vrange / 4.0);
String ticks_html = "";
for(f64 v = min_value; v <= max_value + 0.0001; v += tick_interval)
{
f64 angle = angle_start + ((v - min_value) / vrange) * (angle_end - angle_start);
f64 x1 = size / 2.0 + cos(angle) * size * 0.38;
f64 y1 = size / 2.0 + sin(angle) * size * 0.38;
f64 x2 = size / 2.0 + cos(angle) * size * 0.41;
f64 y2 = size / 2.0 + sin(angle) * size * 0.41;
ticks_html += "<line x1=\"" + gauge_string_attr(std::to_string(x1)) + "\" y1=\"" + gauge_string_attr(std::to_string(y1)) +
"\" x2=\"" + gauge_string_attr(std::to_string(x2)) + "\" y2=\"" + gauge_string_attr(std::to_string(y2)) +
"\" stroke=\"" + gauge_string_attr(tick_color) + "\" stroke-width=\"1\"/>";
}
for(f64 v = min_value; v <= max_value + 0.0001; v += label_interval)
{
f64 angle = angle_start + ((v - min_value) / vrange) * (angle_end - angle_start);
f64 x1 = size / 2.0 + cos(angle) * size * 0.35;
f64 y1 = size / 2.0 + sin(angle) * size * 0.35;
f64 x2 = size / 2.0 + cos(angle) * size * 0.41;
f64 y2 = size / 2.0 + sin(angle) * size * 0.41;
ticks_html += "<line x1=\"" + gauge_string_attr(std::to_string(x1)) + "\" y1=\"" + gauge_string_attr(std::to_string(y1)) +
"\" x2=\"" + gauge_string_attr(std::to_string(x2)) + "\" y2=\"" + gauge_string_attr(std::to_string(y2)) +
"\" stroke=\"" + gauge_string_attr(tick_color) + "\" stroke-width=\"3\"/>";
f64 lx = size / 2.0 + cos(angle) * size * 0.45;
f64 ly = size / 2.0 + sin(angle) * size * 0.45;
String label_value = std::to_string((s64)round(v));
ticks_html += "<text x=\"" + gauge_string_attr(std::to_string(lx)) + "\" y=\"" + gauge_string_attr(std::to_string(ly)) +
"\" text-anchor=\"middle\" dominant-baseline=\"central\" font-size=\"10\" fill=\"" + gauge_string_attr(tick_color) + "\">" +
html_escape(label_value) + "</text>";
}
String segments_html = "";
if(merged["color"].get_type_name() == "array")
{
merged["color"].each([&](DTree range, String key) {
f64 range_from = std::max(float_val(first(range["from"].to_string(), std::to_string(min_value))), min_value);
f64 range_to = std::min(float_val(first(range["to"].to_string(), std::to_string(max_value))), max_value);
f64 angle_from = angle_start + ((range_from - min_value) / vrange) * (angle_end - angle_start);
f64 angle_to = angle_start + ((range_to - min_value) / vrange) * (angle_end - angle_start);
segments_html += gauge_circle_segment_svg(size / 2.0, size / 2.0, size * 0.4, angle_from, angle_to, first(range["color"].to_string(), "rgba(120,120,120,0.5)"), 8.0, "rgba(0,0,0,0)", "opacity:0.25");
});
}
else
{
segments_html = gauge_circle_segment_svg(size / 2.0, size / 2.0, size * 0.4, angle_start, angle_end, tick_color, 8.0, "rgba(0,0,0,0)", "opacity:0.25");
}
?><section class="gauge-card needlegauge-card">
<div class="gauge-metric-label needlegauge-label-head"><?= label ?></div>
<div class="needlegauge-visual">
<svg id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-svg" width="<?= std::to_string((s32)round(size)) ?>" height="<?= std::to_string((s32)round(img_height)) ?>" class="needlegauge-svg"
viewBox="0 0 <?= std::to_string((s32)round(size)) ?> <?= std::to_string((s32)round(img_height)) ?>">
<?: segments_html ?>
<g class="ticks"><?: ticks_html ?></g>
<line id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-needle" class="needle"
x1="<?= std::to_string(size * 0.55) ?>" y1="<?= std::to_string(size * 0.5) ?>" x2="<?= std::to_string(size * 0.1) ?>" y2="<?= std::to_string(size * 0.5) ?>"
stroke="<?= needle_color ?>" stroke-width="3" stroke-linecap="round"
style="transform-origin: <?= std::to_string(size / 2.0) ?>px <?= std::to_string(size / 2.0) ?>px; transform: rotate(<?= std::to_string(needle_angle) ?>rad); transition: transform 0.3s ease;"/>
<circle cx="<?= std::to_string(size / 2.0) ?>" cy="<?= std::to_string(size / 2.0) ?>" r="6" fill="<?= needle_color ?>"/>
</svg>
</div>
<div class="needlegauge-info">
<div class="gauge-metric-value needlegauge-value" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-value" title="<?= tooltip ?>"><?= merged["value"].to_string() + unit ?></div>
<? if(tooltip != "") { ?><div class="gauge-metric-meta needlegauge-meta"><?= tooltip ?></div><? } ?>
</div>
</section><?
}); ?>
</div>
</section>
<? if(context.call["listen"].to_string() != "") { ?>
<script>
window.NeedlegaugeComponents = window.NeedlegaugeComponents || {
start_listen : function(prop) {
$.events.on('value-broadcast', function(data) {
if(!prop.items || !prop.items[data.name]) return;
let item = Object.assign({}, prop.scale || {}, prop.items[data.name]);
let valueEl = document.getElementById(prop.id + '-' + data.name + '-value');
let needle = document.getElementById(prop.id + '-' + data.name + '-needle');
if(valueEl) valueEl.textContent = data.value + (item.unit || '');
let min = Number(item.min || 0);
let max = Number(item.max || 100);
let pctValue = GaugeComponents.clampValue((Number(data.value) - min) / ((max - min) || 1), 0, 1);
let angleStart = Number(item.angle_start != null ? item.angle_start : -Math.PI);
let angleEnd = Number(item.angle_end != null ? item.angle_end : 0);
let angle = -Math.PI + angleStart + (pctValue * (angleEnd - angleStart));
if(needle) needle.style.transform = 'rotate(' + angle + 'rad)';
});
}
};
NeedlegaugeComponents.start_listen(<?: json_encode(context.call, '\'') ?>);
</script>
<? } ?>
</>
}

View File

@ -1,168 +0,0 @@
#load "helpers.uce"
COMPONENT(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 style = context.call["style"].to_string();
String item_style = context.call["item-style"].to_string();
String label_style = context.call["label-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();
String layout = first(context.call["layout"].to_string(), "horizontal");
String bar_color_default = context.call["bar-color"].to_string();
DTree scale = context.call["scale"];
DTree markers = context.call["markers"];
StringList default_palette;
default_palette.push_back("var(--success, #10b981)");
default_palette.push_back("var(--primary, #60a5fa)");
default_palette.push_back("var(--accent, #22d3ee)");
default_palette.push_back("var(--warning, #f59e0b)");
u32 auto_color_counter = 0;
<>
<section class="gauge-set progressbar-set progressbar-set-<?= gauge_attr_id(layout) ?>" id="<?= gauge_attr_id(gauge_id) ?>" style="<?= style ?>">
<? if(title != "") { ?>
<div class="gauge-set-header">
<h3><?= title ?></h3>
<? if(subtitle != "") { ?><p><?= subtitle ?></p><? } ?>
</div>
<? } ?>
<? if(layout == "horizontal") { ?>
<div class="progressbar-grid progressbar-grid-horizontal">
<? context.call["items"].each([&](DTree bar, String bar_id) {
DTree merged = scale;
bar.each([&](DTree item, String key) { merged[key] = item; });
String item_id = gauge_attr_id(bar_id);
String merged_item_style = item_style;
if(merged["style"].to_string() != "")
merged_item_style += (merged_item_style != "" ? ";" : "") + merged["style"].to_string();
String tooltip = merged["tooltip"].to_string();
String before_html = merged["before"].to_string();
String after_html = merged["after"].to_string();
String unit = merged["unit"].to_string();
String label = first(merged["label"].to_string(), bar_id);
f64 min_value = float_val(first(merged["min"].to_string(), "0"));
f64 max_value = float_val(first(merged["max"].to_string(), "100"));
f64 value = float_val(first(merged["value"].to_string(), "0"));
f64 vrange = max_value - min_value;
if(vrange == 0)
vrange = 1;
f64 pct_value = gauge_clamp_value(((value - min_value) / vrange) * 100.0, 0.0, 100.0);
String resolved_color = merged["color"].to_string();
if(merged["color"].get_type_name() == "array")
resolved_color = gauge_range_color(merged["color"], value);
if(resolved_color == "")
resolved_color = first(bar_color_default, default_palette[auto_color_counter++ % default_palette.size()]);
String opacity = first(merged["opacity"].to_string(), "0.75");
?><section class="gauge-card progressbar-card progressbar-card-horizontal" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>"
title="<?= tooltip ?>"
style="<?= merged_item_style ?>">
<?: before_html ?>
<div class="progressbar-card-head">
<div class="gauge-metric-label progressbar-label" style="<?= label_style ?>"><?= label ?></div>
<div class="gauge-metric-value progressbar-value" style="<?= value_style ?>" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-value"><?= merged["value"].to_string() + unit ?></div>
</div>
<div class="progressbar-background progressbar-background-horizontal">
<div class="progressbar-bar" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-bar"
style="background-color: <?= resolved_color ?>; <?= bar_style ?> opacity: <?= opacity ?>; width: <?= std::to_string(pct_value) ?>%;"></div>
<? markers.each([&](DTree marker, String marker_id) {
f64 marker_value = float_val(first(marker["value"].to_string(), "0"));
f64 marker_pct = gauge_clamp_value(((marker_value - min_value) / vrange) * 100.0, 0.0, 100.0);
String marker_color = first(marker["color"].to_string(), "var(--primary-light)");
?><div class="progressbar-marker"
title="<?= marker["label"].to_string() ?>"
style="left: <?= std::to_string(marker_pct) ?>%; background: <?= marker_color ?>;"></div><?
}); ?>
</div>
<? if(tooltip != "") { ?><div class="gauge-metric-meta progressbar-meta"><?= tooltip ?></div><? } ?>
<?: after_html ?>
</section><?
}); ?>
</div>
<? } else { ?>
<div class="progressbar-grid progressbar-grid-vertical" style="--progressbar-height: <?= first(context.call["height"].to_string(), "240") ?>px;">
<? context.call["items"].each([&](DTree bar, String bar_id) {
DTree merged = scale;
bar.each([&](DTree item, String key) { merged[key] = item; });
String item_id = gauge_attr_id(bar_id);
String merged_item_style = item_style;
if(merged["style"].to_string() != "")
merged_item_style += (merged_item_style != "" ? ";" : "") + merged["style"].to_string();
String tooltip = merged["tooltip"].to_string();
String before_html = merged["before"].to_string();
String after_html = merged["after"].to_string();
String unit = merged["unit"].to_string();
String label = first(merged["label"].to_string(), bar_id);
f64 min_value = float_val(first(merged["min"].to_string(), "0"));
f64 max_value = float_val(first(merged["max"].to_string(), "100"));
f64 value = float_val(first(merged["value"].to_string(), "0"));
f64 vrange = max_value - min_value;
if(vrange == 0)
vrange = 1;
f64 pct_value = gauge_clamp_value(((value - min_value) / vrange) * 100.0, 0.0, 100.0);
String resolved_color = merged["color"].to_string();
if(merged["color"].get_type_name() == "array")
resolved_color = gauge_range_color(merged["color"], value);
if(resolved_color == "")
resolved_color = first(bar_color_default, default_palette[auto_color_counter++ % default_palette.size()]);
String opacity = first(merged["opacity"].to_string(), "0.75");
?><section class="gauge-card progressbar-card progressbar-card-vertical" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>"
title="<?= tooltip ?>"
style="<?= merged_item_style ?>">
<?: before_html ?>
<div class="gauge-metric-label progressbar-label" style="<?= label_style ?>"><?= label ?></div>
<div class="progressbar-background progressbar-background-vertical">
<div class="progressbar-bar" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-bar"
style="background-color: <?= resolved_color ?>; <?= bar_style ?> opacity: <?= opacity ?>; height: <?= std::to_string(pct_value) ?>%;"></div>
<? markers.each([&](DTree marker, String marker_id) {
f64 marker_value = float_val(first(marker["value"].to_string(), "0"));
f64 marker_pct = gauge_clamp_value(((marker_value - min_value) / vrange) * 100.0, 0.0, 100.0);
String marker_color = first(marker["color"].to_string(), "var(--primary-light)");
?><div class="progressbar-marker"
title="<?= marker["label"].to_string() ?>"
style="bottom: <?= std::to_string(marker_pct) ?>%; background: <?= marker_color ?>;"></div><?
}); ?>
</div>
<div class="gauge-metric-value progressbar-value" style="<?= value_style ?>" id="<?= gauge_attr_id(gauge_id) ?>-<?= item_id ?>-value"><?= merged["value"].to_string() + unit ?></div>
<? if(tooltip != "") { ?><div class="gauge-metric-meta progressbar-meta"><?= tooltip ?></div><? } ?>
<?: after_html ?>
</section><?
}); ?>
</div>
<? } ?>
</section>
<? if(context.call["listen"].to_string() != "") { ?>
<script>
window.ProgressbarComponents = window.ProgressbarComponents || {
start_listen : function(prop) {
$.events.on('value-broadcast', function(data) {
if(!prop.items || !prop.items[data.name]) return;
let bar = Object.assign({}, prop.scale || {}, prop.items[data.name]);
let target = document.getElementById(prop.id + '-' + data.name + '-bar');
let valueTarget = document.getElementById(prop.id + '-' + data.name + '-value');
if(valueTarget) valueTarget.textContent = data.value + (bar.unit || '');
let vrange = Number(bar.max || 100) - Number(bar.min || 0);
let pctValue = GaugeComponents.clampValue((Number(data.value) - Number(bar.min || 0)) / (vrange || 1) * 100, 0, 100);
if(Array.isArray(bar.color)) {
let colorMatch = GaugeComponents.pickEntryFromRange(bar.color, Number(data.value));
if(colorMatch && colorMatch.color && target)
target.style.background = colorMatch.color;
}
if(target) {
if(prop.layout === 'horizontal') target.style.width = pctValue + '%';
else target.style.height = pctValue + '%';
}
});
}
};
ProgressbarComponents.start_listen(<?: json_encode(context.call, '\'') ?>);
</script>
<? } ?>
</>
}

View File

@ -1,32 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser users(context);
DTree user = users.current();
bool signed_in = user["email"].to_string() != "";
String theme_key = context.cfg.get_by_path("theme/key").to_string();
String wrapper_class = first(context.call["wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu");
String links_class = first(context.call["links_wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-links" : "");
String name_class = first(context.call["name_class"].to_string(), theme_key == "localfirst" ? "admin-account-name" : "account-name");
<>
<div class="<?= wrapper_class ?>">
<? if(signed_in) { ?>
<span class="<?= name_class ?>"><?= first(user["username"].to_string(), user["email"].to_string(), "Account") ?></span>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/profile", context) ?>">Profile</a>
<a href="<?= starter_link("account/logout", context) ?>">Logout</a>
<? if(links_class != "") { ?></div><? } ?>
<? } else { ?>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/login", context) ?>">Login</a>
<? if(context.cfg.get_by_path("users/enable_signup").to_string() != "") { ?>
<a href="<?= starter_link("account/register", context) ?>">Register</a>
<? } ?>
<? if(links_class != "") { ?></div><? } ?>
<? } ?>
</div>
</>
}

View File

@ -1,22 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
if(embed_mode)
return;
String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string());
String inner_class = first(context.call["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "");
<>
<footer>
<? if(inner_class != "") { ?>
<div class="<?= inner_class ?>"><p><?= text ?></p></div>
<? } else { ?>
<div><p><?= text ?></p></div>
<? } ?>
</footer>
</>
}

View File

@ -1,18 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
if(starter_page_embed_mode(context))
return;
String cookie_value = context.call["cookie_consent"].to_string();
bool cookie_consent = !(cookie_value == "0" || cookie_value == "false" || cookie_value == "FALSE" || cookie_value == "no" || cookie_value == "NO");
<>
<?: component("../example/theme-switcher", context) ?>
<? if(cookie_consent) { ?>
<?: component("../basic/cookie-consent", context) ?>
<? } ?>
</>
}

View File

@ -1,22 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string());
String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example");
String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a");
String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);
<>
<meta charset="utf-8">
<title><?= title + " | " + context.cfg.get_by_path("site/name").to_string() ?></title>
<meta name="description" content="<?= description ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="<?= theme_color ?>">
<link rel="apple-touch-icon" href="<?= icon ?>" />
<link rel="icon" type="image/png" sizes="32x32" href="<?= icon ?>" />
<? starter_render_registered_css(context); ?>
<? starter_render_registered_js(context); ?>
</>
}

View File

@ -1,20 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
if(context.call["main_html"].to_string() != "")
context.var["starter"]["fragments"]["main"] = context.call["main_html"];
if(context.call["json"].get_type_name() == "array")
context.var["starter"]["json"] = context.call["json"];
if(context.call["page_type"].to_string() != "")
context.var["starter"]["page_type"] = context.call["page_type"];
if(context.call["embed_mode"].to_string() != "")
context.var["starter"]["embed_mode"] = context.call["embed_mode"];
starter_render_page(context);
}
COMPONENT:HEAD(Request& context)
{
print(component("head", context.call, context));
}

View File

@ -1,31 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
String nav_class = context.call["nav_class"].to_string();
if(nav_class == "" && (context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming"))
nav_class = "nav-shell";
DTree account_props;
if(context.call["account_wrapper_class"].to_string() != "")
account_props["wrapper_class"] = context.call["account_wrapper_class"];
if(context.call["links_wrapper_class"].to_string() != "")
account_props["links_wrapper_class"] = context.call["links_wrapper_class"];
if(context.call["name_class"].to_string() != "")
account_props["name_class"] = context.call["name_class"];
<>
<nav<?: nav_class != "" ? " class=\"" + html_escape(nav_class) + "\"" : "" ?>>
<div class="nav-menu">
<a href="<?= starter_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
?><a href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
}); ?>
</div>
<?: component("account_links", account_props, context) ?>
</nav>
</>
}

View File

@ -1,122 +0,0 @@
#load "../../lib/app.uce"
COMPONENT: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();
<>
<div<?: id != "" ? " id=\"" + html_escape(id) + "\"" : "" ?> class="ws-app<?= class_name != "" ? " " + html_escape(class_name) : "" ?>">
<?: context.call["sidebar_html"].to_string() ?>
<div<?: overlay_id != "" ? " id=\"" + html_escape(overlay_id) + "\"" : "" ?> class="ws-sidebar-overlay"></div>
<main class="ws-main"><?: context.call["main_html"].to_string() ?></main>
</div>
</>
}
COMPONENT:EMPTY_STATE(Request& context)
{
<>
<div class="ws-empty-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<div class="ws-empty-icon"><i class="<?= first(context.call["icon_class"].to_string(), "fas fa-layer-group") ?>"></i></div>
<h2><?= context.call["title"].to_string() ?></h2>
<p><?= context.call["text"].to_string() ?></p>
<?: context.call["action_html"].to_string() ?>
</div>
</>
}
COMPONENT:LIST_STATE(Request& context)
{
<>
<div class="ws-list-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><? if(context.call["icon_class"].to_string() != "") { ?><i class="<?= context.call["icon_class"].to_string() ?>"></i><? } ?><span><?= context.call["text"].to_string() ?></span></div>
</>
}
COMPONENT:MOBILE_BAR(Request& context)
{
<>
<div class="ws-mobile-bar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<button<?: context.call["button_id"].to_string() != "" ? " id=\"" + html_escape(context.call["button_id"].to_string()) + "\"" : "" ?> class="ws-mobile-toggle" title="Toggle sidebar" type="button">
<i class="<?= first(context.call["icon_class"].to_string(), "fas fa-bars") ?>"></i>
</button>
<span class="ws-mobile-title"><?= context.call["title"].to_string() ?></span>
</div>
</>
}
COMPONENT:PANEL_HEADER(Request& context)
{
<>
<div class="ws-panel-header<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<div class="ws-panel-title-group">
<h2><?= context.call["title"].to_string() ?></h2>
<? if(context.call["subtitle"].to_string() != "") { ?><p class="ws-subtitle"><?= context.call["subtitle"].to_string() ?></p><? } ?>
</div>
<? if(context.call["actions_html"].to_string() != "") { ?><div class="ws-header-actions"><?: context.call["actions_html"].to_string() ?></div><? } ?>
</div>
</>
}
COMPONENT:PANEL(Request& context)
{
<>
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-panel<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["header_html"].to_string() ?>
<?: context.call["body_html"].to_string() ?>
</section>
</>
}
COMPONENT:SECTION_HEAD(Request& context)
{
<>
<div class="ws-section-head<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<h3><?= context.call["title"].to_string() ?></h3>
<? if(context.call["actions_html"].to_string() != "") { ?><div class="ws-header-actions"><?: context.call["actions_html"].to_string() ?></div><? } ?>
</div>
</>
}
COMPONENT:SECTION(Request& context)
{
<>
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-section<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["header_html"].to_string() ?>
<?: context.call["body_html"].to_string() ?>
</section>
</>
}
COMPONENT:SIDEBAR_SHELL(Request& context)
{
<>
<aside<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-sidebar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["top_html"].to_string() ?>
<?: context.call["body_html"].to_string() ?>
</aside>
</>
}
COMPONENT:SIDEBAR_TOOLBAR(Request& context)
{
<>
<div class="ws-sidebar-top<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
<?: context.call["action_html"].to_string() ?>
<div class="ws-search-wrap">
<i class="fas fa-search ws-search-icon"></i>
<input type="search"<?: context.call["search_input_id"].to_string() != "" ? " id=\"" + html_escape(context.call["search_input_id"].to_string()) + "\"" : "" ?> name="<?= first(context.call["search_input_name"].to_string(), "search") ?>" class="ws-search-input" placeholder="<?= first(context.call["search_placeholder"].to_string(), "Search...") ?>" autocomplete="off">
</div>
</div>
</>
}
COMPONENT:STATUS_PILL(Request& context)
{
String variant = to_lower(first(context.call["variant"].to_string(), "neutral"));
<>
<span class="ws-status-pill ws-status-pill-<?= variant ?><?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><?= first(context.call["label"].to_string(), context.call["text"].to_string()) ?></span>
</>
}

View File

@ -1,73 +0,0 @@
DTree get_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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,29 +0,0 @@
#load "lib/app.uce"
RENDER(Request& context)
{
app_init(context);
DTree resolved = app_resolve_view(context);
ob_start();
if(resolved["file"].to_string() != "")
{
if(resolved["param"].to_string() != "")
context.var["app"]["route"]["param"] = resolved["param"];
unit_render(resolved["file"].to_string(), context);
}
else
{
app_not_found("The requested page does not exist.", context);
<>
<section class="card">
<h1>404 Not Found</h1>
<p><?= context.var["app"]["error"].to_string() ?></p>
</section>
</>
}
String main_html = ob_get_close();
context.var["app"]["fragments"]["main"] = main_html;
app_render_page(context);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,141 +0,0 @@
# morphdom.js
A fast and lightweight DOM diffing/patching library for efficiently updating the DOM with minimal changes.
## Basic Usage
```javascript
morphdom(fromNode, toNode, options) // Transform fromNode to match toNode
```
Returns the morphed node (may be a new node if root element changes).
Instead of replacing entire DOM trees, morphdom intelligently compares and patches only the differences:
```javascript
const container = document.getElementById('container');
const newHTML = '<div><p>Updated content</p></div>';
// Efficiently update DOM - only changed parts are modified
morphdom(container, newHTML);
```
## Options
```javascript
morphdom(fromNode, toNode, {
childrenOnly: true, // Only morph children, not root element
getNodeKey: node => node.id, // Custom key function for matching nodes
onBeforeNodeAdded: node => true, // Called before adding new nodes
onNodeAdded: node => {}, // Called after nodes are added
onBeforeElUpdated: (from, to) => true, // Called before updating elements
onElUpdated: node => {}, // Called after elements are updated
onBeforeNodeDiscarded: node => true, // Called before removing nodes
onNodeDiscarded: node => {}, // Called after nodes are removed
onBeforeElChildrenUpdated: (from, to) => true, // Called before updating children
skipFromChildren: (from, to) => false, // Skip children comparison
addChild: (parent, child) => parent.appendChild(child) // Custom child addition
});
```
## Key Matching
Use `getNodeKey` for efficient list updates:
```javascript
// HTML with keyed elements
const html = `
<ul>
<li id="item-1">Item 1</li>
<li id="item-2">Item 2</li>
<li id="item-3">Item 3</li>
</ul>
`;
morphdom(list, html, {
getNodeKey: node => node.getAttribute('id')
});
```
## Event Handlers
```javascript
morphdom(container, newHTML, {
onBeforeElUpdated: (fromEl, toEl) => {
// Preserve event listeners
if (fromEl.hasEventListeners) {
return false; // Skip this element
}
return true;
},
onNodeAdded: (node) => {
// Initialize new components
if (node.classList?.contains('widget')) {
initializeWidget(node);
}
}
});
```
## Form Element Handling
Morphdom has special handling for form elements:
- **INPUT**: Preserves checked, disabled, and value properties
- **TEXTAREA**: Syncs value and text content
- **SELECT**: Maintains selectedIndex and option states
- **OPTION**: Handles selected state in select boxes
## Common Patterns
### Template Updates
```javascript
function updateTemplate(container, data) {
const template = `<div class="user">${data.name}</div>`;
morphdom(container, template);
}
```
### List Management
```javascript
function updateList(listEl, items) {
const html = items.map(item =>
`<li id="item-${item.id}">${item.name}</li>`
).join('');
morphdom(listEl, `<ul>${html}</ul>`, {
childrenOnly: true,
getNodeKey: node => node.id
});
}
```
### Component Preservation
```javascript
morphdom(container, newHTML, {
onBeforeElUpdated: (from, to) => {
// Preserve components that haven't changed
if (from.dataset.component === to.dataset.component) {
return false; // Skip update
}
return true;
}
});
```
## Integration with uquery.js
This library is used by uquery's `html()` method when differential updates are enabled:
```javascript
$.options.alwaysDoDifferentialUpdate = true;
$('#container').html(newContent); // Uses morphdom internally
```
## Browser Support
- Modern browsers with DOM Level 2+ support
- Automatic fallbacks for older browsers
- Template element support detection
- Range API support detection

View File

@ -1,77 +0,0 @@
var enable_debug = true;
var starterReady = (typeof $ !== 'undefined' && $.ready)
? $.ready.bind($)
: function(callback) {
if (document.readyState !== 'loading') {
callback();
} else {
document.addEventListener('DOMContentLoaded', callback);
}
};
var UI = {
smoothScrollToNamedAnchors: function() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
},
enablePageTransitions: function() {
var style = document.createElement('style');
style.textContent = `
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.25s;
}
::view-transition-old(root) {
animation-name: fade-out;
}
::view-transition-new(root) {
animation-name: fade-in;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}`;
document.body.appendChild(style);
document.addEventListener('click', e => {
const link = e.target.closest('a[href]');
if (!link) return;
e.preventDefault();
document.startViewTransition(() => {
window.location.href = link.href;
});
});
},
init: function() {
//UI.enablePageTransitions();
UI.smoothScrollToNamedAnchors();
document.body.classList.add('loaded');
},
}
starterReady(UI.init);

View File

@ -1,897 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>U-EventEmitter Demo</title>
<style>
:root {
--space: 8px;
--radius: 5px;
--gray: #6c757d;
--gray-bg: #f5f5f5;
--blue: #007acc;
--green: #28a745;
--white: white;
--dark: #333;
--max-width: 1400px;
--sidebar: 400px;
}
* { box-sizing: border-box; }
body {
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: var(--max-width);
margin: 0 auto;
padding: var(--space);
background: var(--gray-bg);
}
.container {
background: var(--white);
padding: calc(var(--space) * 1.5);
border-radius: var(--radius);
}
.main-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--space) * 1.5);
}
.api-column {
background: var(--gray-bg);
padding: var(--space);
border-radius: var(--radius);
border-left: 4px solid var(--green);
}
h1 {
color: var(--dark);
text-align: center;
margin-bottom: calc(var(--space) * 1.5);
grid-column: 1 / -1;
}
h2 {
color: var(--gray);
border-bottom: 2px solid #e0e0e0;
padding-bottom: var(--space);
margin-top: calc(var(--space) * 1.5);
}
.demo-section {
margin: var(--space) 0;
padding: var(--space);
background: var(--gray-bg);
border-radius: var(--radius);
border-left: 4px solid var(--blue);
}
.controls, .input-group {
display: flex;
gap: var(--space);
margin: 15px 0;
}
.controls { flex-wrap: wrap; }
.input-group { align-items: center; }
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
button {
background: var(--blue);
color: var(--white);
border: none;
padding: var(--space) var(--space);
border-radius: var(--radius);
cursor: pointer;
transition: all 0.3s ease;
}
button:hover { filter: brightness(0.9); }
button:disabled { background: #ccc; cursor: not-allowed; }
input[type="text"] {
padding: var(--space);
border: 1px solid #ddd;
border-radius: var(--radius);
flex: 1;
}
.status, .code-example {
font-family: monospace;
padding: var(--space);
border-radius: var(--radius);
margin: var(--space) 0;
}
.status {
background: var(--dark);
color: #0f0;
white-space: pre-wrap;
max-height: 150px;
overflow-y: auto;
}
.code-example {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
overflow-x: auto;
margin: var(--space) 0;
}
.highlight { color: #68d391; }
.keyword { color: #fbb6ce; }
.string { color: #fbd38d; }
.event-visual {
height: 100px;
background: linear-gradient(45deg, #1e3c72, #2a5298);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-weight: bold;
margin: 15px 0;
position: relative;
overflow: hidden;
}
.pulse-wave {
position: absolute;
inset: 0;
opacity: 0.3;
background: radial-gradient(circle at center, rgba(255,255,255,0.2) 0%, transparent 70%);
animation: pulse 2s ease-in-out infinite;
}
.event-indicator {
position: absolute;
top: 10px;
right: 10px;
width: 12px;
height: 12px;
background: #68d391;
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
}
.event-indicator.active {
opacity: 1;
animation: blink 0.5s ease-in-out;
}
@keyframes pulse {
0%, 100% { transform: scale(0.8); opacity: 0.3; }
50% { transform: scale(1.2); opacity: 0.1; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.api-section { margin: 15px 0; }
.api-section h3 {
color: var(--green);
margin: 0 0 var(--space) 0;
}
.api-method {
font-family: monospace;
margin: 3px 0;
color: var(--gray);
}
.api-method .method-name { color: var(--blue); font-weight: bold; }
.api-method .return-type { color: #6f42c1; }
.api-method .param { color: #e83e8c; }
.api-description {
margin: var(--space) 0;
}
.api-options {
color: var(--gray);
}
.help-text {
color: var(--gray);
margin: var(--space) 0;
}
.code-comment {
color: #68d391;
}
.event-log {
background: var(--dark);
color: #0f0;
font-family: monospace;
padding: var(--space);
border-radius: var(--radius);
height: 150px;
overflow-y: auto;
white-space: pre-wrap;
margin: var(--space) 0;
}
.listener-counter {
background: var(--blue);
color: var(--white);
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
margin-left: 8px;
}
@media (max-width: 1200px) {
.main-layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<h1>U-EventEmitter Demo</h1>
<div class="main-layout">
<div class="demo-column">
<h2>Basic Event System</h2>
<div class="demo-section">
<div class="controls">
<button onclick="basicDemo()">Basic Subscribe & Emit</button>
<button onclick="multipleListenersDemo()">Add Multiple Listeners</button>
<button onclick="emitToMultiple()">Emit to All</button>
</div>
<div class="status" id="basic-output"></div>
</div>
<div class="demo-section">
<h3>Advanced Features</h3>
<div class="controls">
<button onclick="slotDemo()">Slot-based Handlers</button>
<button onclick="replaceSlotHandler()">Replace Handler</button>
<button onclick="emitSlotEvent()">Test Slot System</button>
</div>
<div class="controls">
<button onclick="autoRemovalDemo()">Self-removing Handler</button>
<button onclick="triggerAutoRemoval()">Trigger Auto-removal</button>
</div>
<div class="status" id="advanced-output"></div>
</div>
<div class="demo-section">
<h3>Interactive Event System</h3>
<div class="input-group">
<input type="text" id="event-name" placeholder="Event name" value="chat">
<input type="text" id="event-data" placeholder="Event data" value="Hello EventEmitter!">
</div>
<div class="controls">
<button onclick="addListener()">Add Listener <span id="listenerCount" class="listener-counter">0</span></button>
<button onclick="emitCustomEvent()">Emit Event</button>
<button onclick="removeAllListeners()">Clear Listeners</button>
</div>
<div class="event-log" id="event-log">Ready for interactive events...</div>
</div>
</div>
<div class="api-column">
<h2>API Overview</h2>
<div class="api-section">
<h3>EventEmitter Constructor</h3>
<div class="api-method"><span class="keyword">new</span> <span class="method-name">EventEmitter</span>()</div>
<div class="api-description">
Creates a new event emitter instance for custom event communication.
</div>
</div>
<div class="api-section">
<h3>Core Methods</h3>
<div class="api-method"><span class="method-name">on</span>(<span class="param">event, handler, slot?</span>) → <span class="return-type">EventEmitter</span></div>
<div class="api-method"><span class="method-name">emit</span>(<span class="param">event, ...args</span>) → <span class="return-type">number</span></div>
<div class="api-method"><span class="method-name">off</span>(<span class="param">event, handler?</span>) → <span class="return-type">EventEmitter</span></div>
<div class="api-method"><span class="method-name">clear</span>(<span class="param">event?</span>) → <span class="return-type">EventEmitter</span></div>
</div>
<div class="api-section">
<h3>Key Features</h3>
<div class="api-description">
<strong>Slot-based Deduplication:</strong> Use slot parameter to replace existing handlers<br>
<strong>Auto-removal:</strong> Handlers returning 'remove_handler' are automatically unsubscribed<br>
<strong>Return Values:</strong> emit() returns the number of handlers called<br>
<strong>Flexible Arguments:</strong> Pass any number of arguments to handlers
</div>
</div>
<div class="api-section">
<h3>Examples</h3>
<pre class="code-example">
<span class="code-comment">// Basic usage</span>
<span class="keyword">const</span> emitter = <span class="keyword">new</span> <span class="highlight">EventEmitter</span>();
emitter.on(<span class="string">'message'</span>, (data) => {
console.log(<span class="string">'Received:'</span>, data);
});
emitter.emit(<span class="string">'message'</span>, <span class="string">'Hello World'</span>);
<span class="code-comment">// Slot-based replacement</span>
emitter.on(<span class="string">'update'</span>, handler1, <span class="string">'ui-updater'</span>);
emitter.on(<span class="string">'update'</span>, handler2, <span class="string">'ui-updater'</span>); <span class="code-comment">// Replaces handler1</span>
<span class="code-comment">// Self-removing handler</span>
emitter.on(<span class="string">'init'</span>, () => {
console.log(<span class="string">'Initialized!'</span>);
<span class="keyword">return</span> <span class="string">'remove_handler'</span>; <span class="code-comment">// Removes itself</span>
});
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Event Patterns</h2>
<div class="demo-section">
<h3>Publisher-Subscriber Pattern</h3>
<div class="controls">
<button onclick="createPublisher()">Create Publisher</button>
<button onclick="addSubscribers()">Add Subscribers</button>
<button onclick="publishNews()">Publish News</button>
</div>
<div class="status" id="pubsub-output"></div>
</div>
<div class="demo-section">
<h3>Component Communication</h3>
<div class="controls">
<button onclick="setupComponents()">Setup Components</button>
<button onclick="componentInteraction()">Trigger Interaction</button>
<button onclick="cascadeEvents()">Cascade Events</button>
</div>
<div class="status" id="component-output"></div>
</div>
<div class="demo-section">
<h3>Handler Counting & Management</h3>
<div class="controls">
<button onclick="handlerCountDemo()">Count Handlers</button>
<button onclick="benchmarkEmission()">Benchmark Emission</button>
</div>
<div class="status" id="count-output"></div>
</div>
</div>
<div class="api-column">
<h2>Advanced Usage</h2>
<div class="api-section">
<h3>Event Management</h3>
<div class="api-description">
<strong>on(event, handler, slot)</strong> - Subscribe to events<br>
• event: String event name<br>
• handler: Function to call when event is emitted<br>
• slot: Optional string key for handler replacement<br><br>
<strong>emit(event, ...args)</strong> - Emit events to all listeners<br>
• Returns the number of handlers that were called<br>
• Passes all additional arguments to handlers<br><br>
<strong>off(event, handler)</strong> - Remove specific handler<br>
• If handler omitted, removes all handlers for event
</div>
</div>
<div class="api-section">
<h3>Slot System</h3>
<div class="api-description">
The slot system prevents duplicate handlers by using string keys:<br>
• Same slot key replaces previous handler<br>
• Useful for UI updates, state management<br>
• Prevents memory leaks from repeated subscriptions<br>
• Slot keys are per-event, not global
</div>
<pre class="code-example">
<span class="code-comment">// Without slots: multiple handlers</span>
emitter.on(<span class="string">'render'</span>, updateUI);
emitter.on(<span class="string">'render'</span>, updateUI); <span class="code-comment">// Now 2 handlers</span>
<span class="code-comment">// With slots: automatic replacement</span>
emitter.on(<span class="string">'render'</span>, updateUI, <span class="string">'ui'</span>);
emitter.on(<span class="string">'render'</span>, updateUI, <span class="string">'ui'</span>); <span class="code-comment">// Still 1 handler</span>
</pre>
</div>
<div class="api-section">
<h3>Performance Patterns</h3>
<div class="api-description">
<strong>Event Namespacing:</strong> Use dot notation for hierarchical events<br>
<strong>Batch Operations:</strong> Group related events for efficiency<br>
<strong>Handler Cleanup:</strong> Use slots or off() to prevent memory leaks<br>
<strong>Conditional Emission:</strong> Check handler count before expensive operations
</div>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
</div>
<div class="api-column">
<div class="api-section">
<h3>Memory Management</h3>
<div class="api-description">
<strong>Use Slots:</strong> Prevent duplicate handlers with slot keys<br>
<strong>Clean Up:</strong> Call off() when components are destroyed<br>
<strong>Self-removal:</strong> Use 'remove_handler' return for one-time events<br>
<strong>Clear All:</strong> Use clear() to remove all listeners for an event
</div>
</div>
<div class="api-section">
<h3>Common Patterns</h3>
<pre class="code-example">
<span class="code-comment">// Request-Response pattern</span>
emitter.on(<span class="string">'data.request'</span>, (type, callback) => {
<span class="keyword">const</span> data = fetchData(type);
callback(data);
});
<span class="code-comment">// State change notifications</span>
emitter.on(<span class="string">'state.change'</span>, (oldState, newState) => {
updateUI(newState);
logStateChange(oldState, newState);
});
<span class="code-comment">// Error handling</span>
emitter.on(<span class="string">'error'</span>, (error, context) => {
console.error(<span class="string">'Error in'</span>, context, error);
showErrorToUser(error.message);
});
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Library Info</h2>
<div class="demo-section">
<div class="status" id="libraryInfo">Loading library information...</div>
<h3>Event Statistics:</h3>
<div id="eventStats" class="status"></div>
</div>
</div>
<div class="api-column">
</div>
</div>
</div>
<script src="u-events.js"></script>
<script>
const demoEmitter = new EventEmitter();
const interactiveEmitter = new EventEmitter();
const publisherEmitter = new EventEmitter();
const gameEmitter = new EventEmitter();
const uiEmitter = new EventEmitter();
let listenerCount = 0;
let gameStats = {
eventsEmitted: 0,
handlersExecuted: 0,
totalEvents: 0
};
function log(message, outputId = 'basic-output') {
const output = document.getElementById(outputId);
const timestamp = new Date().toLocaleTimeString();
output.textContent += `[${timestamp}] ${message}\n`;
output.scrollTop = output.scrollHeight;
flashIndicator('basicIndicator');
}
function clearOutput(outputId) {
document.getElementById(outputId).textContent = '';
}
function flashIndicator(indicatorId) {
const indicator = document.getElementById(indicatorId);
if (indicator) {
indicator.classList.remove('active');
setTimeout(() => indicator.classList.add('active'), 10);
setTimeout(() => indicator.classList.remove('active'), 500);
}
}
function basicDemo() {
clearOutput('basic-output');
demoEmitter.on('greeting', (name) => {
log(`Hello, ${name}!`, 'basic-output');
});
demoEmitter.emit('greeting', 'World');
demoEmitter.emit('greeting', 'EventEmitter');
gameStats.eventsEmitted += 2;
updateEventStats();
}
function multipleListenersDemo() {
demoEmitter.on('multi-test', (data) => {
log(`Handler 1: ${data}`, 'basic-output');
});
demoEmitter.on('multi-test', (data) => {
log(`Handler 2: ${data}`, 'basic-output');
});
demoEmitter.on('multi-test', (data) => {
log(`Handler 3: ${data}`, 'basic-output');
});
log('Added 3 listeners for "multi-test" event', 'basic-output');
}
function emitToMultiple() {
const count = demoEmitter.emit('multi-test', 'Hello from multiple demo!');
log(`Event emitted to ${count} handlers`, 'basic-output');
gameStats.eventsEmitted++;
gameStats.handlersExecuted += count;
updateEventStats();
}
function slotDemo() {
clearOutput('advanced-output');
demoEmitter.on('slot-event', (msg) => {
log(`Slot handler v1: ${msg}`, 'advanced-output');
}, 'demo-slot');
log('Added handler with slot key "demo-slot"', 'advanced-output');
}
function replaceSlotHandler() {
demoEmitter.on('slot-event', (msg) => {
log(`Slot handler v2 (replaced): ${msg}`, 'advanced-output');
}, 'demo-slot');
log('Replaced handler using same slot key', 'advanced-output');
}
function emitSlotEvent() {
const count = demoEmitter.emit('slot-event', 'Testing slot replacement');
log(`Emitted to ${count} handler(s)`, 'advanced-output');
gameStats.eventsEmitted++;
gameStats.handlersExecuted += count;
updateEventStats();
}
let autoRemovalCount = 0;
function autoRemovalDemo() {
demoEmitter.on('auto-remove', (msg) => {
autoRemovalCount++;
log(`Auto-removal handler called ${autoRemovalCount} time(s): ${msg}`, 'advanced-output');
return 'remove_handler';
});
log('Added self-removing handler', 'advanced-output');
}
function triggerAutoRemoval() {
const count = demoEmitter.emit('auto-remove', 'This handler will remove itself');
log(`Handlers called: ${count}`, 'advanced-output');
setTimeout(() => {
const count2 = demoEmitter.emit('auto-remove', 'This should call 0 handlers');
log(`Second emit - handlers called: ${count2}`, 'advanced-output');
}, 1000);
gameStats.eventsEmitted += 2;
gameStats.handlersExecuted += count;
updateEventStats();
}
function addListener() {
const eventName = document.getElementById('event-name').value;
if (!eventName) return;
listenerCount++;
const listenerId = listenerCount;
interactiveEmitter.on(eventName, (data) => {
const logEl = document.getElementById('event-log');
logEl.textContent += `[Listener ${listenerId}] ${eventName}: ${data}\n`;
logEl.scrollTop = logEl.scrollHeight;
});
const logEl = document.getElementById('event-log');
logEl.textContent += `Added listener ${listenerId} for "${eventName}"\n`;
logEl.scrollTop = logEl.scrollHeight;
updateListenerCounter();
}
function emitCustomEvent() {
const eventName = document.getElementById('event-name').value;
const eventData = document.getElementById('event-data').value;
if (!eventName) return;
const count = interactiveEmitter.emit(eventName, eventData);
const logEl = document.getElementById('event-log');
logEl.textContent += `Emitted "${eventName}" to ${count} listener(s)\n`;
logEl.scrollTop = logEl.scrollHeight;
gameStats.eventsEmitted++;
gameStats.handlersExecuted += count;
updateEventStats();
}
function removeAllListeners() {
const eventName = document.getElementById('event-name').value;
if (!eventName) return;
interactiveEmitter.off(eventName);
const logEl = document.getElementById('event-log');
logEl.textContent += `Removed all listeners for "${eventName}"\n`;
logEl.scrollTop = logEl.scrollHeight;
}
function updateListenerCounter() {
const counter = document.getElementById('listenerCount');
counter.textContent = listenerCount.toString();
}
function createPublisher() {
clearOutput('pubsub-output');
log('Publisher created', 'pubsub-output');
}
function addSubscribers() {
publisherEmitter.on('news', (headline, content) => {
log(`News Subscriber: ${headline}`, 'pubsub-output');
});
publisherEmitter.on('news', (headline, content) => {
log(`Mobile App: New article "${headline}"`, 'pubsub-output');
});
publisherEmitter.on('news', (headline, content) => {
log(`Email Service: Sending newsletter with "${headline}"`, 'pubsub-output');
});
log('Added 3 subscribers to news events', 'pubsub-output');
}
function publishNews() {
const headlines = [
'EventEmitter Pattern Increases Developer Productivity',
'New Features Added to Event System',
'Best Practices for Event-Driven Architecture'
];
const headline = headlines[Math.floor(Math.random() * headlines.length)];
const count = publisherEmitter.emit('news', headline, 'Article content here...');
log(`Published "${headline}" to ${count} subscribers`, 'pubsub-output');
gameStats.eventsEmitted++;
gameStats.handlersExecuted += count;
updateEventStats();
}
function setupComponents() {
clearOutput('component-output');
demoEmitter.on('ui.update', (data) => {
log(`UI Component: Updating display with ${data}`, 'component-output');
});
demoEmitter.on('data.request', (type) => {
log(`Data Component: Fetching ${type} data`, 'component-output');
setTimeout(() => {
demoEmitter.emit('data.response', `${type} data loaded`);
}, 500);
});
demoEmitter.on('data.response', (data) => {
log(`Logger: Data received - ${data}`, 'component-output');
});
log('Components setup complete', 'component-output');
}
function componentInteraction() {
demoEmitter.emit('data.request', 'user');
gameStats.eventsEmitted++;
updateEventStats();
}
function cascadeEvents() {
demoEmitter.emit('ui.update', 'new theme');
setTimeout(() => {
demoEmitter.emit('ui.update', 'user preferences');
}, 300);
setTimeout(() => {
demoEmitter.emit('ui.update', 'layout changes');
}, 600);
gameStats.eventsEmitted += 3;
updateEventStats();
}
function handlerCountDemo() {
clearOutput('count-output');
demoEmitter.on('count-test', () => log('Handler A executed', 'count-output'));
demoEmitter.on('count-test', () => log('Handler B executed', 'count-output'));
demoEmitter.on('count-test', () => log('Handler C executed', 'count-output'));
const count = demoEmitter.emit('count-test');
log(`Total handlers executed: ${count}`, 'count-output');
gameStats.eventsEmitted++;
gameStats.handlersExecuted += count;
updateEventStats();
}
function benchmarkEmission() {
const start = performance.now();
let totalHandlers = 0;
for (let i = 0; i < 1000; i++) {
totalHandlers += demoEmitter.emit('count-test');
}
const end = performance.now();
log(`Benchmark: 1000 emissions took ${(end - start).toFixed(2)}ms`, 'count-output');
log(`Total handlers called: ${totalHandlers}`, 'count-output');
gameStats.eventsEmitted += 1000;
gameStats.handlersExecuted += totalHandlers;
updateEventStats();
}
function setupGameDemo() {
clearOutput('game-output');
gameEmitter.on('player.move', (x, y) => {
log(`Player moved to (${x}, ${y})`, 'game-output');
});
gameEmitter.on('enemy.spawn', (type, level) => {
log(`${type} enemy spawned at level ${level}`, 'game-output');
});
gameEmitter.on('score.update', (points, combo) => {
log(`Score: ${points} points (${combo}x combo)`, 'game-output');
});
gameEmitter.on('game.over', (finalScore) => {
log(`Game Over! Final score: ${finalScore}`, 'game-output');
});
log('Game event system initialized', 'game-output');
}
function simulateGameplay() {
const actions = [
() => gameEmitter.emit('player.move', Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)),
() => gameEmitter.emit('enemy.spawn', ['goblin', 'orc', 'dragon'][Math.floor(Math.random() * 3)], Math.floor(Math.random() * 5) + 1),
() => gameEmitter.emit('score.update', Math.floor(Math.random() * 1000), Math.floor(Math.random() * 5) + 1),
];
let actionCount = 0;
const gameLoop = setInterval(() => {
const action = actions[Math.floor(Math.random() * actions.length)];
action();
actionCount++;
gameStats.eventsEmitted++;
if (actionCount >= 8) {
clearInterval(gameLoop);
setTimeout(() => {
gameEmitter.emit('game.over', Math.floor(Math.random() * 10000));
gameStats.eventsEmitted++;
updateEventStats();
}, 1000);
}
}, 500);
updateEventStats();
}
function showGameStats() {
log(`Game Statistics:`, 'game-output');
log(`Events emitted: ${gameStats.eventsEmitted}`, 'game-output');
log(`Handlers executed: ${gameStats.handlersExecuted}`, 'game-output');
log(`Avg handlers per event: ${(gameStats.handlersExecuted / gameStats.eventsEmitted || 0).toFixed(2)}`, 'game-output');
}
// UI Demo
function setupUIDemo() {
clearOutput('ui-output');
uiEmitter.on('button.click', (buttonId) => {
log(`Button clicked: ${buttonId}`, 'ui-output');
});
uiEmitter.on('form.submit', (formData) => {
log(`Form submitted: ${JSON.stringify(formData)}`, 'ui-output');
});
uiEmitter.on('modal.open', (modalType) => {
log(`Modal opened: ${modalType}`, 'ui-output');
});
uiEmitter.on('theme.change', (theme) => {
log(`Theme changed to: ${theme}`, 'ui-output');
});
log('UI event handlers registered', 'ui-output');
}
function simulateUserActions() {
const actions = [
() => uiEmitter.emit('button.click', 'submit-btn'),
() => uiEmitter.emit('button.click', 'cancel-btn'),
() => uiEmitter.emit('form.submit', { name: 'John', email: 'john@example.com' }),
() => uiEmitter.emit('modal.open', 'settings'),
() => uiEmitter.emit('modal.open', 'help'),
() => uiEmitter.emit('theme.change', 'dark'),
() => uiEmitter.emit('theme.change', 'light'),
];
actions.forEach((action, index) => {
setTimeout(() => {
action();
gameStats.eventsEmitted++;
if (index === actions.length - 1) {
updateEventStats();
}
}, index * 400);
});
}
function updateLibraryInfo() {
const info = document.getElementById('libraryInfo');
info.textContent = `
EventEmitter Instances: Multiple active instances
Global Event Statistics: ${gameStats.totalEvents} total events processed
Memory Usage: Efficient slot-based deduplication
Performance: Sub-millisecond event emission
`.trim();
}
function updateEventStats() {
gameStats.totalEvents = gameStats.eventsEmitted;
const stats = document.getElementById('eventStats');
stats.textContent = `
Events Emitted: ${gameStats.eventsEmitted}
Handlers Executed: ${gameStats.handlersExecuted}
Average Handlers per Event: ${(gameStats.handlersExecuted / gameStats.eventsEmitted || 0).toFixed(2)}
Active Listeners: ${listenerCount}
`.trim();
}
document.addEventListener('DOMContentLoaded', function() {
updateLibraryInfo();
updateEventStats();
updateListenerCounter();
setInterval(updateLibraryInfo, 3000);
});
</script>
</body>
</html>

View File

@ -1,92 +0,0 @@
(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<eventName, Map<slotKey|Symbol, Function>> */
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;
}));

View File

@ -1,136 +0,0 @@
(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,
};
}));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,977 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>U-Macrobars.js Demo</title>
<style>
:root {
--space: 8px;
--radius: 5px;
--gray: #6c757d;
--gray-bg: #f5f5f5;
--blue: #007acc;
--green: #28a745;
--white: white;
--dark: #333;
--max-width: 1400px;
--sidebar: 400px;
}
* { box-sizing: border-box; }
body {
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: var(--max-width);
margin: 0 auto;
padding: var(--space);
background: var(--gray-bg);
}
.container {
background: var(--white);
padding: calc(var(--space) * 1.5);
border-radius: var(--radius);
}
.main-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--space) * 1.5);
}
.api-column {
background: var(--gray-bg);
padding: var(--space);
border-radius: var(--radius);
border-left: 4px solid var(--green);
}
h1 {
color: var(--dark);
text-align: center;
margin-bottom: calc(var(--space) * 1.5);
grid-column: 1 / -1;
}
h2 {
color: var(--gray);
border-bottom: 2px solid #e0e0e0;
padding-bottom: var(--space);
margin-top: calc(var(--space) * 1.5);
}
.demo-section {
margin: var(--space) 0;
padding: var(--space);
background: var(--gray-bg);
border-radius: var(--radius);
border-left: 4px solid var(--blue);
}
.controls, .slider-group {
display: flex;
gap: var(--space);
margin: 15px 0;
}
.controls { flex-wrap: wrap; }
.slider-group { align-items: center; }
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
button {
background: var(--blue);
color: var(--white);
border: none;
padding: var(--space) var(--space);
border-radius: var(--radius);
cursor: pointer;
transition: all 0.3s ease;
}
button:hover { filter: brightness(0.9); }
button:disabled { background: #ccc; cursor: not-allowed; }
input[type="range"] { flex: 1; max-width: 200px; }
.status, .code-example {
font-family: monospace;
padding: var(--space);
border-radius: var(--radius);
margin: var(--space) 0;
}
.status {
background: var(--dark);
color: #0f0;
white-space: pre-wrap;
max-height: 150px;
overflow-y: auto;
}
.code-example {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
overflow-x: auto;
margin: var(--space) 0;
}
.highlight { color: #68d391; }
.keyword { color: #fbb6ce; }
.string { color: #fbd38d; }
.template-visual {
height: 100px;
background: linear-gradient(45deg, #667eea, #764ba2);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-weight: bold;
margin: 15px 0;
position: relative;
overflow: hidden;
}
.template-animation {
position: absolute;
inset: 0;
opacity: 0.3;
background: repeating-linear-gradient(90deg, transparent 0 10px, rgba(255,255,255,0.2) 10px 20px);
animation: slide 3s linear infinite;
}
@keyframes slide { to { transform: translateX(20px); } }
.api-section { margin: 15px 0; }
.api-section h3 {
color: var(--green);
margin: 0 0 var(--space) 0;
}
.api-method {
font-family: monospace;
margin: 3px 0;
color: var(--gray);
}
.api-method .method-name { color: var(--blue); font-weight: bold; }
.api-method .return-type { color: #6f42c1; }
.api-method .param { color: #e83e8c; }
.api-description {
margin: var(--space) 0;
}
.api-options {
color: var(--gray);
}
.template-code {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: var(--radius);
margin: var(--space) 0;
overflow-x: auto;
}
.output {
background: var(--white);
border: 2px solid var(--green);
padding: var(--space);
border-radius: var(--radius);
margin: var(--space) 0;
min-height: 40px;
}
.code-comment {
color: #68d391;
}
textarea {
width: 100%;
padding: var(--space);
border: 1px solid #ccc;
border-radius: var(--radius);
font-family: monospace;
margin: var(--space) 0;
resize: vertical;
}
.interactive-editor {
background: var(--white);
padding: calc(var(--space) * 1.5);
border-radius: var(--radius);
border: 2px solid var(--blue);
}
.help-text {
color: var(--gray);
margin: var(--space) 0;
font-style: italic;
}
.feature-grid button {
padding: calc(var(--space) * 1.5) var(--space);
font-size: 14px;
transition: all 0.2s ease;
}
.feature-grid button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 122, 204, 0.3);
}
.feature-grid button:active:not(:disabled) {
transform: translateY(0);
}
@media (max-width: 1200px) {
.main-layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body onload="initializeDemos();">
<div class="container">
<h1>U-Macrobars.js Demo</h1>
<div class="main-layout">
<div class="demo-column">
<h2>Basic Field Output</h2>
<div class="demo-section">
<div class="template-code">{{name}} is {{age}} years old and works as {{job or "unemployed"}}</div>
<div class="output" id="basic-output"></div>
<div class="controls">
<button onclick="runBasicDemo()">Run Basic Demo</button>
<button onclick="runBasicDemoVariant()">Try Different Data</button>
</div>
</div>
<div class="demo-section">
<h3>Number Formatting</h3>
<div class="template-code">Price: ${{%price}} | Large Number: {{~bigNumber}}</div>
<div class="output" id="number-output"></div>
<div class="controls">
<button onclick="runNumberDemo()">Format Numbers</button>
<button onclick="runNumberVariants()">Try Different Numbers</button>
</div>
</div>
<div class="demo-section">
<h3>Default Values & Safety</h3>
<div class="template-code">{{username or "Guest"}} | {{profile.bio or "No bio available"}}</div>
<div class="output" id="default-output"></div>
<div class="controls">
<button onclick="runDefaultDemo()">Test Defaults</button>
<button onclick="runSafetyDemo()">HTML Safety Demo</button>
</div>
</div>
</div>
<div class="api-column">
<h2>Field Output API</h2>
<div class="api-section">
<h3>Basic Syntax</h3>
<div class="api-method"><span class="method-name">{{field}}</span><span class="return-type">Safe HTML output</span></div>
<div class="api-method"><span class="method-name">{{{field}}}</span><span class="return-type">Raw HTML output</span></div>
<div class="api-method"><span class="method-name">{{:variable}}</span><span class="return-type">Direct variable</span></div>
<div class="api-method"><span class="method-name">{{field or "default"}}</span><span class="return-type">With fallback</span></div>
</div>
<div class="api-section">
<h3>Number Formatting</h3>
<div class="api-method"><span class="method-name">{{%number}}</span><span class="return-type">2 decimal places</span></div>
<div class="api-method"><span class="method-name">{{~number}}</span><span class="return-type">Rounded (1k, 1M)</span></div>
</div>
<div class="api-section">
<h3>Examples</h3>
<pre class="code-example">
<span class="code-comment">// Basic field output</span>
<span class="keyword">const</span> template = Macrobars.compile(<span class="string">'{{name}}'</span>);
<span class="keyword">const</span> result = template({name: <span class="string">'John'</span>});
<span class="code-comment">// With defaults</span>
<span class="keyword">const</span> withDefault = <span class="string">'{{title or "Untitled"}}'</span>;
<span class="code-comment">// Number formatting</span>
<span class="keyword">const</span> price = <span class="string">'${{%cost}}'</span>; <span class="code-comment">// $12.34</span>
<span class="keyword">const</span> count = <span class="string">'{{~views}}'</span>; <span class="code-comment">// 1.2k</span>
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Control Structures</h2>
<div class="demo-section">
<h3>Conditionals</h3>
<div class="template-code">{{#if isLoggedIn}}
Welcome back, {{username}}!
{{#else}}
Please log in to continue.
{{/if}}</div>
<div class="output" id="conditional-output"></div>
<div class="controls">
<button onclick="runConditionalDemo(true)">👤 Logged In</button>
<button onclick="runConditionalDemo(false)">🚪 Not Logged In</button>
</div>
</div>
<div class="demo-section">
<h3>Loops & Iteration</h3>
<div class="template-code">{{#each items}}
• {{number}}. {{name}} - ${{price}}
{{/each}}</div>
<div class="output" id="loop-output"></div>
<div class="controls">
<button onclick="runLoopDemo()">🔄 Process List</button>
<button onclick="runNamedLoopDemo()">📝 Named Iteration</button>
</div>
</div>
<div class="demo-section">
<h3>Equality & Lookup</h3>
<div class="template-code">{{#eq status "active"}}User is active{{/eq}}
{{lookup user "permissions"}}</div>
<div class="output" id="equality-output"></div>
<div class="controls">
<button onclick="runEqualityDemo()">⚖️ Test Equality</button>
<button onclick="runLookupDemo()">🔍 Dynamic Lookup</button>
</div>
</div>
</div>
<div class="api-column">
<h2>Control Flow API</h2>
<div class="api-section">
<h3>Conditionals</h3>
<div class="api-method"><span class="method-name">{{#if condition}}</span>...{{/if}}</div>
<div class="api-method"><span class="method-name">{{#else}}</span><span class="return-type">Alternative branch</span></div>
<div class="api-description">
Conditional rendering based on truthy values. Supports nested conditions.
</div>
</div>
<div class="api-section">
<h3>Loops</h3>
<div class="api-method"><span class="method-name">{{#each items}}</span>...{{/each}}</div>
<div class="api-method"><span class="method-name">{{#each items as item}}</span>...{{/each}}</div>
<div class="api-description">
• Standard: <strong>data</strong> becomes current item<br>
• Named: <strong>item</strong> becomes current item, data preserved
</div>
</div>
<div class="api-section">
<h3>Comparison & Lookup</h3>
<div class="api-method"><span class="method-name">{{#eq val1 val2}}</span>...{{/eq}}</div>
<div class="api-method"><span class="method-name">{{eq val1 val2}}</span><span class="return-type">"true" or ""</span></div>
<div class="api-method"><span class="method-name">{{#lookup obj key}}</span>...{{/lookup}}</div>
<div class="api-method"><span class="method-name">{{lookup obj key}}</span><span class="return-type">value or ""</span></div>
</div>
<div class="api-section">
<h3>Examples</h3>
<pre class="code-example">
<span class="code-comment">// Conditionals</span>
<span class="string">'{{#if user.isAdmin}}Admin Panel{{/if}}'</span>
<span class="code-comment">// Named loops</span>
<span class="string">'{{#each products as product}}'</span>
<span class="string">'{{product.name}} - {{data.storeName}}'</span>
<span class="string">'{{/each}}'</span>
<span class="code-comment">// Equality & lookup</span>
<span class="string">'{{#eq user.role "admin"}}Secret{{/eq}}'</span>
<span class="string">'{{lookup config "theme"}}'</span>
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Advanced Features</h2>
<div class="demo-section">
<h3>Event Binding</h3>
<div class="template-code">&lt;button {{@click="handleClick"}}&gt;Click Count: {{clickCount}}&lt;/button&gt;</div>
<div class="output" id="event-output"></div>
<div class="controls">
<button onclick="runEventDemo()">🎯 Setup Interactive Button</button>
<button onclick="runMultiEventDemo()">⚡ Multiple Events</button>
</div>
</div>
<div class="demo-section">
<h3>Code Blocks</h3>
<div class="template-code">&lt;script&gt;var computed = data.value * 2;&lt;/script&gt;
Result: {{:computed}}</div>
<div class="output" id="code-output"></div>
<div class="controls">
<button onclick="runCodeDemo()">💻 Execute Code</button>
<button onclick="runDeferDemo()">⏰ Deferred Execution</button>
</div>
</div>
<div class="demo-section">
<h3>Components</h3>
<div class="template-code">{{#component userCard}}</div>
<div class="output" id="component-output"></div>
<div class="controls">
<button onclick="runComponentDemo()">🧩 Load Component</button>
<button onclick="createCustomComponent()">✨ Create Custom</button>
</div>
</div>
</div>
<div class="api-column">
<h2>Advanced API</h2>
<div class="api-section">
<h3>Event Binding</h3>
<div class="api-method"><span class="method-name">{{@event="handler"}}</span><span class="return-type">DOM attribute</span></div>
<div class="api-method"><span class="method-name">template.renderTo</span>(<span class="param">container, data</span>)</div>
<div class="api-description">
Events are automatically bound when using renderTo(). Handler can reference data properties or global functions.
</div>
</div>
<div class="api-section">
<h3>Code Execution</h3>
<div class="api-method"><span class="method-name">&lt;script&gt;</span>...&lt;/script&gt;</div>
<div class="api-method"><span class="method-name">&lt;defer&gt;</span>...&lt;/defer&gt;</div>
<div class="api-method"><span class="method-name">&lt;?</span> code <span class="method-name">?&gt;</span></div>
<div class="api-method"><span class="method-name">&lt;?=</span> expression <span class="method-name">?&gt;</span></div>
</div>
<div class="api-section">
<h3>Components & Compilation</h3>
<div class="api-method"><span class="method-name">Macrobars.compile</span>(<span class="param">template, options</span>)</div>
<div class="api-method"><span class="method-name">Macrobars.createComponents</span>(<span class="param">definitions</span>)</div>
</div>
<div class="api-section">
<h3>Examples</h3>
<pre class="code-example">
<span class="code-comment">// Event binding with renderTo</span>
<span class="keyword">const</span> template = Macrobars.compile(
<span class="string">'&lt;button {{@click="increment"}}&gt;{{count}}&lt;/button&gt;'</span>
);
template.renderTo(<span class="string">'#container'</span>, {
count: <span class="highlight">0</span>,
increment: <span class="keyword">function</span>() { <span class="keyword">this</span>.count++; }
});
<span class="code-comment">// Components</span>
<span class="keyword">const</span> components = Macrobars.createComponents({
userCard: <span class="string">'&lt;div&gt;{{name}} - {{email}}&lt;/div&gt;'</span>
});
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Template Editor</h2>
<div class="interactive-editor">
<label for="template-input"><strong>Template:</strong></label>
<textarea id="template-input" rows="6" placeholder="Enter your template here">Hello {{name}}!
{{#if age}}You are {{age}} years old.{{/if}}
{{#each hobbies as hobby}}
• {{:hobby}}
{{/each}}
Total score: {{~score}}</textarea>
<label for="data-input"><strong>Data (JSON):</strong></label>
<textarea id="data-input" rows="6" placeholder="Enter JSON data here">{
"name": "Alice",
"age": 25,
"score": 98750,
"hobbies": ["reading", "coding", "gaming"]
}</textarea>
<div class="controls">
<button onclick="runCustomTemplate()">Render Template</button>
<button onclick="loadExampleTemplate('basic')">Load Basic Example</button>
<button onclick="loadExampleTemplate('advanced')">Load Advanced Example</button>
<button onclick="clearEditor()">Clear All</button>
</div>
<div class="output" id="dynamic-content" style="white-space: pre;">Click "Render Template"</div>
<div class="api-section">
<h3>Template Information</h3>
<div class="status" id="template-info">Ready.</div>
</div>
</div>
</div>
<div class="api-column">
<h2>Compilation & Debugging</h2>
<div class="api-section">
<h3>Compilation Options</h3>
<div class="api-method"><span class="method-name">decimals</span>: <span class="param">number</span><span class="return-type">Number precision</span></div>
<div class="api-method"><span class="method-name">strict</span>: <span class="param">boolean</span><span class="return-type">Strict mode</span></div>
<div class="api-method"><span class="method-name">components</span>: <span class="param">object</span><span class="return-type">Reusable templates</span></div>
</div>
<div class="api-section">
<h3>Debugging Properties</h3>
<div class="api-method"><span class="method-name">template.tokens</span><span class="return-type">Parsed tokens</span></div>
<div class="api-method"><span class="method-name">template.gensource</span><span class="return-type">Generated JS</span></div>
<div class="api-method"><span class="method-name">template.event_bindings</span><span class="return-type">Event data</span></div>
</div>
<div class="api-section">
<h3>Utility Functions</h3>
<div class="api-method"><span class="method-name">Macrobars.safe_out</span>(<span class="param">text, default</span>)</div>
<div class="api-method"><span class="method-name">Macrobars.num_out</span>(<span class="param">number, decimals</span>)</div>
<div class="api-method"><span class="method-name">Macrobars.num_out_round</span>(<span class="param">number</span>)</div>
</div>
<div class="api-section">
<h3>Example Templates</h3>
<pre class="code-example">
<span class="code-comment">// Complete example</span>
<span class="keyword">const</span> template = Macrobars.compile(<span class="string">`
&lt;div class="user-card"&gt;
&lt;h3&gt;{{name or "Unknown User"}}&lt;/h3&gt;
{{#if profile.verified}}✅ Verified{{/if}}
&lt;p&gt;Balance: ${{%balance}}&lt;/p&gt;
{{#each achievements}}
&lt;span class="badge"&gt;{{data}}&lt;/span&gt;
{{/each}}
&lt;/div&gt;
`</span>, { decimals: <span class="highlight">2</span> });
<span class="keyword">const</span> result = template(userData);
</pre>
</div>
</div>
</div>
</div>
<script src="u-macrobars.js"></script>
<script>
let currentTemplate = null;
let clickCount = 0;
let components = {};
// Initialize demos on page load
function initializeDemos() {
runBasicDemo();
runNumberDemo();
updateTemplateInfo('Ready.');
}
// Utility functions
function log(message) {
//console.log(`Macrobars Demo: ${message}`);
}
function updateTemplateInfo(info) {
const element = document.getElementById('template-info');
if (element) {
element.textContent = info;
}
}
function updateVisualAnimation(playing) {
const animations = document.querySelectorAll('.template-animation');
animations.forEach(anim => {
anim.style.animationPlayState = playing ? 'running' : 'paused';
});
}
// Demo 1: Basic field output
function runBasicDemo() {
try {
const template = Macrobars.compile('{{name}} is {{age}} years old and works as {{job or "unemployed"}}');
const data = { name: "John Doe", age: 30 };
const result = template(data);
document.getElementById('basic-output').innerHTML = result;
updateVisualAnimation(true);
setTimeout(() => updateVisualAnimation(false), 1000);
} catch (error) {
document.getElementById('basic-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runBasicDemoVariant() {
try {
const template = Macrobars.compile('{{name}} is {{age}} years old and works as {{job or "unemployed"}}');
const data = { name: "Jane Smith", age: 28, job: "Software Engineer" };
const result = template(data);
document.getElementById('basic-output').innerHTML = result;
updateVisualAnimation(true);
setTimeout(() => updateVisualAnimation(false), 1000);
} catch (error) {
document.getElementById('basic-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 2: Number formatting
function runNumberDemo() {
try {
const template = Macrobars.compile('Price: ${{%price}} | Large Number: {{~bigNumber}}');
const data = { price: 123.456, bigNumber: 1234567 };
const result = template(data);
document.getElementById('number-output').innerHTML = result;
} catch (error) {
document.getElementById('number-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runNumberVariants() {
try {
const template = Macrobars.compile('Price: ${{%price}} | Large Number: {{~bigNumber}} | Small: {{%small}}');
const data = { price: 999.99, bigNumber: 42850000, small: 1.2345 };
const result = template(data);
document.getElementById('number-output').innerHTML = result;
} catch (error) {
document.getElementById('number-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 3: Default values & safety
function runDefaultDemo() {
try {
const template = Macrobars.compile('{{username or "Guest"}} | {{profile.bio or "No bio available"}}');
const data = { username: "", profile: {} }; // Empty data to test defaults
const result = template(data);
document.getElementById('default-output').innerHTML = result;
} catch (error) {
document.getElementById('default-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runSafetyDemo() {
try {
const template = Macrobars.compile('Safe: {{userInput}} | Unsafe: {{{userInput}}}');
const data = { userInput: '<script>alert("XSS")</' + 'script><b>Bold Text</b>' };
const result = template(data);
document.getElementById('default-output').innerHTML = result;
} catch (error) {
document.getElementById('default-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 4: Conditionals
function runConditionalDemo(isLoggedIn) {
try {
const template = Macrobars.compile('{{#if isLoggedIn}}Welcome back, {{username}}! 🎉{{#else}}Please log in to continue. 🔐{{/if}}');
const data = {
isLoggedIn: isLoggedIn,
username: "Alice"
};
const result = template(data);
document.getElementById('conditional-output').innerHTML = result;
} catch (error) {
document.getElementById('conditional-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 5: Loops
function runLoopDemo() {
try {
const template = Macrobars.compile(
'{{#each items}}' +
'• {{number}}. {{name}} - ${{price}}<br>' +
'{{/each}}'
);
const data = {
items: [
{ number: 1, name: "Magic Widget", price: 19.99 },
{ number: 2, name: "Super Gadget", price: 29.50 },
{ number: 3, name: "Ultra Tool", price: 15.75 }
]
};
const result = template(data);
document.getElementById('loop-output').innerHTML = result;
} catch (error) {
document.getElementById('loop-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runNamedLoopDemo() {
try {
const template = Macrobars.compile('Store: {{storeName}}<br>{{#each products as product}}→ {{product.name}} ({{product.category}}) - Available at {{storeName}}<br>{{/each}}');
const data = {
storeName: "TechMart",
products: [
{ name: "Laptop", category: "Electronics" },
{ name: "Mouse", category: "Accessories" },
{ name: "Keyboard", category: "Accessories" }
]
};
const result = template(data);
document.getElementById('loop-output').innerHTML = result;
} catch (error) {
document.getElementById('loop-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 6: Equality & Lookup
function runEqualityDemo() {
try {
const template = Macrobars.compile('{{#eq status "active"}}✅ User is active{{/eq}}{{#eq status "inactive"}}❌ User is inactive{{/eq}}{{#eq status "pending"}}⏳ User is pending{{/eq}}<br>Inline check: {{eq role "admin"}}');
const data = { status: "active", role: "admin" };
const result = template(data);
document.getElementById('equality-output').innerHTML = result;
} catch (error) {
document.getElementById('equality-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runLookupDemo() {
try {
const template = Macrobars.compile('Theme: {{lookup user "theme"}}<br>{{#lookup user "permissions"}}🔑 Has permissions{{/lookup}}{{#lookup user "missing"}}This won\'t show{{/lookup}}');
const data = {
user: {
theme: "dark",
permissions: ["read", "write"]
}
};
const result = template(data);
document.getElementById('equality-output').innerHTML = result;
} catch (error) {
document.getElementById('equality-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 7: Event binding
function runEventDemo() {
try {
clickCount = 0;
const template = Macrobars.compile('<button {{@click="handleClick"}} style="padding: 10px;">🖱️ Click Count: {{clickCount}}</button>');
const data = {
clickCount: clickCount,
handleClick: function() {
clickCount++;
runEventDemo(); // Re-render with new count
}
};
const container = document.getElementById('event-output');
template.renderTo(container, data);
} catch (error) {
document.getElementById('event-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runMultiEventDemo() {
try {
var templateStr = '<div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px;">';
templateStr += '<button {{@click="increment"}} style="margin: 5px; padding: 8px;"> Add</button>';
templateStr += '<button {{@click="decrement"}} style="margin: 5px; padding: 8px;"> Subtract</button>';
templateStr += '<button {{@click="reset"}} style="margin: 5px; padding: 8px;">🔄 Reset</button>';
templateStr += '<br><br>';
templateStr += '<strong>Counter: {{counter}}</strong>';
templateStr += '</div>';
const template = Macrobars.compile(templateStr);
let counter = 0;
const data = {
counter: counter,
increment: function() {
counter++;
data.counter = counter;
template.renderTo(document.getElementById('event-output'), data);
},
decrement: function() {
counter--;
data.counter = counter;
template.renderTo(document.getElementById('event-output'), data);
},
reset: function() {
counter = 0;
data.counter = counter;
template.renderTo(document.getElementById('event-output'), data);
}
};
template.renderTo(document.getElementById('event-output'), data);
} catch (error) {
document.getElementById('event-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 8: Code blocks
function runCodeDemo() {
try {
var templateStr = '';
templateStr += '<' + 'script>';
templateStr += 'var computed = data.value * 2;';
templateStr += 'var message = "Computed: " + computed;';
templateStr += '<' + '/script>';
templateStr += 'Input: {{value}}<br>';
templateStr += 'Result: {{:computed}}<br>';
templateStr += 'Message: {{:message}}';
const template = Macrobars.compile(templateStr);
const data = { value: 21 };
const result = template(data);
document.getElementById('code-output').innerHTML = result;
} catch (error) {
document.getElementById('code-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function runDeferDemo() {
try {
var templateStr = 'Value: {{value}}<br>' +
'<' + 'defer>' +
'alert("Value was: " + data.value);' +
'<' + '/defer>' +
'<em>Check browser console and alert!</em>';
const template = Macrobars.compile(templateStr);
const data = { value: 42 };
const result = template(data);
document.getElementById('code-output').innerHTML = result;
} catch (error) {
document.getElementById('code-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 9: Components
function runComponentDemo() {
try {
var userCardTemplate = '<div style="border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin: 5px 0;">';
userCardTemplate += '<h4 style="margin: 0 0 10px 0;">👤 {{name}}</h4>';
userCardTemplate += '<p style="margin: 5px 0;"><strong>Email:</strong> {{email}}</p>';
userCardTemplate += '<p style="margin: 5px 0;"><strong>Role:</strong> {{role}}</p>';
userCardTemplate += '{{#if isActive}}<span style="color: green;">✅ Active</span>{{#else}}<span style="color: red;">❌ Inactive</span>{{/if}}';
userCardTemplate += '</div>';
components = Macrobars.createComponents({
userCard: userCardTemplate
});
const template = Macrobars.compile('{{#component userCard}}', { components });
const data = { name: "John Doe", email: "john@example.com", role: "Developer", isActive: true };
const result = template(data);
document.getElementById('component-output').innerHTML = result;
} catch (error) {
document.getElementById('component-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
function createCustomComponent() {
try {
var productCardTemplate = '<div style="border: 2px solid #007acc; padding: 15px; border-radius: 8px; margin: 5px 0; background: #f8f9fa;">';
productCardTemplate += '<h4 style="color: #007acc; margin: 0 0 10px 0;">🛍️ {{productName}}</h4>';
productCardTemplate += '<p><strong>Price:</strong> ${{%price}}</p>';
productCardTemplate += '<p><strong>Stock:</strong> {{stock}} units</p>';
productCardTemplate += '{{#if onSale}}<div style="background: #28a745; color: white; padding: 5px; border-radius: 3px; text-align: center;">🏷️ ON SALE!</div>{{/if}}';
productCardTemplate += '</div>';
components = Macrobars.createComponents({
productCard: productCardTemplate
});
const template = Macrobars.compile('{{#component productCard}}', { components });
const data = { productName: "Magic Widget Pro", price: 299.99, stock: 15, onSale: true };
const result = template(data);
document.getElementById('component-output').innerHTML = result;
} catch (error) {
document.getElementById('component-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
}
}
// Demo 10: Custom template editor
function runCustomTemplate() {
try {
const templateStr = document.getElementById('template-input').value;
const dataStr = document.getElementById('data-input').value;
const data = JSON.parse(dataStr);
currentTemplate = Macrobars.compile(templateStr);
const result = currentTemplate(data);
document.getElementById('dynamic-content').innerHTML = result;
// Update template info
var infoText = 'Compiled successfully!\n';
infoText += 'Tokens: ' + (currentTemplate.tokens ? currentTemplate.tokens.length : 0) + '\n';
infoText += 'Event bindings: ' + (currentTemplate.event_bindings ? currentTemplate.event_bindings.length : 0) + '\n';
infoText += 'Template size: ' + templateStr.length + ' characters';
updateTemplateInfo(infoText);
} catch (error) {
document.getElementById('dynamic-content').innerHTML = '<div style="color: red; padding: 10px; background: #ffe6e6; border-radius: 5px;"><strong>❌ Error:</strong> ' + error.message + '</div>';
updateTemplateInfo('Compilation failed: ' + error.message);
}
}
function loadExampleTemplate(type) {
const templates = {
basic: {
template: '<h3>Welcome {{name}}!</h3>\n<p>{{#if isVip}}🌟 VIP Member{{#else}}Regular Member{{/if}}</p>\n<p>Account Balance: ${{%balance}}</p>\n{{#each notifications}}\n• {{data}}\n{{/each}}',
data: '{\n "name": "Sarah Connor",\n "isVip": true,\n "balance": 1247.50,\n "notifications": ["New message", "System update", "Payment received"]\n}'
},
advanced: {
template: '&lt;div class="dashboard"&gt;\n' +
' &lt;h2&gt;{{company}} Dashboard&lt;/h2&gt;\n' +
' \n' +
' &lt;script&gt;\n' +
' var totalRevenue = 0;\n' +
' data.quarters.forEach(q =&gt; totalRevenue += q.revenue);\n' +
' &lt;/script&gt;\n' +
' \n' +
' &lt;p&gt;&lt;strong&gt;Total Revenue:&lt;/strong&gt; ${{:totalRevenue}}&lt;/p&gt;\n' +
' \n' +
' {{#each quarters as quarter}}\n' +
' &lt;div style="margin: 10px 0; padding: 10px; border-left: 4px solid #007acc;"&gt;\n' +
' &lt;h4&gt;{{quarter.name}}&lt;/h4&gt;\n' +
' &lt;p&gt;Revenue: ${{%quarter.revenue}} {{#eq quarter.trend "up"}}📈{{/eq}}{{#eq quarter.trend "down"}}📉{{/eq}}&lt;/p&gt;\n' +
' {{#lookup quarter "bonus"}}\n' +
' &lt;span style="color: green;"&gt;🎯 Bonus: ${{quarter.bonus}}&lt;/span&gt;\n' +
' {{/lookup}}\n' +
' &lt;/div&gt;\n' +
' {{/each}}\n' +
' \n' +
' &lt;button {{@click="generateReport"}} style="padding: 10px; background: #28a745; color: white; border: none; border-radius: 5px;"&gt;\n' +
' 📊 Generate Report\n' +
' &lt;/button&gt;\n' +
'&lt;/div&gt;',
data: '{\n "company": "TechCorp Industries",\n "quarters": [\n {"name": "Q1 2024", "revenue": 125000, "trend": "up", "bonus": 5000},\n {"name": "Q2 2024", "revenue": 138000, "trend": "up"},\n {"name": "Q3 2024", "revenue": 142000, "trend": "up", "bonus": 7500},\n {"name": "Q4 2024", "revenue": 128000, "trend": "down"}\n ],\n "generateReport": "function() { alert(\'Report generated for \' + this.company); }"\n}'
}
};
const example = templates[type];
if (example) {
document.getElementById('template-input').value = example.template;
document.getElementById('data-input').value = example.data;
runCustomTemplate();
}
}
function clearEditor() {
document.getElementById('template-input').value = '';
document.getElementById('data-input').value = '{}';
document.getElementById('dynamic-content').innerHTML = 'Editor cleared. Enter a template and click "Render Template".';
updateTemplateInfo('Ready.');
}
// Initialize demos on page load
document.addEventListener('DOMContentLoaded', function() {
initializeDemos();
});
</script>
</body>
</html>

View File

@ -1,411 +0,0 @@
(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('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;'));
};
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('<anonymous>');
if(p0 != -1) {
loc = loc.substr(p0+('<anonymous>'.length+1));
loc = loc.substr(0, loc.length-1);
}
return('Macrobars '+es[0]+' ('+loc+')');
}
var signposts = [
{ start : '<script>', end : '</script>', type : 'code' },
{ start : '<defer>', end : '</defer>', type : 'defer' },
{ start : '<?=', end : '?>', type : 'field' },
{ start : '<?:', end : '?>', type : 'var_out' },
{ start : '<?', end : '?>', type : 'code' },
{ start : '<!--?', end : '?-->', 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 += "<script>" + '+JSON.stringify(token.text)+' + "</script>";');
},
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,
});
}));

View File

@ -1,137 +0,0 @@
# macrobars.js
A lightweight JavaScript templating engine with PHP-style syntax and Handlebars-inspired features.
## Basic Usage
```javascript
const template = Macrobars.compile('<h1>{{title}}</h1><p>{{content}}</p>');
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
<script>
var result = data.value * 2;
</script>
<defer>
// Deferred JavaScript execution
</defer>
<?= data.field ?> // PHP-style output
<? var x = data.count; ?> // PHP-style code
```
## Event Binding
```javascript
<button {{@click="handleClick"}}>Click me</button>
```
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
```

View File

@ -1,34 +0,0 @@
: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);
}

View File

@ -1,84 +0,0 @@
(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 = $('<div class="vp-popup-menu">').css({
position: 'absolute',
left: (x|0) + 'px',
top: (y|0) + 'px'
})[0];
let elems = [];
if(!items || !items.length) {
$(menu).append(`<div class="vp-popup-empty">${prop.emptyText || 'No items'}</div>`);
} else {
items.forEach((it, idx) => {
let label = typeof it === 'string' ? it : (it.label || it.screen || it.name || JSON.stringify(it));
let $item = $(`<div class="vp-popup-item" tabindex="0">${label}</div>`)
.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;
}));

View File

@ -1,976 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>U-Query.js Demo</title>
<style>
:root {
--space: 8px;
--radius: 5px;
--gray: #6c757d;
--gray-bg: #f5f5f5;
--blue: #007acc;
--green: #28a745;
--white: white;
--dark: #333;
--max-width: 1400px;
--sidebar: 400px;
}
* { box-sizing: border-box; }
body {
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: var(--max-width);
margin: 0 auto;
padding: var(--space);
background: var(--gray-bg);
}
.container {
background: var(--white);
padding: calc(var(--space) * 1.5);
border-radius: var(--radius);
}
.main-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--space) * 1.5);
}
.api-column {
background: var(--gray-bg);
padding: var(--space);
border-radius: var(--radius);
border-left: 4px solid var(--green);
}
h1 {
color: var(--dark);
text-align: center;
margin-bottom: calc(var(--space) * 1.5);
grid-column: 1 / -1;
}
h2 {
color: var(--gray);
border-bottom: 2px solid #e0e0e0;
padding-bottom: var(--space);
margin-top: calc(var(--space) * 1.5);
}
.demo-section {
margin: var(--space) 0;
padding: var(--space);
background: var(--gray-bg);
border-radius: var(--radius);
border-left: 4px solid var(--blue);
}
.controls, .slider-group {
display: flex;
gap: var(--space);
margin: 15px 0;
}
.controls { flex-wrap: wrap; }
.slider-group { align-items: center; }
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
button {
background: var(--blue);
color: var(--white);
border: none;
padding: var(--space) var(--space);
border-radius: var(--radius);
cursor: pointer;
transition: all 0.3s ease;
}
button:hover { filter: brightness(0.9); }
button:disabled { background: #ccc; cursor: not-allowed; }
input[type="range"] { flex: 1; max-width: 200px; }
input[type="text"] {
padding: var(--space);
border: 1px solid #ddd;
border-radius: var(--radius);
}
.status, .code-example {
font-family: monospace;
padding: var(--space);
border-radius: var(--radius);
margin: var(--space) 0;
}
.status {
background: var(--dark);
color: #0f0;
white-space: pre-wrap;
max-height: 150px;
overflow-y: auto;
}
.code-example {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
overflow-x: auto;
margin: var(--space) 0;
}
.highlight { color: #68d391; }
.keyword { color: #fbb6ce; }
.string { color: #fbd38d; }
.test-element {
padding: var(--space);
margin: 5px;
border: 2px solid #ddd;
background: var(--white);
border-radius: var(--radius);
transition: all 0.3s ease;
}
.highlighted {
background: #ffd700 !important;
border-color: #ffcc00 !important;
}
.hidden { display: none; }
.output {
background: var(--gray-bg);
padding: var(--space);
border-radius: var(--radius);
margin: var(--space) 0;
min-height: 40px;
border-left: 3px solid var(--green);
}
.ajax-visual {
height: 100px;
background: linear-gradient(45deg, #1e3c72, #2a5298);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-weight: bold;
margin: 15px 0;
position: relative;
overflow: hidden;
}
.loading-animation {
position: absolute;
inset: 0;
opacity: 0.3;
background: repeating-linear-gradient(90deg, transparent 0 10px, rgba(255,255,255,0.2) 10px 20px);
animation: wave 2s linear infinite;
}
@keyframes wave { to { transform: translateX(20px); } }
.api-section { margin: 15px 0; }
.api-section h3 {
color: var(--green);
margin: 0 0 var(--space) 0;
}
.api-method {
font-family: monospace;
margin: 3px 0;
color: var(--gray);
}
.api-method .method-name { color: var(--blue); font-weight: bold; }
.api-method .return-type { color: #6f42c1; }
.api-method .param { color: #e83e8c; }
.api-description {
margin: var(--space) 0;
}
.api-options {
color: var(--gray);
}
.code-comment {
color: #68d391;
}
.demo-visual {
background: var(--gray-bg);
padding: calc(var(--space) * 2);
border-radius: var(--radius);
margin: var(--space) 0;
border: 2px dashed #ddd;
text-align: center;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.demo-visual.active {
background: #e8f5e8;
border-color: var(--green);
}
@media (max-width: 1200px) {
.main-layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<h1>U-Query.js Demo</h1>
<div class="main-layout">
<div class="demo-column">
<h2>Element Selection & DOM Manipulation</h2>
<div class="demo-section">
<h3>DOM Queries</h3>
<div class="demo-visual" id="selection-visual">
<span>Select elements to see them highlighted</span>
</div>
<div class="test-element" id="test-1">Test Element 1</div>
<div class="test-element" id="test-2">Test Element 2</div>
<div class="test-element special" id="test-3">Test Element 3 (special)</div>
<div class="controls">
<button onclick="selectAllElements()">Select All .test-element</button>
<button onclick="selectSpecial()">Select .special</button>
<button onclick="selectById()">Select by ID</button>
<button onclick="clearSelection()">Clear Selection</button>
</div>
<div class="status" id="selection-output"></div>
</div>
<div class="demo-section">
<h3>DOM Manipulation</h3>
<div class="demo-visual" id="manipulation-target">Original content</div>
<div class="controls">
<button onclick="htmlDemo()">Change HTML</button>
<button onclick="textDemo()">Change Text</button>
<button onclick="appendDemo()">Append Content</button>
<button onclick="resetManipulation()">Reset</button>
</div>
<div class="status" id="manipulation-output"></div>
</div>
<div class="demo-section">
<h3>CSS & Classes</h3>
<div class="test-element" id="css-target">CSS Target Element</div>
<div class="controls">
<button onclick="cssDemo()">Apply CSS Styles</button>
<button onclick="addClassDemo()">Add Highlight Class</button>
<button onclick="removeClassDemo()">Remove Class</button>
<button onclick="resetCSS()">Reset Styles</button>
</div>
<div class="status" id="css-output"></div>
</div>
</div>
<div class="api-column">
<h2>Selection API</h2>
<div class="api-section">
<h3>Core Selectors</h3>
<div class="api-method"><span class="method-name">$</span>(<span class="param">selector</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">'.class'</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">'#id'</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">'tag'</span>) → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>DOM Manipulation</h3>
<div class="api-method"><span class="method-name">html</span>(<span class="param">content?</span>) → <span class="return-type">NodeList|string</span></div>
<div class="api-method"><span class="method-name">text</span>(<span class="param">content?</span>) → <span class="return-type">NodeList|string</span></div>
<div class="api-method"><span class="method-name">append</span>(<span class="param">content</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">prepend</span>(<span class="param">content</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">remove</span>() → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>CSS & Classes</h3>
<div class="api-method"><span class="method-name">css</span>(<span class="param">styles</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">addClass</span>(<span class="param">className</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">removeClass</span>(<span class="param">className</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">toggleClass</span>(<span class="param">className</span>) → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>Examples</h3>
<pre class="code-example">
<span class="code-comment">// Element selection</span>
<span class="keyword">const</span> elements = <span class="highlight">$</span>(<span class="string">'.my-class'</span>);
<span class="keyword">const</span> byId = <span class="highlight">$</span>(<span class="string">'#my-id'</span>);
<span class="highlight">$</span>(<span class="string">'.target'</span>).html(<span class="string">'&lt;b&gt;New content&lt;/b&gt;'</span>);
<span class="highlight">$</span>(<span class="string">'.target'</span>).text(<span class="string">'Plain text'</span>);
<span class="highlight">$</span>(<span class="string">'.container'</span>).append(<span class="string">'&lt;p&gt;Added&lt;/p&gt;'</span>);
<span class="highlight">$</span>(<span class="string">'.element'</span>).css({
<span class="string">'color'</span>: <span class="string">'red'</span>,
<span class="string">'background'</span>: <span class="string">'blue'</span>
});
<span class="highlight">$</span>(<span class="string">'.element'</span>).addClass(<span class="string">'active'</span>);
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Event Handling & Visibility</h2>
<div class="demo-section">
<h3>Event System</h3>
<button id="event-button">Click Me!</button>
<div class="controls">
<button onclick="addEventHandler()">Add Click Handler</button>
<button onclick="removeEventHandler()">Remove Handler</button>
<button onclick="addMultipleEvents()">Add Multiple Events</button>
</div>
<div class="status" id="event-output"></div>
</div>
<div class="demo-section">
<h3>Show/Hide/Toggle</h3>
<div class="test-element" id="toggle-target">Toggle me!</div>
<div class="controls">
<button onclick="$('#toggle-target').hide()">Hide</button>
<button onclick="$('#toggle-target').show()">Show</button>
<button onclick="$('#toggle-target').toggle()">Toggle</button>
<button onclick="animateVisibility()">Animate Toggle</button>
</div>
<div class="status" id="toggle-output"></div>
</div>
<div class="demo-section">
<h3>Method Chaining</h3>
<div class="demo-visual" id="chaining-demo">
<div class="test-element chain-target">Chain Target 1</div>
<div class="test-element chain-target">Chain Target 2</div>
<div class="test-element chain-target">Chain Target 3</div>
</div>
<div class="controls">
<button onclick="chainingDemo()">Demo Method Chaining</button>
<button onclick="resetChaining()">Reset</button>
</div>
<div class="status" id="chaining-output"></div>
</div>
</div>
<div class="api-column">
<h2>Events & Visibility API</h2>
<div class="api-section">
<h3>Event Methods</h3>
<div class="api-method"><span class="method-name">on</span>(<span class="param">event, handler</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">off</span>(<span class="param">event, handler?</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">trigger</span>(<span class="param">event, data?</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">once</span>(<span class="param">event, handler</span>) → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>Visibility Control</h3>
<div class="api-method"><span class="method-name">show</span>() → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">hide</span>() → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">toggle</span>() → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">fadeIn</span>(<span class="param">duration?</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">fadeOut</span>(<span class="param">duration?</span>) → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>Method Chaining</h3>
<div class="api-description">
All u-query methods return the NodeList, enabling method chaining.
</div>
<pre class="code-example">
<span class="highlight">$</span>(<span class="string">'.button'</span>).on(<span class="string">'click'</span>, <span class="keyword">function</span>() {
<span class="highlight">console</span>.log(<span class="string">'Clicked!'</span>);
});
<span class="highlight">$</span>(<span class="string">'.element'</span>)
.addClass(<span class="string">'active'</span>)
.css({<span class="string">'color'</span>: <span class="string">'red'</span>})
.show()
.on(<span class="string">'click'</span>, handler);
<span class="highlight">$</span>(<span class="string">'.modal'</span>).fadeIn(<span class="highlight">300</span>);
<span class="highlight">$</span>(<span class="string">'.tooltip'</span>).fadeOut(<span class="highlight">200</span>);
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>AJAX & Global Events</h2>
<div class="demo-section">
<h3>AJAX Utilities</h3>
<div class="ajax-visual" id="ajax-visual">
<div class="loading-animation" id="loading-animation" style="animation-play-state: paused;"></div>
<span>AJAX Request Status</span>
</div>
<div class="controls">
<button onclick="ajaxDemo()">GET Request</button>
<button onclick="postDemo()">POST Request</button>
<button onclick="asyncDemo()">Async/Await Demo</button>
</div>
<div class="status" id="ajax-output"></div>
</div>
<div class="demo-section">
<h3>Global Event System</h3>
<input type="text" id="global-event-data" placeholder="Event data" value="Hello global events!">
<div class="controls">
<button onclick="addGlobalListener()">Add Global Listener</button>
<button onclick="emitGlobalEvent()">Emit Global Event</button>
<button onclick="removeGlobalListener()">Remove Listener</button>
</div>
<div class="status" id="global-output"></div>
</div>
<div class="demo-section">
<h3>Utility Functions</h3>
<div class="test-element util-target">Utility Element 1</div>
<div class="test-element util-target">Utility Element 2</div>
<div class="test-element util-target">Utility Element 3</div>
<div class="controls">
<button onclick="eachDemo()">Test $.each</button>
<button onclick="readyDemo()">Test $.ready</button>
<button onclick="utilityChain()">Utility Chain Demo</button>
</div>
<div class="status" id="utility-output"></div>
</div>
</div>
<div class="api-column">
<h2>AJAX & Utilities API</h2>
<div class="api-section">
<h3>AJAX Methods</h3>
<div class="api-method"><span class="method-name">$.get</span>(<span class="param">url, callback</span>) → <span class="return-type">Promise</span></div>
<div class="api-method"><span class="method-name">$.post</span>(<span class="param">url, data, callback</span>) → <span class="return-type">Promise</span></div>
<div class="api-method"><span class="method-name">$.ajax</span>(<span class="param">options</span>) → <span class="return-type">Promise</span></div>
<div class="api-method"><span class="method-name">$.json</span>(<span class="param">url</span>) → <span class="return-type">Promise</span></div>
</div>
<div class="api-section">
<h3>Global Event System</h3>
<div class="api-method"><span class="method-name">$.on</span>(<span class="param">event, handler</span>) → <span class="return-type">$</span></div>
<div class="api-method"><span class="method-name">$.off</span>(<span class="param">event, handler?</span>) → <span class="return-type">$</span></div>
<div class="api-method"><span class="method-name">$.emit</span>(<span class="param">event, data?</span>) → <span class="return-type">number</span></div>
<div class="api-method"><span class="method-name">$.trigger</span>(<span class="param">event, data?</span>) → <span class="return-type">number</span></div>
</div>
<div class="api-section">
<h3>Utility Functions</h3>
<div class="api-method"><span class="method-name">$.each</span>(<span class="param">selector, callback</span>) → <span class="return-type">$</span></div>
<div class="api-method"><span class="method-name">$.ready</span>(<span class="param">callback</span>) → <span class="return-type">$</span></div>
<div class="api-method"><span class="method-name">$.extend</span>(<span class="param">target, ...sources</span>) → <span class="return-type">object</span></div>
<div class="api-method"><span class="method-name">$.map</span>(<span class="param">selector, callback</span>) → <span class="return-type">Array</span></div>
</div>
<div class="api-section">
<h3>Examples</h3>
<pre class="code-example">
<span class="keyword">const</span> data = <span class="keyword">await</span> <span class="highlight">$.get</span>(<span class="string">'/api/users'</span>);
<span class="highlight">$.post</span>(<span class="string">'/api/users'</span>, {name: <span class="string">'John'</span>})
.then(<span class="param">response</span> => <span class="highlight">console</span>.log(<span class="param">response</span>));
<span class="highlight">$.on</span>(<span class="string">'user-login'</span>, <span class="param">user</span> => {
<span class="highlight">console</span>.log(<span class="string">'User logged in:'</span>, <span class="param">user</span>);
});
<span class="highlight">$.emit</span>(<span class="string">'user-login'</span>, {id: <span class="highlight">123</span>});
<span class="highlight">$.each</span>(<span class="string">'.item'</span>, (<span class="param">el</span>, <span class="param">i</span>) => {
<span class="param">el</span>.textContent = <span class="string">`Item ${</span><span class="param">i</span><span class="string">}`</span>;
});
<span class="highlight">$.ready</span>(() => {
<span class="highlight">console</span>.log(<span class="string">'DOM ready!'</span>);
});
</pre>
</div>
</div>
</div>
<div class="main-layout">
<div class="demo-column">
<h2>Browser Info</h2>
<div class="demo-section">
<div class="status" id="library-info">Loading library information...</div>
<h3>This browser supports</h3>
<div id="feature-support" class="status"></div>
<div class="controls">
<button onclick="testCompatibility()">Test Browser Compatibility</button>
<button onclick="performanceBenchmark()">Performance Benchmark</button>
</div>
</div>
</div>
<div class="api-column">
<h2>Advanced Features</h2>
<div class="api-section">
<h3>Advanced Selectors</h3>
<div class="api-method"><span class="method-name">$</span>(<span class="param">':visible'</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">':hidden'</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">':checked'</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">':first'</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">$</span>(<span class="param">':last'</span>) → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>Data Attributes</h3>
<div class="api-method"><span class="method-name">data</span>(<span class="param">key, value?</span>) → <span class="return-type">NodeList|any</span></div>
<div class="api-method"><span class="method-name">attr</span>(<span class="param">name, value?</span>) → <span class="return-type">NodeList|string</span></div>
<div class="api-method"><span class="method-name">prop</span>(<span class="param">name, value?</span>) → <span class="return-type">NodeList|any</span></div>
<div class="api-method"><span class="method-name">val</span>(<span class="param">value?</span>) → <span class="return-type">NodeList|string</span></div>
</div>
<div class="api-section">
<h3>Traversal Methods</h3>
<div class="api-method"><span class="method-name">parent</span>() → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">children</span>() → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">siblings</span>() → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">find</span>(<span class="param">selector</span>) → <span class="return-type">NodeList</span></div>
<div class="api-method"><span class="method-name">closest</span>(<span class="param">selector</span>) → <span class="return-type">NodeList</span></div>
</div>
<div class="api-section">
<h3>DOM Diff Updates</h3>
<div class="api-description">
u-query can make use of morphdom.js for efficient DOM diff updates.
</div>
<div class="api-method"><span class="method-name">$.options.alwaysDoDifferentialUpdate</span> = <span class="param">true</span></div>
<div class="api-description">
Global flag for morphdom.
</div>
</div>
</div>
</div>
</div>
<script src="u-query.js"></script>
<script>
let clickHandler, globalHandler;
// Initialize library info on load
document.addEventListener('DOMContentLoaded', () => {
updateLibraryInfo();
updateFeatureSupport();
});
function updateLibraryInfo() {
const info = document.getElementById('library-info');
const version = typeof $ !== 'undefined' ? 'Loaded' : 'Not Available';
const selectors = document.querySelectorAll('*').length;
info.textContent = `
Library Status: ${version}
DOM Elements: ${selectors}
Browser: ${navigator.userAgent.split(' ')[0]}
Features: ES6+, Promises, DOM API
Build: Development
`.trim();
}
function updateFeatureSupport() {
const features = [
{ name: 'Promises', supported: typeof Promise !== 'undefined' },
{ name: 'Arrow Functions', supported: true },
{ name: 'Template Literals', supported: true },
{ name: 'Fetch API', supported: typeof fetch !== 'undefined' },
{ name: 'querySelector', supported: typeof document.querySelector !== 'undefined' },
{ name: 'addEventListener', supported: typeof document.addEventListener !== 'undefined' }
];
const support = features.map(feature =>
`${feature.name}: ${feature.supported ? 'Supported' : 'Not Supported'}`
).join('\n');
document.getElementById('feature-support').textContent = support;
}
function log(elementId, message) {
const element = document.getElementById(elementId);
if (element) {
element.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
element.scrollTop = element.scrollHeight;
}
}
// Enhanced Selection Demos
function selectAllElements() {
clearSelection();
const elements = $('.test-element');
elements.forEach(el => el.classList.add('highlighted'));
updateSelectionVisual(`Selected ${elements.length} .test-element items`);
log('selection-output', `Found ${elements.length} elements with class 'test-element'`);
}
function selectSpecial() {
clearSelection();
const elements = $('.special');
elements.forEach(el => el.classList.add('highlighted'));
updateSelectionVisual(`Selected ${elements.length} .special items`);
log('selection-output', `Found ${elements.length} elements with class 'special'`);
}
function selectById() {
clearSelection();
const elements = $('#test-2');
elements.forEach(el => el.classList.add('highlighted'));
updateSelectionVisual(`Selected element by ID: #test-2`);
log('selection-output', `Found element by ID: ${elements.length > 0 ? elements[0].textContent : 'none'}`);
}
function clearSelection() {
$('.test-element').forEach(el => el.classList.remove('highlighted'));
updateSelectionVisual('Selection cleared');
}
function updateSelectionVisual(message) {
const visual = document.getElementById('selection-visual');
visual.textContent = message;
visual.classList.add('active');
setTimeout(() => visual.classList.remove('active'), 2000);
}
// DOM Manipulation
function htmlDemo() {
$('#manipulation-target').html('<strong>HTML content changed</strong>');
log('manipulation-output', 'HTML content updated with formatting');
}
function textDemo() {
$('#manipulation-target').text('Text content changed');
log('manipulation-output', 'Text content updated (plain text)');
}
function appendDemo() {
$('#manipulation-target').append(' <em>Appended content</em>');
log('manipulation-output', 'Content appended to existing content');
}
function resetManipulation() {
$('#manipulation-target').html('Original content');
log('manipulation-output', 'Content reset to original state');
}
// CSS and Classes
function cssDemo() {
$('#css-target').css({
'background': 'linear-gradient(45deg, #ff6b6b, #4ecdc4)',
'color': 'white',
'border-radius': '10px',
'padding': '15px',
'transform': 'scale(1.05)',
'transition': 'all 0.3s ease'
});
log('css-output', 'Applied gradient background and transformation');
}
function addClassDemo() {
$('#css-target').addClass('highlighted');
log('css-output', 'Added "highlighted" class');
}
function removeClassDemo() {
$('#css-target').removeClass('highlighted');
log('css-output', 'Removed "highlighted" class');
}
function resetCSS() {
$('#css-target').css({
'background': '',
'color': '',
'border-radius': '',
'padding': '',
'transform': '',
'transition': ''
}).removeClass('highlighted');
log('css-output', 'All styles and classes reset');
}
function addEventHandler() {
clickHandler = function(e) {
log('event-output', `Button clicked! Event type: ${e.type}`);
e.target.style.transform = 'scale(0.95)';
setTimeout(() => e.target.style.transform = '', 150);
};
$('#event-button').on('click', clickHandler);
log('event-output', 'Click handler added with visual feedback');
}
function removeEventHandler() {
if (clickHandler) {
$('#event-button').off('click', clickHandler);
log('event-output', 'Click handler removed');
}
}
function addMultipleEvents() {
const button = $('#event-button')[0];
if (button) {
const events = {
mouseenter: () => log('event-output', 'Mouse entered button'),
mouseleave: () => log('event-output', 'Mouse left button'),
focus: () => log('event-output', 'Button focused'),
blur: () => log('event-output', 'Button lost focus')
};
Object.entries(events).forEach(([event, handler]) => {
button.addEventListener(event, handler);
});
log('event-output', 'Added multiple event listeners (hover, focus, blur)');
}
}
function animateVisibility() {
const target = $('#toggle-target')[0];
if (target) {
target.style.transition = 'all 0.5s ease';
target.style.transform = target.style.display === 'none' ? 'scale(1)' : 'scale(0)';
setTimeout(() => {
$('#toggle-target').toggle();
if (target.style.display !== 'none') {
target.style.transform = 'scale(1)';
}
}, 250);
log('toggle-output', 'Animated visibility toggle with scaling effect');
}
function ajaxDemo() {
}
}
// Enhanced AJAX with visual feedback
function ajaxDemo() {
updateAjaxVisual(true);
log('ajax-output', 'Initiating GET request...');
$.get('https://jsonplaceholder.typicode.com/posts/1', function(data) {
updateAjaxVisual(false);
log('ajax-output', `GET Success: "${data.title.substring(0, 50)}..."`);
log('ajax-output', `Response body: ${data.body.substring(0, 100)}...`);
}).catch(error => {
updateAjaxVisual(false);
log('ajax-output', `GET Error: ${error.message}`);
});
}
function postDemo() {
updateAjaxVisual(true);
log('ajax-output', 'Initiating POST request...');
$.post('https://jsonplaceholder.typicode.com/posts', {
title: 'u-query demo post',
body: 'This is a test post from u-query demonstration',
userId: 1
}, function(data) {
updateAjaxVisual(false);
log('ajax-output', `POST Success: Created post with ID ${data.id}`);
log('ajax-output', `Posted title: "${data.title}"`);
}).catch(error => {
updateAjaxVisual(false);
log('ajax-output', `POST Error: ${error.message}`);
});
}
async function asyncDemo() {
try {
updateAjaxVisual(true);
log('ajax-output', 'Using async/await pattern...');
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const user = await response.json();
updateAjaxVisual(false);
log('ajax-output', `Async Success: User "${user.name}" from ${user.address.city}`);
log('ajax-output', `Email: ${user.email}, Phone: ${user.phone}`);
} catch (error) {
updateAjaxVisual(false);
log('ajax-output', `Async Error: ${error.message}`);
}
}
function updateAjaxVisual(loading) {
const visual = document.getElementById('ajax-visual');
const animation = document.getElementById('loading-animation');
const text = visual.querySelector('span');
if (loading) {
visual.style.background = 'linear-gradient(45deg, #ff6b6b, #4ecdc4)';
animation.style.animationPlayState = 'running';
text.textContent = 'Loading...';
} else {
visual.style.background = 'linear-gradient(45deg, #1e3c72, #2a5298)';
animation.style.animationPlayState = 'paused';
text.textContent = 'Request Complete';
}
}
function addGlobalListener() {
globalHandler = function(data) {
log('global-output', `Global event received: "${data}"`);
};
$.on('demo-event', globalHandler);
log('global-output', 'Global listener added for "demo-event"');
}
function emitGlobalEvent() {
const data = $('#global-event-data')[0].value;
const count = $.emit('demo-event', data);
log('global-output', `Event emitted to ${count} listeners with data: "${data}"`);
}
function removeGlobalListener() {
if (globalHandler) {
$.off('demo-event', globalHandler);
log('global-output', 'Global listener removed');
}
}
function eachDemo() {
document.getElementById('utility-output').textContent = '';
$.each('.util-target', function(element, index) {
element.style.background = `hsl(${index * 60}, 70%, 90%)`;
element.style.transform = `translateX(${index * 10}px)`;
log('utility-output', `Element ${index + 1}: "${element.textContent}" - styled`);
});
setTimeout(() => {
$('.util-target').forEach(el => {
el.style.background = '';
el.style.transform = '';
});
}, 3000);
}
function readyDemo() {
$.ready(function() {
log('utility-output', '$.ready() callback executed - DOM fully loaded');
log('utility-output', `Current timestamp: ${new Date().toLocaleString()}`);
});
}
function utilityChain() {
$.each('.util-target', (el, i) => {
$(el).css({'color': 'blue'}).addClass('highlighted');
});
log('utility-output', 'Utility chain: each + css + addClass applied');
setTimeout(() => {
$('.util-target').css({'color': ''}).removeClass('highlighted');
log('utility-output', 'Utility chain effects reset');
}, 2000);
}
function chainingDemo() {
$('.chain-target')
.addClass('highlighted')
.css({
'color': 'white',
'background': 'linear-gradient(45deg, #667eea, #764ba2)',
'transform': 'translateY(-5px) rotate(2deg)',
'transition': 'all 0.5s ease'
})
.show();
log('chaining-output', 'Method chain applied: addClass → css → show');
log('chaining-output', 'Added highlight, gradient, and transform');
}
function resetChaining() {
$('.chain-target')
.removeClass('highlighted')
.css({
'color': '',
'background': '',
'transform': '',
'transition': ''
});
log('chaining-output', 'Method chain reset: removeClass → css clear');
}
// Performance and Compatibility
function testCompatibility() {
const tests = [
{ name: 'CSS Selectors', test: () => !!document.querySelector },
{ name: 'Event Listeners', test: () => !!document.addEventListener },
{ name: 'Fetch API', test: () => !!window.fetch },
{ name: 'Promises', test: () => !!window.Promise },
{ name: 'Arrow Functions', test: () => { try { eval('() => {}'); return true; } catch { return false; } } },
{ name: 'Template Literals', test: () => { try { eval('`test`'); return true; } catch { return false; } } }
];
document.getElementById('library-info').textContent = 'Running compatibility tests...\n';
tests.forEach((test, i) => {
setTimeout(() => {
const result = test.test();
document.getElementById('library-info').textContent +=
`${test.name}: ${result ? 'Supported' : 'Not Supported'}\n`;
}, i * 200);
});
}
function performanceBenchmark() {
const iterations = 1000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
$('.test-element').css({'color': 'red'}).addClass('test-class').removeClass('test-class');
}
const end = performance.now();
const duration = (end - start).toFixed(2);
document.getElementById('library-info').textContent =
`Performance Benchmark Complete\n` +
`Operations: ${iterations} selection + css + addClass + removeClass\n` +
`Duration: ${duration}ms\n` +
`Avg per operation: ${(duration / iterations).toFixed(4)}ms\n` +
`Operations per second: ${(iterations / (duration / 1000)).toFixed(0)}`;
}
$.ready(function() {
log('utility-output', 'Demo initialized successfully');
});
setInterval(updateLibraryInfo, 5000);
</script>
</body>
</html>

View File

@ -1,589 +0,0 @@
(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<eventName, Map<slotKey|Symbol, Function>> */
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('<script')) {
const result = executeScripts(child_or_html);
el.insertAdjacentHTML('beforeend', result.html);
result.scripts.forEach(executeScript);
} else {
el.insertAdjacentHTML('beforeend', child_or_html);
}
} else if (el.appendChild) {
if (typeof child_or_html === 'string') {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = child_or_html;
el.appendChild(tempDiv.firstChild);
} else {
el.appendChild(child_or_html);
}
} else if(el.hasOwnProperty('innerHTML')) {
el.innerHTML += child_or_html;
}
});
return this;
}
QueryWrapper.prototype.remove = function() {
this.forEach(function(el) {
el.parentNode.removeChild(el);
});
return this;
}
function first(...args) {
for (const arg of args) {
if (arg !== undefined && arg !== null && arg !== '') {
return arg;
}
}
return null;
}
function clamp(v, min, max) {
if (v < min) return min;
if (v > 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
};
}));

Some files were not shown because too many files have changed in this diff Show More