I think I need to change the documentation format

This commit is contained in:
Udo 2026-04-22 01:32:05 +00:00
parent b53eb6e4f1
commit 8223dcc6b3
222 changed files with 1932 additions and 39753 deletions

View File

@ -69,6 +69,8 @@ Useful related runtime patterns:
Useful helpers for that data model now include:
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
- `DTree::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
- `json_encode(String)` for emitting JavaScript-safe string literals directly
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
- `path_join(base, child)` for filesystem-style path assembly

View File

@ -14,6 +14,16 @@ PRECOMPILE_FILES_IN=
# PUBLIC SITE DIRECTORY USED FOR STARTUP SCAN WHEN PRECOMPILE_FILES_IN IS EMPTY
SITE_DIRECTORY=site
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
JIT_COMPILE_ON_REQUEST=1
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1
# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS
# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED
COMPILE_FAILURE_RETRY_SECONDS=10
# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS
PROACTIVE_COMPILE_CHECK_INTERVAL=60

View File

@ -3,5 +3,6 @@ Time and Date Functions
time_precise
time
time_format_local
time_format_relative
time_format_utc
time_parse

View File

@ -23,6 +23,8 @@ RENDER(Request& context)
{
String page = first(context.get["p"], "index");
String page_title = page;
nibble(page_title, "_");
<><html>
<head>
@ -34,7 +36,7 @@ RENDER(Request& context)
<a href="index.uce">UCE Docs</a><?
if(page != "index")
{
?><span class="dim"> / </span><?= page ?><?
?><span class="dim"> / </span><?= page_title ?><?
}
?>
</h1>
@ -78,6 +80,14 @@ RENDER(Request& context)
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">directive</span></a></div>
<?
}
else if(ft.substr(0, 2) == "3_")
{
String fn = ft;
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">info</span></a></div>
<?
}
else
{
?>
@ -135,18 +145,28 @@ RENDER(Request& context)
{
auto doc = split(file_get_contents("pages/"+page+".txt"), "\n");
// Pre-extract related lines for sidebar
StringList rel_lines;
bool in_rel = false;
// Pre-extract sidebar-only sections so the article body stays focused.
StringList equiv_lines;
StringList see_lines;
String sidebar_section = "";
for(auto s : doc)
{
if(s == ":related") { in_rel = true; }
else if(s != "" && s.substr(0, 1) == ":") { in_rel = false; }
else if(in_rel && s != "") { rel_lines.push_back(s); }
if(s != "" && s.substr(0, 1) == ":")
{
sidebar_section = s.substr(1);
}
else if(sidebar_section == "related" && s != "")
{
equiv_lines.push_back(s);
}
else if(sidebar_section == "see" && s != "")
{
see_lines.push_back(s);
}
}
String detail_class = "detail-layout";
if(rel_lines.size() == 0) detail_class += " no-sidebar";
if(equiv_lines.size() == 0 && see_lines.size() == 0) detail_class += " no-sidebar";
?><div class="<?= detail_class ?>">
<article class="doc-detail"><?
String layout_class = "text";
@ -169,7 +189,7 @@ RENDER(Request& context)
?></div><?
}
section_hidden = (new_class == "related");
section_hidden = (new_class == "related" || new_class == "see");
layout_class = new_class;
if(!section_hidden)
@ -191,10 +211,6 @@ RENDER(Request& context)
{
?><h3>Description</h3><?
}
else if(layout_class == "see")
{
?><h3>Related</h3><?
}
else
{
?><h3><?= layout_class ?></h3><?
@ -216,17 +232,6 @@ RENDER(Request& context)
{
?><div><b><?= trim(nibble(s, ":")) ?></b> : <?= trim(s) ?></div><?
}
else if(layout_class == "see")
{
if(s[0] == '>')
{
render_see_section(s.substr(1));
}
else
{
?><div><a href="index.uce?p=<?= trim(s) ?>"><?= trim(s) ?><span class="dim">()</span></a></div><?
}
}
else
{
?><div><?: markdown_to_html(s) ?></div><?
@ -239,14 +244,33 @@ RENDER(Request& context)
}
?></article><?
if(rel_lines.size() > 0)
if(equiv_lines.size() > 0 || see_lines.size() > 0)
{
?><aside class="detail-sidebar">
<div class="sidebar-card">
<h3>PHP &amp; JS Equivalents</h3><?
for(auto rl : rel_lines)
<div class="sidebar-card"><?
if(equiv_lines.size() > 0)
{
?><div><?: markdown_to_html(rl) ?></div><?
?><h3>PHP &amp; JS Equivalents</h3><?
for(auto rl : equiv_lines)
{
?><div><?: markdown_to_html(rl) ?></div><?
}
}
if(see_lines.size() > 0)
{
?><h3 class="sidebar-subhead">Related</h3><?
for(auto sl : see_lines)
{
if(sl[0] == '>')
{
render_see_section(sl.substr(1));
}
else
{
?><div><a href="index.uce?p=<?= trim(sl) ?>"><?= trim(sl) ?><span class="dim">()</span></a></div><?
}
}
}
?></div>
</aside><?

111
site/doc/pages/0_DTree.txt Normal file
View File

@ -0,0 +1,111 @@
:sig
DTree
:desc
Dynamic tree/container type used throughout UCE for structured data.
:Overview
`DTree` is UCE's general-purpose structured value type.
This is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers.
:Value Kinds
- `DTree` can hold:
- `String`
- `f64`
- `bool`
- pointer
- nested map of child `DTree` values (map-shaped `DTree` values can be used to represent list-like data when the keys are numeric strings in sequence)
:Used in
`context.var`
`context.cfg`
`context.call`
`context.connection`
`json_decode()` results
`unit_call()` return values
`unit_info()`
:Reading Values
`["key"]` accesses or creates a child node.
`.has("key")` checks whether a child exists without creating it.
`.key("key")` returns a child pointer when it already exists.
`.get_or_create("key")` returns a child pointer and creates it when missing.
`.get_by_path("a/b/c")` traverses nested children without creating missing keys.
`.to_string()` reads scalar content as text.
`.to_s64()`, `.to_u64()`, and `.to_f64()` perform best-effort numeric conversion.
`.to_bool()` performs best-effort boolean conversion.
`.to_stringmap()` converts a map-shaped tree into `StringMap`.
`operator[]` creates missing entries, just like `std::map`.
`.has()` and `.key()` are the non-mutating lookup helpers.
`.get_by_path()` is the non-creating traversal helper.
`.json_decode()` currently stores JSON numbers as string-valued `DTree` nodes, so typed numeric conversion is the normal way to consume those values.
References are dereferenced automatically in most normal reads.
:Conversion Rules
Scalar-looking strings are trimmed before numeric and boolean parsing.
`.to_bool()` understands common textual forms such as `true`, `false`, `yes`, `no`, `1`, and `0`.
`bool` values convert numerically to `1` / `0`.
Pointer values convert numerically when read as numbers.
Single-value maps can act as scalar wrappers for numeric and boolean conversion.
Invalid numeric input falls back to `0`.
`.to_stringmap()` converts map children key-by-key using each child's `to_string()`.
:Writing Values
`.set(String)`
`.set(f64)`
`.set_bool(bool)`
`.set(StringMap)`
`.set_array()`
`.push(...)`
`.pop()`
`.remove(key)`
`.clear()`
Use `.set_array()` when you want list-style behavior with `push()` / `pop()`.
:Inspection Helpers
`.has(key)`
`.key(key)`
`.get_or_create(key)`
`.get_type_name()`
`.is_array()`
`.is_list()`
`.to_json()`
:each()
`each(std::function<void (DTree t, String key)> f)` iterates over the current tree value.
For map-shaped `DTree` values, the callback is invoked once for each child entry and receives:
`t` as the child value
`key` as the child key
For non-map values, `each()` still invokes the callback once:
`t` is the current value
`key` is an empty string
Typical usage:
`context.var["items"].each([&](DTree item, String key) { print(key, ": ", item.to_string(), "\n"); });`
:Examples
`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()`
`if(DTree* user = payload.key("user")) { print(user->to_json()); }`
`payload.get_or_create("headers")->get_or_create("Content-Type")->set("text/plain")`
`StringMap headers = payload["headers"].to_stringmap()`
:related
**PHP:** Nested associative arrays, `stdClass`, decoded JSON trees, and helper accessors for deep array paths.
**JavaScript / Node.js:** Plain objects, arrays, `Map`, and JSON-shaped data passed between handlers.
:see
>types
get_by_path
json_decode

View File

@ -1,32 +0,0 @@
:sig
DTree
:desc
Dynamic tree/container type used throughout UCE for structured data.
`DTree` can hold a `String`, `f64`, `bool`, pointer, or a nested map of child `DTree` values.
Use `t["key"]` to access or create child entries. Use `push()` / `pop()` when treating it like an array-like container with numeric string keys.
Common uses include:
- `json_decode()` / `json_encode()`
- `context.var`
- `context.call`
- `unit_call()` return values
Useful methods include:
- `to_string()`
- `to_json()`
- `get_type_name()`
- `get_by_path()`
- `set_bool()`
- `remove()`
- `clear()`
- `each()`
:related
**PHP:** Nested associative arrays, `stdClass`, decoded JSON trees, and helper accessors for deep array paths.
**JavaScript / Node.js:** Plain objects, arrays, `Map`, and JSON-shaped data passed between handlers.
:see
>types

View File

@ -8,6 +8,23 @@ return value : a DTree object containing the deserialized JSON data
:desc
Deserializes 's' into a DTree structure.
The returned structure is usually consumed through `DTree` accessors such as:
- `tree["key"].to_string()`
- `tree["count"].to_u64()`
- `tree["enabled"].to_bool()`
Current runtime note:
- JSON objects and arrays become map-shaped `DTree` values
- JSON booleans become native `bool` `DTree` values
- JSON strings become native `String` `DTree` values
- JSON numbers currently deserialize as string-valued `DTree` nodes, so typed conversions like `to_f64()` and `to_u64()` are the normal way to read numeric content
:see
DTree
json_encode
to_bool
to_f64
to_u64
:related
**PHP:** `json_decode()`

View File

@ -105,6 +105,18 @@ Returns a formatted date. This is based on the Linux date() command. The formatt
%Z alphabetic time zone abbreviation (e.g., EDT)
%deltaS total elapsed seconds between the timestamp and now
%deltaM total elapsed minutes between the timestamp and now
%deltaH total elapsed hours between the timestamp and now
%deltad total elapsed days between the timestamp and now
%deltam total elapsed 30-day months between the timestamp and now
%deltaY total elapsed 365-day years between the timestamp and now
By default, date pads numeric fields with zeroes. The following
optional flags may follow '%':
@ -123,6 +135,7 @@ Returns a formatted date. This is based on the Linux date() command. The formatt
:see
>time
>time_format_relative
:related
**PHP:** `date()` and `DateTime` formatting in the server local timezone

View File

@ -0,0 +1,55 @@
:sig
String time_format_relative(u64 timestamp, String format_very_recent = "", u64 medium_recency_seconds = 0, String format_medium_recent = "", u64 not_recent_seconds = 0, String format_not_recent = "")
:params
timestamp : Unix timestamp to compare against the current time
format_very_recent : output format for very recent timestamps, defaults to `just now`
medium_recency_seconds : cutoff between the very-recent and medium-recent formats, defaults to `90`
format_medium_recent : output format for medium-recent timestamps, defaults to `%deltaM minutes ago`
not_recent_seconds : cutoff between the medium-recent and not-recent formats, defaults to `5400`
format_not_recent : output format for older timestamps, defaults to `%deltaH hours ago`
return value : a formatted relative-time string
:desc
Formats a timestamp relative to the current time using the same formatting engine as `time_format_local()` and `time_format_utc()`.
The chosen format depends on the elapsed time:
- if `now - timestamp` is less than `medium_recency_seconds`, use `format_very_recent`
- else if it is less than `not_recent_seconds`, use `format_medium_recent`
- otherwise use `format_not_recent`
The custom relative-time sequences available in all time formatters are:
:pre
%deltaS total elapsed seconds between the timestamp and now
%deltaM total elapsed minutes between the timestamp and now
%deltaH total elapsed hours between the timestamp and now
%deltad total elapsed days between the timestamp and now
%deltam total elapsed 30-day months between the timestamp and now
%deltaY total elapsed 365-day years between the timestamp and now
:pre
Default behavior examples:
time_format_relative(time() - 12)
=> just now
time_format_relative(time() - 600)
=> 10 minutes ago
time_format_relative(time() - 7200)
=> 2 hours ago
:see
>time
>time_format_local
>time_format_utc
:related
**PHP:** `DateTimeImmutable` diff formatting or libraries such as Carbon `diffForHumans()`
**JavaScript / Node.js:** relative time helpers such as `Intl.RelativeTimeFormat`, `date-fns/formatDistanceToNow`, or Luxon

View File

@ -105,6 +105,18 @@ Returns a formatted date in the GMT/UTC timezone. This is based on the Linux dat
%Z alphabetic time zone abbreviation (e.g., EDT)
%deltaS total elapsed seconds between the timestamp and now
%deltaM total elapsed minutes between the timestamp and now
%deltaH total elapsed hours between the timestamp and now
%deltad total elapsed days between the timestamp and now
%deltam total elapsed 30-day months between the timestamp and now
%deltaY total elapsed 365-day years between the timestamp and now
By default, date pads numeric fields with zeroes. The following
optional flags may follow '%':
@ -123,6 +135,7 @@ Returns a formatted date in the GMT/UTC timezone. This is based on the Linux dat
:see
>time
>time_format_relative
:related
**PHP:** `gmdate()` and UTC `DateTime` formatting

View File

@ -1,6 +1,35 @@
/* UCE Documentation Theme
Color palette: deep blue gradient · warm gold accents · frosted glass surfaces */
@font-face {
font-family: 'default_sans';
src: url('../examples/uce-starter/themes/common/fonts/b612/b612-regular.ttf') format('truetype');
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'default_sans';
src: url('../examples/uce-starter/themes/common/fonts/b612/b612-bold.ttf') format('truetype');
font-style: normal;
font-weight: 700;
font-display: swap;
}
@font-face {
font-family: 'default_mono';
src: url('../examples/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf') format('truetype');
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'default_mono';
src: url('../examples/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf') format('truetype');
font-style: normal;
font-weight: 700;
font-display: swap;
}
:root {
--bg: #113399;
--bg-grad: linear-gradient(168deg, #1a3daa 0%, #113399 40%, #0d2880 100%);
@ -20,8 +49,8 @@
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25);
--shadow-glow: 0 0 20px rgba(240, 196, 48, 0.06);
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
--font-sans: 'default_sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
--font-mono: 'default_mono', 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
--radius: 10px;
--radius-lg: 16px;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
@ -327,6 +356,10 @@ a:hover {
border-bottom: 1px solid var(--border);
}
.sidebar-card .sidebar-subhead {
margin-top: 18px;
}
.sidebar-card div {
padding: 3px 0;
font-size: 0.875rem;

View File

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

View File

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

View File

@ -1,27 +1,28 @@
#load "../config/settings.uce"
#load "user.class.h"
String starter_fs_root(Request& context)
String app_fs_root(Request& context)
{
String root = context.var["starter"]["fs_root"].to_string();
String root = context.var["app"]["fs_root"].to_string();
if(root == "")
root = cwd_get();
return(root);
}
String starter_script_url(Request& context)
String app_script_url(Request& context)
{
String url = context.var["starter"]["script_url"].to_string();
String url = context.var["app"]["script_url"].to_string();
if(url == "")
url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
return(url);
}
String starter_base_url(Request& context)
String app_base_url(Request& context)
{
String base = context.var["starter"]["base_url"].to_string();
String base = context.var["app"]["base_url"].to_string();
if(base == "")
{
base = dirname(starter_script_url(context));
base = dirname(app_script_url(context));
if(base == "")
base = "/";
if(base[base.length() - 1] != '/')
@ -30,15 +31,16 @@ String starter_base_url(Request& context)
return(base);
}
String starter_fs_path(String relative, Request& context)
String app_fs_path(String relative, Request& context)
{
return(path_join(starter_fs_root(context), relative));
return(path_join(app_fs_root(context), relative));
}
String starter_link(String path, Request& context);
String starter_link(String path, StringMap params, Request& context);
String app_link(String path, Request& context);
String app_link(String path, StringMap params, Request& context);
void app_init(Request& context);
String starter_first_route_segment(Request& context)
String app_first_route_segment(Request& context)
{
String query = context.params["QUERY_STRING"];
for(auto part : split(query, "&"))
@ -51,10 +53,10 @@ String starter_first_route_segment(Request& context)
return("");
}
DTree starter_make_route(Request& context)
DTree app_make_route(Request& context)
{
DTree route;
String path = starter_first_route_segment(context);
String path = app_first_route_segment(context);
path = trim(path);
while(path.length() > 0 && path[0] == '/')
path = path.substr(1);
@ -69,10 +71,10 @@ DTree starter_make_route(Request& context)
return(route);
}
DTree starter_resolve_view(Request& context, String base_dir = "views")
DTree app_resolve_view(Request& context, String base_dir = "views")
{
DTree result;
DTree route = context.var["starter"]["route"];
DTree route = context.var["app"]["route"];
String lpath = first(route["l_path"].to_string(), "index");
String base = trim(base_dir);
if(base != "" && base[base.length() - 1] == '/')
@ -110,128 +112,54 @@ DTree starter_resolve_view(Request& context, String base_dir = "views")
return(result);
}
DTree starter_default_config()
void app_register_css(String path, Request& context)
{
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);
context.var["app"]["assets"]["css"][path] = path;
}
void starter_register_css(String path, Request& context)
void app_register_js(String path, Request& context)
{
context.var["starter"]["assets"]["css"][path] = path;
context.var["app"]["assets"]["js"][path] = path;
}
void starter_register_js(String path, Request& context)
String app_asset_url(String path, Request& context)
{
context.var["starter"]["assets"]["js"][path] = path;
}
String starter_asset_url(String path, Request& context)
{
String url = starter_base_url(context) + path;
String fs_path = starter_fs_path(path, context);
String url = app_base_url(context) + path;
String fs_path = app_fs_path(path, context);
if(file_exists(fs_path))
url += "?v=" + std::to_string((u64)file_mtime(fs_path));
return(url);
}
void starter_render_registered_css(Request& context)
void app_render_registered_css(Request& context)
{
context.var["starter"]["assets"]["css"].each([&](DTree item, String key) {
context.var["app"]["assets"]["css"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><link rel="stylesheet" href="<?= starter_asset_url(path, context) ?>" /></>
<><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /></>
}
});
}
void starter_render_registered_js(Request& context)
void app_render_registered_js(Request& context)
{
context.var["starter"]["assets"]["js"].each([&](DTree item, String key) {
context.var["app"]["assets"]["js"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><script src="<?= starter_asset_url(path, context) ?>"></script></>
<><script src="<?= app_asset_url(path, context) ?>"></script></>
}
});
}
String starter_link(String path, Request& context)
String app_link(String path, Request& context)
{
StringMap params;
return(starter_link(path, params, context));
return(app_link(path, params, context));
}
String starter_link(String path, StringMap params, Request& context)
String app_link(String path, StringMap params, Request& context)
{
String query = "";
path = trim(path);
@ -242,33 +170,33 @@ String starter_link(String path, StringMap params, Request& context)
query += "&" + extra;
else if(query == "")
query = extra;
String url = starter_script_url(context);
String url = app_script_url(context);
if(query != "")
url += "?" + query;
return(url);
}
void starter_redirect(String path, Request& context)
void app_redirect(String path, Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = starter_link(path, context);
context.header["Location"] = app_link(path, context);
}
void starter_not_found(String message, Request& context)
void app_not_found(String message, Request& context)
{
context.set_status(404, "Not Found");
context.var["starter"]["error"] = message;
context.var["starter"]["page_title"] = "404 Not Found";
context.var["app"]["error"] = message;
context.var["app"]["page_title"] = "404 Not Found";
}
String starter_menu_href(String menu_key, DTree menu_item, Request& context)
String app_menu_href(String menu_key, DTree menu_item, Request& context)
{
if(menu_item["external"].to_string() != "")
return("/" + menu_key);
return(starter_link(menu_key, context));
return(app_link(menu_key, context));
}
String starter_html_class(Request& context)
String app_html_class(Request& context)
{
String classes = "no-js";
if(context.cfg.get_by_path("theme/mode").to_string() == "dark")
@ -276,15 +204,15 @@ String starter_html_class(Request& context)
return(classes);
}
String starter_page_main_html(Request& context)
String app_page_main_html(Request& context)
{
String main_html = context.call["main_html"].to_string();
if(main_html == "")
main_html = context.var["starter"]["fragments"]["main"].to_string();
main_html = context.var["app"]["fragments"]["main"].to_string();
return(main_html);
}
bool starter_bool_value(DTree value, bool fallback = false)
bool app_bool_value(DTree value, bool fallback = false)
{
String raw = trim(value.to_string());
if(raw == "")
@ -301,22 +229,22 @@ bool starter_bool_value(DTree value, bool fallback = false)
);
}
bool starter_request_embed_mode(Request& context)
bool app_request_embed_mode(Request& context)
{
return(starter_bool_value(context.var["starter"]["embed_mode"]));
return(app_bool_value(context.var["app"]["embed_mode"]));
}
bool starter_page_embed_mode(Request& context)
bool app_page_embed_mode(Request& context)
{
String embed_value = context.call["embed_mode"].to_string();
if(embed_value != "")
return(starter_bool_value(context.call["embed_mode"]));
return(starter_request_embed_mode(context));
return(app_bool_value(context.call["embed_mode"]));
return(app_request_embed_mode(context));
}
String starter_theme_page_component(Request& context)
String app_theme_page_component(Request& context)
{
String page_type = first(context.var["starter"]["page_type"].to_string(), "html");
String page_type = first(context.var["app"]["page_type"].to_string(), "html");
if(page_type == "blank")
return("themes/common/page.blank.uce");
if(page_type == "json")
@ -330,17 +258,17 @@ String starter_theme_page_component(Request& context)
return(theme_path + "page.html.uce");
}
void starter_render_page(Request& context)
void app_render_page(Request& context)
{
if(context.flags.status >= 300 && context.flags.status < 400)
return;
DTree page_props;
page_props["main_html"] = context.var["starter"]["fragments"]["main"];
if(context.var["starter"]["json"].get_type_name() == "array")
page_props["json"] = context.var["starter"]["json"];
page_props["main_html"] = context.var["app"]["fragments"]["main"];
if(context.var["app"]["json"].get_type_name() == "array")
page_props["json"] = context.var["app"]["json"];
String page_component = starter_theme_page_component(context);
String page_component = app_theme_page_component(context);
if(page_component != "" && file_exists(page_component))
{
print(component(page_component, page_props, context));
@ -349,27 +277,115 @@ void starter_render_page(Request& context)
context.set_status(500, "Internal Server Error");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("UCE Starter is missing the page template component: " + page_component);
print("UCE app is missing the page template component: " + page_component);
}
// Backward-compatible aliases for older starter example units.
using StarterUser = AppUser;
void starter_boot(Request& context)
{
if(context.var["starter"]["booted"].to_string() == "1")
app_init(context);
}
String starter_link(String path, Request& context)
{
return(app_link(path, context));
}
String starter_link(String path, StringMap params, Request& context)
{
return(app_link(path, params, context));
}
void starter_register_css(String path, Request& context)
{
app_register_css(path, context);
}
void starter_register_js(String path, Request& context)
{
app_register_js(path, context);
}
String starter_asset_url(String path, Request& context)
{
return(app_asset_url(path, context));
}
void starter_render_registered_css(Request& context)
{
app_render_registered_css(context);
}
void starter_render_registered_js(Request& context)
{
app_render_registered_js(context);
}
bool starter_page_embed_mode(Request& context)
{
return(app_page_embed_mode(context));
}
String starter_page_main_html(Request& context)
{
return(app_page_main_html(context));
}
String starter_html_class(Request& context)
{
return(app_html_class(context));
}
String starter_menu_href(String menu_key, DTree menu_item, Request& context)
{
return(app_menu_href(menu_key, menu_item, context));
}
void starter_render_page(Request& context)
{
app_render_page(context);
}
void starter_redirect(String path, Request& context)
{
app_redirect(path, context);
}
void starter_not_found(String message, Request& context)
{
app_not_found(message, context);
}
DTree starter_make_route(Request& context)
{
return(app_make_route(context));
}
DTree starter_resolve_view(Request& context, String base_dir = "views")
{
return(app_resolve_view(context, base_dir));
}
void app_init(Request& context)
{
if(context.var["app"]["booted"].to_string() == "1")
return;
context.var["starter"]["booted"] = "1";
context.var["starter"]["fs_root"] = cwd_get();
context.var["starter"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
context.var["app"]["booted"] = "1";
context.var["app"]["fs_root"] = cwd_get();
context.var["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
String base_url = dirname(context.var["starter"]["script_url"].to_string());
String base_url = dirname(context.var["app"]["script_url"].to_string());
if(base_url == "")
base_url = "/";
if(base_url[base_url.length() - 1] != '/')
base_url.append(1, '/');
context.var["starter"]["base_url"] = base_url;
context.var["app"]["base_url"] = base_url;
DTree config = starter_default_config();
String requested_theme = first(context.get["theme"], context.cookies["starter_theme"], config["theme"]["key"].to_string());
DTree config = get_config();
String requested_theme = first(context.get["theme"], context.cookies["app_theme"], config["theme"]["key"].to_string());
if(config["theme"]["options"][requested_theme].to_string() == "" && config["theme"]["options"][requested_theme].get_type_name() != "array")
requested_theme = config["theme"]["key"].to_string();
@ -379,24 +395,24 @@ void starter_boot(Request& context)
config["theme"]["key"] = requested_theme;
context.cfg = config;
context.var["cfg"].set_reference(&context.cfg);
context.var["starter"]["config"].set_reference(&context.cfg);
context.var["starter"]["route"] = starter_make_route(context);
context.var["starter"]["page_type"] = "html";
context.var["starter"]["page_title"] = first(config["menu"][context.var["starter"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
context.var["starter"]["embed_mode"].set_bool(context.get["embed"] != "");
context.var["app"]["config"].set_reference(&context.cfg);
context.var["app"]["route"] = app_make_route(context);
context.var["app"]["page_type"] = "html";
context.var["app"]["page_title"] = first(config["menu"][context.var["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
context.var["app"]["embed_mode"].set_bool(context.get["embed"] != "");
if(context.get["theme"] != "" && context.get["theme"] == requested_theme)
{
set_cookie("starter_theme", requested_theme, time() + (86400 * 180));
context.cookies["starter_theme"] = requested_theme;
set_cookie("app_theme", requested_theme, time() + (86400 * 180));
context.cookies["app_theme"] = requested_theme;
}
session_start();
StarterUser(context).is_signed_in();
AppUser(context).is_signed_in();
starter_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
starter_register_css("themes/common/fontawesome/css/all.min.css", context);
starter_register_js("js/u-query.js", context);
starter_register_js("js/morphdom.js", context);
starter_register_js("js/site.js", context);
app_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
app_register_css("themes/common/fontawesome/css/all.min.css", context);
app_register_js("js/u-query.js", context);
app_register_js("js/morphdom.js", context);
app_register_js("js/site.js", context);
}

View File

@ -1,18 +1,18 @@
#pragma once
class StarterUser
struct AppUser
{
public:
Request& context;
explicit StarterUser(Request& request)
explicit AppUser(Request& request)
: context(request)
{
}
static String session_key()
{
return("starter_user_id");
return("app_user_id");
}
static String normalize_email(String email)
@ -23,12 +23,12 @@ public:
static String hash_id(String raw)
{
raw = normalize_email(raw);
return(gen_sha1("uce-starter:" + raw).substr(0, 12));
return(gen_sha1("uce-app:" + raw).substr(0, 12));
}
String root_path()
{
return(first(context.cfg.get_by_path("filebase/path").to_string(), "/tmp/uce-starter-data"));
return(first(context.cfg.get_by_path("filebase/path").to_string(), "/tmp/uce-app-data"));
}
String dir(String email)
@ -44,7 +44,7 @@ public:
static String password_hash(String password, String salt)
{
String hash = "uce-starter:" + password + ":" + salt;
String hash = "uce-app:" + password + ":" + salt;
for(s32 i = 0; i < 2048; i++)
hash = gen_sha1(hash + ":" + std::to_string(i));
return(hash);
@ -148,7 +148,7 @@ public:
session_start();
context.session[session_key()] = email;
context.var["starter"]["current_user"] = user;
context.var["app"]["current_user"] = user;
result["result"].set_bool(true);
result["profile"] = user;
return(result);
@ -156,7 +156,7 @@ public:
bool is_signed_in()
{
if(context.var["starter"]["current_user"]["email"].to_string() != "")
if(context.var["app"]["current_user"]["email"].to_string() != "")
return(true);
String user_id = context.session[session_key()];
if(user_id == "")
@ -167,20 +167,20 @@ public:
context.session.erase(session_key());
return(false);
}
context.var["starter"]["current_user"] = user;
context.var["app"]["current_user"] = user;
return(true);
}
DTree current()
{
if(is_signed_in())
return(context.var["starter"]["current_user"]);
return(context.var["app"]["current_user"]);
return(DTree());
}
void logout()
{
context.session.erase(session_key());
context.var["starter"]["current_user"].clear();
context.var["app"]["current_user"].clear();
}
};

View File

@ -1,134 +0,0 @@
# Web App Starter - AI Assistant Instructions
## Project Architecture
**Custom PHP framework** with server-side rendering focus, component-based UI, and clean URL routing.
### Core Flow
1. `index.php``URL::MakeRoute()``views/{page}.php` → theme template
2. All requests flow through `index.php` (single entry point)
3. Views render into `URL::$fragments['main']`, then wrapped by theme
### Key Directories
- `lib/` - Core classes (URL, DB, Log, Profiler, components)
- `views/` - Page templates (maps to URL routes)
- `components/` - Reusable UI components
- `config/settings.php` - Main configuration
- `themes/` - Page layout templates
- `js/` - Client-side code (uquery.js, morphdom.js, etc.)
## Core Classes & Functions
### Configuration
```php
cfg('key/path') // Get config value
cfg('url/pretty') // true=clean URLs, false=query strings
```
### URL Routing
```php
URL::MakeRoute() // Parse URL into URL::$route
URL::Link($path, $params) // Generate URLs (respects pretty URL setting)
URL::$route['page'] // Target view (default: 'index')
URL::$route['l-path'] // Full URL path
URL::$fragments['main'] // Main content area
```
### Components
**Files return arrays with render functions:**
```php
<?php return [
'render' => function($prop) { return '<div>...</div>'; },
'about' => 'Component description'
];
```
**Usage:**
```php
component('path/to/component', ['prop' => 'value'])
component('path/to/component:method', $props) // Specific render method
component_declare('name', $definition) // Inline declaration
```
### Database
```php
DB::Query($sql, $params) // Execute any SQL query
DB::Get($sql, $params) // Get multiple rows as array
DB::GetRowWithQuery($sql, $params) // Get single row
DB::GetRow($table, $keyvalue) // Get row by primary key
DB::GetRow($table, $id, $keyname) // Get row by specific key
DB::GetRowsMatch($table, $criteria) // Get rows matching criteria
DB::Insert($table, $data) // Insert new row, returns ID
DB::Commit($table, $data) // Insert or update (REPLACE)
DB::Update($table, $where, $data) // Update existing rows
DB::RemoveRow($table, $keyvalue) // Delete row by key
```
### Logging
```php
Log::debug($module, $text) // log/debug.YYYY-mm.log
Log::text($module, $text) // log/log.YYYY-mm.log
Log::audit($module, $text) // System journal
```
### Profiler
```php
Profiler::log($text, $indent_level) // Runtime profiling
Profiler::$log // Access logged data
```
## Coding Conventions
### File Structure
- Views: `views/{pagename}.php`
- Components: `components/{category}/{name}.php`
- Classes: `lib/{name}.class.php`
- Utilities: `lib/{name}.php`
### JS and PHP Coding Style
- Use `<?=` for output, use escaping functions safe(), asafe(), and jsafe() as appropriate
- snake_case for functions/methods
- PascalCase for classes
- kebab-case for file/directory names
- Always include `<?php` opening tag
- Use uquery.js (documentation: uquery.md) for DOM manipulation
### Component Pattern
```php
<?php return [
'render' => function($prop) {
return "<div class='{$prop['class']}'>{$prop['content']}</div>";
},
'render:variant' => function($prop) { /* alternative render */ },
'about' => 'Component description'
];
```
### URL Handling
- Use `URL::Link()` for all internal links
- Views auto-map to URLs (`views/users.php``/users`)
## Built-in Libraries
- **uquery.js** - jQuery-like DOM manipulation
- **morphdom.js** - DOM diffing
- **macrobars.js** - JavaScript templating
## Common Patterns
### New Page
1. Create `views/{pagename}.php`
2. Set URL::$page_type to 'json' or 'blank' if page is AJAX
3. Content (auto-wrapped by theme page variant controlled by URL::$page_type)
### New Component
1. Create `components/{category}/{name}.php`
2. Return array with `render` function
3. Use `component('components/{category}/{name}', $props)`
### Error Handling
- Logging: Use `Log::debug()` for development, `Log::audit()` for security
- Profiling: Use `Profiler::log()` for performance analysis
## Development Notes
- Server-side rendering preferred over SPA approach
- Component system encourages reusability
- Single entry point simplifies routing/middleware

View File

@ -1,66 +0,0 @@
# Logs
logs
*.log
.fuse*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
log/
private/
public/
config/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

View File

@ -1,208 +0,0 @@
# Web App Starter
Simple web app starter package, expects to run in a domain's root directory but will run everywhere as long as cfg('url/pretty') is set to false.
Don't be afraid of server-side rendering! But we have JS options too.
## Nginx Configuration
### Pretty URLs Support (Optional)
For pretty URLs (`cfg('url/pretty') = true`), add this configuration to enable clean URLs like `/users/profile` instead of `/?users/profile`:
```nginx
location / {
try_files $uri $uri/ /index.php?$args;
}
# the rest of the owl
location ~ ^/(config|lib|private|\.git)/ {
deny all;
return 404;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
```
**Note**: Without pretty URL configuration, set `cfg('url/pretty') = false` in your config/settings.php and URLs will use query string format: `/?page&param=value`. You can generate URLs with `URL::Link()` which will automatically use the appropriate format if you want to keep your options open (as to whether to use pretty URLs or not).
## Routing
`URL::MakeRoute()` creates `URL::$route` data. For example, a request to `http://example.com/some/url/path`
would look like this:
```
Array
(
[page] => index
[l-path] => some/url/path
)
```
The `page` key refers to the view should be rendered. By default, this is just `'index'`, leading to
`'views/index.php'` getting invoked. If the URL's path element starts with a colon, the entire path
is used as the `page` value.
The `l-path` key refers to the actual URL path as given by the browser. For example, the `'views/index.php'`
view uses this to invoke the correct sub-view.
The starter also supports two additional patterns backported from downstream apps:
- directory index routing: `/workspace` can resolve to `views/workspace/index.php`
- parent-path fallback: `/workspace/activity` can resolve to `views/workspace/index.php` with `URL::$route['param'] === 'activity'`
## Components
Components are reusable PHP-backed view fragments. A component file returns an array with one or more render functions plus optional metadata.
The default starter convention is to keep reusable components in `components/`. The loader supports explicit paths like `components/example/theme-switcher` and shorthand names like `example/theme-switcher`, which resolve under `components/`.
```php
<?php return [
'render' => function($prop) {
return '<div class="my-component">' . $prop['content'] . '</div>';
},
'about' => 'Description of what this component does',
];
```
### Using Components
```php
<?= component('components/example/theme-switcher') ?>
```
**Using a Specific Render Method Explicitly:**
```php
<?= component_call('components/workspace/panel', 'render', ['title' => 'Status']) ?>
```
The older `component-name:method` shorthand is still supported for backwards compatibility, but `component_call()` or an explicit `render_call` prop is the preferred API because it is clearer and easier to grep.
## Dashboard Primitives
The starter now includes a small set of dashboard-focused building blocks backported from a more complex production frontend:
- `js/u-format.js` for human-readable byte, count, and duration formatting plus unit-aware parsing
- `js/u-sortable-table.js` for lightweight sortable HTML tables with persisted sort state
- `js/u-timeseries-chart.js` for multi-series canvas charts without pulling in a charting framework
- `components/data/summary-metrics.php`, `components/data/sortable-table.php`, and `components/data/timeseries-chart.php`
- `views/dashboard.php` as a working end-to-end example
## Workspace Primitives
The starter also includes a second backport slice from the `uh-ai` portal app:
- `components/workspace/*` for app shells, sidebars, panel headers, status pills, empty states, and compact utility buttons
- `themes/common/css/workspace.css` for the shared workspace shell styling
- `js/u-workspace-shell.js` for simple responsive sidebar toggling
- `views/workspace/index.php` as a nested-route demo for shell-style products
## Theme Families
The starter now includes named theme families derived from the `uh-ai` portal app and the `uh-llm2` admin shell:
- `portal-light` for the B612-based corporate portal look
- `portal-dark` for the glassy dark portal shell and the current default starter theme
- `localfirst` for the cyan/orange llm2-style admin shell
Theme choice is resolved server-side from `config/settings.php`, with a validated theme key stored in the `starter_theme` cookie. Theme metadata such as descriptions, footer text, and browser theme colors now live in the same config entry as the theme path, so the gallery, page shell, and docs all read from the same source of truth.
To compare themes side by side, use `/?themes`. The gallery embeds a shared `/?theme-preview` route under every available theme so you can evaluate shell chrome, content density, and form styling with the same fixture.
The page shell is now split between theme CSS/assets and shared helper functions in `lib/theme_helpers.php`, which keeps the four top-nav themes aligned while still allowing `localfirst` to provide its own admin-shell layout.
The homepage demo form now includes proper `id` and `name` attributes so the starter does not emit the earlier label/field accessibility warnings on the landing page.
## Gauge Primitives
The gauges demo now contains two gauge families from `uh-llm2`:
- `components/gauges/progressbar.php` and `components/gauges/needlegauge.php` for the original bar and needle gauges
- `components/gauges/arcgauge.php` for the llm2-style SVG KPI arc gauges with optional watermark tracking
Abstract gauge classes live in `themes/common/css/gauges.css`, so the components stay theme-aware through CSS variables instead of shipping a competing visual system.
## Testing
The starter now includes a no-dependency smoke suite under `tests/`.
Run it with:
```bash
php tests/smoke.php
```
It covers route resolution, link generation, component lookup, shorthand component rendering, and centralized theme metadata.
### Inline Components
You can also declare components directly in code:
```php
component_declare('my-button', [
'render' => function($prop) {
return "<button class='{$prop['class']}'>{$prop['text']}</button>";
}
]);
```
## Logging
This package includes a very basic Log class.
```php
Log::debug($module, $text) /* writes to log/debug.[Y]-[m].log */
Log::text($module, $text) /* writes to log/log.[Y]-[m].log */
Log::audit($module, $text) /* writes to system journal */
```
`$module` should be a short string identifying the module or context of the log line.
`$text` text to be logged.
## Profiler
A basic profiling fixture.
```php
Profiler::log($text, $indent_level = 0)
```
`$text` name/text describing the checkpoint to be profiled.
Profiler logs are not committed to disk. Log data for the current request is stored in
`Profiler::$log`.
## Batteries included
```
- lib/ulib.php - convenience functions
- lib/profiler.class.php - profiling
- lib/log.class.php - logging
- lib/url.class.php - URL routing
- lib/components.php - component rendering
- lib/odt.class.php - ODT document generator
- lib/db.class.php - database access
- js/morphdom.js - DOM diffing
- js/uquery.js - DOM manipulation (my attempt at keeping the good parts of jQuery)
- js/site.js - site-wide JS, as a starting point
- js/macrobars.js - JS templating
- js/ag-grid - ag-Grid
```
## License (MIT Open Source)
Copyright 2018-2025 udo@openfu.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,66 +0,0 @@
#!/usr/bin/env bash
csensitive=false
patterns=()
# Parse flags and patterns
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--case-sensitive)
csensitive=true
shift
;;
-h|--help)
echo "Usage: $0 [-c|--case-sensitive] <pattern> [<pattern>...]" >&2
exit 1
;;
-* )
echo "Unknown option: $1" >&2
echo "Usage: $0 [-c|--case-sensitive] <pattern> [<pattern>...]" >&2
exit 1
;;
* )
patterns+=("$1")
shift
;;
esac
done
# Ensure at least one pattern is provided
if (( ${#patterns[@]} == 0 )); then
echo "Usage: $0 [-c|--case-sensitive] <pattern> [<pattern>...]" >&2
exit 1
fi
# Build common grep options
grep_opts=( -nR -H --color=tty )
if ! $csensitive; then
grep_opts+=( -i )
fi
# Build -e arguments for each pattern
pattern_opts=()
for pat in "${patterns[@]}"; do
pattern_opts+=( -e "$pat" )
done
# Define search paths
declare -a php_sea=(".")
declare -a css_sea=(".")
declare -a js_sea=(".")
# Search PHP files
for vs in "${php_sea[@]}"; do
grep "${grep_opts[@]}" --include="*.php" "${pattern_opts[@]}" "$vs"
done
# Search CSS files (exclude fontawesome)
for vs in "${css_sea[@]}"; do
grep "${grep_opts[@]}" --include="*.css" "${pattern_opts[@]}" "$vs" \
| grep -iv "fontawesome"
done
# Search JS files
for vs in "${js_sea[@]}"; do
grep "${grep_opts[@]}" --include="*.js" "${pattern_opts[@]}" "$vs"
done

View File

@ -1,283 +0,0 @@
<?php return [
'render' => function($prop) {
$services = $prop['services'] ?? [
'google' => [
'name' => 'Google',
'color' => '#4285f4',
'icon' => 'fab fa-google',
'scope' => 'openid email profile',
'auth_url' => 'https://accounts.google.com/oauth/authorize',
'token_url' => 'https://oauth2.googleapis.com/token',
'user_info_url' => 'https://www.googleapis.com/oauth2/v2/userinfo'
],
'github' => [
'name' => 'GitHub',
'color' => '#333333',
'icon' => 'fab fa-github',
'scope' => 'user:email',
'auth_url' => 'https://github.com/login/oauth/authorize',
'token_url' => 'https://github.com/login/oauth/access_token',
'user_info_url' => 'https://api.github.com/user'
],
'discord' => [
'name' => 'Discord',
'color' => '#5865f2',
'icon' => 'fab fa-discord',
'scope' => 'identify email',
'auth_url' => 'https://discord.com/api/oauth2/authorize',
'token_url' => 'https://discord.com/api/oauth2/token',
'user_info_url' => 'https://discord.com/api/users/@me'
],
'twitch' => [
'name' => 'Twitch',
'color' => '#9146ff',
'icon' => 'fab fa-twitch',
'scope' => 'user:read:email',
'auth_url' => 'https://id.twitch.tv/oauth2/authorize',
'token_url' => 'https://id.twitch.tv/oauth2/token',
'user_info_url' => 'https://api.twitch.tv/helix/users'
]
];
$title = $prop['title'] ?? 'Sign in with OAuth';
$subtitle = $prop['subtitle'] ?? 'Choose your preferred authentication method';
$callback_url = $prop['callback_url'] ?? URL::Link('auth/callback');
?>
<form class="card">
<div>
<h2><?= safe($title) ?></h2>
<label><?= safe($subtitle) ?></label>
</div>
<?php foreach($services as $service_key => $service): ?>
<div data-service="<?= safe($service_key) ?>">
<button type="button" class="btn" onclick="initiateOAuth('<?= safe($service_key) ?>')">
<i class="<?= safe($service['icon']) ?>" style="color: <?= safe($service['color']) ?>"></i>
Continue with <?= safe($service['name']) ?>
</button>
<div class="loading" style="display: none;">
<span>Connecting...</span>
</div>
<label>Secure authentication via <?= safe($service['name']) ?></label>
</div>
<?php endforeach; ?>
<div class="banner" id="oauth-status" style="display: none;">
<div class="status-icon"></div>
<div class="status-message"></div>
</div>
</form>
<div class="card" id="oauth-debug" style="display: none;">
<h4>Debug Information</h4>
<pre id="oauth-debug-content"></pre>
</div>
<script>
// OAuth configuration for different services
const oauthConfig = {
<?php foreach($services as $service_key => $service): ?>
<?= $service_key ?>: {
clientId: '<?= safe($prop[$service_key . '_client_id'] ?? 'YOUR_' . strtoupper($service_key) . '_CLIENT_ID') ?>',
clientSecret: '<?= safe($prop[$service_key . '_client_secret'] ?? '') ?>', // Only needed for server-side flow
redirectUri: '<?= safe($callback_url) ?>',
scope: '<?= safe($service['scope']) ?>',
authUrl: '<?= safe($service['auth_url']) ?>',
tokenUrl: '<?= safe($service['token_url']) ?>',
userInfoUrl: '<?= safe($service['user_info_url']) ?>',
additionalParams: {
<?php
// Add service-specific parameters
switch ($service_key) {
case 'google':
echo "'access_type': 'offline', 'prompt': 'select_account'";
break;
case 'discord':
echo "'prompt': 'consent'";
break;
case 'github':
echo "'allow_signup': 'true'";
break;
case 'twitch':
echo "'force_verify': 'true'";
break;
}
?>
}
},
<?php endforeach; ?>
};
// Global OAuth functions
window.initiateOAuth = function(service) {
const $serviceDiv = $(`[data-service="${service}"]`);
const $button = $serviceDiv.find('.btn');
const $loading = $serviceDiv.find('.loading');
const $status = $('#oauth-status');
const $debug = $('#oauth-debug');
// Show loading state
$button.hide();
$loading.show();
// Hide previous status/debug info
$status.hide();
$debug.hide();
if (oauthConfig[service]) {
initiateOAuthFlow(service);
} else {
showError('Unsupported OAuth service: ' + service);
resetButton(service);
}
function resetButton(service) {
setTimeout(() => {
$(`[data-service="${service}"] .btn`).show();
$(`[data-service="${service}"] .loading`).hide();
}, 1000);
}
};
function initiateOAuthFlow(service) {
const config = oauthConfig[service];
if (config.clientId.includes('YOUR_')) {
showError(`${service.charAt(0).toUpperCase() + service.slice(1)} OAuth not configured. Please set ${service}_client_id in the component properties.`);
$(`[data-service="${service}"] .btn`).show();
$(`[data-service="${service}"] .loading`).hide();
return;
}
// Build OAuth URL
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
response_type: 'code',
state: generateState()
});
// Add service-specific parameters
for (const [key, value] of Object.entries(config.additionalParams || {})) {
params.set(key, value);
}
const authUrl = `${config.authUrl}?${params.toString()}`;
// Store state in session storage for verification
sessionStorage.setItem('oauth_state', params.get('state'));
sessionStorage.setItem('oauth_service', service);
// Also store in PHP session for callback handler
<?php if (session_status() === PHP_SESSION_ACTIVE): ?>
$.post('<?= URL::Link('auth/store-oauth-session') ?>', {
oauth_service: service,
oauth_state: params.get('state')
}).catch(console.error);
<?php endif; ?>
// Debug information
showDebug({
service: service,
authUrl: authUrl,
clientId: config.clientId,
redirectUri: config.redirectUri,
scope: config.scope,
state: params.get('state')
});
// Redirect to OAuth provider
window.location.href = authUrl;
}
function generateState() {
return btoa(Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)).replace(/[^a-zA-Z0-9]/g, '');
}
function showStatus(message, type = 'loading') {
const $status = $('#oauth-status');
const $icon = $status.find('.status-icon');
const $message = $status.find('.status-message');
$status.removeClass('success error loading').addClass(type).show();
let icon = '';
switch(type) {
case 'success': icon = '✓'; break;
case 'error': icon = '✗'; break;
case 'loading': icon = '⏳'; break;
}
$icon.text(icon);
$message.text(message);
}
function showError(message) {
showStatus(message, 'error');
}
function showSuccess(message) {
showStatus(message, 'success');
}
function showDebug(data) {
$('#oauth-debug-content').text(JSON.stringify(data, null, 2));
$('#oauth-debug').show();
}
// Check for OAuth callback parameters on page load
$.ready(function() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
showError('OAuth failed: ' + (urlParams.get('error_description') || error));
return;
}
if (code && state) {
const storedState = sessionStorage.getItem('oauth_state');
const service = sessionStorage.getItem('oauth_service');
if (state !== storedState) {
showError('Invalid OAuth state. Possible security issue.');
return;
}
showStatus('Authorization successful! Processing...', 'success');
showDebug({
step: 'callback_received',
service: service,
code: code.substring(0, 20) + '...',
state: state,
next_steps: [
'Send code to backend OAuth handler',
'Exchange code for access token',
'Get user profile from OAuth provider',
'Create or login user account',
'Set session and redirect to dashboard'
]
});
// Clean up session storage
sessionStorage.removeItem('oauth_state');
sessionStorage.removeItem('oauth_service');
// Remove OAuth parameters from URL for cleaner display
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
});
</script>
<?php
},
'about' => 'OAuth authentication component with support for Google and other providers. Handles the complete OAuth flow including state verification and callback processing.'
];

View File

@ -1,245 +0,0 @@
<?php return [
'render' => function($prop) {
?>
<!-- Cookie Consent Banner -->
<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;
">
<?= first($prop['text'] ?? false,
'We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic.
By clicking "Accept All", you consent to our use of cookies.') ?>
<a href="#" id="cookie-policy-link" style="
color: var(--primary);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s ease;
"
onmouseover="this.style.borderColor='var(--primary)';"
onmouseout="this.style.borderColor='transparent';">
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;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
"
onmouseover="this.style.background='var(--surface-elevated)'; this.style.borderColor='var(--primary)'; this.style.color='var(--text-primary)';"
onmouseout="this.style.background='transparent'; this.style.borderColor='var(--border)'; this.style.color='var(--text-secondary)';">
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;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
box-shadow: var(--shadow-sm);
"
onmouseover="this.style.background='var(--primary-dark, #3b82f6)'; this.style.transform='translateY(-1px)'; this.style.boxShadow='var(--shadow-md)';"
onmouseout="this.style.background='var(--primary)'; this.style.transform='translateY(0)'; this.style.boxShadow='var(--shadow-sm)';">
Accept All
</button>
</div>
</div>
</div>
<script>
(function() {
// Cookie consent functionality
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 = <?= jsafe(first($prop['cookie_name'] ?? false, 'cookie_consent')) ?>;
let COOKIE_EXPIRY_DAYS = <?= jsafe(first($prop['expiry_days'] ?? false, 365)) ?>;
// Check if user has already made a choice
let existingConsent = getCookie(COOKIE_NAME);
<?php
if(!empty($prop['reset']))
{
$_COOKIE[$COOKIE_NAME] = '';
?>existingConsent = false;<?php
}
?>
if (!existingConsent) {
setTimeout(() => {
cookieConsent.style.transform = 'translateY(0)';
}, 1);
}
// Handle accept button
acceptButton.addEventListener('click', function() {
setCookie(COOKIE_NAME, 'accepted', COOKIE_EXPIRY_DAYS);
hideBanner();
// Initialize analytics or other tracking here if needed
console.log('Cookies accepted');
});
// Handle reject button
rejectButton.addEventListener('click', function() {
setCookie(COOKIE_NAME, 'rejected', COOKIE_EXPIRY_DAYS);
hideBanner();
// Disable tracking here if needed
console.log('Cookies rejected');
});
// Handle policy link (you can customize this URL)
policyLink.addEventListener('click', function(e) {
e.preventDefault();
// You can change this to your actual privacy policy URL
window.open('/privacy-policy', '_blank');
});
function hideBanner() {
cookieConsent.style.transform = 'translateY(100%)';
setTimeout(() => {
cookieConsent.style.display = 'none';
}, 300);
}
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;
}
// Keyboard accessibility
cookieConsent.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
// Treat Escape as reject
rejectButton.click();
}
});
// Focus management for accessibility
if (!existingConsent) {
setTimeout(() => {
acceptButton.focus();
}, 1100);
}
})();
</script>
<style>
/* Responsive adjustments for cookie consent */
@media (max-width: 768px) {
#cookie-consent > div {
flex-direction: column !important;
align-items: stretch !important;
gap: 1rem !important;
}
#cookie-consent > div > div:last-child {
justify-content: center !important;
}
#cookie-accept, #cookie-reject {
flex: 1 !important;
text-align: center !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
#cookie-consent {
border-top-width: 2px !important;
}
#cookie-accept, #cookie-reject {
border-width: 2px !important;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
#cookie-consent, #cookie-accept, #cookie-reject {
transition: none !important;
}
}
</style>
<?php
},
'about' => 'A GDPR-compliant cookie consent banner that appears at the bottom of the page with accept/reject options'
];

View File

@ -1,130 +0,0 @@
<?php
if (!function_exists('sortable_table_format_bytes')) {
function sortable_table_format_bytes($bytes, $disk = false)
{
if ($bytes === null || $bytes === '') return '--';
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$value = (float)$bytes;
$unitIndex = 0;
while (abs($value) >= 1024 && $unitIndex < count($units) - 1) {
$value /= 1024;
$unitIndex += 1;
}
$decimals = $disk ? ($unitIndex >= 4 ? 2 : ($unitIndex >= 1 ? 1 : 0)) : ($unitIndex === 0 ? 0 : 1);
return number_format($value, $decimals) . ' ' . $units[$unitIndex];
}
}
if (!function_exists('sortable_table_format_value')) {
function sortable_table_format_value($value, $row, $column)
{
$format = $column['format'] ?? null;
if (is_callable($format)) {
return [$format($value, $row, $column), $value];
}
switch ($format) {
case 'number':
return [number_format((float)$value), (float)$value];
case 'bytes':
return [sortable_table_format_bytes($value, false), (float)$value];
case 'disk-bytes':
return [sortable_table_format_bytes($value, true), (float)$value];
case 'percent':
return [number_format((float)$value, 1) . '%', (float)$value];
case 'duration-ms':
$number = (float)$value;
if ($number >= 1000) {
return [number_format($number / 1000, $number >= 10000 ? 0 : 1) . ' s', $number];
}
return [number_format($number, $number >= 100 ? 0 : 1) . ' ms', $number];
case 'bool':
return [$value ? 'Yes' : 'No', $value ? 1 : 0];
default:
if (is_bool($value)) {
return [$value ? 'Yes' : 'No', $value ? 1 : 0];
}
return [(string)$value, is_scalar($value) ? $value : ''];
}
}
}
return [
'render' => function($prop) {
include_js('js/u-format.js');
include_js('js/u-sortable-table.js');
$tableId = (string)($prop['id'] ?? ('sortable-table-' . uniqid()));
$title = (string)($prop['title'] ?? '');
$subtitle = (string)($prop['subtitle'] ?? '');
$columns = is_array($prop['columns'] ?? null) ? $prop['columns'] : [];
$rows = is_array($prop['rows'] ?? null) ? $prop['rows'] : [];
$emptyLabel = (string)($prop['empty_label'] ?? 'No data available');
$storageKey = (string)($prop['storage_key'] ?? ('starter.sort.' . $tableId));
$sort = is_array($prop['sort'] ?? null) ? $prop['sort'] : [];
$initOptions = ['storageKey' => $storageKey];
if (isset($sort['column'])) {
$initOptions['initialSort'] = [
'column' => (int)$sort['column'],
'direction' => (($sort['direction'] ?? 'asc') === 'desc') ? 'desc' : 'asc',
];
}
$jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
ob_start();
?>
<div class="dashboard-panel">
<?php if ($title !== '' || $subtitle !== ''): ?>
<div class="dashboard-panel-header">
<?php if ($title !== ''): ?><h2><?= safe($title) ?></h2><?php endif; ?>
<?php if ($subtitle !== ''): ?><p><?= safe($subtitle) ?></p><?php endif; ?>
</div>
<?php endif; ?>
<div class="dashboard-table-wrap">
<table id="<?= asafe($tableId) ?>" class="u-sortable-table">
<thead>
<tr>
<?php foreach ($columns as $column): ?>
<?php
$align = (string)($column['align'] ?? 'left');
$sortable = !isset($column['sortable']) || $column['sortable'];
?>
<th scope="col" class="align-<?= asafe($align) ?>"<?= !$sortable ? ' data-sortable="false"' : '' ?>><?= safe((string)($column['label'] ?? $column['key'] ?? 'Column')) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php if (!$rows): ?>
<tr data-empty-row="1"><td colspan="<?= count($columns) ?: 1 ?>" class="muted"><?= safe($emptyLabel) ?></td></tr>
<?php endif; ?>
<?php foreach ($rows as $row): ?>
<tr>
<?php foreach ($columns as $column): ?>
<?php
$key = (string)($column['key'] ?? '');
$value = $row[$key] ?? '';
list($displayValue, $sortValue) = sortable_table_format_value($value, $row, $column);
if (isset($column['sort_value']) && is_callable($column['sort_value'])) {
$sortValue = $column['sort_value']($value, $row, $column);
}
?>
<td class="align-<?= asafe((string)($column['align'] ?? 'left')) ?>" data-sort-value="<?= asafe((string)$sortValue) ?>"><?= safe((string)$displayValue) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<script>
(function () {
if (typeof USortableTable === 'undefined') return;
USortableTable.init(<?= jsafe($tableId) ?>, <?= json_encode($initOptions, $jsonFlags) ?>);
}());
</script>
<?php
return ob_get_clean();
},
'about' => 'Lightweight sortable HTML table with remembered sort state and human-readable data formatting',
];

View File

@ -1,37 +0,0 @@
<?php return [
'render' => function($prop) {
$items = is_array($prop['items'] ?? null) ? $prop['items'] : [];
$title = (string)($prop['title'] ?? '');
$subtitle = (string)($prop['subtitle'] ?? '');
ob_start();
?>
<div class="dashboard-panel">
<?php if ($title !== '' || $subtitle !== ''): ?>
<div class="dashboard-panel-header">
<?php if ($title !== ''): ?><h2><?= safe($title) ?></h2><?php endif; ?>
<?php if ($subtitle !== ''): ?><p><?= safe($subtitle) ?></p><?php endif; ?>
</div>
<?php endif; ?>
<div class="dashboard-stat-grid">
<?php foreach ($items as $item): ?>
<?php
$tone = preg_replace('/[^a-z0-9_-]+/i', '', (string)($item['tone'] ?? 'info'));
$tag = !empty($item['href']) ? 'a' : 'div';
$href = !empty($item['href']) ? ' href="' . asafe((string)$item['href']) . '"' : '';
?>
<<?= $tag ?> class="dashboard-stat-card tone-<?= asafe($tone) ?>"<?= $href ?>>
<div class="dashboard-stat-label"><?= safe((string)($item['label'] ?? 'Metric')) ?></div>
<div class="dashboard-stat-value"><?= safe((string)($item['value'] ?? '--')) ?></div>
<?php if (!empty($item['meta'])): ?>
<div class="dashboard-stat-meta"><?= safe((string)$item['meta']) ?></div>
<?php endif; ?>
</<?= $tag ?>>
<?php endforeach; ?>
</div>
</div>
<?php
return ob_get_clean();
},
'about' => 'Responsive metric cards extracted from dashboard-style pages and adapted for generic starter overviews',
];

View File

@ -1,297 +0,0 @@
<?php return [
'render' => function($prop) {
// Generate unique ID for this table instance
$tableId = $prop['id'] ?? 'data-grid-' . uniqid();
$data = $prop['items'] ?? [];
$columns = $prop['columns'] ?? null;
$options = $prop['options'] ?? [];
// Auto-generate columns if not provided
if (!$columns && !empty($data)) {
$columns = [];
$firstItem = reset($data);
if (is_array($firstItem)) {
foreach (array_keys($firstItem) as $key) {
$columns[] = [
'field' => $key,
'headerName' => ucfirst(str_replace('_', ' ', $key)),
'sortable' => true,
'filter' => true,
'resizable' => true
];
}
}
}
// Default grid options (Community edition compatible)
$defaultOptions = [
'pagination' => true,
'paginationPageSize' => 20,
'suppressMenuHide' => true,
'animateRows' => true,
'defaultColDef' => [
'sortable' => true,
'filter' => true,
'resizable' => true,
'minWidth' => 100
]
];
$gridOptions = array_merge($defaultOptions, $options);
$gridOptions['columnDefs'] = $columns;
$gridOptions['rowData'] = $data;
?>
<!-- ag-Grid Data Table Component -->
<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;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 0.25rem;">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<line x1="9" y1="9" x2="15" y2="9"/>
<line x1="9" y1="15" x2="15" y2="15"/>
</svg>
<?= count($data) ?> rows
</span>
<!-- Search Input -->
<div style="position: relative; display: flex; align-items: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" stroke-width="2" style="position: absolute; left: 0.75rem; z-index: 1;">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<input
type="text"
id="<?= $tableId ?>-search"
placeholder="Search all columns..."
style="
padding: 0.5rem 0.75rem 0.5rem 2.5rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface);
color: var(--text-primary);
font-size: 0.875rem;
width: 250px;
transition: all 0.2s ease;
"
onFocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 2px var(--primary-light)';"
onBlur="this.style.borderColor='var(--border)'; this.style.boxShadow='none';"
/>
</div>
</div>
<div style="display: flex; gap: 0.5rem;">
<button id="<?= $tableId ?>-export-csv" style="
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s ease;
"
onmouseover="this.style.background='var(--primary)'; this.style.color='white'; this.style.borderColor='var(--primary)';"
onmouseout="this.style.background='var(--surface)'; this.style.color='var(--text-secondary)'; this.style.borderColor='var(--border)';">
📊 Export CSV
</button>
<button id="<?= $tableId ?>-clear-filters" style="
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s ease;
"
onmouseover="this.style.background='var(--surface-elevated)'; this.style.borderColor='var(--primary)';"
onmouseout="this.style.background='var(--surface)'; this.style.borderColor='var(--border)';">
🔄 Clear Filters
</button>
</div>
</div>
<div id="<?= $tableId ?>" class="ag-theme-alpine-dark" style="
height: <?= $prop['height'] ?? '400px' ?>;
width: 100%;
border: 1px solid var(--border);
border-radius: 0 0 0.5rem 0.5rem;
overflow: hidden;
"></div>
</div>
<!-- ag-Grid Theme Override Styles -->
<style>
.ag-theme-alpine-dark {
--ag-background-color: var(--surface);
--ag-foreground-color: var(--text-primary);
--ag-border-color: var(--border);
--ag-secondary-border-color: var(--border);
--ag-header-background-color: var(--surface-elevated);
--ag-header-foreground-color: var(--text-primary);
--ag-odd-row-background-color: var(--surface);
--ag-even-row-background-color: var(--surface-elevated);
--ag-row-hover-color: var(--surface-hover);
--ag-selected-row-background-color: var(--primary-light);
--ag-range-selection-background-color: var(--primary-light);
--ag-range-selection-border-color: var(--primary);
--ag-input-focus-border-color: var(--primary);
--ag-minier-foreground-color: var(--text-secondary);
--ag-subtle-text-color: var(--text-secondary);
--ag-disabled-foreground-color: var(--text-muted);
}
.ag-theme-alpine-dark .ag-header-cell-label {
font-weight: 600;
}
.ag-theme-alpine-dark .ag-cell {
border-right: 1px solid var(--border);
}
.ag-theme-alpine-dark .ag-header-cell {
border-right: 1px solid var(--border);
}
.ag-theme-alpine-dark .ag-paging-panel {
border-top: 1px solid var(--border);
background: var(--surface-elevated);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.ag-grid-toolbar {
flex-direction: column !important;
gap: 0.75rem !important;
align-items: stretch !important;
}
.ag-grid-toolbar > div {
justify-content: center !important;
}
}
</style>
<script>
if (typeof agGrid === 'undefined') {
const agGridCSS = document.createElement('link');
agGridCSS.rel = 'stylesheet';
agGridCSS.href = 'js/ag-grid/ag-grid.css';
document.head.appendChild(agGridCSS);
const agGridThemeCSS = document.createElement('link');
agGridThemeCSS.rel = 'stylesheet';
agGridThemeCSS.href = 'js/ag-grid/ag-theme-alpine.css';
document.head.appendChild(agGridThemeCSS);
const agGridScript = document.createElement('script');
agGridScript.src = 'js/ag-grid/ag-grid-community.min.js';
agGridScript.onload = function() {
initializeGrid_<?= $tableId ?>();
};
document.head.appendChild(agGridScript);
} else {
initializeGrid_<?= $tableId ?>();
}
function initializeGrid_<?= $tableId ?>() {
const gridOptions = <?= json_encode($gridOptions, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
// Enhanced column definitions with better formatting
gridOptions.columnDefs = gridOptions.columnDefs.map(col => {
// Add value formatters based on data type
if (col.field && gridOptions.rowData.length > 0) {
const sampleValue = gridOptions.rowData[0][col.field];
if (typeof sampleValue === 'number') {
col.type = 'numericColumn';
col.cellClass = 'ag-right-aligned-cell';
col.valueFormatter = params => {
if (params.value != null) {
return new Intl.NumberFormat().format(params.value);
}
return '';
};
} else if (typeof sampleValue === 'string' && !isNaN(Date.parse(sampleValue)) && sampleValue.includes('-')) {
col.valueFormatter = params => {
if (params.value) {
const date = new Date(params.value);
return date.toLocaleDateString();
}
return '';
};
} else if (typeof sampleValue === 'boolean') {
col.cellRenderer = params => {
return params.value ?
'<span style="color: #10b981;">✓ Yes</span>' :
'<span style="color: #ef4444;">✗ No</span>';
};
}
}
return col;
});
// Initialize the grid
const gridDiv = document.querySelector('#<?= $tableId ?>');
if (gridDiv && window.agGrid) {
const gridApi = agGrid.createGrid(gridDiv, gridOptions);
// Toolbar button handlers
document.getElementById('<?= $tableId ?>-export-csv')?.addEventListener('click', () => {
gridApi.exportDataAsCsv({
fileName: 'data-export-' + new Date().toISOString().split('T')[0] + '.csv'
});
});
document.getElementById('<?= $tableId ?>-clear-filters')?.addEventListener('click', () => {
gridApi.setFilterModel(null);
if (gridApi.setQuickFilter) {
gridApi.setQuickFilter('');
}
// Clear search input
const searchInput = document.getElementById('<?= $tableId ?>-search');
if (searchInput) {
searchInput.value = '';
}
});
// Auto-size columns on first data render
gridApi.addEventListener('firstDataRendered', () => {
gridApi.autoSizeAllColumns();
});
// Connect search input to quick filter
const searchInput = document.getElementById('<?= $tableId ?>-search');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const filterText = e.target.value;
if (gridApi.setQuickFilter) {
gridApi.setQuickFilter(filterText);
}
});
}
// Store grid API for external access
window['gridApi_<?= $tableId ?>'] = gridApi;
console.log('ag-Grid initialized for <?= $tableId ?> with', gridOptions.rowData.length, 'rows');
}
}
</script>
<?php
},
'about' => 'A powerful data grid component powered by ag-Grid with sorting, filtering, pagination, and export capabilities'
];

View File

@ -1,44 +0,0 @@
<?php return [
'render' => function($prop) {
include_js('js/u-format.js');
include_js('js/u-timeseries-chart.js');
$chartId = (string)($prop['id'] ?? ('ts-chart-' . uniqid()));
$canvasId = $chartId . '-canvas';
$title = (string)($prop['title'] ?? '');
$subtitle = (string)($prop['subtitle'] ?? '');
$height = max(180, (int)($prop['height'] ?? 320));
$series = array_values(is_array($prop['series'] ?? null) ? $prop['series'] : []);
$xLabels = array_values(is_array($prop['x_labels'] ?? null) ? $prop['x_labels'] : []);
$jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
ob_start();
?>
<div class="dashboard-panel">
<?php if ($title !== '' || $subtitle !== ''): ?>
<div class="dashboard-panel-header">
<?php if ($title !== ''): ?><h2><?= safe($title) ?></h2><?php endif; ?>
<?php if ($subtitle !== ''): ?><p><?= safe($subtitle) ?></p><?php endif; ?>
</div>
<?php endif; ?>
<canvas id="<?= asafe($canvasId) ?>" class="dashboard-chart-canvas" style="height: <?= $height ?>px"></canvas>
</div>
<script>
(function () {
if (typeof UTimeSeriesChart === 'undefined') return;
var chart = new UTimeSeriesChart(<?= jsafe($canvasId) ?>, {
xAxisLabel: <?= jsafe((string)($prop['x_axis_label'] ?? 'Time')) ?>,
yAxisLeftLabel: <?= jsafe((string)($prop['y_axis_left_label'] ?? 'Value')) ?>,
yAxisRightLabel: <?= jsafe((string)($prop['y_axis_right_label'] ?? '')) ?>,
yAxisLeftFormat: <?= jsafe((string)($prop['y_axis_left_format'] ?? 'number')) ?>,
yAxisRightFormat: <?= jsafe((string)($prop['y_axis_right_format'] ?? 'number')) ?>
});
chart.setData(<?= json_encode($series, $jsonFlags) ?>, <?= json_encode($xLabels, $jsonFlags) ?>);
window[<?= jsafe('chart_' . $chartId) ?>] = chart;
}());
</script>
<?php
return ob_get_clean();
},
'about' => 'Canvas-based multi-series time-series chart component backported from a production dashboard',
];

View File

@ -1,111 +0,0 @@
<?php return [
'render' => function($prop) {
$title = $prop['title'] ?? 'Trusted by Leading Companies';
$logos = $prop['logos'] ?? [
['name' => 'TechCorp', 'url' => 'img/cat01.jpg'],
['name' => 'StartupX', 'url' => 'img/cat01.jpg'],
['name' => 'DevStudio', 'url' => 'img/cat01.jpg'],
['name' => 'WebFlow', 'url' => 'img/cat01.jpg'],
['name' => 'CodeLab', 'url' => 'img/cat01.jpg'],
['name' => 'AppCraft', 'url' => 'img/cat01.jpg']
];
?>
<div class="brands-section">
<div class="brands-container">
<h3 class="brands-title"><?= safe($title) ?></h3>
<div class="brands-grid">
<?php foreach($logos as $index => $logo): ?>
<div class="brand-item" style="animation-delay: <?= $index * 0.1 ?>s">
<img src="<?= safe($logo['url']) ?>" alt="<?= safe($logo['name']) ?>" />
</div>
<?php endforeach; ?>
</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;
animation: fadeInScale 0.6s ease-out forwards;
opacity: 0;
transform: scale(0.8);
}
.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%);
}
@keyframes fadeInScale {
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 768px) {
.brands-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.brands-section {
padding: 2rem 0;
}
}
</style>
<?php
},
'about' => 'A brand/logo showcase section with hover effects and animations'
]; ?>

View File

@ -1,224 +0,0 @@
<?php return [
'render' => function($prop) {
$title = $prop['title'] ?? 'Ready to Get Started?';
$subtitle = $prop['subtitle'] ?? 'Join thousands of developers building amazing applications with our framework.';
$cta_text = $prop['cta_text'] ?? 'Start Building Now';
$cta_link = $prop['cta_link'] ?? '#';
$secondary_text = $prop['secondary_text'] ?? 'View Documentation';
$secondary_link = $prop['secondary_link'] ?? '#';
?>
<div class="cta-section">
<div class="cta-container">
<div class="cta-content">
<h2 class="cta-title"><?= safe($title) ?></h2>
<p class="cta-subtitle"><?= safe($subtitle) ?></p>
<div class="cta-actions">
<a href="<?= safe($cta_link) ?>" class="btn btn-large cta-primary"><?= safe($cta_text) ?></a>
<a href="<?= safe($secondary_link) ?>" class="btn btn-outline btn-large cta-secondary"><?= safe($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-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="hexagons" width="28" height="24" patternUnits="userSpaceOnUse"><polygon points="14,2 26,8 26,20 14,26 2,20 2,8" fill="none" stroke="white" stroke-width="0.5" opacity="0.1"/></pattern></defs><rect width="100%" height="100%" fill="url(%23hexagons)"/></svg>');
}
.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-content {
text-align: left;
}
.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-primary:hover {
background: transparent;
color: white;
border-color: white;
}
.cta-secondary {
border-color: rgba(255, 255, 255, 0.5);
color: white;
}
.cta-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-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;
animation: float 6s ease-in-out infinite;
}
.shape-2 {
width: 60px;
height: 60px;
border-radius: 50%;
top: 20px;
right: 0;
animation: float 8s ease-in-out infinite reverse;
}
.shape-3 {
width: 100px;
height: 40px;
border-radius: 20px;
bottom: 40px;
left: 20px;
animation: float 7s ease-in-out infinite;
}
.shape-4 {
width: 50px;
height: 50px;
border-radius: 10px;
bottom: 0;
right: 30px;
animation: float 5s ease-in-out infinite reverse;
transform: rotate(45deg);
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-15px) rotate(180deg);
}
}
@media (max-width: 968px) {
.cta-container {
grid-template-columns: 1fr;
text-align: center;
gap: 2rem;
}
.cta-content {
text-align: center;
}
.cta-title {
font-size: 2.5rem;
}
.cta-visual {
height: 200px;
}
.floating-shapes {
width: 150px;
height: 150px;
}
}
@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>
<?php
},
'about' => 'A compelling call-to-action section with animated floating shapes and gradient background'
]; ?>

View File

@ -1,203 +0,0 @@
<?php return [
'render' => function($prop) {
$features = $prop['features'] ?? [
[
'icon' => '⚡',
'title' => 'Lightning Fast',
'description' => 'Optimized for speed and performance with modern web technologies.'
],
[
'icon' => '🎨',
'title' => 'Beautiful Design',
'description' => 'Carefully crafted components with attention to detail and user experience.'
],
[
'icon' => '📱',
'title' => 'Mobile First',
'description' => 'Fully responsive design that works perfectly on all devices.'
],
[
'icon' => '🔧',
'title' => 'Easy to Use',
'description' => 'Simple and intuitive component system for rapid development.'
],
[
'icon' => '🛡️',
'title' => 'Secure',
'description' => 'Built with security best practices and modern PHP standards.'
],
[
'icon' => '🚀',
'title' => 'Scalable',
'description' => 'Architecture designed to grow with your application needs.'
]
];
?>
<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">
<?php foreach($features as $index => $feature): ?>
<div class="feature-card" style="animation-delay: <?= $index * 0.1 ?>s">
<div class="feature-icon">
<?= $feature['icon'] ?>
</div>
<h3 class="feature-title"><?= safe($feature['title']) ?></h3>
<p class="feature-description"><?= safe($feature['description']) ?></p>
<div class="feature-overlay"></div>
</div>
<?php endforeach; ?>
</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);
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
transform: translateY(30px);
}
.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);
transition: all 0.3s ease;
}
.feature-card:hover .feature-icon {
transform: scale(1.1) rotate(5deg);
}
.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;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, var(--primary), var(--secondary));
opacity: 0;
transition: opacity 0.3s ease;
}
.feature-card:hover .feature-overlay {
opacity: 0.05;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@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;
}
.feature-icon {
font-size: 2.5rem;
padding: 0.75rem;
}
}
</style>
<?php
},
'about' => 'A modern features grid with hover animations and gradient effects'
]; ?>

View File

@ -1,264 +0,0 @@
<?php return [
'render' => function($prop) {
$title = $prop['title'] ?? 'Welcome to the Present';
$subtitle = $prop['subtitle'] ?? 'Experience no-quite-modern web development with our cutting-edge framework';
$cta_text = $prop['cta_text'] ?? 'Get Started';
$cta_link = $prop['cta_link'] ?? '#';
?>
<div class="hero-section">
<div class="hero-content">
<h1 class="hero-title"><?= safe($title) ?></h1>
<p class="hero-subtitle"><?= safe($subtitle) ?></p>
<div class="hero-actions">
<a href="<?= safe($cta_link) ?>" class="btn btn-large hero-cta"><?= safe($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;
top: 0;
left: 0;
right: 0;
bottom: 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;
padding: 0;
box-shadow: none;
border: none;
color: var(--text-primary);
}
.hero-title::before {
display: none;
}
.hero-subtitle {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.9;
line-height: 1.6;
color: var(--text-primary);
}
.hero-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.hero-cta {
background: var(--surface);
color: var(--primary);
border: none;
}
.hero-cta:hover {
background: var(--surface-hover);
transform: translateY(-2px);
}
.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) {
const text = heroTitle.textContent;
heroTitle.textContent = '';
let i = 0;
const typeWriter = () => {
if (i < text.length) {
heroTitle.textContent += text.charAt(i);
i++;
setTimeout(typeWriter, 50);
}
};
// Start typing animation after a short delay
setTimeout(typeWriter, 500);
}
const heroSection = document.querySelector('.hero-section');
if (heroSection) {
createFloatingParticles(heroSection);
}
function createFloatingParticles(container) {
const particleCount = 20;
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
Object.assign(particle.style, {
position: 'absolute',
width: Math.random() * 4 + 1 + 'px',
height: Math.random() * 4 + 1 + 'px',
background: 'rgba(255, 255, 255, 0.3)',
borderRadius: '50%',
left: Math.random() * 100 + '%',
top: Math.random() * 100 + '%',
animation: `float ${Math.random() * 3 + 2}s ease-in-out infinite`,
animationDelay: Math.random() * 2 + 's'
});
container.appendChild(particle);
}
}
});
</script>
<?php
},
'about' => 'A modern hero section with animated floating elements and gradient background'
]; ?>

View File

@ -1,310 +0,0 @@
<?php return [
'render' => function($prop) {
$plans = $prop['plans'] ?? [
[
'name' => 'Starter',
'price' => 'Free',
'period' => '',
'description' => 'Perfect for small projects and learning',
'features' => [
'Up to 3 projects',
'Community support',
'Core components',
'Basic documentation'
],
'cta' => 'Get Started',
'popular' => false
],
[
'name' => 'Professional',
'price' => '$29',
'period' => '/month',
'description' => 'Ideal for growing businesses and teams',
'features' => [
'Unlimited projects',
'Advanced components',
'Priority support',
'Team collaboration',
'Custom themes',
'Analytics dashboard'
],
'cta' => 'Start Free Trial',
'popular' => true
],
[
'name' => 'Enterprise',
'price' => '$99',
'period' => '/month',
'description' => 'For large organizations with advanced needs',
'features' => [
'Everything in Professional',
'Custom integrations',
'Dedicated support',
'SLA guarantee',
'Advanced security',
'White-label options'
],
'cta' => 'Contact Sales',
'popular' => false
]
];
?>
<div class="pricing-section">
<div class="pricing-container">
<div class="pricing-header">
<h2>Choose Your Plan</h2>
<p>Start building great applications today with our flexible pricing options</p>
</div>
<div class="pricing-grid">
<?php foreach($plans as $index => $plan): ?>
<div class="pricing-card <?= $plan['popular'] ? 'popular' : '' ?>" style="animation-delay: <?= $index * 0.2 ?>s">
<?php if($plan['popular']): ?>
<div class="popular-badge">Most Popular</div>
<?php endif; ?>
<div class="plan-header">
<h3 class="plan-name"><?= safe($plan['name']) ?></h3>
<div class="plan-price">
<span class="price"><?= safe($plan['price']) ?></span>
<?php if($plan['period']): ?>
<span class="period"><?= safe($plan['period']) ?></span>
<?php endif; ?>
</div>
<p class="plan-description"><?= safe($plan['description']) ?></p>
</div>
<div class="plan-features">
<ul>
<?php foreach($plan['features'] as $feature): ?>
<li>
<span class="check-icon"></span>
<?= safe($feature) ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="plan-footer">
<button class="btn <?= $plan['popular'] ? 'btn-primary' : 'btn-outline' ?> btn-large plan-cta">
<?= safe($plan['cta']) ?>
</button>
</div>
</div>
<?php endforeach; ?>
</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-header p {
font-size: 1.25rem;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
.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;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
transform: translateY(30px);
}
.pricing-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-xl);
border-color: var(--primary);
}
.pricing-card.popular {
border-color: var(--primary);
box-shadow: var(--shadow-lg);
transform: scale(1.05);
}
.pricing-card.popular:hover {
transform: translateY(-8px) scale(1.07);
}
.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;
box-shadow: var(--shadow-md);
}
.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);
}
.plan-price {
margin-bottom: 1rem;
}
.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 {
margin-bottom: 2rem;
}
.plan-features ul {
list-style: none;
padding: 0;
margin: 0;
}
.plan-features li {
display: flex;
align-items: center;
padding: 0.75rem 0;
color: var(--text-primary);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.plan-features li:last-child {
border-bottom: none;
}
.check-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: var(--primary);
color: white;
border-radius: 50%;
font-size: 0.75rem;
font-weight: bold;
margin-right: 0.75rem;
flex-shrink: 0;
}
.plan-footer {
text-align: center;
}
.plan-cta {
width: 100%;
justify-content: center;
}
.pricing-footer {
text-align: center;
color: var(--text-secondary);
font-size: 0.9rem;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.pricing-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.pricing-card {
padding: 2rem 1.5rem;
}
.pricing-card.popular {
transform: none;
}
.pricing-card.popular:hover {
transform: translateY(-4px);
}
.pricing-header h2 {
font-size: 2rem;
}
.price {
font-size: 2.5rem;
}
}
</style>
<?php
},
'about' => 'A modern pricing table with popular plan highlighting and smooth animations'
]; ?>

View File

@ -1,179 +0,0 @@
<?php return [
'render' => function($prop) {
$stats = $prop['stats'] ?? [
['number' => '99.9%', 'label' => 'Uptime'],
['number' => '500ms', 'label' => 'Average Response'],
['number' => '50K+', 'label' => 'Active Users'],
['number' => '24/7', 'label' => 'Support']
];
?>
<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">
<?php foreach($stats as $index => $stat): ?>
<div class="stat-item" style="animation-delay: <?= $index * 0.2 ?>s">
<div class="stat-number" data-target="<?= safe($stat['number']) ?>"><?= safe($stat['number']) ?></div>
<div class="stat-label"><?= safe($stat['label']) ?></div>
<div class="stat-bar">
<div class="stat-fill" style="animation-delay: <?= ($index * 0.2) + 0.5 ?>s"></div>
</div>
</div>
<?php endforeach; ?>
</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-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="dots" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="1" fill="currentColor" opacity="0.1"/></pattern></defs><rect width="100%" height="100%" fill="url(%23dots)"/></svg>');
color: var(--text-muted);
}
.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);
transition: all 0.3s ease;
animation: fadeInScale 0.8s ease-out forwards;
opacity: 0;
transform: scale(0.8);
}
.stat-item:hover {
transform: scale(1.05);
background: var(--surface-hover);
border-color: var(--border-hover);
box-shadow: var(--shadow-lg);
}
.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;
line-height: 1;
}
.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;
position: relative;
}
.stat-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--accent));
border-radius: 2px;
width: 0;
animation: fillBar 1.5s ease-out forwards;
}
@keyframes fadeInScale {
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fillBar {
to {
width: 100%;
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.stat-item {
padding: 1.5rem 1rem;
}
.stat-number {
font-size: 2.5rem;
}
.stats-header h2 {
font-size: 2rem;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
<?php
},
'about' => 'An animated statistics section with gradient backgrounds and progress bars'
]; ?>

View File

@ -1,214 +0,0 @@
<?php return [
'render' => function($prop) {
$testimonials = $prop['testimonials'] ?? [
[
'name' => 'Sarah Johnson',
'role' => 'Senior Developer at TechCorp',
'avatar' => 'img/cat01.jpg',
'content' => 'This framework has revolutionized our development process. The component system is intuitive and the performance is outstanding.',
'rating' => 5
],
[
'name' => 'Michael Chen',
'role' => 'CTO at StartupX',
'avatar' => 'img/cat01.jpg',
'content' => 'We switched from our legacy system to this framework and saw immediate improvements in both development speed and code quality.',
'rating' => 5
],
[
'name' => 'Emily Rodriguez',
'role' => 'Full Stack Developer',
'avatar' => 'img/cat01.jpg',
'content' => 'The documentation is excellent and the learning curve is gentle. Perfect for both beginners and experienced developers.',
'rating' => 5
]
];
?>
<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">
<?php foreach($testimonials as $index => $testimonial): ?>
<div class="testimonial-card" style="animation-delay: <?= $index * 0.2 ?>s">
<div class="testimonial-content">
<div class="quote-icon">"</div>
<p><?= safe($testimonial['content']) ?></p>
<div class="stars">
<?php for($i = 0; $i < $testimonial['rating']; $i++): ?>
<span class="star"></span>
<?php endfor; ?>
</div>
</div>
<div class="testimonial-author">
<img src="<?= safe($testimonial['avatar']) ?>" alt="<?= safe($testimonial['name']) ?>" class="avatar">
<div class="author-info">
<div class="author-name"><?= safe($testimonial['name']) ?></div>
<div class="author-role"><?= safe($testimonial['role']) ?></div>
</div>
</div>
</div>
<?php endforeach; ?>
</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);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
transform: translateY(30px);
}
.testimonial-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-xl);
border-color: var(--primary);
}
.testimonial-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
}
.testimonial-content {
margin-bottom: 2rem;
}
.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);
transition: all 0.3s ease;
}
.testimonial-card:hover .avatar {
border-color: var(--primary);
transform: scale(1.05);
}
.author-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.author-role {
color: var(--text-secondary);
font-size: 0.9rem;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@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>
<?php
},
'about' => 'A testimonials section with user photos, ratings, and smooth animations'
]; ?>

View File

@ -1,153 +0,0 @@
<?php return [
'render' => function($prop) {
$themes = cfg('theme/options');
$currentTheme = (string)cfg('theme/key');
$currentLabel = (string)first(cfg('theme/label'), $themes[$currentTheme]['label'] ?? $currentTheme);
$routePath = (string)(URL::$route['l-path'] ?? '');
$buildThemeLink = function($themeKey) use($routePath) {
return URL::Link($routePath, ['theme' => $themeKey]);
};
?>
<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"><?= safe($currentLabel) ?></span>
</button>
<div id="theme-menu" class="theme-menu" hidden>
<div class="theme-menu-title">Available Themes</div>
<div class="theme-menu-list">
<?php foreach($themes as $themeKey => $themeInfo): ?>
<a class="theme-option<?= $themeKey === $currentTheme ? ' is-active' : '' ?>" href="<?= asafe($buildThemeLink($themeKey)) ?>">
<span><?= safe((string)$themeInfo['label']) ?></span>
<?php if($themeKey === $currentTheme): ?><small>Active</small><?php endif; ?>
</a>
<?php endforeach; ?>
</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>
<?php
},
'about' => 'A floating theme picker that switches between all configured starter theme families'
];

View File

@ -1,105 +0,0 @@
<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,
});
});
}
};
</script><?php
include_js('components/gauges/common.js');
include_css('themes/common/css/gauges.css');
return [
'render' => function($prop) {
$prop['id'] = !empty($prop['id']) ? $prop['id'] : 'arcgauge-'.uniqid();
$prop['scale'] = $prop['scale'] ?? array();
$prop['items'] = $prop['items'] ?? array();
?>
<div class="arcgauge-set" id="<?= asafe($prop['id']) ?>" style="<?= asafe((string)($prop['style'] ?? '')) ?>">
<?php if(!empty($prop['title'])) { ?>
<div class="arcgauge-set-header">
<h3><?= safe((string)$prop['title']) ?></h3>
<?php if(!empty($prop['subtitle'])) { ?><p><?= safe((string)$prop['subtitle']) ?></p><?php } ?>
</div>
<?php } ?>
<div class="arcgauge-grid">
<?php foreach($prop['items'] as $item_id => $item) {
$item = array_merge($prop['scale'], $item);
$value = (float)first($item['value'], 0);
$max = (float)first($item['max'], 100);
$precision = isset($item['precision']) ? (int)$item['precision'] : 1;
$pct = $max > 0 ? clamp(($value / $max) * 100, 0, 100) : 0;
$arc_length = round(($pct / 100) * 157.08, 1);
$display_value = number_format($value, $precision).safe((string)($item['unit'] ?? ''));
$resolved_color = '#10b981';
if(isset($item['color']))
{
if(is_array($item['color']))
{
$color_entry = pick_entry_from_range($item['color'], $value);
$resolved_color = first($color_entry['color'] ?? false, $resolved_color);
}
else
{
$resolved_color = (string)$item['color'];
}
}
else if($pct >= 85)
{
$resolved_color = 'var(--error, #ef4444)';
}
else if($pct >= 60)
{
$resolved_color = 'var(--warning, #f59e0b)';
}
else
{
$resolved_color = 'var(--success, #10b981)';
}
?>
<section class="arcgauge-card">
<div class="arcgauge-label"><?= safe((string)first($item['label'], ucfirst((string)$item_id))) ?></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="<?= asafe($prop['id']) ?>-<?= asafe($item_id) ?>-arc" class="arcgauge-arc-dyn" d="M 10 60 A 50 50 0 0 1 110 60" fill="none" stroke="<?= asafe($resolved_color) ?>" stroke-width="5" stroke-linecap="round" stroke-dasharray="<?= asafe((string)$arc_length) ?> 157.08"/>
<?php if(!empty($item['watermark_prefix'])) { ?>
<line id="<?= asafe($item['watermark_prefix']) ?>WmLo" class="arcgauge-watermark arcgauge-watermark-lo" x1="0" y1="0" x2="0" y2="0" opacity="0"/>
<line id="<?= asafe($item['watermark_prefix']) ?>WmHi" class="arcgauge-watermark arcgauge-watermark-hi" x1="0" y1="0" x2="0" y2="0" opacity="0"/>
<?php } ?>
<text id="<?= asafe($prop['id']) ?>-<?= asafe($item_id) ?>-text" class="arcgauge-value" x="60" y="47" text-anchor="middle"><?= safe($display_value) ?></text>
<text class="arcgauge-caption" x="60" y="62" text-anchor="middle"><?= safe((string)first($item['caption'], '')) ?></text>
</svg>
<div class="arcgauge-meta" id="<?= asafe($prop['id']) ?>-<?= asafe($item_id) ?>-meta"><?= safe((string)first($item['meta'], '--')) ?></div>
</section>
<?php } ?>
</div>
</div>
<?php
if(!empty($prop['listen']))
{
?><script>
ArcgaugeComponents.start_listen(<?= jsafe($prop) ?>);
</script><?php
}
}
];

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,173 +0,0 @@
<script>
window.NeedlegaugeComponents = window.NeedlegaugeComponents || {
start_listen : function(prop) {
$.events.on('value-broadcast', function(data) {
if(prop.items[data.name]) {
// update value text
let item = Object.assign({}, prop.scale, prop.items[data.name]);
$('#' + prop.id + '-' + data.name + '-value').text(data.value + (item.unit || ''));
let vrange = (item.max || 100) - (item.min || 0);
let pct_value = GaugeComponents.clampValue((data.value - (item.min || 0)) / vrange, 0, 1);
let angle = -Math.PI + item.angle_start + (pct_value * (item.angle_end - item.angle_start));
$('#' + prop.id + '-' + data.name + '-needle').css('transform', 'rotate(' + angle + 'rad)');
}
});
},
}
</script><?php
include_js('components/gauges/common.js');
include_css('themes/common/css/gauges.css');
return [
'render' => function($prop) {
$prop['id'] = !empty($prop['id']) ? $prop['id'] : 'needlegauge-' . uniqid();
$prop['style'] = (string)($prop['style'] ?? '');
$prop['items'] = (array)($prop['items'] ?? array());
$prop['scale'] = (array)($prop['scale'] ?? array());
$prop['size'] = first($prop['size'] ?? false, 200);
$prop['subtitle'] = (string)($prop['subtitle'] ?? '');
$prop['scale']['angle_start'] = first($prop['scale']['angle_start'], -pi());
$prop['scale']['angle_end'] = first($prop['scale']['angle_end'], 0);
$prop['img_height'] = first($prop['img_height'] ?? false,
$prop['size'] * max(cos(first($prop['scale']['angle_start'], -pi())), cos(first($prop['scale']['angle_end'], 0))));
?>
<section class="gauge-set needlegauge-set" id="<?= asafe($prop['id']) ?>" style="<?= asafe($prop['style']) ?>">
<?php if(isset($prop['title'])) { ?>
<div class="gauge-set-header">
<h3><?= safe($prop['title']) ?></h3>
<?php if($prop['subtitle'] !== '') { ?><p><?= safe($prop['subtitle']) ?></p><?php } ?>
</div>
<?php } ?>
<div class="needlegauge-grid">
<?php
foreach($prop['items'] as $item_id => $item)
{
$item = array_merge($prop['scale'], $item);
$item['min'] = first($item['min'] ?? false, 0);
$item['max'] = first($item['max'] ?? false, 100);
$item['tooltip'] = (string)($item['tooltip'] ?? '');
$item['unit'] = (string)($item['unit'] ?? '');
$item['color'] = $item['color'] ?? false;
$item['label'] = (string)first($item['label'] ?? false, ucfirst((string)$item_id));
$vrange = ($item['max'] - $item['min']);
$pct_value = clamp(($item['value'] - $item['min']) / $vrange, 0, 1);
$needle_angle = -pi() + $item['angle_start'] + ($pct_value * ($item['angle_end'] - $item['angle_start']));
$needle_color = first($item['color'] ?? false, '#888888');
if(is_array($item['color'])) {
$color_entry = pick_entry_from_range($item['color'], $item['value']);
$needle_color = first($color_entry['color'] ?? false, '#888888');
}
$tick_interval = first($item['ticks-every'], $vrange / 20);
$label_interval = first($item['value-labels-every'], $vrange / 4);
$tick_number = 0;
$ticks_html = '';
$min_angle = first($item['angle_start']);
$max_angle = first($item['angle_end']);
$tick_color = first($item['tick-color'], '#888888');
for($v = $item['min']; $v <= $item['max']; $v += $tick_interval)
{
$tick_number++;
$angle = $min_angle + (($v-$item['min'])/$vrange) * ($max_angle - $min_angle);
$x1 = $prop['size']/2 + cos($angle) * $prop['size']*0.38;
$y1 = $prop['size']/2 + sin($angle) * $prop['size']*0.38;
$x2 = $prop['size']/2 + cos($angle) * $prop['size']*0.41;
$y2 = $prop['size']/2 + sin($angle) * $prop['size']*0.41;
$ticks_html .= '<line x1="'.($x1).'" y1="'.($y1).'" x2="'.($x2).'" y2="'.($y2).'"
stroke="'. $tick_color .'" stroke-width="1"/>';
}
for($v = $item['min']; $v <= $item['max']; $v += $label_interval)
{
$tick_number++;
$angle = $min_angle + (($v-$item['min'])/$vrange) * ($max_angle - $min_angle);
$x1 = $prop['size']/2 + cos($angle) * $prop['size']*0.35;
$y1 = $prop['size']/2 + sin($angle) * $prop['size']*0.35;
$x2 = $prop['size']/2 + cos($angle) * $prop['size']*0.41;
$y2 = $prop['size']/2 + sin($angle) * $prop['size']*0.41;
$ticks_html .= '<line x1="'.($x1).'" y1="'.($y1).'" x2="'.($x2).'" y2="'.($y2).'"
stroke="'. $tick_color .'" stroke-width="3"/>';
$lx = $prop['size']/2 + cos($angle) * $prop['size']*0.45;
$ly = $prop['size']/2 + sin($angle) * $prop['size']*0.45;
$ticks_html .= '<text x="'.($lx).'" y="'.($ly).'" text-anchor="middle" dominant-baseline="central"
font-size="10" fill="'. $tick_color .'">'.($v == 0 ? '0' : $v).'</text>';
}
?>
<section class="gauge-card needlegauge-card">
<div class="gauge-metric-label needlegauge-label-head"><?= safe($item['label']) ?></div>
<div class="needlegauge-visual">
<svg id="<?= $prop['id'] ?>-<?= $item_id ?>-svg" width="<?= $prop['size'] ?>" height="<?= $prop['img_height'] ?>" class="needlegauge-svg"
viewBox="0 0 <?= $prop['size'] ?> <?= $prop['img_height'] ?>">
<?php
if(is_array($item['color']))
{
foreach($item['color'] as $range)
{
$angle_from = $min_angle + ((max($range['from'], $item['min']) - $item['min']) / $vrange) * ($max_angle - $min_angle);
$angle_to = $min_angle + ((min($range['to'], $item['max']) - $item['min']) / $vrange) * ($max_angle - $min_angle);
$color = $range['color'] ?? 'rgba(120,120,120,0.5)';
SVG::circle_segment($prop['size']/2, $prop['size']/2, $prop['size']*0.4,
$angle_from, $angle_to, $color, 8, 'rgba(0,0,0,0)', 'opacity:0.25');
}
}
else
{
SVG::circle_segment($prop['size']/2, $prop['size']/2, $prop['size']*0.4,
$min_angle, $max_angle, $tick_color, 8, 'rgba(0,0,0,0)', 'opacity:0.25');
}
?>
<g class="ticks">
<?= $ticks_html ?>
</g>
<line id="<?= $prop['id'] ?>-<?= $item_id ?>-needle" class="needle"
x1="<?= $prop['size']*0.55 ?>" y1="<?= $prop['size']*0.5 ?>" x2="<?= $prop['size']*0.1 ?>" y2="<?= $prop['size']*0.5 ?>"
stroke="<?= ($needle_color) ?>" stroke-width="3" stroke-linecap="round"
style="transform-origin: <?= $prop['size']/2 ?>px <?= $prop['size']/2 ?>px;
transform: rotate(<?= ($needle_angle) ?>rad); transition: transform 0.3s ease;"/>
<circle cx="<?= $prop['size']/2 ?>" cy="<?= $prop['size']/2 ?>" r="6" fill="<?= $needle_color ?>"/>
</svg>
</div>
<div class="needlegauge-info">
<div class="gauge-metric-value needlegauge-value" id="<?= $prop['id'] ?>-<?= $item_id ?>-value"
title="<?= asafe($item['tooltip']) ?>">
<?= safe($item['value']) ?><?= safe($item['unit']) ?>
</div>
<?php if($item['tooltip'] !== '') { ?><div class="gauge-metric-meta needlegauge-meta"><?= safe($item['tooltip']) ?></div><?php } ?>
</div>
</section>
<?php
}
?>
</div>
</section>
<?php
if(!empty($prop['listen']))
{
?><script>
NeedlegaugeComponents.start_listen(<?= jsafe($prop) ?>);
</script><?php
}
}
];

View File

@ -1,203 +0,0 @@
<script>
window.ProgressbarComponents = window.ProgressbarComponents || {
start_listen : function(prop) {
$.events.on('value-broadcast', function(data) {
if(prop.items[data.name]) {
// update value text
let bar = Object.assign({}, prop.scale, prop.items[data.name]);
$('#' + prop.id + '-' + data.name + '-value').text(data.value + (bar.unit || ''));
// update bar width/height
let vrange = (bar.max || 100) - (bar.min || 0);
let pct_value = GaugeComponents.clampValue((data.value - (bar.min || 0)) / vrange * 100, 0, 100);
if(Array.isArray(bar.color)) {
let colorMatch = GaugeComponents.pickEntryFromRange(bar.color, Number(data.value));
if(colorMatch && colorMatch.color)
$('#' + prop.id + '-' + data.name + '-bar').css('background', colorMatch.color);
}
if(prop.layout === 'horizontal') {
$('#' + prop.id + '-' + data.name + '-bar').css('width', pct_value + '%');
} else {
$('#' + prop.id + '-' + data.name + '-bar').css('height', pct_value + '%');
}
}
});
},
}
</script><?php
include_js('components/gauges/common.js');
include_css('themes/common/css/gauges.css');
return [
'render' => function($prop) {
$prop['id'] = !empty($prop['id']) ? $prop['id'] : 'progressbar-'.uniqid();
$prop['style'] = (string)($prop['style'] ?? '');
$prop['item-style'] = (string)($prop['item-style'] ?? '');
$prop['label-style'] = (string)($prop['label-style'] ?? '');
$prop['value-style'] = (string)($prop['value-style'] ?? '');
$prop['bar-style'] = (string)($prop['bar-style'] ?? '');
$prop['items'] = (array)($prop['items'] ?? array());
$default_palette = [
'var(--success, #10b981)',
'var(--primary, #60a5fa)',
'var(--accent, #22d3ee)',
'var(--warning, #f59e0b)',
];
$auto_color_counter = 0;
if(empty($prop['scale'])) $prop['scale'] = [];
$layout = first($prop['layout'] ?? false, 'horizontal');
?>
<section class="gauge-set progressbar-set progressbar-set-<?= asafe($layout) ?>" id="<?= $prop['id'] ?>" style="<?= $prop['style'] ?>">
<?php if(isset($prop['title'])) { ?>
<div class="gauge-set-header">
<h3><?= safe($prop['title']) ?></h3>
<?php if(!empty($prop['subtitle'])) { ?><p><?= safe((string)$prop['subtitle']) ?></p><?php } ?>
</div>
<?php } ?>
<?php if($layout == 'horizontal') { ?>
<div class="progressbar-grid progressbar-grid-horizontal">
<?php foreach($prop['items'] as $bar_id => $bar)
{
$bar = array_merge($prop['scale'], $bar);
$bar['style'] = (string)($bar['style'] ?? '');
$bar['tooltip'] = (string)($bar['tooltip'] ?? '');
$bar['before'] = $bar['before'] ?? '';
$bar['after'] = $bar['after'] ?? '';
$bar['unit'] = (string)($bar['unit'] ?? '');
$bar['color'] = $bar['color'] ?? false;
$vrange = (first($bar['max'], 100) - first($bar['min'], 0));
$bar['pct-value'] = clamp(($bar['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100);
if(is_array($bar['color']))
{
$color_entry = pick_entry_from_range($bar['color'], $bar['value']);
$bar['color'] = $color_entry['color'] ?? false;
}
if(!$bar['color'])
$bar['color'] = first($prop['bar-color'] ?? false, $default_palette[$auto_color_counter++ % sizeof($default_palette)]);
?>
<section class="gauge-card progressbar-card progressbar-card-horizontal" id="<?= $prop['id'] ?>-<?= safe($bar_id) ?>"
title="<?= asafe($bar['tooltip']) ?>"
style="<?= $prop['item-style'] ?>;<?= $bar['style'] ?>">
<?= $bar['before'] ?>
<div class="progressbar-card-head">
<div class="gauge-metric-label progressbar-label" style="<?= $prop['label-style'] ?>">
<?= safe($bar['label']) ?>
</div>
<div class="gauge-metric-value progressbar-value" style="<?= $prop['value-style'] ?>"
id="<?= $prop['id'] ?>-<?= safe($bar_id) ?>-value">
<?= safe($bar['value']) ?><?= safe($bar['unit']) ?>
</div>
</div>
<div class="progressbar-background progressbar-background-horizontal">
<div class="progressbar-bar" id="<?= $prop['id'] ?>-<?= safe($bar_id) ?>-bar"
style="background-color: <?= safe($bar['color']) ?>;
<?= $prop['bar-style'] ?>
opacity: <?= isset($bar['opacity']) ? safe($bar['opacity']) : '0.75' ?>;
width: <?= safe($bar['pct-value']) ?>%;">
</div>
<?php
if(isset($prop['markers'])) {
foreach($prop['markers'] as $marker_id => $marker) {
$marker_pct = clamp(($marker['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100);
?>
<div class="progressbar-marker"
title="<?= asafe($marker['label']) ?>"
style="left: <?= ($marker_pct) ?>%;
background: <?= first($marker['color'] ?? false, 'var(--primary-light)') ?>;">
</div>
<?php
}
}
?>
</div>
<?php if($bar['tooltip'] !== '') { ?><div class="gauge-metric-meta progressbar-meta"><?= safe($bar['tooltip']) ?></div><?php } ?>
<?= $bar['after'] ?>
</section>
<?php } ?>
</div>
<?php } else { ?>
<div class="progressbar-grid progressbar-grid-vertical" style="--progressbar-height: <?= safe((string)first($prop['height'] ?? false, 240)) ?>px;">
<?php
if(sizeof($prop['items']) > 0)
{
foreach($prop['items'] as $bar_id => $bar)
{
$bar = array_merge($prop['scale'], $bar);
$bar['style'] = (string)($bar['style'] ?? '');
$bar['tooltip'] = (string)($bar['tooltip'] ?? '');
$bar['before'] = $bar['before'] ?? '';
$bar['after'] = $bar['after'] ?? '';
$bar['unit'] = (string)($bar['unit'] ?? '');
$bar['color'] = $bar['color'] ?? false;
$vrange = (first($bar['max'], 100) - first($bar['min'], 0));
$bar['pct-value'] = clamp(($bar['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100);
if(is_array($bar['color']))
{
$color_entry = pick_entry_from_range($bar['color'], $bar['value']);
$bar['color'] = $color_entry['color'] ?? false;
}
if(!$bar['color'])
$bar['color'] = first($prop['bar-color'] ?? false, $default_palette[$auto_color_counter++ % sizeof($default_palette)]);
?>
<section class="gauge-card progressbar-card progressbar-card-vertical" id="<?= $prop['id'] ?>-<?= safe($bar_id) ?>"
title="<?= asafe($bar['tooltip']) ?>"
style="<?= $prop['item-style'] ?>;<?= $bar['style'] ?>">
<?= $bar['before'] ?>
<div class="gauge-metric-label progressbar-label" style="<?= $prop['label-style'] ?>">
<?= safe($bar['label']) ?>
</div>
<div class="progressbar-background progressbar-background-vertical">
<div class="progressbar-bar" id="<?= $prop['id'] ?>-<?= safe($bar_id) ?>-bar"
style="background-color: <?= safe($bar['color']) ?>;
<?= $prop['bar-style'] ?>
opacity: <?= isset($bar['opacity']) ? safe($bar['opacity']) : '0.75' ?>;
height: <?= safe($bar['pct-value']) ?>%;">
</div>
<?php
// Render markers for vertical layout
if(isset($prop['markers'])) {
foreach($prop['markers'] as $marker_id => $marker) {
$marker_pct = clamp(($marker['value'] - first($bar['min'], 0)) / $vrange * 100, 0, 100);
?>
<div class="progressbar-marker"
title="<?= asafe($marker['label']) ?>"
style="bottom: <?= ($marker_pct) ?>%;
background: <?= first($marker['color'] ?? false, 'var(--primary-light)') ?>;">
</div>
<?php
}
}
?>
</div>
<div class="gauge-metric-value progressbar-value" style="<?= $prop['value-style'] ?>"
id="<?= $prop['id'] ?>-<?= safe($bar_id) ?>-value">
<?= safe($bar['value']) ?><?= safe($bar['unit']) ?>
</div>
<?php if($bar['tooltip'] !== '') { ?><div class="gauge-metric-meta progressbar-meta"><?= safe($bar['tooltip']) ?></div><?php } ?>
<?= $bar['after'] ?>
</section>
<?php
}
}
?>
</div>
<?php } ?>
</section>
<?php
if(!empty($prop['listen']))
{
?><script>
ProgressbarComponents.start_listen(<?= jsafe($prop) ?>);
</script><?php
}
}
];

View File

@ -1,25 +0,0 @@
<?php return [
'render' => function($prop) {
include_css('themes/common/css/workspace.css');
include_js('js/u-workspace-shell.js');
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$sidebar = (string)($prop['sidebar_html'] ?? '');
$main = (string)($prop['main_html'] ?? '');
$overlayId = trim((string)($prop['overlay_id'] ?? ''));
ob_start();
?>
<div<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-app<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<?= $sidebar ?>
<div<?= $overlayId !== '' ? ' id="' . asafe($overlayId) . '"' : '' ?> class="ws-sidebar-overlay"></div>
<main class="ws-main">
<?= $main ?>
</main>
</div>
<?php
return ob_get_clean();
},
'about' => 'Generic workspace app shell with sidebar, overlay, and main content area',
];

View File

@ -1,22 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$icon = trim((string)($prop['icon_class'] ?? 'fas fa-layer-group'));
$title = (string)($prop['title'] ?? '');
$text = (string)($prop['text'] ?? '');
$actionHtml = (string)($prop['action_html'] ?? '');
ob_start();
?>
<div<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-empty-state<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<div class="ws-empty-icon"><i class="<?= asafe($icon) ?>"></i></div>
<h2><?= safe($title) ?></h2>
<p><?= safe($text) ?></p>
<?= $actionHtml ?>
</div>
<?php
return ob_get_clean();
},
'about' => 'Centered empty-state block for shell panels and placeholder screens',
];

View File

@ -1,21 +0,0 @@
<?php return [
'render' => function($prop) {
$tag = trim((string)($prop['tag'] ?? 'button'));
if (!in_array($tag, ['button', 'span', 'a'], true)) $tag = 'button';
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$title = trim((string)($prop['title'] ?? ''));
$icon = trim((string)($prop['icon_class'] ?? ''));
$text = (string)($prop['text'] ?? '');
$attrs = trim((string)($prop['attrs'] ?? ''));
$href = trim((string)($prop['href'] ?? ''));
$type = trim((string)($prop['type'] ?? 'button'));
ob_start();
?>
<<?= $tag ?><?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-icon-btn<?= $class !== '' ? ' ' . asafe($class) : '' ?>"<?= $title !== '' ? ' title="' . asafe($title) . '"' : '' ?><?= $tag === 'button' ? ' type="' . asafe($type) . '"' : '' ?><?= $tag === 'a' && $href !== '' ? ' href="' . asafe($href) . '"' : '' ?><?= $attrs !== '' ? ' ' . $attrs : '' ?>><?php if ($icon !== ''): ?><i class="<?= asafe($icon) ?>"></i><?php endif; ?><?php if ($text !== ''): ?><span><?= safe($text) ?></span><?php endif; ?></<?= $tag ?>>
<?php
return ob_get_clean();
},
'about' => 'Small utility icon button for shell toolbars and compact actions',
];

View File

@ -1,15 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$icon = trim((string)($prop['icon_class'] ?? ''));
$text = (string)($prop['text'] ?? '');
ob_start();
?>
<div<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-list-state<?= $class !== '' ? ' ' . asafe($class) : '' ?>"><?php if ($icon !== ''): ?><i class="<?= asafe($icon) ?>"></i><?php endif; ?><span><?= safe($text) ?></span></div>
<?php
return ob_get_clean();
},
'about' => 'Compact sidebar/list placeholder state with optional icon',
];

View File

@ -1,24 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$buttonId = trim((string)($prop['button_id'] ?? ''));
$buttonClass = trim((string)($prop['button_class'] ?? ''));
$titleId = trim((string)($prop['title_id'] ?? ''));
$titleClass = trim((string)($prop['title_class'] ?? ''));
$title = (string)($prop['title'] ?? '');
$icon = trim((string)($prop['icon_class'] ?? 'fas fa-bars'));
ob_start();
?>
<div<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-mobile-bar<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<button<?= $buttonId !== '' ? ' id="' . asafe($buttonId) . '"' : '' ?> class="ws-mobile-toggle<?= $buttonClass !== '' ? ' ' . asafe($buttonClass) : '' ?>" title="Toggle sidebar" type="button">
<i class="<?= asafe($icon) ?>"></i>
</button>
<span<?= $titleId !== '' ? ' id="' . asafe($titleId) . '"' : '' ?> class="ws-mobile-title<?= $titleClass !== '' ? ' ' . asafe($titleClass) : '' ?>"><?= safe($title) ?></span>
</div>
<?php
return ob_get_clean();
},
'about' => 'Compact mobile header bar for workspace layouts with a sidebar toggle',
];

View File

@ -1,25 +0,0 @@
<?php return [
'render' => function($prop) {
$class = trim((string)($prop['class'] ?? ''));
$title = (string)($prop['title'] ?? '');
$subtitle = (string)($prop['subtitle'] ?? '');
$titleId = trim((string)($prop['title_id'] ?? ''));
$subtitleId = trim((string)($prop['subtitle_id'] ?? ''));
$actionsHtml = (string)($prop['actions_html'] ?? '');
ob_start();
?>
<div class="ws-panel-header<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<div class="ws-panel-title-group">
<h2<?= $titleId !== '' ? ' id="' . asafe($titleId) . '"' : '' ?>><?= safe($title) ?></h2>
<?php if ($subtitle !== ''): ?>
<p<?= $subtitleId !== '' ? ' id="' . asafe($subtitleId) . '"' : '' ?> class="ws-subtitle"><?= safe($subtitle) ?></p>
<?php endif; ?>
</div>
<?php if ($actionsHtml !== ''): ?><div class="ws-header-actions"><?= $actionsHtml ?></div><?php endif; ?>
</div>
<?php
return ob_get_clean();
},
'about' => 'Panel heading with title, optional subtitle, and actions slot',
];

View File

@ -1,19 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$attrs = trim((string)($prop['attrs'] ?? ''));
$header = (string)($prop['header_html'] ?? '');
$body = (string)($prop['body_html'] ?? '');
ob_start();
?>
<section<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-panel<?= $class !== '' ? ' ' . asafe($class) : '' ?>"<?= $attrs !== '' ? ' ' . $attrs : '' ?>>
<?= $header ?>
<?= $body ?>
</section>
<?php
return ob_get_clean();
},
'about' => 'Flexible workspace panel container with separate header and body slots',
];

View File

@ -1,17 +0,0 @@
<?php return [
'render' => function($prop) {
$class = trim((string)($prop['class'] ?? ''));
$title = (string)($prop['title'] ?? '');
$actionsHtml = (string)($prop['actions_html'] ?? '');
ob_start();
?>
<div class="ws-section-head<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<h3><?= safe($title) ?></h3>
<?php if ($actionsHtml !== ''): ?><div class="ws-header-actions"><?= $actionsHtml ?></div><?php endif; ?>
</div>
<?php
return ob_get_clean();
},
'about' => 'Compact section heading for grouped workspace content',
];

View File

@ -1,19 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$attrs = trim((string)($prop['attrs'] ?? ''));
$header = (string)($prop['header_html'] ?? '');
$body = (string)($prop['body_html'] ?? '');
ob_start();
?>
<section<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-section<?= $class !== '' ? ' ' . asafe($class) : '' ?>"<?= $attrs !== '' ? ' ' . $attrs : '' ?>>
<?= $header ?>
<?= $body ?>
</section>
<?php
return ob_get_clean();
},
'about' => 'Stacked workspace section wrapper for grouped content blocks',
];

View File

@ -1,18 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$top = (string)($prop['top_html'] ?? '');
$body = (string)($prop['body_html'] ?? '');
ob_start();
?>
<aside<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-sidebar<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<?= $top ?>
<?= $body ?>
</aside>
<?php
return ob_get_clean();
},
'about' => 'Generic workspace sidebar wrapper with separate top and body slots',
];

View File

@ -1,26 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$actionHtml = (string)($prop['action_html'] ?? '');
$searchId = trim((string)($prop['search_id'] ?? ''));
$searchClass = trim((string)($prop['search_class'] ?? ''));
$searchInputId = trim((string)($prop['search_input_id'] ?? ''));
$searchInputName = trim((string)($prop['search_input_name'] ?? 'search'));
$searchInputClass = trim((string)($prop['search_input_class'] ?? ''));
$searchPlaceholder = (string)($prop['search_placeholder'] ?? 'Search...');
ob_start();
?>
<div<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-sidebar-top<?= $class !== '' ? ' ' . asafe($class) : '' ?>">
<?= $actionHtml ?>
<div<?= $searchId !== '' ? ' id="' . asafe($searchId) . '"' : '' ?> class="ws-search-wrap<?= $searchClass !== '' ? ' ' . asafe($searchClass) : '' ?>">
<i class="fas fa-search ws-search-icon"></i>
<input type="search"<?= $searchInputId !== '' ? ' id="' . asafe($searchInputId) . '"' : '' ?> name="<?= asafe($searchInputName) ?>" class="ws-search-input<?= $searchInputClass !== '' ? ' ' . asafe($searchInputClass) : '' ?>" placeholder="<?= asafe($searchPlaceholder) ?>" autocomplete="off">
</div>
</div>
<?php
return ob_get_clean();
},
'about' => 'Sidebar toolbar with optional action area and compact search field',
];

View File

@ -1,17 +0,0 @@
<?php return [
'render' => function($prop) {
$id = trim((string)($prop['id'] ?? ''));
$class = trim((string)($prop['class'] ?? ''));
$label = trim((string)($prop['label'] ?? $prop['text'] ?? ''));
$variant = preg_replace('/[^a-z0-9_-]/i', '', strtolower(trim((string)($prop['variant'] ?? 'neutral'))));
if ($variant === '') $variant = 'neutral';
$title = trim((string)($prop['title'] ?? ''));
ob_start();
?>
<span<?= $id !== '' ? ' id="' . asafe($id) . '"' : '' ?> class="ws-status-pill ws-status-pill-<?= asafe($variant) ?><?= $class !== '' ? ' ' . asafe($class) : '' ?>"<?= $title !== '' ? ' title="' . asafe($title) . '"' : '' ?>><?= safe($label) ?></span>
<?php
return ob_get_clean();
},
'about' => 'Compact semantic status badge for neutral, info, success, warning, and danger states',
];

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,40 +0,0 @@
<?php
include('config/settings.php');
include('lib/ulib.php');
include('lib/components.php');
include('lib/theme_helpers.php');
URL::MakeRoute();
Profiler::log('main content: start', 1);
ob_start();
$route_match = URL::ResolveViewFile('views');
if($route_match)
{
if(isset($route_match['param']))
URL::$route['param'] = $route_match['param'];
require($route_match['file']);
}
else
{
URL::NotFound('The requested page does not exist.');
URL::$route['page-title'] = '404 Not Found';
echo '<section class="card">';
echo '<h1>404 Not Found</h1>';
echo '<p>'.safe(URL::$error).'</p>';
echo '</section>';
}
URL::$fragments['main'] = ob_get_clean();
Profiler::log('main content: end', -1);
$page_template = cfg('theme/path').'/page.'.URL::$page_type.'.php';
if(!file_exists($page_template))
$page_template = 'themes/common/page.'.URL::$page_type.'.php';
if(!file_exists($page_template))
die('fatal error: page template not found ('.$page_template.')');
require($page_template);
Log::audit('page:'.URL::$route['page'], URL::$route['l-path']);

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,65 +0,0 @@
var enable_debug = true;
var UI = {
smoothScrollToNamedAnchors: function() {
$('a[href^="#"]').each(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() {
$(document.body).append(`<style>
::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; }
}</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');
},
}
$.ready(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
};
}));

View File

@ -1,168 +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.USortableTable = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
function getTable(target) {
if (!target) return null;
if (typeof target === 'string') return document.getElementById(target);
return target.nodeType === 1 ? target : null;
}
function getStorageKey(table, options) {
if (options && options.storageKey) return options.storageKey;
if (table && table.id) return `u-sortable-table.${table.id}`;
return null;
}
function loadSort(table, options) {
const storageKey = getStorageKey(table, options);
if (!storageKey || !globalScope.localStorage) return null;
try {
const raw = globalScope.localStorage.getItem(storageKey);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !Number.isInteger(parsed.column) || (parsed.direction !== 'asc' && parsed.direction !== 'desc')) {
return null;
}
return parsed;
} catch (error) {
return null;
}
}
function saveSort(table, column, direction, options) {
const storageKey = getStorageKey(table, options);
if (!storageKey || !globalScope.localStorage) return;
try {
globalScope.localStorage.setItem(storageKey, JSON.stringify({ column, direction }));
} catch (error) {
// Ignore storage errors.
}
}
function parseSortValue(text) {
const normalized = String(text || '').trim();
if (!normalized) return { type: 'text', value: '' };
if (globalScope.UFormat && typeof globalScope.UFormat.parseUnitNumber === 'function') {
const unitValue = globalScope.UFormat.parseUnitNumber(normalized);
if (unitValue != null) {
return { type: 'number', value: unitValue };
}
}
const timestamp = Date.parse(normalized);
if (!Number.isNaN(timestamp)) {
return { type: 'number', value: timestamp };
}
return { type: 'text', value: normalized.toLowerCase() };
}
function setHeaderState(table, columnIndex, direction) {
Array.from(table.querySelectorAll('thead th')).forEach((header, index) => {
header.classList.remove('sorted-asc', 'sorted-desc');
header.setAttribute('aria-sort', 'none');
if (index === columnIndex) {
header.classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');
}
});
}
function sortTable(table, columnIndex, direction, options, persist) {
const tbody = table.querySelector('tbody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.children.length > 0 && !row.hasAttribute('data-empty-row'));
if (!rows.length) return;
rows.sort((rowA, rowB) => {
const cellA = rowA.children[columnIndex];
const cellB = rowB.children[columnIndex];
const rawA = cellA ? (cellA.getAttribute('data-sort-value') || cellA.textContent) : '';
const rawB = cellB ? (cellB.getAttribute('data-sort-value') || cellB.textContent) : '';
const valueA = parseSortValue(rawA);
const valueB = parseSortValue(rawB);
let comparison = 0;
if (valueA.type === 'number' && valueB.type === 'number') {
comparison = valueA.value - valueB.value;
} else {
comparison = String(valueA.value).localeCompare(String(valueB.value), undefined, {
numeric: true,
sensitivity: 'base',
});
}
return direction === 'asc' ? comparison : -comparison;
});
rows.forEach((row) => tbody.appendChild(row));
setHeaderState(table, columnIndex, direction);
if (persist !== false) {
saveSort(table, columnIndex, direction, options);
}
}
function getInitialSort(table, options) {
const saved = loadSort(table, options);
if (saved) return saved;
if (options && options.initialSort) {
return {
column: Number(options.initialSort.column || 0),
direction: options.initialSort.direction === 'desc' ? 'desc' : 'asc',
};
}
return null;
}
function attachHeader(header, table, columnIndex, options) {
if (header.getAttribute('data-sortable') === 'false') return;
header.classList.add('sortable');
header.tabIndex = 0;
header.addEventListener('click', function () {
const current = loadSort(table, options);
const nextDirection = current && current.column === columnIndex && current.direction === 'asc' ? 'desc' : 'asc';
sortTable(table, columnIndex, nextDirection, options, true);
});
header.addEventListener('keydown', function (event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
header.click();
});
}
function init(target, options = {}) {
const table = getTable(target);
if (!table || table.dataset.sortableTableInit === '1') return table;
Array.from(table.querySelectorAll('thead th')).forEach((header, index) => attachHeader(header, table, index, options));
table.dataset.sortableTableInit = '1';
const initialSort = getInitialSort(table, options);
if (initialSort) {
sortTable(table, initialSort.column, initialSort.direction, options, false);
}
return table;
}
function initAll(selector = '.u-sortable-table') {
Array.from(document.querySelectorAll(selector)).forEach((table) => init(table));
}
return {
init,
initAll,
parseSortValue,
sortTable,
};
}));

View File

@ -1,165 +0,0 @@
:root {
--vp-color-primary: rgba(255, 125, 55, 1);
--vp-color-primary-focus: rgba(255, 120, 60, 0.4);
--vp-background: rgba(0,0,0,0.2);
--vp-color-black: #000;
--vp-color-gray: #666;
--vp-color-white: #fff;
--vp-space-s: 4px;
--vp-space-l: 6px;
--vp-size-icon: 12px;
--vp-size-nav-height: 48px;
--vp-opacity: 0.4;
--vp-opacity-active: 0.9;
--vp-z-pane: 2;
--vp-z-toolbar: 3;
--vp-z-popup: 9999;
--vp-duration: 0.25s;
--vp-easing: ease;
}
#fabric {
position: absolute;
top: var(--vp-size-nav-height);
left: 0; right: 0; bottom: 0;
display: flex;
overflow: hidden;
}
.vp-split {
flex: 1 1 auto;
display: flex;
position: relative;
overflow: hidden;
}
.vp-split.row { flex-direction: row; }
.vp-split.col { flex-direction: column; }
.viewport-pane {
flex: 1 1 0;
position: relative;
border: var(--vp-space-s) solid var(--color-border);
background: var(--vp-background);
padding: var(--vp-space-l);
overflow: auto;
font-family: console;
transition:
border-color var(--vp-duration) var(--vp-easing),
box-shadow var(--vp-duration) var(--vp-easing),
background var(--vp-duration) var(--vp-easing);
}
.vp-split > .viewport-pane {
transition:
flex var(--vp-duration) var(--vp-easing),
opacity var(--vp-duration) var(--vp-easing);
}
.vp-no-transition,
.vp-no-transition * {
transition: none !important;
}
.viewport-pane:before {
content: attr(data-title);
position: absolute;
top: var(--vp-space-s);
right: var(--vp-space-s);
opacity: var(--vp-opacity-medium);
pointer-events: none;
}
.viewport-pane.focused {
border-color: var(--vp-color-primary-focus);
z-index: var(--vp-z-pane);
}
.viewport-pane.focused:before {
opacity: var(--vp-opacity-active);
color: var(--color-highlight);
}
.vp-divider {
background: var(--color-border);
opacity: var(--vp-opacity);
position: relative;
flex: 0 0 auto;
}
.vp-divider.row {
width: var(--vp-space-l);
cursor: col-resize;
}
.vp-divider.col {
height: var(--vp-space-l);
cursor: row-resize;
}
.vp-divider:after {
content:'';
position: absolute;
inset: 0;
transition:
background var(--vp-duration) var(--vp-easing),
opacity var(--vp-duration) var(--vp-easing);
background: var(--color-border);
}
.vp-divider.row:after {
background: var(--color-border);
}
.vp-divider:hover:after {
background: var(--color-highlight);
opacity: var(--vp-opacity);
}
.vp-divider.dragging:after {
background: var(--color-highlight);
opacity: var(--vp-opacity-active);
}
.vp-help-overlay {
position: absolute;
top: var(--vp-space-s);
left: var(--vp-space-s);
opacity: var(--vp-opacity);
pointer-events: none;
}
.vp-pane-toolbar {
position: absolute;
top: var(--vp-space-s);
right: var(--vp-space-s);
display: flex;
gap: var(--vp-space-s);
z-index: var(--vp-z-toolbar);
}
.vp-pane-toolbar button {
padding: 0 var(--vp-space-l);
border: 1px solid var(--color-border);
background: var(--vp-background);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.vp-pane-toolbar button:hover {
background: var(--color-highlight);
color: var(--vp-color-black);
text-shadow: none;
}
.vp-icon {
width: var(--vp-size-icon);
height: var(--vp-size-icon);
display: block;
}

View File

@ -1,388 +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.TilingViewportManager = factory(root.$);
}
}(typeof self !== 'undefined' ? self : this, function ($) {
'use strict';
// Note: This module expects showPopupMenu to be available globally
// or you may need to adjust the dependency list above to include it
let TilingViewportManager = function(container, options = {}) {
let idCounter = 1, rootNode = null, focusedPane = null, containerEl = null;
let nextId = () => 'vp_' + (idCounter++);
let paneMeta = new Map();
let pendingViewLoads = [];
let defaultoptions = {
minPaneSize: 100,
keyboard: true,
showHelp: true,
closeTransitionTimeout: 400,
toolbarButtons: ['split-v','split-h','views','close'],
standardViewList: [],
cssVars: { '--vp-duration': '.22s', '--vp-easing': 'ease' }
};
options = Object.assign({}, defaultoptions, options);
let createPane = (content, props = {}) => {
let id = nextId();
let $el = $('<div class="viewport-pane">').attr('data-id', id);
if(props.title) $el.attr('data-title', props.title);
$el.html(content || `<div style="opacity:.65;">Use Alt+&lt;H V O X ↑ ← → ↓&gt;</div>`)
.on('mousedown', () => focusPaneByEl($el[0]));
attachPaneToolbar($el[0]);
paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props});
if(props.view?.screen) pendingViewLoads.push({id, view: props.view});
return {type: 'pane', id, el: $el[0]};
};
let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); };
let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); };
let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id')));
let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r);
let getNodeById = id => findNodeById(rootNode, id) || null;
let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r);
let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc);
let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); };
let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); };
let split = (direction, newPaneProps = {}) => {
if(!focusedPane) return;
let meta = paneMeta.get(focusedPane.id);
if(meta?.canSplit === false) return;
let paneRect = focusedPane.el.getBoundingClientRect();
let availableSpace = direction === 'row' ? paneRect.width : paneRect.height;
let requiredSpace = 2 * options.minPaneSize;
if(availableSpace < requiredSpace) {
//console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`);
return;
}
let parentPath = findParentPath(rootNode, focusedPane.id);
let newPane = createPane(undefined, newPaneProps);
let animatedSplit = null, newIndex = -1;
if(!parentPath?.length) {
rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]};
animatedSplit = rootNode; newIndex = 1;
} else {
let parent = parentPath.at(-1);
if(parent.type === 'split' && parent.direction === direction) {
let idx = parent.children.indexOf(focusedPane);
parent.children.splice(idx+1, 0, newPane);
parent.sizes = parent.sizes || parent.children.map(()=>1);
parent.sizes.splice(idx+1, 0, 0);
renormalizeSizes(parent);
animatedSplit = parent; newIndex = idx+1;
} else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) {
parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0];
animatedSplit = parent; newIndex = parent.children.length-1;
} else {
let idx = parent.children.indexOf(focusedPane);
let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]};
parent.children.splice(idx, 1, created);
animatedSplit = created; newIndex = 1;
}
}
render();
if(animatedSplit && newIndex >= 0) {
requestAnimationFrame(() => requestAnimationFrame(() => {
animatedSplit.sizes = animatedSplit.children.map(()=>1);
renormalizeSizes(animatedSplit);
animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`));
}));
}
focusPane(newPane); return newPane;
};
let splitVertical = () => split('row');
let splitHorizontal = () => split('col');
let closeFocused = () => {
if(!focusedPane) return;
let meta = paneMeta.get(focusedPane.id);
if(meta?.canClose === false) return;
if(rootNode === focusedPane) { buildInitial(); return; }
if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id);
if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id);
let parentPath = findParentPath(rootNode, focusedPane.id);
if(!parentPath?.length) return;
let parent = parentPath.at(-1);
if(parent.type !== 'split') return;
let idx = parent.children.indexOf(focusedPane);
let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice();
let oldSize = oldSizes[idx] || 0;
let remaining = 1 - oldSize;
let newSizes = oldSizes.slice();
if(remaining > 0) {
for(let i=0;i<newSizes.length;i++) if(i!==idx) newSizes[i] = newSizes[i] / remaining; else newSizes[i] = 0;
} else {
let count = parent.children.length - 1 || 1;
for(let i=0;i<newSizes.length;i++) newSizes[i] = (i===idx) ? 0 : 1/count;
}
parent.sizes = newSizes;
parent.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`));
if(focusedPane.el) {
focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease';
focusedPane.el.style.opacity = '0';
}
let done = false;
let cleanup = () => {
if(done) return; done = true;
parent.children.splice(idx,1);
parent.sizes?.splice(idx,1);
paneMeta.delete(focusedPane.id);
if(parent.children.length === 1) {
let only = parent.children[0];
parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only);
} else if(parent.sizes) renormalizeSizes(parent);
render(); focusPane(paneList(rootNode)[0]);
};
let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } };
if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd);
setTimeout(() => cleanup(), 350);
};
let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; };
let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); };
let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); };
let serialize = (node=rootNode) => {
let ser = n => !n ? null : n.type==='pane' ?
{type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} :
{type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)};
return {focus: focusedPane?.id, layout: ser(node)};
};
let restore = data => {
if(!data) return;
let wrapper = data.layout ? data : {layout:data};
paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0;
let idNums = [];
let build = l => l && l.type==='pane' ?
(() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view});
if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); }
let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); }
return pane; })() :
l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null;
rootNode = build(wrapper.layout) || createPane();
if(idNums.length) idCounter = Math.max(...idNums) + 1;
render();
pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0;
focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]);
};
let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); };
let renderNode = node => {
if(node.type==='pane') return node.el;
node.el = node.el || $('<div>')[0];
$(node.el).empty().addClass(`vp-split ${node.direction}`);
if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); }
node.children.forEach((ch,i) => {
let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl);
if(i < node.children.length-1) { let div = $('<div>').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); }
});
return node.el;
};
let setupDivider = (div, splitNode, leftIdx) => {
$(div).on('mousedown', e => {
e.preventDefault(); $(div).addClass('dragging');
$(containerEl).addClass('vp-no-transition');
let isRow = splitNode.direction === 'row';
let startPos = isRow ? e.clientX : e.clientY;
let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect());
let prop = isRow ? 'width' : 'height';
let startPixels = childRects.map(r => r[prop]);
let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]];
let totalPixels = startPixels.reduce((a,b)=>a+b,0);
let onMove = ev => {
let curPos = isRow ? ev.clientX : ev.clientY;
let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos];
if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize];
if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize];
startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx;
splitNode.sizes = startPixels.map(p => p / totalPixels);
splitNode.children.forEach((ch,i)=> {
if(ch.el) {
let flexValue = `${splitNode.sizes[i]} 1 0`;
$(ch.el).css('flex', flexValue);
}
});
};
let onUp = () => {
$(div).removeClass('dragging');
$(containerEl).removeClass('vp-no-transition');
$(document).off('mousemove', onMove).off('mouseup', onUp);
};
$(document).on('mousemove', onMove).on('mouseup', onUp);
});
$(div).on('dblclick', () => {
$(containerEl).addClass('vp-no-transition');
let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len);
splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`));
setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0);
});
};
let attachPaneToolbar = el => {
let bar = $('<div>').addClass('vp-pane-toolbar').html(`
<button title="Split Vert" data-act="split-v" aria-label="Split vertically">
<svg class="vp-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="1" y="4" width="8" height="16" rx="1" fill="currentColor" />
<rect x="15" y="4" width="8" height="16" rx="1" fill="currentColor" />
</svg>
</button>
<button title="Split Horiz" data-act="split-h" aria-label="Split horizontally">
<svg class="vp-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="3" y="1" width="18" height="8" rx="1" fill="currentColor" />
<rect x="3" y="15" width="18" height="8" rx="1" fill="currentColor" />
</svg>
</button>
<button title="Views" data-act="views">O</button>
<button title="Close" data-act="close">X</button>`)[0];
el.appendChild(bar);
$(bar).on('mousedown', ev => ev.stopPropagation());
$(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el);
let button = ev.target.closest('button[data-act]');
let act = button ? button.getAttribute('data-act') : null;
if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused();
else if(act==='views') {
// compute button position
let rect = button.getBoundingClientRect();
let items = options.standardViewList.slice();
showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => {
// support string or object {screen}
let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it});
loadView(el.dataset.id, view);
}, container: document.body, emptyText: 'No views available' });
}
});
refreshToolbarState(el.dataset.id);
};
let refreshToolbarState = paneId => {
let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0];
if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return;
let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false];
['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } });
let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); }
};
let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props);
if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; }
if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload;
paneMeta.set(paneId, meta); refreshToolbarState(paneId); };
let getPaneProps = paneId => paneMeta.get(paneId);
let focusDirection = (dx, dy) => {
if(!focusedPane) return;
let panes = Array.from(containerEl.querySelectorAll('.viewport-pane'));
let cur = focusedPane.el.getBoundingClientRect();
let candidates = panes.filter(p => p !== focusedPane.el).map(p => {
let r = p.getBoundingClientRect();
if(dx) {
if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null;
let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top));
let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right;
if(primaryDist < -1) return null;
return {el:p, score: primaryDist*1000 + (cur.height - overlap)};
}
if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null;
let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left));
let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom;
if(primaryDist < -1) return null;
return {el:p, score: primaryDist*1000 + (cur.width - overlap)};
}).filter(Boolean);
if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el);
let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2];
let fallback = panes.filter(p => p !== focusedPane.el).map(p => {
let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2];
let [vx, vy] = [px-cx, py-cy];
if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null;
return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))};
}).filter(Boolean);
if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el);
};
let injectHelpOverlay = () => {
if(!options.showHelp) return;
if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``}));
};
let getPaneEl = id => findNodeById(rootNode, id)?.el || null;
let openViewsPopupForPane = (paneId) => {
let node = findNodeById(rootNode, paneId || focusedPane?.id);
if(!node || !node.el) return;
let rect = node.el.getBoundingClientRect();
let x = Math.max(8, rect.right - 80);
let y = rect.top + 24;
let items = options.standardViewList.slice();
showPopupMenu(x, y, items, { onSelect: it => {
let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it});
loadView(node.id, view);
}, container: document.body, emptyText: 'No views available' });
};
let loadView = (paneId, view) => {
let node = findNodeById(rootNode, paneId);
if(!node || !node.el) return; // nothing to load into
let meta = paneMeta.get(paneId);
try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); }
if(meta) meta.view = view;
node.el.innerHTML = `<div style="opacity:.4;">Loading ${view.screen||'...'}...</div>`;
$(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})});
};
let handleKeys = e => {
if(!e.altKey || !options.keyboard) return;
let actions = {
'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(),
'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(),
'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(),
'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(),
'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext()
};
if(actions[e.code] || actions[e.key]) {
(actions[e.code] || actions[e.key])();
e.preventDefault();
}
};
containerEl = typeof container === 'string' ? document.querySelector(container) : container;
if(!containerEl) throw new Error('TilingViewportManager: container not found');
if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k]));
buildInitial();
if(options.keyboard) document.addEventListener('keydown', handleKeys);
this.splitVertical = splitVertical;
this.splitHorizontal = splitHorizontal;
this.closeFocused = closeFocused;
this.focusNext = focusNext;
this.focusPrev = focusPrev;
this.serialize = serialize;
this.restore = restore;
this.setPaneProps = setPaneProps;
this.getPaneProps = getPaneProps;
this.options = options;
this.loadView = loadView;
this.getPaneEl = getPaneEl;
this.getNodeById = getNodeById;
Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) });
Object.defineProperty(this, 'focused', { get: () => focusedPane?.id });
};
return TilingViewportManager;
}));

View File

@ -1,354 +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.UTimeSeriesChart = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
class TimeSeriesChart {
constructor(canvasOrId, options = {}) {
this.canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId;
if (!this.canvas || !this.canvas.getContext) {
throw new Error('UTimeSeriesChart requires a canvas element');
}
this.ctx = this.canvas.getContext('2d');
const theme = this.resolveThemeColors();
this.options = Object.assign({
xAxisLabel: 'Time',
yAxisLeftLabel: 'Value',
yAxisRightLabel: '',
yAxisLeftFormat: 'number',
yAxisRightFormat: 'number',
padding: { top: 18, right: 62, bottom: 34, left: 58 },
gridColor: theme.gridColor,
axisColor: theme.axisColor,
textColor: theme.textColor,
legendBackground: theme.legendBackground,
legendTextColor: theme.legendTextColor,
tooltipBackground: theme.tooltipBackground,
tooltipBorder: theme.tooltipBorder,
tooltipTitleColor: theme.tooltipTitleColor,
tooltipTextColor: theme.tooltipTextColor,
hoverLineColor: 'rgba(255,255,255,0.35)',
}, options);
this.series = [];
this.xLabels = [];
this.hoverIndex = null;
this.hoverX = 0;
this.width = 0;
this.height = 0;
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onResize = this.onResize.bind(this);
this.canvas.addEventListener('mousemove', this.onMouseMove);
this.canvas.addEventListener('mouseleave', this.onMouseLeave);
globalScope.addEventListener('resize', this.onResize);
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(this.onResize);
this.resizeObserver.observe(this.canvas);
}
this.resize();
}
resolveThemeColors() {
const styles = globalScope.getComputedStyle ? globalScope.getComputedStyle(document.documentElement) : null;
const pick = function (name, fallback) {
if (!styles) return fallback;
const value = styles.getPropertyValue(name).trim();
return value || fallback;
};
return {
gridColor: pick('--border', 'rgba(148, 163, 184, 0.20)'),
axisColor: pick('--border-hover', pick('--border', 'rgba(148, 163, 184, 0.42)')),
textColor: pick('--text-secondary', '#64748b'),
legendBackground: pick('--surface', 'rgba(15, 23, 42, 0.72)'),
legendTextColor: pick('--text-primary', '#0f172a'),
tooltipBackground: pick('--surface', '#0f172a'),
tooltipBorder: pick('--border', 'rgba(148, 163, 184, 0.30)'),
tooltipTitleColor: pick('--primary', '#3b82f6'),
tooltipTextColor: pick('--text-primary', '#0f172a'),
};
}
destroy() {
this.canvas.removeEventListener('mousemove', this.onMouseMove);
this.canvas.removeEventListener('mouseleave', this.onMouseLeave);
globalScope.removeEventListener('resize', this.onResize);
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
onResize() {
this.resize();
}
onMouseLeave() {
this.hoverIndex = null;
this.draw();
}
resize() {
const rect = this.canvas.getBoundingClientRect();
const cssWidth = Math.max(280, Math.round(rect.width || this.canvas.width || 640));
const cssHeight = Math.max(180, Math.round(rect.height || this.canvas.height || 320));
const pixelRatio = globalScope.devicePixelRatio || 1;
this.width = cssWidth;
this.height = cssHeight;
this.canvas.width = Math.round(cssWidth * pixelRatio);
this.canvas.height = Math.round(cssHeight * pixelRatio);
this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
this.draw();
}
setData(series, xLabels = []) {
this.series = (Array.isArray(series) ? series : []).map((item) => ({
key: item.key || '',
label: item.label || 'Series',
color: item.color || '#60a5fa',
axis: item.axis === 'right' ? 'right' : 'left',
unit: item.unit || '',
format: item.format || '',
maxHint: Number(item.maxHint || 0),
decimals: Number.isInteger(item.decimals) ? item.decimals : 2,
values: Array.isArray(item.values) ? item.values.map((value) => Number(value || 0)) : [],
}));
this.xLabels = Array.isArray(xLabels) ? xLabels : [];
this.draw();
}
getPlotRect() {
const padding = this.options.padding;
return {
x: padding.left,
y: padding.top,
w: this.width - padding.left - padding.right,
h: this.height - padding.top - padding.bottom,
};
}
getMaxForAxis(axis) {
const relevant = this.series.filter((item) => item.axis === axis);
if (!relevant.length) return 1;
let max = 1;
relevant.forEach((item) => {
const localMax = Math.max(...(item.values.length ? item.values : [0]), item.maxHint || 0, 1);
if (localMax > max) max = localMax;
});
return max;
}
xForIndex(index, pointCount, plot) {
if (pointCount <= 1) return plot.x;
return plot.x + (index / (pointCount - 1)) * plot.w;
}
yForValue(value, axisMax, plot) {
const clamped = Math.max(0, Number(value || 0));
return plot.y + plot.h - ((clamped / axisMax) * plot.h);
}
formatValue(value, format, unit, decimals) {
const number = Number(value || 0);
if (globalScope.UFormat) {
if (format === 'bytes' && globalScope.UFormat.formatBytes) return globalScope.UFormat.formatBytes(number);
if (format === 'disk-bytes' && globalScope.UFormat.formatDiskBytes) return globalScope.UFormat.formatDiskBytes(number);
if (format === 'count' && globalScope.UFormat.formatCount) return globalScope.UFormat.formatCount(number);
if (format === 'duration-ms' && globalScope.UFormat.formatDurationMs) return globalScope.UFormat.formatDurationMs(number);
}
return `${number.toFixed(decimals)}${unit || ''}`;
}
drawGridAndAxes(plot, maxLeft, maxRight) {
const ctx = this.ctx;
ctx.strokeStyle = this.options.gridColor;
ctx.lineWidth = 1;
for (let index = 0; index <= 4; index += 1) {
const y = plot.y + (plot.h / 4) * index;
ctx.beginPath();
ctx.moveTo(plot.x, y);
ctx.lineTo(plot.x + plot.w, y);
ctx.stroke();
const leftValue = maxLeft - ((maxLeft / 4) * index);
const leftTick = this.formatValue(leftValue, this.options.yAxisLeftFormat, '', 1);
ctx.fillStyle = this.options.textColor;
ctx.font = '11px Inter, sans-serif';
const leftWidth = ctx.measureText(leftTick).width;
ctx.fillText(leftTick, Math.max(4, plot.x - leftWidth - 8), y + 4);
if (this.options.yAxisRightLabel) {
const rightValue = maxRight - ((maxRight / 4) * index);
const rightTick = this.formatValue(rightValue, this.options.yAxisRightFormat, '', 1);
ctx.fillText(rightTick, plot.x + plot.w + 8, y + 4);
}
}
ctx.strokeStyle = this.options.axisColor;
ctx.beginPath();
ctx.moveTo(plot.x, plot.y);
ctx.lineTo(plot.x, plot.y + plot.h);
ctx.lineTo(plot.x + plot.w, plot.y + plot.h);
ctx.stroke();
ctx.fillStyle = this.options.textColor;
ctx.font = '12px Inter, sans-serif';
ctx.save();
ctx.translate(14, plot.y + (plot.h / 2));
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.options.yAxisLeftLabel, 0, 0);
ctx.restore();
if (this.options.yAxisRightLabel) {
ctx.save();
ctx.translate(this.width - 14, plot.y + (plot.h / 2));
ctx.rotate(Math.PI / 2);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.options.yAxisRightLabel, 0, 0);
ctx.restore();
}
const xLabel = this.options.xAxisLabel;
const xLabelWidth = ctx.measureText(xLabel).width;
ctx.fillText(xLabel, plot.x + plot.w - xLabelWidth, this.height - 10);
}
drawSeries(plot, maxLeft, maxRight) {
const ctx = this.ctx;
const pointCount = Math.max(...this.series.map((item) => item.values.length), 0);
if (pointCount <= 0) return;
this.series.forEach((item) => {
if (!item.values.length) return;
const axisMax = item.axis === 'right' ? maxRight : maxLeft;
ctx.beginPath();
item.values.forEach((value, index) => {
const x = this.xForIndex(index, item.values.length, plot);
const y = this.yForValue(value, axisMax, plot);
if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.strokeStyle = item.color;
ctx.lineWidth = 2;
ctx.stroke();
});
if (this.series.length > 1) {
ctx.fillStyle = this.options.legendBackground;
ctx.fillRect(plot.x + 4, plot.y + 4, Math.min(plot.w - 8, 320), 24);
let offsetX = plot.x + 12;
this.series.forEach((item) => {
ctx.fillStyle = item.color;
ctx.fillRect(offsetX, plot.y + 13, 14, 3);
offsetX += 20;
ctx.fillStyle = this.options.legendTextColor;
ctx.font = '11px Inter, sans-serif';
ctx.fillText(item.label, offsetX, plot.y + 17);
offsetX += ctx.measureText(item.label).width + 18;
});
}
}
drawHover(plot, maxLeft, maxRight) {
if (this.hoverIndex == null) return;
const ctx = this.ctx;
const pointCount = Math.max(...this.series.map((item) => item.values.length), 0);
if (pointCount <= 0) return;
const index = Math.max(0, Math.min(pointCount - 1, this.hoverIndex));
const x = this.xForIndex(index, pointCount, plot);
ctx.strokeStyle = this.options.hoverLineColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, plot.y);
ctx.lineTo(x, plot.y + plot.h);
ctx.stroke();
const lines = [];
lines.push(this.xLabels[index] || `Sample ${index + 1}`);
this.series.forEach((item) => {
if (index >= item.values.length) return;
const axisMax = item.axis === 'right' ? maxRight : maxLeft;
const value = item.values[index] ?? 0;
const y = this.yForValue(value, axisMax, plot);
ctx.fillStyle = item.color;
ctx.beginPath();
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
ctx.fill();
lines.push(`${item.label}: ${this.formatValue(value, item.format || (item.axis === 'right' ? this.options.yAxisRightFormat : this.options.yAxisLeftFormat), item.unit || '', item.decimals)}`);
});
ctx.font = '11px Inter, sans-serif';
const padding = 8;
const lineHeight = 14;
const tooltipWidth = Math.max(...lines.map((line) => ctx.measureText(line).width)) + padding * 2;
const tooltipHeight = lines.length * lineHeight + padding * 2 - 2;
let tooltipX = this.hoverX + 12;
let tooltipY = plot.y + 8;
if (tooltipX + tooltipWidth > this.width - 4) tooltipX = this.hoverX - tooltipWidth - 12;
if (tooltipX < 4) tooltipX = 4;
if (tooltipY + tooltipHeight > this.height - 4) tooltipY = this.height - tooltipHeight - 4;
ctx.fillStyle = this.options.tooltipBackground;
ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
ctx.strokeStyle = this.options.tooltipBorder;
ctx.strokeRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
lines.forEach((line, lineIndex) => {
ctx.fillStyle = lineIndex === 0 ? this.options.tooltipTitleColor : this.options.tooltipTextColor;
ctx.fillText(line, tooltipX + padding, tooltipY + padding + 10 + lineIndex * lineHeight);
});
}
onMouseMove(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
this.hoverX = x;
const plot = this.getPlotRect();
const pointCount = Math.max(...this.series.map((item) => item.values.length), 0);
if (pointCount <= 0) {
this.hoverIndex = null;
this.draw();
return;
}
const relative = Math.max(0, Math.min(plot.w, x - plot.x));
this.hoverIndex = pointCount <= 1 ? 0 : Math.round((relative / plot.w) * (pointCount - 1));
this.draw();
}
draw() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.width, this.height);
const plot = this.getPlotRect();
if (plot.w <= 0 || plot.h <= 0) return;
const maxLeft = this.getMaxForAxis('left');
const maxRight = this.getMaxForAxis('right');
this.drawGridAndAxes(plot, maxLeft, maxRight);
this.drawSeries(plot, maxLeft, maxRight);
this.drawHover(plot, maxLeft, maxRight);
}
}
return TimeSeriesChart;
}));

View File

@ -1,68 +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.UWorkspaceShell = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
function getElement(target) {
if (!target) return null;
if (typeof target === 'string') return document.getElementById(target);
return target && target.nodeType === 1 ? target : null;
}
function bindShell(options) {
const sidebar = getElement(options.sidebarId || options.sidebar);
const overlay = getElement(options.overlayId || options.overlay);
const toggle = getElement(options.toggleButtonId || options.toggle);
const closeOnNav = options.closeOnNav !== false;
if (!sidebar || !overlay) return null;
function setOpen(open) {
sidebar.classList.toggle('is-open', !!open);
overlay.classList.toggle('is-open', !!open);
}
function toggleOpen() {
setOpen(!sidebar.classList.contains('is-open'));
}
if (toggle) {
toggle.addEventListener('click', toggleOpen);
}
overlay.addEventListener('click', function () {
setOpen(false);
});
if (closeOnNav) {
sidebar.querySelectorAll('a').forEach(function (link) {
link.addEventListener('click', function () {
setOpen(false);
});
});
}
globalScope.addEventListener('resize', function () {
if (globalScope.innerWidth > 860) {
setOpen(false);
}
});
return {
open: function () { setOpen(true); },
close: function () { setOpen(false); },
toggle: toggleOpen,
};
}
return {
init: bindShell,
};
}));

View File

@ -1,111 +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.Connection = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
var Connection = {
auto_reconnect : false,
established : false,
update_indicator : (status) => {
var status_colors = {
'offline' : 'red',
'online' : 'lightgreen',
'error' : 'DarkOrange',
};
$('#connection-status').text(status).css('color', status_colors[status] || 'gray');
},
debug : true,
auto_reconnect : true,
cmd_waiting_rels : {},
server_url : '',
init : () => {
new EventSystem(Connection);
},
start : (url = null) => {
if(url) Connection.server_url = url;
Connection.update_indicator('offline');
if(Connection.socket) Connection.socket.close();
Connection.socket = new WebSocket(Connection.server_url);
Connection.socket.onmessage = function(rawmsg) {
var msg = JSON.parse(rawmsg.data);
if(Connection.debug) console.log('CONNECTION MSG', msg);
if(msg.type) Connection.trigger(msg.type, msg);
Connection.trigger('message', msg);
}
Connection.socket.onclose = function() {
Connection.update_indicator('offline');
if(Connection.debug) console.log('CONNECTION CLOSED');
Connection.established = false;
Connection.trigger('close', {});
}
Connection.socket.onerror = function(error) {
Connection.update_indicator('error');
console.error('CONNECTION', error);
Connection.established = false;
}
Connection.socket.onopen = function() {
Connection.update_indicator('online');
if(Connection.debug) console.log('CONNECTION ESTABLISHED');
Connection.established = true;
Connection.trigger('open', {});
}
setTimeout(Connection.reconnect, 2000);
},
deauth : () => {
Game.session = {};
},
reconnect : () => {
if(!Connection.established && Connection.auto_reconnect)
Connection.start();
else
setTimeout(Connection.reconnect, 2000);
},
queue : [],
dequeue : () => {
var dq = Connection.queue;
Connection.queue = [];
dq.forEach(function(fm) {
Connection.send(fm);
});
},
send : (msg) => {
if(Connection.established) {
if(typeof msg == 'function')
msg();
else
Connection.socket.send(JSON.stringify(msg));
} else {
Connection.queue.push(msg);
}
},
close : () => {
Connection.auto_reconnect = false;
Connection.socket.close();
},
}
return Connection;
}));

View File

@ -1,199 +0,0 @@
<?php
// Example component definition file used by the components system:
/*<?php return [
'begin' => function($prop, &$context) {
$context['current_component_id'] = $prop['id'];
return('<div class="my-component">');
},
'end' => function($prop) {
?><script>
console.log('Component <?= safe($prop['id']) ?> finalized');
</script><?php
return('</div>');
},
'render' => function($prop, &$context) {
return($context['begin']($prop, $context) . $context['end']($prop));
},
'about' => 'This is an example component that wraps content in a div and logs when it is finalized.',
]; */
$GLOBALS['render_funcs'] = array();
$GLOBALS['id_counter'] = $GLOBALS['id_counter'] ?? 1;
function component_error_banner($s)
{
return '<div class="banner">'.safe($s).'</div>';
}
function component_caller_dir()
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
return isset($trace[1]['file']) ? dirname($trace[1]['file']) : getcwd();
}
function component_normalize_name($file_name)
{
return preg_replace('/\.php$/', '', trim((string)$file_name));
}
function component_resolve_file($file_name, $search_path = false)
{
$component_name = component_normalize_name($file_name);
$candidates = array($component_name.'.php');
if($search_path && !str_starts_with($component_name, 'components/'))
$candidates[] = rtrim($search_path, '/').'/'.$component_name.'.php';
if(!str_starts_with($component_name, 'components/'))
$candidates[] = 'components/'.$component_name.'.php';
foreach(array_unique($candidates) as $candidate)
{
if(file_exists($candidate))
return $candidate;
}
return false;
}
/**
* Loads a component from file system and registers it in the global registry
*
* Component files are searched in this order:
* 1. exact file path passed to the loader
* 2. caller-relative path (for shorthand component names)
* 3. components/{component_name}.php
*
* Component files should return an array with render functions like:
* return ['render' => function($prop) { ... }];
*/
function component_get_func($file_name, $return_false_if_not_found = false, $search_path = false)
{
$component_name = component_normalize_name($file_name);
$result = [];
// Check if component is already loaded in global registry
if(!isset($GLOBALS['render_funcs'][$component_name]))
{
$component_file = component_resolve_file($component_name, $search_path);
if($component_file)
{
$result['component_file'] = $component_file;
}
else
{
// Component file not found - create error component
if($return_false_if_not_found) return(false);
$result['component_file'] = false;
$result['error'] = 'Component not found: '.$component_name;
$result['render'] = function() use($component_name) {
return component_error_banner('component not found: '.$component_name);
};
}
if($result['component_file'])
foreach(require($result['component_file']) as $k => $v)
$result[$k] = $v;
// Register component in global registry for future use
$GLOBALS['render_funcs'][$component_name] = $result;
}
return($GLOBALS['render_funcs'][$component_name]);
}
/**
* Checks if a component exists without loading it
* Returns true if component file can be found, false otherwise
*/
function component_exists($file_name)
{
$caller_dir = component_caller_dir();
return(component_get_func($file_name, true, $caller_dir) !== false);
}
/**
* Loads a component and returns its definition
* Same as component_get_func but with caller directory detection
*/
function component_load($file_name)
{
$caller_dir = component_caller_dir();
return(component_get_func($file_name, true, $caller_dir));
}
function component_call($file_name, $render_call, $prop = array())
{
$prop['render_call'] = $render_call;
return component($file_name, $prop);
}
/**
* Declares an inline component directly in code (not from file)
*
* @param string $file_name - Component name for registry
* @param array $render_prop - Component definition with render functions
*
* Example:
* component_declare('my-button', [
* 'render' => function($prop) { return "<button>{$prop['text']}</button>"; }
* ]);
*/
function component_declare($file_name, $render_prop)
{
// Register the inline component directly in global registry
$GLOBALS['render_funcs'][$file_name] = $render_prop;
}
/**
* Main component rendering function - the heart of the component system
*
* @param string $file_name - Component name or path
* @param array $prop - Properties/data to pass to component
* @return mixed - Component output from the selected render method
*
* Component calling examples:
* component('components/example/theme-switcher')
* component_call('components/workspace/panel', 'render', ['title' => 'Status'])
* component('workspace/panel', ['render_call' => 'render'])
*/
function component($file_name, $prop = array())
{
Profiler::log('component '.$file_name, 1);
$caller_dir = component_caller_dir();
if($file_name == '')
return(component_error_banner('[component name empty]'));
$file_name = component_normalize_name($file_name);
// Default render method is 'render', but can be overridden
$prop['render_call'] = first($prop['render_call'] ?? false, 'render');
// Support for calling specific render methods: 'component:method'
if(stristr($file_name, ':')) // you can specify which render function to call
{
$prop['render_call'] = $file_name;
$file_name = nibble(':', $prop['render_call']);
}
// Get component definition from registry (load if not already loaded)
$renderer = $GLOBALS['render_funcs'][$file_name] ?? false;
if(!$renderer)
{
// Component not in registry - try to load from file
component_get_func($file_name, false, $caller_dir);
$renderer = $GLOBALS['render_funcs'][$file_name] ?? false;
}
// Generate unique ID for component instance if not provided
$prop['id'] = !empty($prop['id']) ? $prop['id'] : 'c'.($GLOBALS['id_counter']++);
$prop['filename'] = $file_name;
if(isset($renderer[$prop['render_call']]) && is_callable($renderer[$prop['render_call']]))
$result = ($renderer[$prop['render_call']]($prop, $renderer));
else if(isset($renderer[$prop['render_call']]))
$result = ($renderer[$prop['render_call']]);
else
$result = component_error_banner('component render method not found: '.$file_name.':'.$prop['render_call']);
Profiler::log(false, -1);
return($result);
}

View File

@ -1,381 +0,0 @@
<?php
class DB
{
static $dataCache = array();
static $link = false;
static $lastQuery = '';
static $keyDef = array();
static $affectedRows = 0;
static $writeOps = 0;
static $readOps = 0;
static $track_changes_in_tables = array();
static function isConnected()
{
return(self::$link instanceof mysqli);
}
static function connect()
{
if(self::$link) return;
if(!cfg('db/user')) die('database not configured');
Profiler::Log('DB::Connect() start');
self::$link = mysqli_connect(cfg('db/host'), cfg('db/user'), cfg('db/password'), cfg('db/database'), ini_get("mysqli.default_port"), cfg('db/socket')) or
critical('The database connection to server '.cfg('db/user').'@'.cfg('db/host').
' could not be established (code: '.@mysqli_connect_errno(self::$link).': '.@mysqli_connect_error(self::$link).')');
#if(mysqli_character_set_name(self::$link) != 'utf8')
mysqli_set_charset(self::$link, 'utf8mb4');
self::$track_changes_in_tables = cfg('track/changes');
Profiler::Log('DB::Connect() done');
}
static function Update($table, $searchCriteria, $updateFields)
{
self::$writeOps++;
DB::Query('UPDATE #'.DB::Safe($table).'
SET '.DB::MakeSetList($updateFields).'
WHERE '.DB::MakeSetList($searchCriteria, ' AND '));
}
static function GetCached($query, $parameters = null)
{
$cacheKey = 'dbq-'.md5($query.json_encode($parameters));
$result = Cache::Get($cacheKey);
if(!$result)
{
$result = self::Get($query, $parameters);
Cache::Set($cacheKey, $result);
}
return($result);
}
# get a list of datasets matching the $query
static function Get($query, $parameters = null, $keyByField = null)
{
self::$readOps++;
$result = array();
$query = self::ParseQueryParams($query, $parameters);
$lines = mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' {query: '.$query.' }');
while ($line = mysqli_fetch_array($lines, MYSQLI_ASSOC))
{
if ($keyByField !== null && isset($line[$keyByField]))
$result[$line[$keyByField]] = $line;
else
$result[] = $line;
}
mysqli_free_result($lines);
Profiler::Log('DB::Get('.substr($query, 0, 40).'...)');
return $result;
}
# gets a list of keys for the table
static function Keys($tablename)
{
if(isset(self::$keyDef[$tablename]))
return(self::$keyDef[$tablename]);
self::$readOps++;
$result = array();
$sql = 'SHOW KEYS FROM `'.$tablename.'`';
$res = mysqli_query(self::$link, $sql) or critical('Cannot get keys // '.mysqli_error(self::$link));
while ($row = @mysqli_fetch_assoc($res))
{
if ($row['Key_name']=='PRIMARY')
array_push($result, $row['Column_name']);
}
Profiler::Log('DB::Keys('.$tablename.') REBUILD KEY CACHE');
self::$keyDef[$tablename] = $result;
return($result);
}
# get column info for $table
static function Info($table)
{
self::$readOps++;
$result = array('fields' => array(), 'info' => array());
foreach(self::Get('SHOW FULL COLUMNS FROM #'.$table) as $fld)
{
$ds = array();
foreach($fld as $k => $v)
{
$k = strtolower($k);
if($k == 'comment')
{
$p = explode(',', $v);
$v = false;
if(sizeof($p) > 0) foreach($p as $pi)
{
$pk = trim(nibble('=', $pi));
$ds['_'.$pk] = trim($pi);
}
}
if($v)
$ds[$k] = $v;
}
$ds['caption'] = first($ds['_title'], $ds['field']);
$result['fields'][$ds['field']] = $ds;
}
return($result);
}
# updates/creates the $dataset in the $tablename
static function Insert($tablename, $dataset)
{
self::$writeOps++;
$query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset).
') VALUES('.DB::MakeValuesList($dataset).')';
mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).'{ '.$query.' }');
self::$affectedRows += mysqli_affected_rows(self::$link);
return(mysqli_insert_id(self::$link));
}
# updates/creates the $dataset in the $tablename
static function Commit($tablename, &$dataset)
{
self::$writeOps++;
$keynames = self::Keys($tablename);
$keyname = $keynames[0];
$keyvalue = $dataset[$keyname];
Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') start');
$cache_entry = $tablename.':'.$keyname.':'.$keyvalue;
unset(self::$dataCache[$cache_entry]);
$query='REPLACE INTO '.$tablename.' ('.DB::MakeNamesList($dataset).
') VALUES('.DB::MakeValuesList($dataset).');';
# keeping this around just in case, but performance seems the same:
# $query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset).
# ') VALUES('.DB::MakeValuesList($dataset).')
# ON DUPLICATE KEY UPDATE '.DB::MakeSetList($dataset).';';
mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { '.$query.' }');
$dataset[$keyname] = first($dataset[$keyname], mysqli_insert_id(self::$link));
self::$dataCache[$cache_entry] = $dataset;
Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') done');
return($dataset[$keyname]);
}
static function GetRowsMatch($table, $matchOptions, $fillIfEmpty = true)
{
self::$readOps++;
$where = array('1');
foreach($matchOptions as $k => $v)
$where[] = '('.$k.'="'.DB::Safe($v).'")';
$iwhere = implode(' AND ', $where);
$query = 'SELECT * FROM '.($table).
' WHERE '.$iwhere;
$resultDS = self::GetRowWithQuery($query);
if ($fillIfEmpty && sizeof($resultDS) == 0)
foreach($matchOptions as $k => $v)
$resultDS[$k] = $v;
Profiler::Log('DB::GetRowsMatch('.$table.') done');
return($resultDS);
}
# from table $tablename, get dataset with key $keyvalue
static function GetRow($tablename, $keyvalue, $keyname = '', $options = array())
{
if($keyvalue == '0') return(array());
$fields = @$options['fields'];
$fields = first($fields, '*');
if (!self::$link) return(array());
if ($keyname == '')
{
$keynames = self::Keys($tablename);
$keyname = $keynames[0];
}
$cache_entry = $tablename.':'.$keyname.':'.$keyvalue;
if(isset(self::$dataCache[$cache_entry])) return(self::$dataCache[$cache_entry]);
$join = isset($options['join']) ? ' '.$options['join'] : '';
$query = 'SELECT '.$fields.' FROM '.$tablename.$join.' WHERE '.$keyname.'="'.DB::Safe($keyvalue).'";';
$queryResult = mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { Query: "'.$query.'" }');
self::$readOps++;
if ($line = @mysqli_fetch_array($queryResult, MYSQLI_ASSOC))
{
mysqli_free_result($queryResult);
self::$dataCache[$cache_entry] = $line;
Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.')');
return($line);
}
else
$result = array();
Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.') #fail');
return($result);
}
static function RemoveRow($tablename, $keyvalue, $keyname = null)
{
if ($keyname == null)
{
$keynames = self::Keys($tablename);
$keyname = $keynames[0];
}
$res = (mysqli_query(self::$link, 'DELETE FROM '.$tablename.' WHERE '.$keyname.'="'.
DB::Safe($keyvalue).'";')
or critical(' Cannot remove dataset // '.mysqli_error(self::$link)));
Profiler::Log('DB::RemoveRow('.$tablename.', '.$keyvalue.') done');
self::$affectedRows += mysqli_affected_rows(self::$link);
self::$writeOps++;
return($res);
}
// retrieve dataset identified by SQL $query
static function GetRowWithQuery($query, $parameters = null)
{
$query = self::ParseQueryParams($query, $parameters);
$queryResult = mysqli_query(self::$link, $query);
if(!$queryResult)
return(critical('Error getting data // '.mysqli_error(self::$link).'{ '.$query.' }'));
if ($line = mysqli_fetch_array($queryResult, MYSQLI_ASSOC))
{
$result = $line;
mysqli_free_result($queryResult);
}
else
$result = array();
Profiler::Log('DB::GetRowWithQuery('.$query.')');
self::$readOps++;
return($result);
}
# execute a simple update $query
static function Query($query, $parameters = null)
{
$query = self::parseQueryParams($query, $parameters);
if (substr($query, -1, 1) == ';')
$query = substr($query, 0, -1);
$result = (mysqli_query(self::$link, $query)
or critical(' Query error // '.mysqli_error(self::$link)));
Profiler::Log('DB::Query('.$query.') done');
self::$affectedRows += mysqli_affected_rows(self::$link);
self::$writeOps++;
return($result);
}
# create a comma-separated list of keys in $dataset
static function MakeNamesList(&$dataset)
{
$result = '';
if (sizeof($dataset) > 0)
foreach (array_keys($dataset) as $k)
{
if ($k!='')
$result = $result.','.$k;
}
return(substr($result, 1));
}
# make a name-value list for UPDATE-queries
static function MakeValuesList(&$dataset)
{
$result = '';
if (sizeof($dataset) > 0)
foreach ($dataset as $k => $v)
{
if ($k!='')
$result = $result.',"'.DB::safe($v).'"';
}
return(substr($result, 1));
}
static function MakeSetList(&$dataset, $concat = ', ')
{
$result = array();
if (sizeof($dataset) > 0) foreach ($dataset as $k => $v)
{
if(substr($k, -1) == '+' || substr($k, -1) == '-')
{
$op = substr($k, -1);
$k = substr($k, 0, -1);
$result[] = $k.' = '.$k.' '.$op.' "'.DB::safe($v).'"';
}
else
{
$result[] = $k.' = "'.DB::safe($v).'"';
}
}
return(implode($concat, $result));
}
static function ParseQueryParams($query, $parameters = null)
{
if ($parameters != null)
{
$pctr = 0;
$result = '';
for($a = 0; $a < strlen($query); $a++)
{
$chr = substr($query, $a, 1);
if ($chr == '?')
{
$result .= '"'.DB::Safe($parameters[$pctr]).'"';
$pctr++;
}
else if ($chr == '&')
{
$result .= ''.intval($parameters[$pctr]).'';
$pctr++;
}
else if ($chr == ':')
{
$paramName = '';
$a += 1;
$pFormat = 'string';
if($query[$a] == ':')
{
$pFormat = 'number';
$a += 1;
}
while(!ctype_space($chr = substr($query, $a, 1)) && $a < strlen($query))
{
$paramName .= $chr;
$a += 1;
}
if($pFormat == 'number')
$result .= ' '.($parameters[$paramName]+0).' ';
else
$result .= ' "'.DB::Safe($parameters[$paramName]).'" ';
}
else
$result .= $chr;
}
}
else
$result = $query;
$q = str_replace('#', cfg('db/prefix'), $result);
self::$lastQuery = $q;
return($q);
}
static function Safe($raw)
{
if(!self::$link)
return(addslashes($raw));
else
return(mysqli_real_escape_string(self::$link, $raw));
}
}
DB::connect();

View File

@ -1,144 +0,0 @@
# db.class.php
A PHP database abstraction layer for MySQL/MariaDB with caching, parameter binding, and profiling.
## Connection
```php
DB::connect() // Auto-connects using config
DB::isConnected() // Check connection status
```
Uses configuration from `cfg('db/host')`, `cfg('db/user')`, etc.
## Basic Queries
```php
DB::Query($sql, $params) // Execute any SQL query
DB::Get($sql, $params) // Get multiple rows as array
DB::GetRowWithQuery($sql, $params) // Get single row
DB::GetCached($sql, $params) // Cached version of Get()
```
## CRUD Operations
### Reading Data
```php
DB::GetRow($table, $keyvalue) // Get row by primary key
DB::GetRow($table, $id, $keyname) // Get row by specific key
DB::GetRowsMatch($table, $criteria) // Get rows matching criteria
```
### Writing Data
```php
DB::Insert($table, $data) // Insert new row, returns ID
DB::Commit($table, $data) // Insert or update (REPLACE)
DB::Update($table, $where, $data) // Update existing rows
DB::RemoveRow($table, $keyvalue) // Delete row by key
```
## Parameter Binding
### Positional Parameters
```php
DB::Query('SELECT * FROM users WHERE id = ? AND name = ?', [$id, $name]);
DB::Query('SELECT * FROM users WHERE age > & AND active = ?', [$age, $active]);
```
### Named Parameters
```php
DB::Query('SELECT * FROM users WHERE name = :name', ['name' => $username]);
DB::Query('SELECT * FROM users WHERE age > ::age', ['age' => 25]); // Number format
```
Parameter formats:
- `?` - String (escaped)
- `&` - Number (unescaped integer)
- `:name` - Named string parameter
- `::name` - Named number parameter
## Table Information
```php
DB::Keys($table) // Get primary key column names
DB::Info($table) // Get full column information
```
## Utility Methods
```php
DB::Safe($string) // Escape string for SQL
DB::MakeNamesList($array) // Create column list for INSERT
DB::MakeValuesList($array) // Create values list for INSERT
DB::MakeSetList($array, $separator) // Create SET clause for UPDATE
```
## Advanced Features
### Table Prefix Support
```php
DB::Query('SELECT * FROM #users') // # replaced with cfg('db/prefix')
```
### Increment/Decrement Operations
```php
DB::Update('users', ['id' => 1], ['score+' => 10]); // score = score + 10
DB::Update('users', ['id' => 1], ['lives-' => 1]); // lives = lives - 1
```
### Caching
```php
$users = DB::GetCached('SELECT * FROM users WHERE active = 1');
// Subsequent calls return cached results
```
### Statistics
```php
DB::$affectedRows // Rows affected by last operation
DB::$readOps // Count of read operations
DB::$writeOps // Count of write operations
DB::$lastQuery // Last executed query
```
## Examples
### Basic Usage
```php
// Insert new user
$userId = DB::Insert('users', [
'name' => 'John Doe',
'email' => 'john@example.com',
'active' => 1
]);
// Get user by ID
$user = DB::GetRow('users', $userId);
// Update user
DB::Update('users', ['id' => $userId], ['last_login' => date('Y-m-d H:i:s')]);
// Find users
$activeUsers = DB::Get('SELECT * FROM users WHERE active = ?', [1]);
```
### Complex Queries
```php
// Named parameters
$results = DB::Get('
SELECT * FROM #posts
WHERE author_id = :author
AND created_date > :date
AND status = :status
', [
'author' => $authorId,
'date' => '2023-01-01',
'status' => 'published'
]);
// Mixed parameter types
DB::Query('UPDATE #users SET score = score + ::points WHERE id = :id', [
'points' => 100, // Number (unescaped)
'id' => $userId // String (escaped)
]);
```

View File

@ -1,217 +0,0 @@
<?php
class Filebase
{
static function hash($s = false)
{
if($s === false) $s = time().random_int(0, 2147483647);
$s = strtolower(substr(trim($s), 0, 64));
return(substr(base_convert_ex(
sha1(sha1('qw0e983124o521öl34u9087'.$s)),
'0123456789abcdef',
'0123456789abcdefghijklmnopqrstuvwxyz'
), -10));
}
static function make_bucket_path($p)
{
if(stristr($p, '/') !== false)
{
$seg = explode('/', $p);
$p = array_shift($seg);
return(substr($p, -2).'/'.$p.'/'.implode('/', $seg));
}
return(substr($p, -2).'/'.$p);
}
static function write_file($filename, $data)
{
$fp = fopen($filename, "c+");
if(!$fp)
{
Log::text('<ERR>', 'file_put_contents_ex('.$filename.') could not write file');
return;
}
if (flock($fp, LOCK_EX))
{
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, $data);
}
else
{
Log::text('<ERR>', 'file_put_contents_ex('.$filename.') could not acquire lock');
}
fclose($fp);
}
static function delete_file($file_name)
{
unlink($file_name);
}
static function read_file($file_name)
{
$fsz = filesize($file_name);
if($fsz == 0)
return('');
$fp = fopen($file_name, "rb+");
if(!$fp)
return('');
if (flock($fp, LOCK_SH)) {
$content = fread($fp, $fsz);
flock($fp, LOCK_UN);
} else {
Log::text('<ERR>', 'read_file('.$file_name.') could not acquire lock');
}
fclose($fp);
return($content);
}
static function write_data($class, $bucket, $type, $data)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true);
$fn = $storage_path.'/'.$type.'.json';
self::write_file($fn, json_encode($data));
}
static function read_data($class, $bucket, $type)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
$fn = $storage_path.'/'.$type.'.json';
return(json_decode(self::read_file($fn), true));
}
static function delete_data($class, $bucket, $type)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
return(self::delete_file($storage_path.'/'.$type.'.json'));
}
static function get_data_filename($class, $bucket, $type)
{
return(first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket).'/'.$type.'.json');
}
static function list_bucket($class, $bucket)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
foreach(explode(chr(10), trim(shell_exec('ls -1 '.escapeshellarg($storage_path)))) as $name)
{
if(substr($name, 0, 1) != '_' && trim($name) != '')
$items[] = $name;
}
return($items);
}
static function search_bucket($class, $bucket, $q)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
foreach(explode(chr(10), trim(shell_exec('grep -irlF '.escapeshellarg($q).' '.escapeshellarg($storage_path)))) as $l)
{
$name = substr($l, strlen($storage_path)+1, -5);
if(substr($name, 0, 1) != '_' && trim($name) != '')
$items[] = $name;
}
return($items);
}
static function delete_bucket($class, $bucket)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
if(stristr($storage_path, '*') !== false) return;
if(stristr($storage_path, '?') !== false) return;
$result = trim(shell_exec('rm -r '.escapeshellarg($storage_path).' 2>&1'));
return($result);
}
static function write_log($class, $bucket, $type, $data)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true);
WriteToFile($storage_path.'/'.$type.'.log', json_encode($data).chr(10));
}
static function read_log($class, $bucket, $type, $line_count = 8, $offset = false)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
return(get_json_tail($storage_path.'/'.$type.'.log', $line_count, $offset));
}
static function line_count($class, $bucket, $type)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
return(intval(trim(shell_exec('wc -l '.escapeshellarg($storage_path.'/'.$type.'.log')))));
}
static function get_json_tail($from_file, $line_count = 8, $offset = false)
{
if($offset > 0)
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count)));
}
else
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file)));
}
return(json_lines($lines));
}
static function get_tail($from_file, $line_count = 8, $offset = false)
{
$line_count = intval($line_count);
if($offset > 0)
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count)));
}
else
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file)));
}
return(explode(chr(10), $lines));
}
static function get_all_lines($from_file)
{
return(explode(chr(10), trim(file_get_contents($from_file))));
}
static function truncate_log($from_file, $lines = 128)
{
$lines = intval($lines);
$tmp = tempnam(sys_get_temp_dir(), 'trunc_');
shell_exec('tail -n '.$lines.' '.escapeshellarg($from_file).' > '.escapeshellarg($tmp).' && cp '.escapeshellarg($tmp).' '.escapeshellarg($from_file));
@unlink($tmp);
return true;
}
static function json_lines($lines)
{
if($lines == '')
{
return(array());
}
else
{
$result = array();
foreach(explode(chr(10), $lines) as $line)
$result[] = json_decode($line, true);
return($result);
}
}
}

View File

@ -1,111 +0,0 @@
<?php
class HTTP
{
/**
* Make a POST request with JSON or form data
*/
public static function post($url, $data = [], $headers = [])
{
$ch = curl_init();
$default_headers = [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
'User-Agent: Web-App-Starter/1.0'
];
$headers = array_merge($default_headers, $headers);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => is_array($data) ? http_build_query($data) : $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => 'cURL Error: ' . $error, 'http_code' => 0];
}
$decoded = json_decode($response, true);
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'raw' => $response,
'data' => $decoded ?: $response,
'error' => $http_code >= 400 ? "HTTP Error $http_code" : null
];
}
/**
* Make a GET request
*/
public static function get($url, $params = [], $headers = [])
{
if (!empty($params)) {
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params);
}
$ch = curl_init();
$default_headers = [
'Accept: application/json',
'User-Agent: Web-App-Starter/1.0'
];
$headers = array_merge($default_headers, $headers);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => 'cURL Error: ' . $error, 'http_code' => 0];
}
$decoded = json_decode($response, true);
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'raw' => $response,
'data' => $decoded ?: $response,
'error' => $http_code >= 400 ? "HTTP Error $http_code" : null
];
}
/**
* Make a request with Bearer token authentication
*/
public static function get_with_token($url, $token, $params = [])
{
return self::get($url, $params, [
'Authorization: Bearer ' . $token
]);
}
}

View File

@ -1,41 +0,0 @@
<?php
class Log
{
static $app_name = 'webappstarter';
static function make($module, $text)
{
if(is_array($text))
$text = str_replace(array('"', '\\'), '', json_encode($text));
$seg = array();
$seg[] = first($_SESSION['username'] ?? false, 'anonymous');
$seg[] = first($_SERVER['REMOTE_ADDR'] ?? false, 'cli');
$seg[] = round(memory_get_peak_usage()/1024).'kB';
$seg[] = round(Profiler::get_time()).'ms';
$seg[] = $module;
$seg[] = $text;
return($seg);
}
static function audit($module, $text = '', $class = 'warning')
{
$seg = self::make($module, $text);
shell_exec('echo '.escapeshellarg(implode('|', $seg)).' | systemd-cat -t '.(self::$app_name).' -p '.escapeshellarg($class));
}
static function debug($module, $text)
{
write_to_file('log/debug.'.gmdate('Y-m').'.log',
implode(chr(9), self::make($module, $text)).chr(10));
}
static function text($module, $text)
{
write_to_file('log/log.'.gmdate('Y-m').'.log',
implode(chr(9), self::make($module, $text)).chr(10));
}
}

View File

@ -1,468 +0,0 @@
<?php
/**
* ODT Template Processing and PDF Generation Class
*
* This class provides functionality to process OpenDocument Text (ODT) templates
* with placeholder substitution and automatic PDF conversion using LibreOffice.
*
* REQUIREMENTS:
* - PHP with shell_exec() enabled
* - unzip command available in system PATH
* - LibreOffice (soffice) installed for PDF conversion
*
* BASIC USAGE:
*
* 1. Create an ODT template file with placeholders
* 2. Initialize the ODT class with template path
* 3. Prepare the template (extracts and loads content)
* 4. Replace placeholders with your data
* 5. Generate output ODT and PDF files
*
* EXAMPLE:
*
* $odt = new ODT('/path/to/template.odt');
*
* if ($odt->prepare()) {
* $data = [
* 'customer_name' => 'John Doe',
* 'invoice_date' => time(),
* 'total_amount' => 1250.50,
* 'items' => [
* ['name' => 'Product A', 'price' => 500.00, 'qty' => 1],
* ['name' => 'Product B', 'price' => 750.50, 'qty' => 1]
* ]
* ];
*
* $odt->replace_placeholders($data);
* $pdf_path = $odt->create_output('/path/to/output.odt');
*
* if ($pdf_path) {
* echo "PDF created: " . $pdf_path;
* } else {
* echo "Error: " . $odt->error_msg;
* }
* } else {
* echo "Error: " . $odt->error_msg;
* }
*
* PLACEHOLDER SYNTAX:
*
* In your ODT template, use LibreOffice placeholders (Insert > Field > Other > Variables > User Field)
* or direct placeholder markup:
*
* Simple placeholders:
* <text:placeholder>customer_name</text:placeholder>
*
* Formatted placeholders with JSON properties in description:
* <text:placeholder text:description='{"format":"date"}'>invoice_date</text:placeholder>
* <text:placeholder text:description='{"format":"currency","decimals":2}'>total_amount</text:placeholder>
*
* SUPPORTED FORMATS:
*
* - "date" - Formats timestamp as date (uses cfg('date_format') or custom template)
* - "time" - Formats timestamp as time (uses cfg('time_format') or custom template)
* - "datetime" - Formats timestamp as datetime (uses cfg('datetime_format') or custom template)
* - "currency" - Formats number as currency with symbol
* - "number" - Formats number with specified decimal places
* - "duration" - Converts seconds to "Xh Ymin" format
*
* FORMAT OPTIONS:
*
* - "template": Custom date/time format string (e.g., "Y-m-d H:i:s")
* - "decimals": Number of decimal places for currency/number formatting
* - "default": Default value if placeholder data is empty
* - "emptyifzero": Show empty string if numeric value is zero
* - "delsegmentifzero": Delete entire segment if numeric value is zero
* - "newline": Add newlines "before", "after", or "before,after" the value
*
* LISTS AND TABLES:
*
* For repeating table rows, use a special placeholder in a table row:
* <text:placeholder text:placeholder-type="table">items</text:placeholder>
*
* This will repeat the table row for each item in the 'items' array.
* Within the repeated row, access item properties with 'item.' prefix:
* <text:placeholder>item.name</text:placeholder>
* <text:placeholder>item.price</text:placeholder>
*
* The class automatically calculates sums for numeric fields:
* <text:placeholder>sums.items.price</text:placeholder>
*
* ERROR HANDLING:
*
* Always check the return values and $odt->error_msg for error details:
* - prepare() returns false on failure
* - create_output() returns false on failure, PDF path on success
* - Check $odt->error_msg for specific error messages
*
* PROPERTIES:
*
* - $template_name: Path to the ODT template file
* - $content_xml: Loaded and processed content.xml from ODT
* - $temp_dir: Temporary directory for ODT extraction
* - $error_msg: Last error message
* - $odt_filename: Path to generated ODT file
* - $pdf_filename: Path to generated PDF file
* - $debug_output: Debug output from shell commands
*
* NOTES:
*
* - Temporary files are automatically cleaned up in destructor
* - PDF conversion requires LibreOffice to be installed and accessible
* - The class assumes UTF-8 encoding for all text content
* - Newlines in data are converted to ODT line breaks
* - All string values are properly escaped for XML
*/
class ODT
{
public $template_name;
public $content_xml;
public $temp_dir;
public $error_msg;
public $odt_filename;
public $pdf_filename;
public $debug_output;
function __construct($template_name)
{
$this->template_name = $template_name;
if(!file_exists($this->template_name)) {
$this->error_msg = 'Template not found: '.$template_name;
}
$this->temp_dir = '/tmp/'.uniqid();
}
/**
* Check if all required command line tools are available and working
* @return array Array with 'status' (bool) and 'messages' (array of status messages)
*/
static function check_requirements()
{
$results = [
'status' => true,
'messages' => []
];
$unzip_check = shell_exec('which unzip 2>/dev/null');
if (empty(trim($unzip_check))) {
$results['status'] = false;
$results['messages'][] = 'ERROR: unzip command not found in PATH';
} else {
$results['messages'][] = 'OK: unzip found at ' . trim($unzip_check);
$unzip_version = shell_exec('unzip -v 2>&1 | head -1');
if ($unzip_version) {
$results['messages'][] = 'INFO: ' . trim($unzip_version);
}
}
$zip_check = shell_exec('which zip 2>/dev/null');
if (empty(trim($zip_check))) {
$results['status'] = false;
$results['messages'][] = 'ERROR: zip command not found in PATH';
} else {
$results['messages'][] = 'OK: zip found at ' . trim($zip_check);
}
$soffice_check = shell_exec('which soffice 2>/dev/null') || '';
if (empty(trim($soffice_check))) {
$results['status'] = false;
$results['messages'][] = 'ERROR: LibreOffice (soffice) not found in PATH';
} else {
$results['messages'][] = 'OK: LibreOffice found at ' . trim($soffice_check);
$soffice_version = shell_exec('soffice --version 2>&1');
if ($soffice_version) {
$results['messages'][] = 'INFO: ' . trim($soffice_version);
}
$headless_test = shell_exec('timeout 10 soffice --headless --help 2>&1') || '';
if (strpos($headless_test, 'headless') !== false || strpos($headless_test, 'convert-to') !== false) {
$results['messages'][] = 'OK: LibreOffice headless mode is working';
} else {
$results['status'] = false;
$results['messages'][] = 'ERROR: LibreOffice headless mode test failed';
$results['messages'][] = 'DEBUG: ' . trim($headless_test);
}
}
if (!is_dir('/tmp')) {
$results['status'] = false;
$results['messages'][] = 'ERROR: /tmp directory does not exist';
} elseif (!is_writable('/tmp')) {
$results['status'] = false;
$results['messages'][] = 'ERROR: /tmp directory is not writable';
} else {
$results['messages'][] = 'OK: /tmp directory exists and is writable';
}
$test_temp_dir = '/tmp/odt_test_' . uniqid();
if (!mkdir($test_temp_dir, 0777, true)) {
$results['status'] = false;
$results['messages'][] = 'ERROR: Cannot create temporary directories in /tmp';
} else {
$results['messages'][] = 'OK: Can create temporary directories';
rmdir($test_temp_dir);
}
return $results;
}
static function parse_attributes($tag) {
$attributes = [];
while($tag != '')
{
$a_name = nibble('="', $tag);
$a_value = nibble('"', $tag);
$attributes[trim($a_name)] = trim($a_value);
}
return $attributes;
}
function prepare()
{
// Create the temporary directory
if (!mkdir($this->temp_dir, 0777, true)) {
$this->error_msg = 'Failed to create temp directory';
return false;
}
// Unzip the template into the temp directory using shell_exec()
$command = 'unzip ' . escapeshellarg($this->template_name) . ' -d ' . escapeshellarg($this->temp_dir) . ' 2>&1';
$output = shell_exec($command);
if ($output === null) {
$this->error_msg = 'Failed to unzip the template';
return false;
}
// Load content.xml into memory
$content_file = $this->temp_dir . '/content.xml';
if (!file_exists($content_file)) {
$this->error_msg = 'content.xml not found';
return false;
}
$this->content_xml = file_get_contents($content_file);
if ($this->content_xml === false) {
$this->error_msg = 'Failed to read content.xml';
return false;
}
return true;
}
function odt_entities($raw)
{
return str_replace(chr(10), '<text:line-break/>', safe($raw, ENT_XML1 | ENT_QUOTES, 'UTF-8'));
}
function break_into_segments($s)
{
$segment_handlers = [
'<table:table-row ' => function(&$cake, &$seg) {
$rest_of_row = nibble('</table:table-row>', $cake);
# check whether this segment contains an items marker indicating we should
# iterate this segment over a list in the data
$items_marker = '<text:placeholder text:placeholder-type="table">';
if(strpos($rest_of_row, $items_marker) !== false)
{
$r0 = nibble($items_marker, $rest_of_row);
$items_param = trim(str_replace(['&lt;', '&gt;'], '',
nibble('</text:placeholder>', $rest_of_row)));
$seg[] = [
'type' => 'list',
'content' => '<table:table-row '.$r0.$rest_of_row.'</table:table-row>',
'fields' => $items_param];
}
else $seg[] = [
'type' => 'flat',
'content' => '<table:table-row '.$rest_of_row.'</table:table-row>'];
},
];
$seg = [];
while($s != '')
{
$seg_start_found = false;
$sc = nibble(array_keys($segment_handlers), $s, $seg_start_found);
$seg[] = ['type' => 'flat', 'content' => $sc];
if($seg_start_found !== false)
{
$segment_handlers[$seg_start_found]($s, $seg);
}
}
return($seg);
}
function run_segment($seg, $params)
{
$result = '';
$sc = $seg['content'];
while($sc != '')
{
$placeholder_found = false;
$result .= nibble('<text:placeholder', $sc, $placeholder_found);
if($placeholder_found)
{
$p_attributes = ODT::parse_attributes(nibble('>', $sc));
$p_prop = [];
if(isset($p_attributes['text:description']) && $p_attributes['text:description'])
{
if(substr($p_attributes['text:description'], 0, 1) == '{')
{
$decoded = json_decode(
str_replace('&quot;', '"', $p_attributes['text:description']), true);
if($decoded !== null) {
$p_prop = $decoded;
}
}
else
{
$p_prop['format'] = $p_attributes['text:description'];
}
}
$p_content = str_replace(array('&gt;', '&lt;'), '',
nibble('</text:placeholder>', $sc));
$placeholder_key = $p_content;
$value = first($params[$placeholder_key] ?? null, $p_prop['default'] ?? null, '');
switch($p_prop['format'] ?? '')
{
case('date'):
{
if($value == 0) $value = '-'; else
$value = date(first($p_prop['template'], cfg('date_format')), intval($value));
} break;
case('time'):
{
if($value == 0) $value = '-'; else
$value = date(first($p_prop['template'], cfg('time_format')), intval($value));
} break;
case('datetime'):
{
if($value == 0) $value = '-'; else
$value = date(first($p_prop['template'], cfg('datetime_format')), intval($value));
} break;
case('currency'):
{
$value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', '').' €';
} break;
case('number'):
{
$value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', '');
} break;
case('duration'):
{
$value = intval($value);
$h = floor($value/3600); $value -= $h*3600;
$m = floor($value/60); $value -= $m*60;
$s = $value;
$value = '';
if($h > 0) $value .= $h.'h ';
$value .= $m.'min';
} break;
}
if(($p_prop['emptyifzero'] ?? false) && floatval($value) == 0)
$value = '';
if(($p_prop['delsegmentifzero'] ?? false) && floatval($value) == 0)
return('');
if(!is_string($value))
$value = trim($value);
if(($p_prop['newline'] ?? false) && $value != '')
{
if(str_contains($p_prop['newline'], 'before'))
$value = chr(10).$value;
if(str_contains($p_prop['newline'], 'after'))
$value = $value.chr(10);
}
if(!is_array($value))
$result .= $this->odt_entities($value);
}
}
return $result;
}
function replace_placeholders(&$params)
{
$result = '';
foreach($this->break_into_segments($this->content_xml) as $seg)
{
if($seg['type'] == 'list')
{
if(isset($params[$seg['fields']]) && is_array($params[$seg['fields']])) {
foreach($params[$seg['fields']] as $item)
{
$np = $params;
foreach($item as $k => $v)
{
$np['item.'.$k] = $v;
$params['sums.'.$seg['fields'].'.'.$k] += floatval($v);
}
$result .= $this->run_segment($seg, $np);
}
}
}
else
{
$result .= $this->run_segment($seg, $params);
}
}
return $this->content_xml = $result;
}
function create_output($out_filename)
{
$this->odt_filename = $out_filename;
$content_file = $this->temp_dir . '/content.xml';
if (file_put_contents($content_file, $this->content_xml) === false) {
$this->error_msg = 'Failed to write content.xml';
return false; //
}
$this->debug_output = trim(
shell_exec('cd '.escapeshellarg($this->temp_dir).' ; zip -r ../tmp.odt * 2>&1'));
$zip_output = dirname($this->temp_dir).'/tmp.odt';
if(!file_exists($zip_output))
{
$this->error_msg = 'Failed to create tmp ODT file (shell "'.$this->debug_output.'")';
return false;
}
if(file_exists($out_filename)) {
unlink($out_filename);
}
shell_exec('mv '.escapeshellarg($zip_output).' '.escapeshellarg($out_filename));
if (!file_exists($out_filename)) {
$this->error_msg = 'Failed to create ODT file "'.$out_filename.'"';
return false; //
}
$this->pdf_filename = $pdf_output = pathinfo($out_filename, PATHINFO_DIRNAME) . '/' . pathinfo($out_filename, PATHINFO_FILENAME) . '.pdf';
$command = 'HOME=/tmp ; soffice --headless --convert-to pdf ' . escapeshellarg($out_filename) . ' --outdir ' . escapeshellarg(pathinfo($out_filename, PATHINFO_DIRNAME)) . ' 2>&1';
$this->debug_output = shell_exec($command);
if (!file_exists($pdf_output)) {
$this->error_msg = 'PDF conversion failed';
return false; //
}
return $pdf_output;
}
function __destruct()
{
$this->delete_directory($this->temp_dir);
}
function delete_directory($dir)
{
if (!is_dir($dir)) return;
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$path = "$dir/$item";
is_dir($path) ? $this->delete_directory($path) : unlink($path);
}
rmdir($dir);
}
}

View File

@ -1,42 +0,0 @@
<?php
class Profiler
{
static $log = array();
static $start = 0;
static $last = 0;
static $current = 0;
static $indent_str = '';
static $indent_level = 0;
# makes a commented profiler entry
static function log($text, $indent_delta = 0)
{
$thistime = microtime(true);
$absoluteMS = $thistime - self::$start;
self::$indent_level += $indent_delta;
if($indent_delta < 0) // if less than zero, update indent before logging
self::$indent_str = str_repeat(' ', self::$indent_level * 2);
if($text) self::$log[] =
number_format($absoluteMS*1000, 3).'ms | '.
number_format(1000*($thistime - self::$last), 3).'ms | '.
ceil(memory_get_usage()/1024).' kB | '.self::$indent_str.$text;
if($indent_delta > 0) // if greater than zero, update indent after logging
self::$indent_str = str_repeat(' ', self::$indent_level * 2);
self::$last = $thistime;
self::$current = $absoluteMS;
return($thistime);
}
static function get_time()
{
return(1000*(microtime(true) - self::$start));
}
static function start()
{
self::$start = self::$last = microtime(true);
}
}

View File

@ -1,32 +0,0 @@
<?php
class SVG
{
static function circle_segment($midx, $midy, $radius, $start_angle, $end_angle,
$stroke_color = 'rgba(120,120,120,1.0)', $stroke_width = 1, $fill_color = 'rgba(0,0,0,0)', $style = '')
{
$start_x = $midx + $radius * cos($start_angle);
$start_y = $midy + $radius * sin($start_angle);
$end_x = $midx + $radius * cos($end_angle);
$end_y = $midy + $radius * sin($end_angle);
while ($start_angle < 0) $start_angle += 2 * M_PI;
while ($end_angle < 0) $end_angle += 2 * M_PI;
while ($start_angle >= 2 * M_PI) $start_angle -= 2 * M_PI;
while ($end_angle >= 2 * M_PI) $end_angle -= 2 * M_PI;
$angle_diff = $end_angle - $start_angle;
if ($angle_diff < 0) $angle_diff += 2 * M_PI;
$large_arc_flag = ($angle_diff > M_PI) ? 1 : 0;
$sweep_flag = 1;
?>
<path d="M <?= $start_x ?> <?= $start_y ?> A <?= $radius ?> <?= $radius ?> 0 <?= $large_arc_flag ?> <?= $sweep_flag ?> <?= $end_x ?> <?= $end_y ?>"
style="<?= $style ?>" fill="<?= $fill_color ?>" stroke="<?= $stroke_color ?>" stroke-width="<?= $stroke_width ?>" />
<?php
}
}

View File

@ -1,157 +0,0 @@
<?php
function theme_option($key, $default = null)
{
$themes = cfg('theme/options');
$current_theme_key = (string)cfg('theme/key');
$current_theme = is_array($themes) && isset($themes[$current_theme_key]) ? $themes[$current_theme_key] : array();
return array_key_exists($key, $current_theme) ? $current_theme[$key] : $default;
}
function theme_html_class($extra_class = '')
{
$classes = array('no-js');
if(cfg('theme/mode') === 'dark')
$classes[] = 'dark-theme';
if($extra_class !== '')
$classes[] = $extra_class;
return trim(implode(' ', $classes));
}
function theme_render_head($overrides = array())
{
$theme_path = (string)cfg('theme/path');
$theme_path_common = 'themes/common/';
$description = first($overrides['description'] ?? false, theme_option('meta_description', ''), 'Web App Starter theme');
$theme_color = first($overrides['theme_color'] ?? false, theme_option('theme_color', ''), '#0f172a');
$apple_icon = url_root().ltrim($theme_path, '/').'icon.png';
?>
<meta charset="utf-8">
<title><?= first(URL::$route['page-title'] ?? false, cfg('site/default_page_title')).' | '.cfg('site/name') ?></title>
<meta name="description" content="<?= asafe((string)$description) ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="<?= asafe((string)$theme_color) ?>">
<link rel="apple-touch-icon" href="<?= $apple_icon ?>">
<link rel="icon" type="image/png" sizes="32x32" href="<?= $apple_icon ?>">
<?php
include_css($theme_path.'css/style.css');
include_css($theme_path_common.'fontawesome/css/all.min.css');
include_js('js/u-query.js');
include_js('js/morphdom.js');
include_js('js/site.js');
foreach((array)($overrides['extra_css'] ?? array()) as $extra_css)
include_css($extra_css);
foreach((array)($overrides['extra_js'] ?? array()) as $extra_js)
include_js($extra_js);
}
function theme_render_global_controls($embed_mode, $options = array())
{
if($embed_mode)
return;
$show_theme_switcher = array_key_exists('theme_switcher', $options) ? (bool)$options['theme_switcher'] : true;
$show_cookie_consent = array_key_exists('cookie_consent', $options) ? (bool)$options['cookie_consent'] : true;
if($show_theme_switcher)
echo component('components/example/theme-switcher');
if($show_cookie_consent)
echo component('components/basic/cookie-consent');
}
function theme_menu_href($menu_key, $menu_item)
{
if(!empty($menu_item['external']))
return '/'.ltrim((string)$menu_key, '/');
return URL::Link((string)$menu_key);
}
function theme_get_user_context()
{
require_once __DIR__.'/user.class.php';
$signed_in = User::IsSignedIn();
$profile = $signed_in ? (User::$current_profile ?? array()) : array();
return array(
'signed_in' => $signed_in,
'profile' => $profile,
'avatar' => first($profile['avatar_url'] ?? false, $profile['profile_image'] ?? false),
'display_name' => first($profile['username'] ?? false, $profile['email'] ?? false, 'Account'),
);
}
function theme_render_account_links($options = array())
{
$context = theme_get_user_context();
$wrapper_class = trim((string)first($options['wrapper_class'] ?? false, 'nav-account'));
$name_class = trim((string)first($options['name_class'] ?? false, 'account-name'));
$links_wrapper_class = trim((string)($options['links_wrapper_class'] ?? ''));
$show_avatar = !empty($options['show_avatar']);
$show_name = array_key_exists('show_name', $options) ? (bool)$options['show_name'] : true;
?><div class="<?= asafe($wrapper_class) ?>"><?php
if($context['signed_in'])
{
if($show_avatar && $context['avatar'])
{
?><img src="<?= safe($context['avatar']) ?>" alt="Profile" class="nav-avatar" onerror="this.style.display='none';"><?php
}
if($show_name)
{
?><span class="<?= asafe($name_class) ?>"><?= safe((string)$context['display_name']) ?></span><?php
}
if($links_wrapper_class !== '')
?><div class="<?= asafe($links_wrapper_class) ?>"><?php
?><a href="<?= URL::Link('account/profile') ?>">Profile</a>
<a href="<?= URL::Link('account/logout') ?>">Logout</a><?php
if($links_wrapper_class !== '')
?></div><?php
}
else
{
if($links_wrapper_class !== '')
?><div class="<?= asafe($links_wrapper_class) ?>"><?php
?><a href="<?= URL::Link('account/login') ?>">Login</a><?php
if(cfg('users/enable_signup'))
{
?><a href="<?= URL::Link('account/register') ?>">Register</a><?php
}
if($links_wrapper_class !== '')
?></div><?php
}
?></div><?php
}
function theme_render_standard_nav($options = array())
{
$nav_class = trim((string)($options['nav_class'] ?? ''));
$account_wrapper_class = trim((string)first($options['account_wrapper_class'] ?? false, 'nav-account'));
$show_avatar = array_key_exists('show_avatar', $options) ? (bool)$options['show_avatar'] : true;
?><nav<?= $nav_class !== '' ? ' class="'.asafe($nav_class).'"' : '' ?>>
<div class="nav-menu">
<a href="<?= URL::Link('') ?>"><?= cfg('site/name') ?></a>
<?php foreach((array)cfg('menu') as $menu_key => $menu_item) if(empty($menu_item['hidden'])) { ?>
<a href="<?= theme_menu_href($menu_key, $menu_item) ?>"><?= safe($menu_item['title']) ?></a>
<?php } ?>
</div>
<?php theme_render_account_links(array(
'wrapper_class' => $account_wrapper_class,
'name_class' => 'account-name',
'show_avatar' => $show_avatar,
)); ?>
</nav><?php
}
function theme_render_footer($text = null, $options = array())
{
if(!empty($options['embed_mode']))
return;
$footer_text = first($text, theme_option('footer_text', false), cfg('site/name'));
$inner_class = trim((string)($options['inner_class'] ?? ''));
?><footer><?php
if($inner_class !== '')
{
?><div class="<?= asafe($inner_class) ?>"><p><?= safe((string)$footer_text) ?></p></div><?php
}
else
{
?><div style="max-width: 1200px; margin: 0 auto; padding: 0 1rem;"><p><?= safe((string)$footer_text) ?></p></div><?php
}
?></footer><?php
}

View File

@ -1,113 +0,0 @@
# ulib.php
A collection of PHP utility functions for common web development tasks.
## Autoloading & Setup
The library automatically registers a class autoloader and starts the profiler:
```php
// Auto-loads classes from lib/{classname}.class.php
require_once 'lib/ulib.php';
```
## Asset Management
```php
include_js('js/script.js') // Outputs <script> tag with cache busting
include_css('css/style.css') // Outputs <link> tag with cache busting
get_file_location($file) // Find file in include paths
```
Assets are automatically versioned with `filemtime()` for cache busting.
## HTML/Security Functions
```php
safe($text) // HTML escape for content
asafe($text) // HTML escape for attributes (strips newlines)
jsafe($data) // JSON encode (alias for json_encode)
```
## Configuration
```php
cfg('database/host') // Get config value with slash notation
cfg('url/root') // Access nested config arrays
```
Reads from `$GLOBALS['config']` with support for nested keys using `/` separator.
## Utility Functions
```php
first($a, $b, $c) // Return first non-empty value
alnum($text, '_', true) // Keep only alphanumeric + spaces
write_to_file($file, $content) // Append content to file
```
### first() Examples
```php
$name = first($user_input, $default_name, 'Anonymous');
$config = first($_GET['theme'], $_COOKIE['theme'], 'default');
```
## String Processing
### String Parsing
```php
nibble(':', $path, $found) // Extract substring before delimiter
// Example: nibble(':', 'user:pass', $found) → 'user', $path becomes 'pass'
```
### Date/Time
```php
age_to_string($timestamp) // Human-friendly time differences
// Examples: "just now", "5 min ago", "2 h ago", "Mon 14:30"
```
### Hash Generation
```php
make_hash() // Generate random hash (10 chars)
make_hash($input, 20) // Hash specific input (20 chars)
```
### Base Conversion
```php
base_convert_any($num, $from, $to) // Convert between any number bases
// Example: base_convert_any('FF', '0123456789ABCDEF', '0123456789') → '255'
```
## Advanced Features
- **Include Path Resolution**: Searches multiple paths for files
- **Automatic Permissions**: Sets 0777 on written files
- **Cache Busting**: Automatic versioning for JS/CSS
- **Reference Parameters**: Functions like `nibble()` modify input variables
- **Flexible Base Conversion**: Support for custom character sets
## Common Usage Patterns
```php
// Configuration-driven asset loading
include_css(cfg('theme/css_file'));
// Safe template output
echo '<div title="' . asafe($user_input) . '">' . safe($content) . '</div>';
// Fallback values
$title = first($_POST['title'], $default_title, 'Untitled');
// URL parsing
$protocol = nibble('://', $url); // Extract 'http' from 'http://example.com'
// Time display
echo 'Posted ' . age_to_string($post_timestamp);
```
## Error Handling
Functions include built-in error handling:
- `get_file_location()` dies with error message if file not found
- `write_to_file()` uses error suppression for chmod
- Most functions return safe defaults for invalid input

View File

@ -1,254 +0,0 @@
<?php
spl_autoload_register(function ($class_name) {
$classFile = 'lib/'.strtolower($class_name).'.class.php';
if(file_exists($classFile))
{
include($classFile);
return;
}
});
Profiler::start();
function url_root()
{
$root = (string)cfg('url/root');
if($root !== '' && substr($root, -1) !== '/')
$root .= '/';
return $root;
}
function get_file_location($file, $error_if_not_found = true)
{
$file = ltrim((string)$file, '/');
if(str_contains($file, '..'))
{
if($error_if_not_found)
die('invalid file path: '.$file);
return false;
}
if(file_exists($file)) return $file;
foreach((array)$GLOBALS['config']['site']['include_paths'] as $path)
{
$path = rtrim((string)$path, '/').'/';
if(file_exists($path.$file)) return $path.$file;
}
if($error_if_not_found)
die('file not found: '.$file);
return false;
}
function asset_already_included($file_location)
{
static $included_assets = array();
if(isset($included_assets[$file_location]))
return true;
$included_assets[$file_location] = true;
return false;
}
function include_js($src_file)
{
if(!($file_location = get_file_location($src_file))) return;
if(asset_already_included($file_location)) return;
?><script src="<?= url_root().$file_location ?>?v=<?= filemtime($file_location) ?>"></script><?php
}
function include_css($src_file)
{
if(!($file_location = get_file_location($src_file))) return;
if(asset_already_included($file_location)) return;
?><link rel="stylesheet" href="<?= url_root().$file_location ?>?v=<?= filemtime($file_location) ?>" /><?php
}
# **************************** GENERAL UTILITY FUNCTIONS ******************************
// escapes a string for use in HTML attributes
function asafe($s)
{
if($s === null) return '';
return htmlspecialchars(str_replace(array("\r", "\n"), ' ', $s), ENT_QUOTES, 'UTF-8');
}
// escapes a string for use in HTML text
function safe($s)
{
if($s === null) return '';
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function jsafe($s)
{
return json_encode($s);
}
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(!is_array($array)) return [];
$result = [];
foreach($array as $pv)
if($value >= $pv['from'] && $value <= $pv['to']) $result = $pv;
return $result;
}
/**
* Can have any number of arguments. Returns the first of its arguments that is not false, empty string, or null.
*/
function first()
{
$args = func_get_args();
foreach($args as $v)
{
if(isset($v) && $v !== false && $v !== '' && $v !== null)
return($v);
}
return('');
}
function alnum($s, $replace_with = '_', $trim = true)
{
if($trim) $s = trim(strtolower($s));
return(preg_replace("/[^[:alnum:][:space:]]/u", $replace_with, $s));
}
/**
* Append a string to the given file.
*/
function write_to_file($filename, $content)
{
if (is_array($content)) $content = json_encode($content);
$open = fopen($filename, 'a+');
fwrite($open, $content);
fclose($open);
@chmod($filename, 0777);
}
# **************************** ARRAY FUNCTIONS ******************************
/**
* Returns a value from the $GLOBALS['config'] array identified by $key.
* Sub-array values can be addressed by using the '/' character as a separator.
*/
function cfg($key)
{
$config = $GLOBALS['config'];
$seg = explode('/', $key);
$lastSeg = array_pop($seg);
foreach($seg as $s)
{
if(is_array($config) && array_key_exists($s, $config) && is_array($config[$s]))
$config = $config[$s];
else
return null;
}
if(!is_array($config) || !array_key_exists($lastSeg, $config))
return null;
return($config[$lastSeg]);
}
# **************************** STRING/FORMATTING FUNCTIONS ******************************
/**
* Convert any base number into another number of another base system.
*/
function base_convert_any($numberInput, $fromBaseInput, $toBaseInput)
{
if ($fromBaseInput==$toBaseInput) return $numberInput;
$fromBase = str_split($fromBaseInput,1);
$toBase = str_split($toBaseInput,1);
$number = str_split($numberInput,1);
$fromLen=strlen($fromBaseInput);
$toLen=strlen($toBaseInput);
$numberLen=strlen($numberInput);
$retval='';
if ($toBaseInput == '0123456789')
{
$retval=0;
for ($i = 1;$i <= $numberLen; $i++)
$retval = bcadd($retval, bcmul(array_search($number[$i-1], $fromBase),bcpow($fromLen,$numberLen-$i)));
return $retval;
}
if ($fromBaseInput != '0123456789')
$base10=base_convert_any($numberInput, $fromBaseInput, '0123456789');
else
$base10 = $numberInput;
if ($base10<strlen($toBaseInput))
return $toBase[$base10];
while($base10 != '0')
{
$retval = $toBase[bcmod($base10,$toLen)].$retval;
$base10 = bcdiv($base10,$toLen,0);
}
return $retval;
}
/**
* Convert a Unix timestamp into a human-friendly short form.
*/
function age_to_string($unixDate, $new = 'just now', $ago = 'ago')
{
if($unixDate == 0) return('-');
$result = '';
$oneMinute = 60;
$oneHour = $oneMinute*60;
$oneDay = $oneHour*24;
$difference = time() - $unixDate;
if ($difference < $oneMinute)
$result = $new;
else if ($difference < $oneHour)
$result = round($difference/$oneMinute).' min '.$ago;
else if ($difference < $oneDay)
$result = floor($difference/$oneHour).' h '.$ago;
else if ($difference < $oneDay*5)
$result = gmdate('D H:i', $unixDate);
else if ($difference < $oneDay*365)
$result = gmdate('M dS H:i', $unixDate);
else
$result = date('d. M Y H:i', $unixDate);
return($result);
}
/**
* Given the separator string $segdiv, cut a piece of &$cake off that precedes $segdiv,
* and return that piece. If there are no instances of $segdiv in &$cake, nibble()
* returns the entirety of &$cake and sets &$cake to an empty string.
*/
function nibble($segdiv, &$cake, &$found = false)
{
$p = strpos($cake, $segdiv);
if ($p === false)
{
$result = $cake;
$cake = '';
$found = false;
}
else
{
$result = substr($cake, 0, $p);
$cake = substr($cake, $p + strlen($segdiv));
$found = true;
}
return $result;
}
function make_hash($s = false, $length = 10)
{
if($s === false) $s = time().random_int(0, 2147483647);
$s = strtolower(substr(trim($s), 0, 64));
return(substr(base_convert_ex(
sha1(sha1('qw0e983124o521öl34u9087'.$s)),
'0123456789abcdef',
'0123456789abcdefghijklmnopqrstuvwxyz'
), -$length));
}

View File

@ -1,126 +0,0 @@
<?php
class URL
{
static $locator = '';
static $route = array();
static $error = '';
static $title = 'TITLE';
static $page_type = 'html';
static $fragments = [];
# extracts the locator string from parameters or the URI
static function ParseRequestURI()
{
$uri = (string)first($_SERVER['REQUEST_URI']);
$parsed_uri = parse_url($uri);
$loc = (string)($parsed_uri['path'] ?? '');
$query_string = (string)($parsed_uri['query'] ?? '');
if($query_string !== '')
{
$route_found = false;
$query_parts = explode('&', $query_string);
$named_parts = array();
foreach($query_parts as $query_part)
{
if($query_part === '')
continue;
if(!$route_found && str_contains($query_part, '=') === false)
{
$loc = rawurldecode($query_part);
$route_found = true;
continue;
}
$named_parts[] = $query_part;
}
if(sizeof($named_parts) > 0)
{
$params = array();
parse_str(implode('&', $named_parts), $params);
$_REQUEST = array_merge($_REQUEST, $params);
}
}
self::$locator = $loc;
return($loc);
}
static function NotFound($message = 'resource not found')
{
header("HTTP/1.0 404 Not Found");
self::$error = $message;
}
static $tried = array();
# determines which view to show given a locator string
static function MakeRoute($lc = false)
{
$route = $GLOBALS['config']['url'];
if(!$lc)
$lc = URL::ParseRequestURI();
if(str_starts_with($lc, $GLOBALS['config']['url']['root']))
$lc = substr($lc, strlen($GLOBALS['config']['url']['root']));
$seg = array();
foreach(explode('/', $lc) as $s)
if(substr($s, 0, 1) != '.' && $s != '') # strip unnecessary prefixes
$seg[] = $s;
$route['l-path'] = implode('/', $seg);
$route['page'] = first($seg[0] ?? false, 'index');
if(isset($seg[0]) && $seg[0] !== '' && substr($seg[0], 0, 1) == ':')
{
$route['page'] = substr($route['l-path'], 1);
}
if(sizeof(self::$route) == 0) self::$route = $route;
return($route);
}
static function ResolveViewFile($base_dir = 'views')
{
$lpath = first(self::$route['l-path'], 'index');
$base_dir = rtrim($base_dir, '/');
$content_file = $base_dir.'/'.$lpath;
if(file_exists($content_file.'.php'))
return array('file' => $content_file.'.php');
$dir_index_file = $content_file.'/index.php';
if(file_exists($dir_index_file))
return array('file' => $dir_index_file);
$lpath_parts = array_values(array_filter(explode('/', $lpath), function($segment) {
return $segment !== '';
}));
if(sizeof($lpath_parts) > 1)
{
$last_seg = array_pop($lpath_parts);
$parent_file = $base_dir.'/'.implode('/', $lpath_parts).'/index.php';
if(file_exists($parent_file))
return array('file' => $parent_file, 'param' => $last_seg);
}
return false;
}
static function Link($path, $params = false)
{
$path = ltrim((string)$path, '?');
$query = $params !== false ? http_build_query($params) : '';
if(cfg('url/pretty'))
{
return($GLOBALS['config']['url']['root'].$path.($query !== '' ? '?'.$query : ''));
}
else
{
if($path === '')
return($GLOBALS['config']['url']['root'].($query !== '' ? '?'.$query : ''));
return($GLOBALS['config']['url']['root'].'?'.$path.($query !== '' ? '&'.$query : ''));
}
}
# redirect to URL and quit
static function Redirect($url = '', $params = array())
{
header('location: '.self::Link($url, $params));
die();
}
}

View File

@ -1,134 +0,0 @@
# url.class.php
A PHP URL routing and handling class for web applications with support for pretty URLs and request parsing.
## Static Properties
```php
URL::$locator // Current URL path
URL::$route // Parsed route information
URL::$error // Error message for 404s
URL::$title // Page title
URL::$page_type // Response type (default: 'html')
URL::$fragments // URL fragments array
URL::$tried // Attempted routes (debugging)
```
## Request Parsing
```php
URL::ParseRequestURI() // Extract locator from REQUEST_URI
```
Parses URLs and populates `$_REQUEST` with query parameters. Handles both pretty URLs and query string format.
## Route Generation
```php
URL::MakeRoute($locator) // Generate route from URL path
```
Creates route array with:
- `l-path` - Clean URL path segments
- `page` - Target page/view (defaults to 'index')
- URL config values
### Special Route Handling
- Paths starting with `:` use the entire path as page name
- Strips dots and empty segments for security
- Removes root path prefix
## URL Generation
```php
URL::Link($path, $params) // Generate application URL
```
Creates URLs based on `cfg('url/pretty')` setting:
- **Pretty URLs**: `/path?param=value`
- **Query URLs**: `/?path&param=value`
## Navigation
```php
URL::Redirect($url, $params) // Redirect and exit
URL::NotFound($message) // Send 404 response
```
## Examples
### Basic Routing
```php
// Parse current request
$locator = URL::ParseRequestURI(); // Returns 'users/profile'
$route = URL::MakeRoute(); // Creates route array
// Access route data
echo URL::$route['l-path']; // 'users/profile'
echo URL::$route['page']; // 'index' (default)
```
### URL Generation
```php
// Generate links
$userLink = URL::Link('users/123'); // '/users/123'
$searchLink = URL::Link('search', ['q' => 'term']); // '/search?q=term'
// Redirect examples
URL::Redirect('login'); // Redirect to login
URL::Redirect('dashboard', ['tab' => 'settings']); // With parameters
```
### Special Page Routes
```php
// URL: /special/admin/users
// If URL starts with ':special', entire path becomes page name
URL::MakeRoute(':special/admin/users');
// Results in: page = 'special/admin/users'
```
### Error Handling
```php
// Set 404 status
URL::NotFound('Page not found');
echo URL::$error; // 'Page not found'
// Check attempted routes
var_dump(URL::$tried); // Array of attempted route resolutions
```
## Configuration Integration
Uses configuration values:
- `cfg('url/root')` - Application root path
- `cfg('url/pretty')` - Enable/disable pretty URLs
- `$GLOBALS['config']['url']` - URL configuration array
## URL Parsing Logic
1. **Extract Path**: Removes query string from REQUEST_URI
2. **Parse Parameters**: Converts query string to $_REQUEST entries
3. **Clean Segments**: Removes dots, empty segments, security prefixes
4. **Route Resolution**: Determines target page and path
5. **Special Handling**: Processes colon-prefixed paths
## Security Features
- **Dot Prevention**: Strips segments starting with '.'
- **Path Sanitization**: Removes empty and problematic segments
- **Root Stripping**: Removes application root from paths
## Pretty URL Support
Supports both URL formats:
```php
// Pretty URLs (url/pretty = true)
/users/profile/edit
/search?q=term
// Query URLs (url/pretty = false)
/?users/profile/edit
/?search&q=term
```

View File

@ -1,111 +0,0 @@
<?php
class User
{
public static $session_key = 'user_id';
public static $current_profile = null;
static function LoadById($id)
{
if (empty($id)) return false;
$data = Filebase::read_data('users', Filebase::hash($id), 'account');
if (!$data) return false;
$data['id'] = $id;
self::$current_profile = $data;
return $data;
}
static function SaveById($id)
{
if (empty(self::$current_profile)) return false;
if (empty($id)) return false;
Filebase::write_data('users', Filebase::hash($id), 'account', self::$current_profile);
return true;
}
static function Create($basic_profile)
{
if (empty($basic_profile['email']) || empty($basic_profile['password'])) {
return ['result' => false, 'message' => 'email_and_password_required'];
}
$email = strtolower(trim($basic_profile['email']));
$existing = Filebase::read_data('users', Filebase::hash($email), 'account');
if ($existing) return ['result' => false, 'message' => 'user_exists'];
$now = time();
$stored = $basic_profile;
$stored['email'] = $email;
$stored['password_hash'] = password_hash($basic_profile['password'], PASSWORD_DEFAULT);
$stored['created'] = $now;
if (!isset($stored['roles'])) $stored['roles'] = ['user'];
Filebase::write_data('users', Filebase::hash($email), 'account', $stored);
return ['result' => true, 'id' => $email, 'profile' => $stored];
}
static function AuthWithPassword($password, $basic_profile = false)
{
if ($basic_profile === false) {
if (!isset($_POST['email'])) return ['result' => false, 'message' => 'email_missing'];
$email = strtolower(trim($_POST['email']));
$basic_profile = Filebase::read_data('users', Filebase::hash($email), 'account');
if ($basic_profile) $basic_profile['id'] = $email;
}
if (!$basic_profile) return ['result' => false, 'message' => 'no_such_user'];
if (!isset($basic_profile['password_hash'])) return ['result' => false, 'message' => 'no_password_set'];
if (password_verify($password, $basic_profile['password_hash'])) {
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
$_SESSION[self::$session_key] = $basic_profile['id'];
self::$current_profile = $basic_profile;
return ['result' => true, 'profile' => $basic_profile];
}
return ['result' => false, 'message' => 'invalid_password'];
}
static function Permission($thing_name)
{
if (!self::IsSignedIn()) return false;
$roles = self::$current_profile['roles'] ?? [];
if (in_array('admin', $roles)) return true;
$perm_map = [
'edit' => ['admin', 'editor'],
'view' => ['admin', 'editor', 'user'],
];
$allowed = $perm_map[$thing_name] ?? ['admin'];
return count(array_intersect($roles, $allowed)) > 0;
}
static function IsSignedIn()
{
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
if (isset($_SESSION[self::$session_key]) && $_SESSION[self::$session_key]) {
if (self::$current_profile) return true;
$id = $_SESSION[self::$session_key];
$profile = Filebase::read_data('users', Filebase::hash($id), 'account');
if ($profile) {
$profile['id'] = $id;
self::$current_profile = $profile;
return true;
}
unset($_SESSION[self::$session_key]);
}
return false;
}
static function Logout()
{
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
unset($_SESSION[self::$session_key]);
self::$current_profile = null;
return true;
}
}

View File

@ -1,18 +0,0 @@
<?php
$project_root = dirname(__DIR__);
chdir($project_root);
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/web-app-starter/?';
$_SERVER['PHP_SELF'] = '/web-app-starter/index.php';
$_SERVER['DOCUMENT_URI'] = '/web-app-starter/index.php';
$_GET = array();
$_POST = array();
$_REQUEST = array();
$_COOKIE = array();
require $project_root.'/config/settings.php';
require $project_root.'/lib/ulib.php';
require $project_root.'/lib/components.php';
require $project_root.'/lib/theme_helpers.php';

View File

@ -1,52 +0,0 @@
<?php
require __DIR__.'/bootstrap.php';
function test_fail($message)
{
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
function test_assert($condition, $message)
{
if(!$condition)
test_fail($message);
echo "ok - {$message}\n";
}
function reset_request($request_uri)
{
$_SERVER['REQUEST_URI'] = $request_uri;
$_GET = array();
$_POST = array();
$_REQUEST = array();
URL::$route = array();
URL::$locator = '';
URL::MakeRoute();
}
reset_request('/web-app-starter/?themes&theme=portal-dark');
$themes_route = URL::ResolveViewFile('views');
test_assert($themes_route !== false && $themes_route['file'] === 'views/themes.php', 'theme gallery route resolves to the expected view');
reset_request('/web-app-starter/?workspace/projects');
$workspace_route = URL::ResolveViewFile('views');
test_assert($workspace_route !== false && $workspace_route['file'] === 'views/workspace/index.php', 'nested workspace route resolves to parent index');
test_assert(($workspace_route['param'] ?? null) === 'projects', 'nested workspace route exposes the trailing path segment as param');
test_assert(URL::Link('', ['theme' => 'portal-dark']) === '/web-app-starter/?theme=portal-dark', 'root theme link omits the empty route segment');
test_assert(URL::Link('themes', ['theme' => 'portal-dark']) === '/web-app-starter/?themes&theme=portal-dark', 'named route links keep the route segment and query parameters');
test_assert(component_exists('components/example/theme-switcher'), 'component existence works for explicit component paths');
test_assert(component_exists('example/theme-switcher'), 'component existence works for shorthand component paths');
ob_start();
component('example/theme-switcher');
$theme_switcher_html = ob_get_clean();
test_assert(str_contains($theme_switcher_html, 'theme-switcher'), 'component rendering works through the shorthand component API');
$portal_dark = cfg('theme/options/portal-dark');
test_assert(!empty($portal_dark['description']) && !empty($portal_dark['footer_text']), 'theme metadata is centralized in config');
echo "All smoke tests passed.\n";

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