diff --git a/README.md b/README.md index d7c04ea..05ce666 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 41dfb26..56933fa 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -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 diff --git a/site/doc/areas/time.txt b/site/doc/areas/time.txt index b085287..1506cef 100644 --- a/site/doc/areas/time.txt +++ b/site/doc/areas/time.txt @@ -3,5 +3,6 @@ Time and Date Functions time_precise time time_format_local +time_format_relative time_format_utc time_parse diff --git a/site/doc/index.uce b/site/doc/index.uce index 0f38682..2072212 100644 --- a/site/doc/index.uce +++ b/site/doc/index.uce @@ -23,6 +23,8 @@ RENDER(Request& context) { String page = first(context.get["p"], "index"); + String page_title = page; + nibble(page_title, "_"); <> @@ -34,7 +36,7 @@ RENDER(Request& context) UCE Docs / / @@ -78,6 +80,14 @@ RENDER(Request& context)
directive
+
info
+ @@ -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"; ?>

Description

Related

:
') - { - render_see_section(s.substr(1)); - } - else - { - ?>
()
0) + if(equiv_lines.size() > 0 || see_lines.size() > 0) { ?> 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 diff --git a/site/doc/pages/0_context.txt b/site/doc/pages/0_Request.txt similarity index 100% rename from site/doc/pages/0_context.txt rename to site/doc/pages/0_Request.txt diff --git a/site/doc/pages/1_preprocessor.txt b/site/doc/pages/3_C++ Preprocessor.txt similarity index 100% rename from site/doc/pages/1_preprocessor.txt rename to site/doc/pages/3_C++ Preprocessor.txt diff --git a/site/doc/pages/DTree.txt b/site/doc/pages/DTree.txt deleted file mode 100644 index 6cdd37a..0000000 --- a/site/doc/pages/DTree.txt +++ /dev/null @@ -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 diff --git a/site/doc/pages/json_decode.txt b/site/doc/pages/json_decode.txt index 4535b41..b1edb77 100644 --- a/site/doc/pages/json_decode.txt +++ b/site/doc/pages/json_decode.txt @@ -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()` diff --git a/site/doc/pages/time_format_local.txt b/site/doc/pages/time_format_local.txt index 872d733..27ef3fe 100644 --- a/site/doc/pages/time_format_local.txt +++ b/site/doc/pages/time_format_local.txt @@ -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 diff --git a/site/doc/pages/time_format_relative.txt b/site/doc/pages/time_format_relative.txt new file mode 100644 index 0000000..0724d18 --- /dev/null +++ b/site/doc/pages/time_format_relative.txt @@ -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 \ No newline at end of file diff --git a/site/doc/pages/time_format_utc.txt b/site/doc/pages/time_format_utc.txt index 789b844..e215866 100644 --- a/site/doc/pages/time_format_utc.txt +++ b/site/doc/pages/time_format_utc.txt @@ -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 diff --git a/site/doc/style.css b/site/doc/style.css index 1f639ba..0be01cb 100644 --- a/site/doc/style.css +++ b/site/doc/style.css @@ -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; diff --git a/site/examples/uce-starter/config/settings.uce b/site/examples/uce-starter/config/settings.uce new file mode 100644 index 0000000..ce371f6 --- /dev/null +++ b/site/examples/uce-starter/config/settings.uce @@ -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); +} \ No newline at end of file diff --git a/site/examples/uce-starter/index.uce b/site/examples/uce-starter/index.uce index 43e15f0..6c5513f 100644 --- a/site/examples/uce-starter/index.uce +++ b/site/examples/uce-starter/index.uce @@ -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); <>

404 Not Found

-

+

} 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); } diff --git a/site/examples/uce-starter/lib/app.uce b/site/examples/uce-starter/lib/app.uce index 4f3c4bd..0a2d92f 100644 --- a/site/examples/uce-starter/lib/app.uce +++ b/site/examples/uce-starter/lib/app.uce @@ -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 != "") { - <> + <> } }); } -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 != "") { - <> + <> } }); } -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); } diff --git a/site/examples/uce-starter/lib/user.class.h b/site/examples/uce-starter/lib/user.class.h index 61ff4ab..6932641 100644 --- a/site/examples/uce-starter/lib/user.class.h +++ b/site/examples/uce-starter/lib/user.class.h @@ -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(); } }; diff --git a/site/examples/web-app-starter/.ai/instructions.md b/site/examples/web-app-starter/.ai/instructions.md deleted file mode 100755 index facb2e0..0000000 --- a/site/examples/web-app-starter/.ai/instructions.md +++ /dev/null @@ -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 - function($prop) { return '
...
'; }, - 'about' => 'Component description' -]; -``` - -**Usage:** -```php -component('path/to/component', ['prop' => 'value']) -component('path/to/component:method', $props) // Specific render method -component_declare('name', $definition) // Inline declaration -``` - -### Database -```php -DB::Query($sql, $params) // Execute any SQL query -DB::Get($sql, $params) // Get multiple rows as array -DB::GetRowWithQuery($sql, $params) // Get single row -DB::GetRow($table, $keyvalue) // Get row by primary key -DB::GetRow($table, $id, $keyname) // Get row by specific key -DB::GetRowsMatch($table, $criteria) // Get rows matching criteria -DB::Insert($table, $data) // Insert new row, returns ID -DB::Commit($table, $data) // Insert or update (REPLACE) -DB::Update($table, $where, $data) // Update existing rows -DB::RemoveRow($table, $keyvalue) // Delete row by key -``` - -### Logging -```php -Log::debug($module, $text) // log/debug.YYYY-mm.log -Log::text($module, $text) // log/log.YYYY-mm.log -Log::audit($module, $text) // System journal -``` - -### Profiler -```php -Profiler::log($text, $indent_level) // Runtime profiling -Profiler::$log // Access logged data -``` - -## Coding Conventions - -### File Structure -- Views: `views/{pagename}.php` -- Components: `components/{category}/{name}.php` -- Classes: `lib/{name}.class.php` -- Utilities: `lib/{name}.php` - -### JS and PHP Coding Style -- Use ` function($prop) { - return "
{$prop['content']}
"; - }, - 'render:variant' => function($prop) { /* alternative render */ }, - 'about' => 'Component description' -]; -``` - -### URL Handling -- Use `URL::Link()` for all internal links -- Views auto-map to URLs (`views/users.php` → `/users`) - -## Built-in Libraries -- **uquery.js** - jQuery-like DOM manipulation -- **morphdom.js** - DOM diffing -- **macrobars.js** - JavaScript templating - -## Common Patterns - -### New Page -1. Create `views/{pagename}.php` -2. Set URL::$page_type to 'json' or 'blank' if page is AJAX -3. Content (auto-wrapped by theme page variant controlled by URL::$page_type) - -### New Component -1. Create `components/{category}/{name}.php` -2. Return array with `render` function -3. Use `component('components/{category}/{name}', $props)` - -### Error Handling -- Logging: Use `Log::debug()` for development, `Log::audit()` for security -- Profiling: Use `Profiler::log()` for performance analysis - -## Development Notes -- Server-side rendering preferred over SPA approach -- Component system encourages reusability -- Single entry point simplifies routing/middleware diff --git a/site/examples/web-app-starter/.gitignore b/site/examples/web-app-starter/.gitignore deleted file mode 100755 index f499afe..0000000 --- a/site/examples/web-app-starter/.gitignore +++ /dev/null @@ -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 diff --git a/site/examples/web-app-starter/README.md b/site/examples/web-app-starter/README.md deleted file mode 100755 index 1eb29e7..0000000 --- a/site/examples/web-app-starter/README.md +++ /dev/null @@ -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¶m=value`. You can generate URLs with `URL::Link()` which will automatically use the appropriate format if you want to keep your options open (as to whether to use pretty URLs or not). - -## Routing - -`URL::MakeRoute()` creates `URL::$route` data. For example, a request to `http://example.com/some/url/path` -would look like this: - -``` -Array -( - [page] => index - [l-path] => some/url/path -) -``` - -The `page` key refers to the view should be rendered. By default, this is just `'index'`, leading to -`'views/index.php'` getting invoked. If the URL's path element starts with a colon, the entire path -is used as the `page` value. - -The `l-path` key refers to the actual URL path as given by the browser. For example, the `'views/index.php'` -view uses this to invoke the correct sub-view. - -The starter also supports two additional patterns backported from downstream apps: - -- directory index routing: `/workspace` can resolve to `views/workspace/index.php` -- parent-path fallback: `/workspace/activity` can resolve to `views/workspace/index.php` with `URL::$route['param'] === 'activity'` - -## Components - -Components are reusable PHP-backed view fragments. A component file returns an array with one or more render functions plus optional metadata. - -The default starter convention is to keep reusable components in `components/`. The loader supports explicit paths like `components/example/theme-switcher` and shorthand names like `example/theme-switcher`, which resolve under `components/`. - -```php - function($prop) { - return '
' . $prop['content'] . '
'; - }, - - 'about' => 'Description of what this component does', - -]; -``` - -### Using Components - -```php - -``` - -**Using a Specific Render Method Explicitly:** -```php - 'Status']) ?> -``` - -The older `component-name:method` shorthand is still supported for backwards compatibility, but `component_call()` or an explicit `render_call` prop is the preferred API because it is clearer and easier to grep. - -## Dashboard Primitives - -The starter now includes a small set of dashboard-focused building blocks backported from a more complex production frontend: - -- `js/u-format.js` for human-readable byte, count, and duration formatting plus unit-aware parsing -- `js/u-sortable-table.js` for lightweight sortable HTML tables with persisted sort state -- `js/u-timeseries-chart.js` for multi-series canvas charts without pulling in a charting framework -- `components/data/summary-metrics.php`, `components/data/sortable-table.php`, and `components/data/timeseries-chart.php` -- `views/dashboard.php` as a working end-to-end example - -## Workspace Primitives - -The starter also includes a second backport slice from the `uh-ai` portal app: - -- `components/workspace/*` for app shells, sidebars, panel headers, status pills, empty states, and compact utility buttons -- `themes/common/css/workspace.css` for the shared workspace shell styling -- `js/u-workspace-shell.js` for simple responsive sidebar toggling -- `views/workspace/index.php` as a nested-route demo for shell-style products - -## Theme Families - -The starter now includes named theme families derived from the `uh-ai` portal app and the `uh-llm2` admin shell: - -- `portal-light` for the B612-based corporate portal look -- `portal-dark` for the glassy dark portal shell and the current default starter theme -- `localfirst` for the cyan/orange llm2-style admin shell - -Theme choice is resolved server-side from `config/settings.php`, with a validated theme key stored in the `starter_theme` cookie. Theme metadata such as descriptions, footer text, and browser theme colors now live in the same config entry as the theme path, so the gallery, page shell, and docs all read from the same source of truth. - -To compare themes side by side, use `/?themes`. The gallery embeds a shared `/?theme-preview` route under every available theme so you can evaluate shell chrome, content density, and form styling with the same fixture. - -The page shell is now split between theme CSS/assets and shared helper functions in `lib/theme_helpers.php`, which keeps the four top-nav themes aligned while still allowing `localfirst` to provide its own admin-shell layout. - -The homepage demo form now includes proper `id` and `name` attributes so the starter does not emit the earlier label/field accessibility warnings on the landing page. - -## Gauge Primitives - -The gauges demo now contains two gauge families from `uh-llm2`: - -- `components/gauges/progressbar.php` and `components/gauges/needlegauge.php` for the original bar and needle gauges -- `components/gauges/arcgauge.php` for the llm2-style SVG KPI arc gauges with optional watermark tracking - -Abstract gauge classes live in `themes/common/css/gauges.css`, so the components stay theme-aware through CSS variables instead of shipping a competing visual system. - -## Testing - -The starter now includes a no-dependency smoke suite under `tests/`. - -Run it with: - -```bash -php tests/smoke.php -``` - -It covers route resolution, link generation, component lookup, shorthand component rendering, and centralized theme metadata. - -### Inline Components - -You can also declare components directly in code: - -```php -component_declare('my-button', [ - 'render' => function($prop) { - return ""; - } -]); -``` - -## Logging - -This package includes a very basic Log class. - -```php -Log::debug($module, $text) /* writes to log/debug.[Y]-[m].log */ -Log::text($module, $text) /* writes to log/log.[Y]-[m].log */ -Log::audit($module, $text) /* writes to system journal */ -``` - -`$module` should be a short string identifying the module or context of the log line. - -`$text` text to be logged. - -## Profiler - -A basic profiling fixture. - -```php -Profiler::log($text, $indent_level = 0) -``` - -`$text` name/text describing the checkpoint to be profiled. - -Profiler logs are not committed to disk. Log data for the current request is stored in -`Profiler::$log`. - -## Batteries included -``` -- lib/ulib.php - convenience functions -- lib/profiler.class.php - profiling -- lib/log.class.php - logging -- lib/url.class.php - URL routing -- lib/components.php - component rendering -- lib/odt.class.php - ODT document generator -- lib/db.class.php - database access - -- js/morphdom.js - DOM diffing -- js/uquery.js - DOM manipulation (my attempt at keeping the good parts of jQuery) -- js/site.js - site-wide JS, as a starting point -- js/macrobars.js - JS templating - -- js/ag-grid - ag-Grid -``` -## License (MIT Open Source) - -Copyright 2018-2025 udo@openfu.com - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/site/examples/web-app-starter/codesearch b/site/examples/web-app-starter/codesearch deleted file mode 100755 index 3d03d31..0000000 --- a/site/examples/web-app-starter/codesearch +++ /dev/null @@ -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] [...]" >&2 - exit 1 - ;; - -* ) - echo "Unknown option: $1" >&2 - echo "Usage: $0 [-c|--case-sensitive] [...]" >&2 - exit 1 - ;; - * ) - patterns+=("$1") - shift - ;; - esac -done - -# Ensure at least one pattern is provided -if (( ${#patterns[@]} == 0 )); then - echo "Usage: $0 [-c|--case-sensitive] [...]" >&2 - exit 1 -fi - -# Build common grep options -grep_opts=( -nR -H --color=tty ) -if ! $csensitive; then - grep_opts+=( -i ) -fi - -# Build -e arguments for each pattern -pattern_opts=() -for pat in "${patterns[@]}"; do - pattern_opts+=( -e "$pat" ) -done - -# Define search paths -declare -a php_sea=(".") -declare -a css_sea=(".") -declare -a js_sea=(".") - -# Search PHP files -for vs in "${php_sea[@]}"; do - grep "${grep_opts[@]}" --include="*.php" "${pattern_opts[@]}" "$vs" -done - -# Search CSS files (exclude fontawesome) -for vs in "${css_sea[@]}"; do - grep "${grep_opts[@]}" --include="*.css" "${pattern_opts[@]}" "$vs" \ - | grep -iv "fontawesome" -done - -# Search JS files -for vs in "${js_sea[@]}"; do - grep "${grep_opts[@]}" --include="*.js" "${pattern_opts[@]}" "$vs" -done diff --git a/site/examples/web-app-starter/components/auth/oauth-client.php b/site/examples/web-app-starter/components/auth/oauth-client.php deleted file mode 100644 index a9e086b..0000000 --- a/site/examples/web-app-starter/components/auth/oauth-client.php +++ /dev/null @@ -1,283 +0,0 @@ - function($prop) { - $services = $prop['services'] ?? [ - 'google' => [ - 'name' => 'Google', - 'color' => '#4285f4', - 'icon' => 'fab fa-google', - 'scope' => 'openid email profile', - 'auth_url' => 'https://accounts.google.com/oauth/authorize', - 'token_url' => 'https://oauth2.googleapis.com/token', - 'user_info_url' => 'https://www.googleapis.com/oauth2/v2/userinfo' - ], - 'github' => [ - 'name' => 'GitHub', - 'color' => '#333333', - 'icon' => 'fab fa-github', - 'scope' => 'user:email', - 'auth_url' => 'https://github.com/login/oauth/authorize', - 'token_url' => 'https://github.com/login/oauth/access_token', - 'user_info_url' => 'https://api.github.com/user' - ], - 'discord' => [ - 'name' => 'Discord', - 'color' => '#5865f2', - 'icon' => 'fab fa-discord', - 'scope' => 'identify email', - 'auth_url' => 'https://discord.com/api/oauth2/authorize', - 'token_url' => 'https://discord.com/api/oauth2/token', - 'user_info_url' => 'https://discord.com/api/users/@me' - ], - 'twitch' => [ - 'name' => 'Twitch', - 'color' => '#9146ff', - 'icon' => 'fab fa-twitch', - 'scope' => 'user:read:email', - 'auth_url' => 'https://id.twitch.tv/oauth2/authorize', - 'token_url' => 'https://id.twitch.tv/oauth2/token', - 'user_info_url' => 'https://api.twitch.tv/helix/users' - ] - ]; - $title = $prop['title'] ?? 'Sign in with OAuth'; - $subtitle = $prop['subtitle'] ?? 'Choose your preferred authentication method'; - $callback_url = $prop['callback_url'] ?? URL::Link('auth/callback'); - - ?> -
-
-

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

-

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

-

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

-

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

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

-

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

Why Choose Our Framework?

-

Discover the powerful features that make development a breeze

-
-
- $feature): ?> -
-
- -
-

-

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

-

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

Choose Your Plan

-

Start building great applications today with our flexible pricing options

-
-
- $plan): ?> -
- - - - -
-

-
- - - - -
-

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

Trusted by Developers Worldwide

-

Join thousands of developers who have chosen our framework

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

What Developers Say

-

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

-
-
- $testimonial): ?> -
-
-
"
-

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

-

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

-

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

-

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

-

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

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

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

404 Not Found

'; - echo '

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

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

U-Howler.js Demo

- -
-
-

Basic Controls

-
-
-
-
-
-
-
- - - - -
- -
- - - 440 Hz -
- -
- - - 1.0 -
-
- -
-
- - -
- -
-
- -
-

Global Controls

-
- - -
- -
- - - 1.0 -
-
-
- -
-

API Overview

- -
-

Howler (Global)

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

Howl Constructor

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

Core Methods

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

Examples

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

Audio Sprites

-
-

Audio Sprites Demo

-
-
-
-
-
- -
- - -
- -
- - - - - - - - -
- -
-
- -
-

Audio Sprites API

- -
-

Sprite Methods

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

Sprite Configuration

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

Audio Effects

-
-

Filters

-
- - - - - - -
-
- - -
-
- -
-
-

Rate Control

-
- - - 1.0x -
- -
- -
-

Fade Effects

-
- - -
-
-
-
- -
-

Effects API

- -
-

Effect Methods

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

Audio Effects

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

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

Frequency Analysis

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

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

3D Spatial Audio

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

Spatial API

- -
-

3D Spatial Audio

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

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

Environment Presets

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

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

Spatial Examples

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

Library Info

-
-
Loading library information...
- -

Supported Codecs:

-
-
-
- -
-

Events & Utilities

- -
-

Event Listeners

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

Utility Methods

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

Events

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

U-Macrobars.js Demo

- -
-
-

Basic Field Output

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

Number Formatting

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

Default Values & Safety

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

Field Output API

- -
-

Basic Syntax

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

Number Formatting

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

Examples

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

Control Structures

-
-

Conditionals

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

Loops & Iteration

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

Equality & Lookup

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

Control Flow API

- -
-

Conditionals

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

Loops

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

Comparison & Lookup

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

Examples

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

Advanced Features

-
-

Event Binding

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

Code Blocks

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

Components

-
{{#component userCard}}
-
-
- - -
-
-
- -
-

Advanced API

- -
-

Event Binding

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

Code Execution

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

Components & Compilation

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

Examples

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

Template Editor

-
- - - - - - - -
- - - - -
- -
Click "Render Template"
- -
-

Template Information

-
Ready.
-
- -
-
- -
-

Compilation & Debugging

- -
-

Compilation Options

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

Debugging Properties

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

Utility Functions

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

Example Templates

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

{{title}}

{{content}}

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

U-Query.js Demo

- -
-
-

Element Selection & DOM Manipulation

-
-

DOM Queries

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

DOM Manipulation

-
Original content
- -
- - - - -
- -
-
- -
-

CSS & Classes

-
CSS Target Element
- -
- - - - -
- -
-
-
- -
-

Selection API

- -
-

Core Selectors

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

DOM Manipulation

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

CSS & Classes

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

Examples

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

Event Handling & Visibility

-
-

Event System

- - -
- - - -
- -
-
- -
-

Show/Hide/Toggle

-
Toggle me!
- -
- - - - -
- -
-
- -
-

Method Chaining

-
-
Chain Target 1
-
Chain Target 2
-
Chain Target 3
-
- -
- - -
- -
-
-
- -
-

Events & Visibility API

- -
-

Event Methods

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

Visibility Control

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

Method Chaining

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

AJAX & Global Events

-
-

AJAX Utilities

-
-
- AJAX Request Status -
- -
- - - -
- -
-
- -
-

Global Event System

- - -
- - - -
- -
-
- -
-

Utility Functions

-
Utility Element 1
-
Utility Element 2
-
Utility Element 3
- -
- - - -
- -
-
-
- -
-

AJAX & Utilities API

- -
-

AJAX Methods

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

Global Event System

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

Utility Functions

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

Examples

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

Browser Info

-
-
Loading library information...
- -

This browser supports

-
- -
- - -
-
-
- -
-

Advanced Features

- -
-

Advanced Selectors

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

Data Attributes

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

Traversal Methods

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

DOM Diff Updates

-
- u-query can make use of morphdom.js for efficient DOM diff updates. -
-
$.options.alwaysDoDifferentialUpdate = true
-
- Global flag for morphdom. -
-
- -
-
-
- - - - - diff --git a/site/examples/web-app-starter/js/u-query.js b/site/examples/web-app-starter/js/u-query.js deleted file mode 100755 index 907e8ba..0000000 --- a/site/examples/web-app-starter/js/u-query.js +++ /dev/null @@ -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> */ - this._topics = new Map(); - } - - /** - * Subscribe to `topic`. If `slot_key` is provided (truthy), - * it dedupes by that key; otherwise a unique Symbol is used. - * Returns an unsubscribe fn. - */ - on(topic, handler, slot_key = null) { - let map = this._topics.get(topic); - if (!map) { - map = new Map(); - this._topics.set(topic, map); - } - // use the provided slot_key or a fresh Symbol() - const key = slot_key != null ? slot_key : Symbol(); - map.set(key, handler); - return () => this.off(topic, key); - } - - /** - * Unsubscribe by handler function or by slot_key. - */ - off(topic, handlerOrSlotKey) { - const map = this._topics.get(topic); - if (!map) return; - - // if it matches a slotKey directly, remove it - if (map.has(handlerOrSlotKey)) { - map.delete(handlerOrSlotKey); - } else { - // otherwise assume it's a function: remove all matching fns - for (const [key, fn] of map.entries()) { - if (fn === handlerOrSlotKey) { - map.delete(key); - } - } - } - - if (map.size === 0) { - this._topics.delete(topic); - } - } - - /** - * Emit to all handlers on `topic`. Handlers returning - * 'remove_handler' are auto-removed. - * Returns the number of handlers invoked. - */ - emit(topic, ...args) { - let count = 0; - const map = this._topics.get(topic); - if (!map) return count; - - for (const [key, fn] of Array.from(map.entries())) { - const res = fn(...args); - count++; - if (res === 'remove_handler') { - map.delete(key); - } - } - - if (map.size === 0) { - this._topics.delete(topic); - } - return count; - } -} - -class QueryWrapper extends Array { - constructor(elements) { - super(); - if (elements) { - this.push(...(Array.isArray(elements) ? elements : [elements])); - } - } -} - -$ = function(selector_or_element) { - let elements; - if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) { - elements = [selector_or_element]; - } else if (typeof selector_or_element === 'string') { - // Check if string looks like HTML (starts with < and ends with >) - if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') { - // Create element from HTML string - let temp = document.createElement('div'); - temp.innerHTML = selector_or_element.trim(); - elements = Array.from(temp.children); - } else { - // Treat as CSS selector - elements = Array.from(document.querySelectorAll(selector_or_element)); - } - } else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) { - elements = Array.from(selector_or_element); - } else { - elements = [selector_or_element]; - } - return new QueryWrapper(elements); -}; - -$.options = { - alwaysDoDifferentialUpdate: true, -}; - -$.events = new EventEmitter(); -$.on = $.events.on.bind($.events); -$.off = $.events.off.bind($.events); -$.emit = $.events.emit.bind($.events); - -$.each = function(selector, callback) { - let elements; - if (typeof selector === 'string') { - elements = document.querySelectorAll(selector); - } else if (selector instanceof QueryWrapper) { - elements = selector; - } else { - elements = selector; - } - for (let i = 0; i < elements.length; i++) { - callback(elements[i], i); - } -} - -$.post = function(url, data, callback) { - return $.ajax({ - method: 'POST', - url: url, - data: data, - success: callback, - error: function(xhr) { - console.error('POST request failed:', xhr); - } - }); -} - -$.get = function(url, callback) { - return $.ajax({ - method: 'GET', - url: url, - success: callback, - error: function(xhr) { - console.error('GET request failed:', xhr); - } - }); -} - -$.ajax = function(options) { - return new Promise((resolve, reject) => { - var xhr = new XMLHttpRequest(); - xhr.open(options.method || 'GET', options.url, true); - - // Set default headers - if (options.method === 'POST' || options.data) { - xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); - } - - // Set custom headers - if (options.headers) { - for (var key in options.headers) { - xhr.setRequestHeader(key, options.headers[key]); - } - } - - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - var response; - try { - response = JSON.parse(xhr.responseText); - } catch (e) { - // If JSON parsing fails, use raw response - response = xhr.responseText; - } - - if (xhr.status >= 200 && xhr.status < 300) { - if (options.success) options.success(response); - resolve(response); - } else { - if (options.error) options.error(xhr); - reject(xhr); - } - } - }; - - var data = null; - if (options.data) { - data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data); - } - - xhr.send(data); - }); -} - -$.ready = function(callback) { - if (document.readyState !== 'loading') { - callback(); - } else { - document.addEventListener('DOMContentLoaded', callback); - } -} - -// Helper function to execute scripts in HTML content -function executeScripts(htmlContent) { - // Create a temporary container to parse the HTML - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = htmlContent; - - // Find all script tags and disable them temporarily - const scripts = tempDiv.querySelectorAll('script'); - const scriptData = []; - - scripts.forEach((script) => { - // Store script data for later execution - scriptData.push({ - content: script.textContent || script.innerText || '', - src: script.src, - type: script.type, - nonce: script.nonce, - async: script.async, - defer: script.defer - }); - - // Disable the script by changing its type - script.type = 'text/disabled-script'; - }); - - // Return the modified HTML and script data - return { - html: tempDiv.innerHTML, - scripts: scriptData - }; -} - -// Helper function to execute a single script -function executeScript(scriptInfo) { - if (scriptInfo.src) { - // External script - const newScript = document.createElement('script'); - if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { - newScript.type = scriptInfo.type; - } - if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; - if (scriptInfo.async) newScript.async = scriptInfo.async; - if (scriptInfo.defer) newScript.defer = scriptInfo.defer; - newScript.src = scriptInfo.src; - - document.head.appendChild(newScript); - } else if (scriptInfo.content.trim()) { - // Inline script - const newScript = document.createElement('script'); - if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { - newScript.type = scriptInfo.type; - } - if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; - newScript.text = scriptInfo.content; - - // Execute by inserting and immediately removing - document.head.appendChild(newScript); - document.head.removeChild(newScript); - } -} - -// Add all the jQuery-like methods to QueryWrapper -QueryWrapper.prototype.parent = function() { - const parents = Array.from(this).map(el => el.parentNode).filter(el => el); - return new QueryWrapper(parents); -} - -QueryWrapper.prototype.load = function(url, opt = {}) { - return new Promise((resolve, reject) => { - const ajaxOptions = { - method: opt.method || (opt.postData ? 'POST' : 'GET'), - url: url, - success: (response) => { - if(opt.replace) - this.replaceWith(response, opt); - else - this.html(response, opt); - if (opt.onLoad) opt.onLoad(response, null); - resolve(this); - }, - error: (xhr) => { - if (opt.onError) opt.onError(xhr); - reject(new Error('Failed to load: ' + url)); - } - }; - - if (opt.postData) { - ajaxOptions.data = opt.postData; - } - - $.ajax(ajaxOptions); - }); -} - -QueryWrapper.prototype.html = function(opt_content = false, prop = {}) { - if (opt_content === false) { - return Array.from(this).map(el => el.innerHTML).join(''); - } - if(typeof prop.diff == 'undefined') - prop.diff = $.options.alwaysDoDifferentialUpdate; - if(prop.diff === true) { - const temp = document.createElement('div'); - temp.innerHTML = opt_content; - this.forEach(el => { - morphdom(el, temp, { childrenOnly: true }); - }); - } else { - this.forEach(el => { - const result = executeScripts(opt_content); - el.innerHTML = result.html; - result.scripts.forEach(executeScript); - }); - } - return this; -} - -QueryWrapper.prototype.text = function(opt_content = false) { - if (opt_content === false) { - return Array.from(this).map(el => el.textContent).join(''); - } - this.forEach(el => { - el.textContent = opt_content; - }); - return this; -} - -QueryWrapper.prototype.replaceWith = function(content, prop = {}) { - if(typeof prop.diff == 'undefined') - prop.diff = $.options.alwaysDoDifferentialUpdate; - if(prop.diff === true) { - this.forEach(el => { - morphdom(el, content); - }); - } else { - if(typeof content === 'string') { - this.forEach(el => { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = content; - el.parentNode.replaceChild(tempDiv.firstChild, el); - }); - } else if(content instanceof Element) { - this.forEach(el => { - el.parentNode.replaceChild(content.cloneNode(true), el); - }); - } - } - return this; -} - -QueryWrapper.prototype.query = function(selector) { - const found = Array.from(this).reduce((acc, el) => { - const results = el.querySelectorAll(selector); - return acc.concat(Array.from(results)); - }, []); - return new QueryWrapper(found); -} - -QueryWrapper.prototype.find = function(selector) { - const found = Array.from(this).reduce((acc, el) => { - const results = el.querySelectorAll(selector); - return acc.concat(Array.from(results)); - }, []); - return new QueryWrapper(found); -} - -QueryWrapper.prototype.on = function(event, handler) { - this.forEach(function(el) { - el.addEventListener(event, handler); - }); - return this; -} - -QueryWrapper.prototype.off = function(event, handler) { - this.forEach(function(el) { - el.removeEventListener(event, handler); - }); - return this; -} - -QueryWrapper.prototype.hide = function() { - return this.css({ display: 'none' }); -} - -QueryWrapper.prototype.show = function() { - return this.css({ display: '' }); -} - -QueryWrapper.prototype.toggle = function() { - this.forEach(function(el) { - el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none'; - }); - return this; -} - -QueryWrapper.prototype.attr = function(name, value) { - if (value === undefined) { - return this.length > 0 ? this[0].getAttribute(name) : null; - } - this.forEach(function(el) { - el.setAttribute(name, value); - }); - return this; -} - -QueryWrapper.prototype.addClass = function(classNameOrList) { - var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; - this.forEach(function(el) { - classList.forEach(function(classNames) { - // Split space-separated class names - var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); - classes.forEach(function(className) { - el.classList.add(className); - }); - }); - }); - return this; -} - -QueryWrapper.prototype.removeClass = function(classNameOrList) { - var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; - this.forEach(function(el) { - classList.forEach(function(classNames) { - // Split space-separated class names - var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); - classes.forEach(function(className) { - el.classList.remove(className); - }); - }); - }); - return this; -} - -QueryWrapper.prototype.toggleClass = function(classNameOrList, force) { - var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; - this.forEach(function(el) { - classList.forEach(function(classNames) { - // Split space-separated class names - var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); - classes.forEach(function(className) { - if (typeof force !== 'undefined') { - el.classList.toggle(className, force); - } else { - el.classList.toggle(className); - } - }); - }); - }); - return this; -} - -QueryWrapper.prototype.empty = function() { - this.forEach(function(el) { - el.innerHTML = ''; - }); - return this; -} - -QueryWrapper.prototype.css = function(styles, optOrValue) { - // If no arguments or first argument is a string (getter mode) - if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) { - if (this.length === 0) return undefined; - var el = this[0]; - if (typeof styles === 'string') { - // Get computed style for a specific property - return window.getComputedStyle(el)[styles]; - } else { - // Return computed styles object (not commonly used) - return window.getComputedStyle(el); - } - } - - // Setter mode - this.forEach(function(el) { - if (typeof styles === 'string' && arguments.length >= 2) { - // Single property setter: .css('prop', 'value') - el.style[styles] = optOrValue; - } else if (typeof styles === 'object') { - // Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'}) - for (var key in styles) { - el.style[key] = styles[key]; - } - } - }); - return this; -} - -QueryWrapper.prototype.each = function(callback) { - this.forEach(function(el, index) { - callback(el, index); - }); - return this; -} - -QueryWrapper.prototype.append = function(child_or_html) { - this.forEach(function(el) { - if (typeof child_or_html === 'string' && el.insertAdjacentHTML) { - if (child_or_html.includes(' max) return max; - return v; -} - -function pick_entry_from_range(array, value) { - if (!array) return {}; - let result = {}; - for (const [pv] of Object.entries(array)) { - if (value >= pv.from && value <= pv.to) result = pv; - } - return result; -} - -return { - $: $, - EventEmitter: EventEmitter, - QueryWrapper: QueryWrapper, - first: first, - clamp: clamp, - pick_entry_from_range: pick_entry_from_range -}; - -})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-sortable-table.js b/site/examples/web-app-starter/js/u-sortable-table.js deleted file mode 100644 index e9fee4f..0000000 --- a/site/examples/web-app-starter/js/u-sortable-table.js +++ /dev/null @@ -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, - }; -})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-tiling-viewport.css b/site/examples/web-app-starter/js/u-tiling-viewport.css deleted file mode 100644 index 63976c7..0000000 --- a/site/examples/web-app-starter/js/u-tiling-viewport.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-tiling-viewport.js b/site/examples/web-app-starter/js/u-tiling-viewport.js deleted file mode 100644 index 3762033..0000000 --- a/site/examples/web-app-starter/js/u-tiling-viewport.js +++ /dev/null @@ -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 = $('
').attr('data-id', id); - if(props.title) $el.attr('data-title', props.title); - $el.html(content || `
Use Alt+<H V O X ↑ ← → ↓>
`) - .on('mousedown', () => focusPaneByEl($el[0])); - attachPaneToolbar($el[0]); - paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props}); - if(props.view?.screen) pendingViewLoads.push({id, view: props.view}); - return {type: 'pane', id, el: $el[0]}; - }; - - let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); }; - let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); }; - let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id'))); - let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r); - let getNodeById = id => findNodeById(rootNode, id) || null; - let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r); - let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc); - let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); }; - let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); }; - - let split = (direction, newPaneProps = {}) => { - if(!focusedPane) return; - let meta = paneMeta.get(focusedPane.id); - if(meta?.canSplit === false) return; - - let paneRect = focusedPane.el.getBoundingClientRect(); - let availableSpace = direction === 'row' ? paneRect.width : paneRect.height; - let requiredSpace = 2 * options.minPaneSize; - if(availableSpace < requiredSpace) { - //console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`); - return; - } - - let parentPath = findParentPath(rootNode, focusedPane.id); - let newPane = createPane(undefined, newPaneProps); - let animatedSplit = null, newIndex = -1; - if(!parentPath?.length) { - rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; - animatedSplit = rootNode; newIndex = 1; - } else { - let parent = parentPath.at(-1); - if(parent.type === 'split' && parent.direction === direction) { - let idx = parent.children.indexOf(focusedPane); - parent.children.splice(idx+1, 0, newPane); - parent.sizes = parent.sizes || parent.children.map(()=>1); - parent.sizes.splice(idx+1, 0, 0); - renormalizeSizes(parent); - animatedSplit = parent; newIndex = idx+1; - } else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) { - parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0]; - animatedSplit = parent; newIndex = parent.children.length-1; - } else { - let idx = parent.children.indexOf(focusedPane); - let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; - parent.children.splice(idx, 1, created); - animatedSplit = created; newIndex = 1; - } - } - render(); - if(animatedSplit && newIndex >= 0) { - requestAnimationFrame(() => requestAnimationFrame(() => { - animatedSplit.sizes = animatedSplit.children.map(()=>1); - renormalizeSizes(animatedSplit); - animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`)); - })); - } - focusPane(newPane); return newPane; - }; - - let splitVertical = () => split('row'); - let splitHorizontal = () => split('col'); - - let closeFocused = () => { - if(!focusedPane) return; - let meta = paneMeta.get(focusedPane.id); - if(meta?.canClose === false) return; - if(rootNode === focusedPane) { buildInitial(); return; } - if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id); - if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id); - let parentPath = findParentPath(rootNode, focusedPane.id); - if(!parentPath?.length) return; - let parent = parentPath.at(-1); - if(parent.type !== 'split') return; - let idx = parent.children.indexOf(focusedPane); - - let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice(); - let oldSize = oldSizes[idx] || 0; - let remaining = 1 - oldSize; - let newSizes = oldSizes.slice(); - if(remaining > 0) { - for(let i=0;i ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`)); - if(focusedPane.el) { - focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease'; - focusedPane.el.style.opacity = '0'; - } - let done = false; - let cleanup = () => { - if(done) return; done = true; - parent.children.splice(idx,1); - parent.sizes?.splice(idx,1); - paneMeta.delete(focusedPane.id); - if(parent.children.length === 1) { - let only = parent.children[0]; - parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only); - } else if(parent.sizes) renormalizeSizes(parent); - render(); focusPane(paneList(rootNode)[0]); - }; - let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } }; - if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd); - setTimeout(() => cleanup(), 350); - }; - - let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; }; - let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); }; - let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); }; - - let serialize = (node=rootNode) => { - let ser = n => !n ? null : n.type==='pane' ? - {type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} : - {type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)}; - return {focus: focusedPane?.id, layout: ser(node)}; - }; - - let restore = data => { - if(!data) return; - let wrapper = data.layout ? data : {layout:data}; - paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0; - let idNums = []; - let build = l => l && l.type==='pane' ? - (() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view}); - if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); } - let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); } - return pane; })() : - l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null; - rootNode = build(wrapper.layout) || createPane(); - if(idNums.length) idCounter = Math.max(...idNums) + 1; - render(); - pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0; - focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]); - }; - - let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); }; - - let renderNode = node => { - if(node.type==='pane') return node.el; - node.el = node.el || $('
')[0]; - $(node.el).empty().addClass(`vp-split ${node.direction}`); - if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); } - node.children.forEach((ch,i) => { - let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl); - if(i < node.children.length-1) { let div = $('
').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); } - }); - return node.el; - }; - - let setupDivider = (div, splitNode, leftIdx) => { - $(div).on('mousedown', e => { - e.preventDefault(); $(div).addClass('dragging'); - $(containerEl).addClass('vp-no-transition'); - let isRow = splitNode.direction === 'row'; - let startPos = isRow ? e.clientX : e.clientY; - let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect()); - let prop = isRow ? 'width' : 'height'; - let startPixels = childRects.map(r => r[prop]); - let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]]; - let totalPixels = startPixels.reduce((a,b)=>a+b,0); - let onMove = ev => { - let curPos = isRow ? ev.clientX : ev.clientY; - let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos]; - if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize]; - if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize]; - startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx; - splitNode.sizes = startPixels.map(p => p / totalPixels); - splitNode.children.forEach((ch,i)=> { - if(ch.el) { - let flexValue = `${splitNode.sizes[i]} 1 0`; - $(ch.el).css('flex', flexValue); - } - }); - }; - let onUp = () => { - $(div).removeClass('dragging'); - $(containerEl).removeClass('vp-no-transition'); - $(document).off('mousemove', onMove).off('mouseup', onUp); - }; - $(document).on('mousemove', onMove).on('mouseup', onUp); - }); - $(div).on('dblclick', () => { - $(containerEl).addClass('vp-no-transition'); - let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len); - splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`)); - setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0); - }); - }; - - let attachPaneToolbar = el => { - let bar = $('
').addClass('vp-pane-toolbar').html(` - - - - `)[0]; - el.appendChild(bar); - $(bar).on('mousedown', ev => ev.stopPropagation()); - $(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el); - let button = ev.target.closest('button[data-act]'); - let act = button ? button.getAttribute('data-act') : null; - if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused(); - else if(act==='views') { - // compute button position - let rect = button.getBoundingClientRect(); - let items = options.standardViewList.slice(); - showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => { - // support string or object {screen} - let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); - loadView(el.dataset.id, view); - }, container: document.body, emptyText: 'No views available' }); - } - }); - refreshToolbarState(el.dataset.id); - }; - - let refreshToolbarState = paneId => { - let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; - if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return; - let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false]; - ['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } }); - let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); } - }; - - let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props); - if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; } - if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload; - paneMeta.set(paneId, meta); refreshToolbarState(paneId); }; - let getPaneProps = paneId => paneMeta.get(paneId); - - let focusDirection = (dx, dy) => { - if(!focusedPane) return; - let panes = Array.from(containerEl.querySelectorAll('.viewport-pane')); - let cur = focusedPane.el.getBoundingClientRect(); - let candidates = panes.filter(p => p !== focusedPane.el).map(p => { - let r = p.getBoundingClientRect(); - if(dx) { - if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null; - let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top)); - let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right; - if(primaryDist < -1) return null; - return {el:p, score: primaryDist*1000 + (cur.height - overlap)}; - } - if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null; - let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left)); - let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom; - if(primaryDist < -1) return null; - return {el:p, score: primaryDist*1000 + (cur.width - overlap)}; - }).filter(Boolean); - if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el); - let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2]; - let fallback = panes.filter(p => p !== focusedPane.el).map(p => { - let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2]; - let [vx, vy] = [px-cx, py-cy]; - if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null; - return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))}; - }).filter(Boolean); - if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el); - }; - - let injectHelpOverlay = () => { - if(!options.showHelp) return; - if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``})); - }; - - let getPaneEl = id => findNodeById(rootNode, id)?.el || null; - - let openViewsPopupForPane = (paneId) => { - let node = findNodeById(rootNode, paneId || focusedPane?.id); - if(!node || !node.el) return; - let rect = node.el.getBoundingClientRect(); - let x = Math.max(8, rect.right - 80); - let y = rect.top + 24; - let items = options.standardViewList.slice(); - showPopupMenu(x, y, items, { onSelect: it => { - let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); - loadView(node.id, view); - }, container: document.body, emptyText: 'No views available' }); - }; - - let loadView = (paneId, view) => { - let node = findNodeById(rootNode, paneId); - if(!node || !node.el) return; // nothing to load into - let meta = paneMeta.get(paneId); - try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); } - if(meta) meta.view = view; - node.el.innerHTML = `
Loading ${view.screen||'...'}...
`; - $(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})}); - }; - - let handleKeys = e => { - if(!e.altKey || !options.keyboard) return; - let actions = { - 'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(), - 'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(), - 'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(), - 'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(), - 'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext() - }; - if(actions[e.code] || actions[e.key]) { - (actions[e.code] || actions[e.key])(); - e.preventDefault(); - } - }; - - containerEl = typeof container === 'string' ? document.querySelector(container) : container; - if(!containerEl) throw new Error('TilingViewportManager: container not found'); - if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k])); - buildInitial(); - if(options.keyboard) document.addEventListener('keydown', handleKeys); - - this.splitVertical = splitVertical; - this.splitHorizontal = splitHorizontal; - this.closeFocused = closeFocused; - this.focusNext = focusNext; - this.focusPrev = focusPrev; - this.serialize = serialize; - this.restore = restore; - this.setPaneProps = setPaneProps; - this.getPaneProps = getPaneProps; - this.options = options; - this.loadView = loadView; - this.getPaneEl = getPaneEl; - this.getNodeById = getNodeById; - - Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) }); - Object.defineProperty(this, 'focused', { get: () => focusedPane?.id }); - }; - - return TilingViewportManager; -})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-timeseries-chart.js b/site/examples/web-app-starter/js/u-timeseries-chart.js deleted file mode 100644 index b19f3eb..0000000 --- a/site/examples/web-app-starter/js/u-timeseries-chart.js +++ /dev/null @@ -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; -})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-workspace-shell.js b/site/examples/web-app-starter/js/u-workspace-shell.js deleted file mode 100644 index 90fbd31..0000000 --- a/site/examples/web-app-starter/js/u-workspace-shell.js +++ /dev/null @@ -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, - }; -})); \ No newline at end of file diff --git a/site/examples/web-app-starter/js/u-wsconnection.js b/site/examples/web-app-starter/js/u-wsconnection.js deleted file mode 100755 index 9c3b40f..0000000 --- a/site/examples/web-app-starter/js/u-wsconnection.js +++ /dev/null @@ -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; - -})); - diff --git a/site/examples/web-app-starter/lib/components.php b/site/examples/web-app-starter/lib/components.php deleted file mode 100755 index d1179e4..0000000 --- a/site/examples/web-app-starter/lib/components.php +++ /dev/null @@ -1,199 +0,0 @@ - function($prop, &$context) { - $context['current_component_id'] = $prop['id']; - return('
'); - }, - - 'end' => function($prop) { - ?>'); - }, - - 'render' => function($prop, &$context) { - return($context['begin']($prop, $context) . $context['end']($prop)); - }, - - 'about' => 'This is an example component that wraps content in a div and logs when it is finalized.', - - ]; */ - - $GLOBALS['render_funcs'] = array(); - $GLOBALS['id_counter'] = $GLOBALS['id_counter'] ?? 1; - - function component_error_banner($s) - { - return ''; - } - - function component_caller_dir() - { - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); - return isset($trace[1]['file']) ? dirname($trace[1]['file']) : getcwd(); - } - - function component_normalize_name($file_name) - { - return preg_replace('/\.php$/', '', trim((string)$file_name)); - } - - function component_resolve_file($file_name, $search_path = false) - { - $component_name = component_normalize_name($file_name); - $candidates = array($component_name.'.php'); - if($search_path && !str_starts_with($component_name, 'components/')) - $candidates[] = rtrim($search_path, '/').'/'.$component_name.'.php'; - if(!str_starts_with($component_name, 'components/')) - $candidates[] = 'components/'.$component_name.'.php'; - foreach(array_unique($candidates) as $candidate) - { - if(file_exists($candidate)) - return $candidate; - } - return false; - } - - /** - * Loads a component from file system and registers it in the global registry - * - * Component files are searched in this order: - * 1. exact file path passed to the loader - * 2. caller-relative path (for shorthand component names) - * 3. components/{component_name}.php - * - * Component files should return an array with render functions like: - * return ['render' => function($prop) { ... }]; - */ - function component_get_func($file_name, $return_false_if_not_found = false, $search_path = false) - { - $component_name = component_normalize_name($file_name); - $result = []; - // Check if component is already loaded in global registry - if(!isset($GLOBALS['render_funcs'][$component_name])) - { - $component_file = component_resolve_file($component_name, $search_path); - if($component_file) - { - $result['component_file'] = $component_file; - } - else - { - // Component file not found - create error component - if($return_false_if_not_found) return(false); - $result['component_file'] = false; - $result['error'] = 'Component not found: '.$component_name; - $result['render'] = function() use($component_name) { - return component_error_banner('component not found: '.$component_name); - }; - } - if($result['component_file']) - foreach(require($result['component_file']) as $k => $v) - $result[$k] = $v; - // Register component in global registry for future use - $GLOBALS['render_funcs'][$component_name] = $result; - } - return($GLOBALS['render_funcs'][$component_name]); - } - -/** - * Checks if a component exists without loading it - * Returns true if component file can be found, false otherwise - */ -function component_exists($file_name) -{ - $caller_dir = component_caller_dir(); - return(component_get_func($file_name, true, $caller_dir) !== false); -} - -/** - * Loads a component and returns its definition - * Same as component_get_func but with caller directory detection - */ -function component_load($file_name) -{ - $caller_dir = component_caller_dir(); - return(component_get_func($file_name, true, $caller_dir)); -} - -function component_call($file_name, $render_call, $prop = array()) -{ - $prop['render_call'] = $render_call; - return component($file_name, $prop); -} - -/** - * Declares an inline component directly in code (not from file) - * - * @param string $file_name - Component name for registry - * @param array $render_prop - Component definition with render functions - * - * Example: - * component_declare('my-button', [ - * 'render' => function($prop) { return ""; } - * ]); - */ -function component_declare($file_name, $render_prop) -{ - // Register the inline component directly in global registry - $GLOBALS['render_funcs'][$file_name] = $render_prop; -} - -/** - * Main component rendering function - the heart of the component system - * - * @param string $file_name - Component name or path - * @param array $prop - Properties/data to pass to component - * @return mixed - Component output from the selected render method - * - * Component calling examples: - * component('components/example/theme-switcher') - * component_call('components/workspace/panel', 'render', ['title' => 'Status']) - * component('workspace/panel', ['render_call' => 'render']) - */ -function component($file_name, $prop = array()) -{ - Profiler::log('component '.$file_name, 1); - $caller_dir = component_caller_dir(); - if($file_name == '') - return(component_error_banner('[component name empty]')); - $file_name = component_normalize_name($file_name); - - // Default render method is 'render', but can be overridden - $prop['render_call'] = first($prop['render_call'] ?? false, 'render'); - - // Support for calling specific render methods: 'component:method' - if(stristr($file_name, ':')) // you can specify which render function to call - { - $prop['render_call'] = $file_name; - $file_name = nibble(':', $prop['render_call']); - } - - // Get component definition from registry (load if not already loaded) - $renderer = $GLOBALS['render_funcs'][$file_name] ?? false; - if(!$renderer) - { - // Component not in registry - try to load from file - component_get_func($file_name, false, $caller_dir); - $renderer = $GLOBALS['render_funcs'][$file_name] ?? false; - } - - // Generate unique ID for component instance if not provided - $prop['id'] = !empty($prop['id']) ? $prop['id'] : 'c'.($GLOBALS['id_counter']++); - $prop['filename'] = $file_name; - - if(isset($renderer[$prop['render_call']]) && is_callable($renderer[$prop['render_call']])) - $result = ($renderer[$prop['render_call']]($prop, $renderer)); - else if(isset($renderer[$prop['render_call']])) - $result = ($renderer[$prop['render_call']]); - else - $result = component_error_banner('component render method not found: '.$file_name.':'.$prop['render_call']); - - Profiler::log(false, -1); - return($result); -} - diff --git a/site/examples/web-app-starter/lib/db.class.php b/site/examples/web-app-starter/lib/db.class.php deleted file mode 100755 index f12d80d..0000000 --- a/site/examples/web-app-starter/lib/db.class.php +++ /dev/null @@ -1,381 +0,0 @@ - array(), 'info' => array()); - foreach(self::Get('SHOW FULL COLUMNS FROM #'.$table) as $fld) - { - $ds = array(); - foreach($fld as $k => $v) - { - $k = strtolower($k); - if($k == 'comment') - { - $p = explode(',', $v); - $v = false; - if(sizeof($p) > 0) foreach($p as $pi) - { - $pk = trim(nibble('=', $pi)); - $ds['_'.$pk] = trim($pi); - } - } - if($v) - $ds[$k] = $v; - } - $ds['caption'] = first($ds['_title'], $ds['field']); - $result['fields'][$ds['field']] = $ds; - } - return($result); - } - - # updates/creates the $dataset in the $tablename - static function Insert($tablename, $dataset) - { - self::$writeOps++; - $query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset). - ') VALUES('.DB::MakeValuesList($dataset).')'; - - mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).'{ '.$query.' }'); - self::$affectedRows += mysqli_affected_rows(self::$link); - return(mysqli_insert_id(self::$link)); - } - - # updates/creates the $dataset in the $tablename - static function Commit($tablename, &$dataset) - { - self::$writeOps++; - $keynames = self::Keys($tablename); - $keyname = $keynames[0]; - $keyvalue = $dataset[$keyname]; - Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') start'); - - $cache_entry = $tablename.':'.$keyname.':'.$keyvalue; - unset(self::$dataCache[$cache_entry]); - - $query='REPLACE INTO '.$tablename.' ('.DB::MakeNamesList($dataset). - ') VALUES('.DB::MakeValuesList($dataset).');'; - - # keeping this around just in case, but performance seems the same: - # $query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset). - # ') VALUES('.DB::MakeValuesList($dataset).') - # ON DUPLICATE KEY UPDATE '.DB::MakeSetList($dataset).';'; - - mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { '.$query.' }'); - $dataset[$keyname] = first($dataset[$keyname], mysqli_insert_id(self::$link)); - self::$dataCache[$cache_entry] = $dataset; - - Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') done'); - return($dataset[$keyname]); - } - - static function GetRowsMatch($table, $matchOptions, $fillIfEmpty = true) - { - self::$readOps++; - $where = array('1'); - foreach($matchOptions as $k => $v) - $where[] = '('.$k.'="'.DB::Safe($v).'")'; - $iwhere = implode(' AND ', $where); - $query = 'SELECT * FROM '.($table). - ' WHERE '.$iwhere; - $resultDS = self::GetRowWithQuery($query); - if ($fillIfEmpty && sizeof($resultDS) == 0) - foreach($matchOptions as $k => $v) - $resultDS[$k] = $v; - Profiler::Log('DB::GetRowsMatch('.$table.') done'); - return($resultDS); - } - - # from table $tablename, get dataset with key $keyvalue - static function GetRow($tablename, $keyvalue, $keyname = '', $options = array()) - { - if($keyvalue == '0') return(array()); - $fields = @$options['fields']; - $fields = first($fields, '*'); - if (!self::$link) return(array()); - - if ($keyname == '') - { - $keynames = self::Keys($tablename); - $keyname = $keynames[0]; - } - - $cache_entry = $tablename.':'.$keyname.':'.$keyvalue; - if(isset(self::$dataCache[$cache_entry])) return(self::$dataCache[$cache_entry]); - - $join = isset($options['join']) ? ' '.$options['join'] : ''; - $query = 'SELECT '.$fields.' FROM '.$tablename.$join.' WHERE '.$keyname.'="'.DB::Safe($keyvalue).'";'; - $queryResult = mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { Query: "'.$query.'" }'); - self::$readOps++; - - if ($line = @mysqli_fetch_array($queryResult, MYSQLI_ASSOC)) - { - mysqli_free_result($queryResult); - self::$dataCache[$cache_entry] = $line; - Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.')'); - return($line); - } - else - $result = array(); - - Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.') #fail'); - return($result); - } - - static function RemoveRow($tablename, $keyvalue, $keyname = null) - { - if ($keyname == null) - { - $keynames = self::Keys($tablename); - $keyname = $keynames[0]; - } - $res = (mysqli_query(self::$link, 'DELETE FROM '.$tablename.' WHERE '.$keyname.'="'. - DB::Safe($keyvalue).'";') - or critical(' Cannot remove dataset // '.mysqli_error(self::$link))); - Profiler::Log('DB::RemoveRow('.$tablename.', '.$keyvalue.') done'); - self::$affectedRows += mysqli_affected_rows(self::$link); - self::$writeOps++; - return($res); - } - - // retrieve dataset identified by SQL $query - static function GetRowWithQuery($query, $parameters = null) - { - $query = self::ParseQueryParams($query, $parameters); - - $queryResult = mysqli_query(self::$link, $query); - - if(!$queryResult) - return(critical('Error getting data // '.mysqli_error(self::$link).'{ '.$query.' }')); - - if ($line = mysqli_fetch_array($queryResult, MYSQLI_ASSOC)) - { - $result = $line; - mysqli_free_result($queryResult); - } - else - $result = array(); - - Profiler::Log('DB::GetRowWithQuery('.$query.')'); - self::$readOps++; - return($result); - } - - # execute a simple update $query - static function Query($query, $parameters = null) - { - $query = self::parseQueryParams($query, $parameters); - if (substr($query, -1, 1) == ';') - $query = substr($query, 0, -1); - $result = (mysqli_query(self::$link, $query) - or critical(' Query error // '.mysqli_error(self::$link))); - Profiler::Log('DB::Query('.$query.') done'); - self::$affectedRows += mysqli_affected_rows(self::$link); - self::$writeOps++; - return($result); - } - - # create a comma-separated list of keys in $dataset - static function MakeNamesList(&$dataset) - { - $result = ''; - if (sizeof($dataset) > 0) - foreach (array_keys($dataset) as $k) - { - if ($k!='') - $result = $result.','.$k; - } - return(substr($result, 1)); - } - - # make a name-value list for UPDATE-queries - static function MakeValuesList(&$dataset) - { - $result = ''; - if (sizeof($dataset) > 0) - foreach ($dataset as $k => $v) - { - if ($k!='') - $result = $result.',"'.DB::safe($v).'"'; - } - return(substr($result, 1)); - } - - static function MakeSetList(&$dataset, $concat = ', ') - { - $result = array(); - if (sizeof($dataset) > 0) foreach ($dataset as $k => $v) - { - if(substr($k, -1) == '+' || substr($k, -1) == '-') - { - $op = substr($k, -1); - $k = substr($k, 0, -1); - $result[] = $k.' = '.$k.' '.$op.' "'.DB::safe($v).'"'; - } - else - { - $result[] = $k.' = "'.DB::safe($v).'"'; - } - } - return(implode($concat, $result)); - } - - static function ParseQueryParams($query, $parameters = null) - { - if ($parameters != null) - { - $pctr = 0; - $result = ''; - for($a = 0; $a < strlen($query); $a++) - { - $chr = substr($query, $a, 1); - if ($chr == '?') - { - $result .= '"'.DB::Safe($parameters[$pctr]).'"'; - $pctr++; - } - else if ($chr == '&') - { - $result .= ''.intval($parameters[$pctr]).''; - $pctr++; - } - else if ($chr == ':') - { - $paramName = ''; - $a += 1; - $pFormat = 'string'; - if($query[$a] == ':') - { - $pFormat = 'number'; - $a += 1; - } - while(!ctype_space($chr = substr($query, $a, 1)) && $a < strlen($query)) - { - $paramName .= $chr; - $a += 1; - } - if($pFormat == 'number') - $result .= ' '.($parameters[$paramName]+0).' '; - else - $result .= ' "'.DB::Safe($parameters[$paramName]).'" '; - } - else - $result .= $chr; - } - } - else - $result = $query; - $q = str_replace('#', cfg('db/prefix'), $result); - self::$lastQuery = $q; - return($q); - } - - static function Safe($raw) - { - if(!self::$link) - return(addslashes($raw)); - else - return(mysqli_real_escape_string(self::$link, $raw)); - } - -} - -DB::connect(); - diff --git a/site/examples/web-app-starter/lib/db.md b/site/examples/web-app-starter/lib/db.md deleted file mode 100755 index 4620087..0000000 --- a/site/examples/web-app-starter/lib/db.md +++ /dev/null @@ -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) -]); -``` - diff --git a/site/examples/web-app-starter/lib/filebase.class.php b/site/examples/web-app-starter/lib/filebase.class.php deleted file mode 100644 index 298e6a6..0000000 --- a/site/examples/web-app-starter/lib/filebase.class.php +++ /dev/null @@ -1,217 +0,0 @@ -', 'file_put_contents_ex('.$filename.') could not write file'); - return; - } - - if (flock($fp, LOCK_EX)) - { - ftruncate($fp, 0); - rewind($fp); - fwrite($fp, $data); - } - else - { - Log::text('', 'file_put_contents_ex('.$filename.') could not acquire lock'); - } - - fclose($fp); - } - - static function delete_file($file_name) - { - unlink($file_name); - } - - static function read_file($file_name) - { - $fsz = filesize($file_name); - if($fsz == 0) - return(''); - - $fp = fopen($file_name, "rb+"); - - if(!$fp) - return(''); - - if (flock($fp, LOCK_SH)) { - $content = fread($fp, $fsz); - flock($fp, LOCK_UN); - } else { - Log::text('', 'read_file('.$file_name.') could not acquire lock'); - } - - fclose($fp); - - return($content); - } - - static function write_data($class, $bucket, $type, $data) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true); - $fn = $storage_path.'/'.$type.'.json'; - self::write_file($fn, json_encode($data)); - } - - static function read_data($class, $bucket, $type) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - $fn = $storage_path.'/'.$type.'.json'; - return(json_decode(self::read_file($fn), true)); - } - - static function delete_data($class, $bucket, $type) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - return(self::delete_file($storage_path.'/'.$type.'.json')); - } - - static function get_data_filename($class, $bucket, $type) - { - return(first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket).'/'.$type.'.json'); - } - - static function list_bucket($class, $bucket) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - foreach(explode(chr(10), trim(shell_exec('ls -1 '.escapeshellarg($storage_path)))) as $name) - { - if(substr($name, 0, 1) != '_' && trim($name) != '') - $items[] = $name; - } - return($items); - } - - static function search_bucket($class, $bucket, $q) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - foreach(explode(chr(10), trim(shell_exec('grep -irlF '.escapeshellarg($q).' '.escapeshellarg($storage_path)))) as $l) - { - $name = substr($l, strlen($storage_path)+1, -5); - if(substr($name, 0, 1) != '_' && trim($name) != '') - $items[] = $name; - } - return($items); - } - - static function delete_bucket($class, $bucket) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - if(stristr($storage_path, '*') !== false) return; - if(stristr($storage_path, '?') !== false) return; - $result = trim(shell_exec('rm -r '.escapeshellarg($storage_path).' 2>&1')); - return($result); - } - - static function write_log($class, $bucket, $type, $data) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true); - WriteToFile($storage_path.'/'.$type.'.log', json_encode($data).chr(10)); - } - - static function read_log($class, $bucket, $type, $line_count = 8, $offset = false) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - return(get_json_tail($storage_path.'/'.$type.'.log', $line_count, $offset)); - } - - static function line_count($class, $bucket, $type) - { - $storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket); - return(intval(trim(shell_exec('wc -l '.escapeshellarg($storage_path.'/'.$type.'.log'))))); - } - - static function get_json_tail($from_file, $line_count = 8, $offset = false) - { - if($offset > 0) - { - $lines = trim(shell_exec( - 'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count))); - } - else - { - $lines = trim(shell_exec( - 'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file))); - } - return(json_lines($lines)); - } - - static function get_tail($from_file, $line_count = 8, $offset = false) - { - $line_count = intval($line_count); - if($offset > 0) - { - $lines = trim(shell_exec( - 'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count))); - } - else - { - $lines = trim(shell_exec( - 'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file))); - } - return(explode(chr(10), $lines)); - } - - static function get_all_lines($from_file) - { - return(explode(chr(10), trim(file_get_contents($from_file)))); - } - - static function truncate_log($from_file, $lines = 128) - { - $lines = intval($lines); - $tmp = tempnam(sys_get_temp_dir(), 'trunc_'); - shell_exec('tail -n '.$lines.' '.escapeshellarg($from_file).' > '.escapeshellarg($tmp).' && cp '.escapeshellarg($tmp).' '.escapeshellarg($from_file)); - @unlink($tmp); - return true; - } - - static function json_lines($lines) - { - if($lines == '') - { - return(array()); - } - else - { - $result = array(); - foreach(explode(chr(10), $lines) as $line) - $result[] = json_decode($line, true); - return($result); - } - } - -} \ No newline at end of file diff --git a/site/examples/web-app-starter/lib/http.class.php b/site/examples/web-app-starter/lib/http.class.php deleted file mode 100644 index 128df62..0000000 --- a/site/examples/web-app-starter/lib/http.class.php +++ /dev/null @@ -1,111 +0,0 @@ - $url, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => is_array($data) ? http_build_query($data) : $data, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3 - ]); - - $response = curl_exec($ch); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - curl_close($ch); - - if ($error) { - return ['error' => 'cURL Error: ' . $error, 'http_code' => 0]; - } - - $decoded = json_decode($response, true); - - return [ - 'success' => $http_code >= 200 && $http_code < 300, - 'http_code' => $http_code, - 'raw' => $response, - 'data' => $decoded ?: $response, - 'error' => $http_code >= 400 ? "HTTP Error $http_code" : null - ]; - } - - /** - * Make a GET request - */ - public static function get($url, $params = [], $headers = []) - { - if (!empty($params)) { - $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params); - } - - $ch = curl_init(); - - $default_headers = [ - 'Accept: application/json', - 'User-Agent: Web-App-Starter/1.0' - ]; - - $headers = array_merge($default_headers, $headers); - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3 - ]); - - $response = curl_exec($ch); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - curl_close($ch); - - if ($error) { - return ['error' => 'cURL Error: ' . $error, 'http_code' => 0]; - } - - $decoded = json_decode($response, true); - - return [ - 'success' => $http_code >= 200 && $http_code < 300, - 'http_code' => $http_code, - 'raw' => $response, - 'data' => $decoded ?: $response, - 'error' => $http_code >= 400 ? "HTTP Error $http_code" : null - ]; - } - - /** - * Make a request with Bearer token authentication - */ - public static function get_with_token($url, $token, $params = []) - { - return self::get($url, $params, [ - 'Authorization: Bearer ' . $token - ]); - } -} diff --git a/site/examples/web-app-starter/lib/log.class.php b/site/examples/web-app-starter/lib/log.class.php deleted file mode 100755 index c1f1814..0000000 --- a/site/examples/web-app-starter/lib/log.class.php +++ /dev/null @@ -1,41 +0,0 @@ -prepare()) { - * $data = [ - * 'customer_name' => 'John Doe', - * 'invoice_date' => time(), - * 'total_amount' => 1250.50, - * 'items' => [ - * ['name' => 'Product A', 'price' => 500.00, 'qty' => 1], - * ['name' => 'Product B', 'price' => 750.50, 'qty' => 1] - * ] - * ]; - * - * $odt->replace_placeholders($data); - * $pdf_path = $odt->create_output('/path/to/output.odt'); - * - * if ($pdf_path) { - * echo "PDF created: " . $pdf_path; - * } else { - * echo "Error: " . $odt->error_msg; - * } - * } else { - * echo "Error: " . $odt->error_msg; - * } - * - * PLACEHOLDER SYNTAX: - * - * In your ODT template, use LibreOffice placeholders (Insert > Field > Other > Variables > User Field) - * or direct placeholder markup: - * - * Simple placeholders: - * customer_name - * - * Formatted placeholders with JSON properties in description: - * invoice_date - * total_amount - * - * SUPPORTED FORMATS: - * - * - "date" - Formats timestamp as date (uses cfg('date_format') or custom template) - * - "time" - Formats timestamp as time (uses cfg('time_format') or custom template) - * - "datetime" - Formats timestamp as datetime (uses cfg('datetime_format') or custom template) - * - "currency" - Formats number as currency with € symbol - * - "number" - Formats number with specified decimal places - * - "duration" - Converts seconds to "Xh Ymin" format - * - * FORMAT OPTIONS: - * - * - "template": Custom date/time format string (e.g., "Y-m-d H:i:s") - * - "decimals": Number of decimal places for currency/number formatting - * - "default": Default value if placeholder data is empty - * - "emptyifzero": Show empty string if numeric value is zero - * - "delsegmentifzero": Delete entire segment if numeric value is zero - * - "newline": Add newlines "before", "after", or "before,after" the value - * - * LISTS AND TABLES: - * - * For repeating table rows, use a special placeholder in a table row: - * items - * - * This will repeat the table row for each item in the 'items' array. - * Within the repeated row, access item properties with 'item.' prefix: - * item.name - * item.price - * - * The class automatically calculates sums for numeric fields: - * sums.items.price - * - * ERROR HANDLING: - * - * Always check the return values and $odt->error_msg for error details: - * - prepare() returns false on failure - * - create_output() returns false on failure, PDF path on success - * - Check $odt->error_msg for specific error messages - * - * PROPERTIES: - * - * - $template_name: Path to the ODT template file - * - $content_xml: Loaded and processed content.xml from ODT - * - $temp_dir: Temporary directory for ODT extraction - * - $error_msg: Last error message - * - $odt_filename: Path to generated ODT file - * - $pdf_filename: Path to generated PDF file - * - $debug_output: Debug output from shell commands - * - * NOTES: - * - * - Temporary files are automatically cleaned up in destructor - * - PDF conversion requires LibreOffice to be installed and accessible - * - The class assumes UTF-8 encoding for all text content - * - Newlines in data are converted to ODT line breaks - * - All string values are properly escaped for XML - */ - -class ODT -{ - - public $template_name; - public $content_xml; - public $temp_dir; - public $error_msg; - public $odt_filename; - public $pdf_filename; - public $debug_output; - - function __construct($template_name) - { - $this->template_name = $template_name; - if(!file_exists($this->template_name)) { - $this->error_msg = 'Template not found: '.$template_name; - } - $this->temp_dir = '/tmp/'.uniqid(); - } - - /** - * Check if all required command line tools are available and working - * @return array Array with 'status' (bool) and 'messages' (array of status messages) - */ - static function check_requirements() - { - $results = [ - 'status' => true, - 'messages' => [] - ]; - - $unzip_check = shell_exec('which unzip 2>/dev/null'); - if (empty(trim($unzip_check))) { - $results['status'] = false; - $results['messages'][] = 'ERROR: unzip command not found in PATH'; - } else { - $results['messages'][] = 'OK: unzip found at ' . trim($unzip_check); - $unzip_version = shell_exec('unzip -v 2>&1 | head -1'); - if ($unzip_version) { - $results['messages'][] = 'INFO: ' . trim($unzip_version); - } - } - - $zip_check = shell_exec('which zip 2>/dev/null'); - if (empty(trim($zip_check))) { - $results['status'] = false; - $results['messages'][] = 'ERROR: zip command not found in PATH'; - } else { - $results['messages'][] = 'OK: zip found at ' . trim($zip_check); - } - - $soffice_check = shell_exec('which soffice 2>/dev/null') || ''; - if (empty(trim($soffice_check))) { - $results['status'] = false; - $results['messages'][] = 'ERROR: LibreOffice (soffice) not found in PATH'; - } else { - $results['messages'][] = 'OK: LibreOffice found at ' . trim($soffice_check); - - $soffice_version = shell_exec('soffice --version 2>&1'); - if ($soffice_version) { - $results['messages'][] = 'INFO: ' . trim($soffice_version); - } - - $headless_test = shell_exec('timeout 10 soffice --headless --help 2>&1') || ''; - if (strpos($headless_test, 'headless') !== false || strpos($headless_test, 'convert-to') !== false) { - $results['messages'][] = 'OK: LibreOffice headless mode is working'; - } else { - $results['status'] = false; - $results['messages'][] = 'ERROR: LibreOffice headless mode test failed'; - $results['messages'][] = 'DEBUG: ' . trim($headless_test); - } - } - - if (!is_dir('/tmp')) { - $results['status'] = false; - $results['messages'][] = 'ERROR: /tmp directory does not exist'; - } elseif (!is_writable('/tmp')) { - $results['status'] = false; - $results['messages'][] = 'ERROR: /tmp directory is not writable'; - } else { - $results['messages'][] = 'OK: /tmp directory exists and is writable'; - } - - $test_temp_dir = '/tmp/odt_test_' . uniqid(); - if (!mkdir($test_temp_dir, 0777, true)) { - $results['status'] = false; - $results['messages'][] = 'ERROR: Cannot create temporary directories in /tmp'; - } else { - $results['messages'][] = 'OK: Can create temporary directories'; - rmdir($test_temp_dir); - } - - return $results; - } - - static function parse_attributes($tag) { - $attributes = []; - while($tag != '') - { - $a_name = nibble('="', $tag); - $a_value = nibble('"', $tag); - $attributes[trim($a_name)] = trim($a_value); - } - return $attributes; - } - - function prepare() - { - // Create the temporary directory - if (!mkdir($this->temp_dir, 0777, true)) { - $this->error_msg = 'Failed to create temp directory'; - return false; - } - - // Unzip the template into the temp directory using shell_exec() - $command = 'unzip ' . escapeshellarg($this->template_name) . ' -d ' . escapeshellarg($this->temp_dir) . ' 2>&1'; - $output = shell_exec($command); - if ($output === null) { - $this->error_msg = 'Failed to unzip the template'; - return false; - } - - // Load content.xml into memory - $content_file = $this->temp_dir . '/content.xml'; - if (!file_exists($content_file)) { - $this->error_msg = 'content.xml not found'; - return false; - } - - $this->content_xml = file_get_contents($content_file); - if ($this->content_xml === false) { - $this->error_msg = 'Failed to read content.xml'; - return false; - } - - return true; - } - - function odt_entities($raw) - { - return str_replace(chr(10), '', safe($raw, ENT_XML1 | ENT_QUOTES, 'UTF-8')); - } - - function break_into_segments($s) - { - $segment_handlers = [ - ' function(&$cake, &$seg) { - $rest_of_row = nibble('', $cake); - # check whether this segment contains an items marker indicating we should - # iterate this segment over a list in the data - $items_marker = ''; - if(strpos($rest_of_row, $items_marker) !== false) - { - $r0 = nibble($items_marker, $rest_of_row); - $items_param = trim(str_replace(['<', '>'], '', - nibble('', $rest_of_row))); - $seg[] = [ - 'type' => 'list', - 'content' => '', - 'fields' => $items_param]; - } - else $seg[] = [ - 'type' => 'flat', - 'content' => '']; - }, - ]; - $seg = []; - while($s != '') - { - $seg_start_found = false; - $sc = nibble(array_keys($segment_handlers), $s, $seg_start_found); - $seg[] = ['type' => 'flat', 'content' => $sc]; - if($seg_start_found !== false) - { - $segment_handlers[$seg_start_found]($s, $seg); - } - } - return($seg); - } - - function run_segment($seg, $params) - { - $result = ''; - $sc = $seg['content']; - while($sc != '') - { - $placeholder_found = false; - $result .= nibble('', $sc)); - $p_prop = []; - if(isset($p_attributes['text:description']) && $p_attributes['text:description']) - { - if(substr($p_attributes['text:description'], 0, 1) == '{') - { - $decoded = json_decode( - str_replace('"', '"', $p_attributes['text:description']), true); - if($decoded !== null) { - $p_prop = $decoded; - } - } - else - { - $p_prop['format'] = $p_attributes['text:description']; - } - } - $p_content = str_replace(array('>', '<'), '', - nibble('', $sc)); - $placeholder_key = $p_content; - $value = first($params[$placeholder_key] ?? null, $p_prop['default'] ?? null, ''); - switch($p_prop['format'] ?? '') - { - case('date'): - { - if($value == 0) $value = '-'; else - $value = date(first($p_prop['template'], cfg('date_format')), intval($value)); - } break; - case('time'): - { - if($value == 0) $value = '-'; else - $value = date(first($p_prop['template'], cfg('time_format')), intval($value)); - } break; - case('datetime'): - { - if($value == 0) $value = '-'; else - $value = date(first($p_prop['template'], cfg('datetime_format')), intval($value)); - } break; - case('currency'): - { - $value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', '').' €'; - } break; - case('number'): - { - $value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', ''); - } break; - case('duration'): - { - $value = intval($value); - $h = floor($value/3600); $value -= $h*3600; - $m = floor($value/60); $value -= $m*60; - $s = $value; - $value = ''; - if($h > 0) $value .= $h.'h '; - $value .= $m.'min'; - } break; - } - if(($p_prop['emptyifzero'] ?? false) && floatval($value) == 0) - $value = ''; - if(($p_prop['delsegmentifzero'] ?? false) && floatval($value) == 0) - return(''); - if(!is_string($value)) - $value = trim($value); - if(($p_prop['newline'] ?? false) && $value != '') - { - if(str_contains($p_prop['newline'], 'before')) - $value = chr(10).$value; - if(str_contains($p_prop['newline'], 'after')) - $value = $value.chr(10); - } - if(!is_array($value)) - $result .= $this->odt_entities($value); - } - } - return $result; - } - - function replace_placeholders(&$params) - { - $result = ''; - foreach($this->break_into_segments($this->content_xml) as $seg) - { - if($seg['type'] == 'list') - { - if(isset($params[$seg['fields']]) && is_array($params[$seg['fields']])) { - foreach($params[$seg['fields']] as $item) - { - $np = $params; - foreach($item as $k => $v) - { - $np['item.'.$k] = $v; - $params['sums.'.$seg['fields'].'.'.$k] += floatval($v); - } - $result .= $this->run_segment($seg, $np); - } - } - } - else - { - $result .= $this->run_segment($seg, $params); - } - } - return $this->content_xml = $result; - } - - function create_output($out_filename) - { - $this->odt_filename = $out_filename; - $content_file = $this->temp_dir . '/content.xml'; - if (file_put_contents($content_file, $this->content_xml) === false) { - $this->error_msg = 'Failed to write content.xml'; - return false; // - } - - $this->debug_output = trim( - shell_exec('cd '.escapeshellarg($this->temp_dir).' ; zip -r ../tmp.odt * 2>&1')); - $zip_output = dirname($this->temp_dir).'/tmp.odt'; - if(!file_exists($zip_output)) - { - $this->error_msg = 'Failed to create tmp ODT file (shell "'.$this->debug_output.'")'; - return false; - } - if(file_exists($out_filename)) { - unlink($out_filename); - } - shell_exec('mv '.escapeshellarg($zip_output).' '.escapeshellarg($out_filename)); - - if (!file_exists($out_filename)) { - $this->error_msg = 'Failed to create ODT file "'.$out_filename.'"'; - return false; // - } - - $this->pdf_filename = $pdf_output = pathinfo($out_filename, PATHINFO_DIRNAME) . '/' . pathinfo($out_filename, PATHINFO_FILENAME) . '.pdf'; - $command = 'HOME=/tmp ; soffice --headless --convert-to pdf ' . escapeshellarg($out_filename) . ' --outdir ' . escapeshellarg(pathinfo($out_filename, PATHINFO_DIRNAME)) . ' 2>&1'; - $this->debug_output = shell_exec($command); - - if (!file_exists($pdf_output)) { - $this->error_msg = 'PDF conversion failed'; - return false; // - } - - return $pdf_output; - } - - function __destruct() - { - $this->delete_directory($this->temp_dir); - } - - function delete_directory($dir) - { - if (!is_dir($dir)) return; - $items = array_diff(scandir($dir), ['.', '..']); - foreach ($items as $item) { - $path = "$dir/$item"; - is_dir($path) ? $this->delete_directory($path) : unlink($path); - } - rmdir($dir); - } - -} diff --git a/site/examples/web-app-starter/lib/profiler.class.php b/site/examples/web-app-starter/lib/profiler.class.php deleted file mode 100755 index f4f1937..0000000 --- a/site/examples/web-app-starter/lib/profiler.class.php +++ /dev/null @@ -1,42 +0,0 @@ - 0) // if greater than zero, update indent after logging - self::$indent_str = str_repeat(' ', self::$indent_level * 2); - self::$last = $thistime; - self::$current = $absoluteMS; - return($thistime); - } - - static function get_time() - { - return(1000*(microtime(true) - self::$start)); - } - - static function start() - { - self::$start = self::$last = microtime(true); - } - -} diff --git a/site/examples/web-app-starter/lib/svg.class.php b/site/examples/web-app-starter/lib/svg.class.php deleted file mode 100755 index a7d038b..0000000 --- a/site/examples/web-app-starter/lib/svg.class.php +++ /dev/null @@ -1,32 +0,0 @@ -= 2 * M_PI) $start_angle -= 2 * M_PI; - while ($end_angle >= 2 * M_PI) $end_angle -= 2 * M_PI; - - $angle_diff = $end_angle - $start_angle; - if ($angle_diff < 0) $angle_diff += 2 * M_PI; - - $large_arc_flag = ($angle_diff > M_PI) ? 1 : 0; - $sweep_flag = 1; - - ?> - - - - <?= first(URL::$route['page-title'] ?? false, cfg('site/default_page_title')).' | '.cfg('site/name') ?> - - - - - - $signed_in, - 'profile' => $profile, - 'avatar' => first($profile['avatar_url'] ?? false, $profile['profile_image'] ?? false), - 'display_name' => first($profile['username'] ?? false, $profile['email'] ?? false, 'Account'), - ); -} - -function theme_render_account_links($options = array()) -{ - $context = theme_get_user_context(); - $wrapper_class = trim((string)first($options['wrapper_class'] ?? false, 'nav-account')); - $name_class = trim((string)first($options['name_class'] ?? false, 'account-name')); - $links_wrapper_class = trim((string)($options['links_wrapper_class'] ?? '')); - $show_avatar = !empty($options['show_avatar']); - $show_name = array_key_exists('show_name', $options) ? (bool)$options['show_name'] : true; - ?>> - - $account_wrapper_class, - 'name_class' => 'account-name', - 'show_avatar' => $show_avatar, - )); ?> -

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

Login

- -
- -
-
-
- -
-

Register

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

Profile

-

Email:

-

Roles:

-

Created:

-

Logout

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

Register

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

Authentication Failed

-

- -
- -
-
-

Authentication Successful

-

- - - - - - - -
-

Demo Mode - Next Steps for Implementation:

-
-

Service:

-

Code:

-

State:

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

Complete Implementation Example:

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

Debug Information

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

Authentication Demo

- -
-

OAuth Authentication

-

This component provides secure OAuth authentication with popular identity providers.

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

Setup Instructions

-
-
-

1. Create OAuth Applications

-

Create OAuth applications with your preferred providers:

- -

For all providers, add callback URL:

-
- -
-

2. Configure OAuth Component

-

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

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

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

-
- -
-

3. Implement Backend Handler

-

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

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

Built-in Provider Support

-

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

- -
-
-

Google OAuth

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

GitHub OAuth

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

Discord OAuth

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

Twitch OAuth

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

Current Session Status

- -
-

✓ Logged In

-

User ID:

- -

Email:

- -
- -
-

ⓘ Not Logged In

-

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

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

Dark Theme Components

-

See how beautiful our components look in dark mode

- -
-
-

Dark Forms

-
-
- - -
-
- - -
- -
-
- -
-

Notifications

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

Dashboard Primitives

- -
-

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

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

What Was Backported

-

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

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

Gauge Components Demo

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

Event Binding

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

Arc Gauge Controls

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

Demo

-

Try our UNBELIEVABLE components in action

- -
-
-

Modern Forms

-
-
- - -
-
- - -
-
- - -
- -
-
- -
-

Button Variations

-
- - - - -
- -
- - - -
-
-
-
-
- -
-

Theme Families

-

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

-
- -
-

-

- Preview Theme -
- -
-

Starter Themes

-

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

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

Component Development Guidelines

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

Simple Data Table (ag-Grid)

-

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

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

Component System

- -
-

Component Declaration

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

Example Components

-
-
-

hero-section

-

Landing page hero with CTA buttons

-
-
-

features-grid

-

3-column feature showcase

-
-
-

stats-section

-

Animated statistics display

-
-
-

testimonials

-

Customer testimonial carousel

-
-
-

cta-section

-

Call-to-action with background

-
-
-

pricing-table

-

Responsive pricing tiers

-
-
-

brands-showcase

-

Logo grid with animations

-
-
-

theme-switcher

-

Light/dark theme toggle

-
-
-
- -
-

Usage Examples

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

Theme Info

-

This starter pack includes a light and a dark theme

-
-
-

Light Theme

- themes/light/css/style.css -
-
-

Dark Theme

- themes/dark/css/style.css -
-
- -
-
-
-
-
-
- variables.css -
-

CSS Variables

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

Ajax Demo

- -
-

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

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

-

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

- -
- -
-
-

Tokens at a Glance

-
- -
- - -
- -
-
- Primary - Secondary - Accent - Surface -
-
- -
-

System Message States

- - - -
- -
-

Form Controls

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
-

Dense Content Block

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

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

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

-

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

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

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

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

', - ]); - - if ($section === 'activity') { - $mainBody .= component('components/workspace/empty-state', [ - 'icon_class' => 'fas fa-clock-rotate-left', - 'title' => 'No live stream wired yet', - 'text' => 'The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one.', - 'action_html' => 'Open dashboard demo', - ]); - } - - $main = $mobileBar . component('components/workspace/panel', [ - 'header_html' => $header, - 'body_html' => $mainBody, - ]); - - echo component('components/workspace/app-frame', [ - 'id' => 'workspace-demo-shell', - 'overlay_id' => 'workspace-demo-overlay', - 'sidebar_html' => $sidebar, - 'main_html' => $main, - ]); - ?> - \ No newline at end of file diff --git a/site/test/dtree.uce b/site/test/dtree.uce index e8b41d7..1be7961 100644 --- a/site/test/dtree.uce +++ b/site/test/dtree.uce @@ -18,7 +18,7 @@ RENDER(Request& context) t.set("String test"); print("String: ", t.to_string(), "\n"); - t.set(true); + t.set_bool(true); print("bool: ", t.to_string(), "\n"); t.set(1234.5678); @@ -29,16 +29,55 @@ RENDER(Request& context) ?> +

Typed Conversions

+ +
 f64: ", scalar_text.to_f64(), "\n");
+		print("text -> u64: ", scalar_text.to_u64(), "\n");
+		print("text -> s64: ", scalar_text.to_s64(), "\n");
+
+		DTree bool_text;
+		bool_text = "yes";
+		print("yes -> bool: ", bool_text.to_bool() ? "true" : "false", "\n");
+
+		DTree single_value_map;
+		single_value_map["value"] = "123";
+		print("{value:123} -> u64: ", single_value_map.to_u64(), "\n");
+
+		DTree headers;
+		headers["Content-Type"] = "text/plain";
+		headers["X-Test"] = "123";
+		print("map -> StringMap:\n", var_dump(headers.to_stringmap()), "\n");
+
+		?>
+ +

Lookup Helpers

+ +
set("Ada");
+		print("has(user) after create: ", lookup.has("user") ? "true" : "false", "\n");
+		if(DTree* user = lookup.key("user"))
+			print("key(user) after create: ", user->to_string(), "\n");
+
+		?>
+

Nested Tree

set("valueA");
-		t.key("b")->set("valueB");
-		t.key("c")->key("c-d")->set("valueCD");
-		t.key("c")->key("c-e")->set("valueCE");
-		t.key("c")->key("c-f")->set(&t);
-		t.key("g")->key("g-h")->key("h-i")->set("valueHI");
+		t.get_or_create("a")->set("valueA");
+		t.get_or_create("b")->set("valueB");
+		t.get_or_create("c")->get_or_create("c-d")->set("valueCD");
+		t.get_or_create("c")->get_or_create("c-e")->set("valueCE");
+		t.get_or_create("c")->get_or_create("c-f")->set(&t);
+		t.get_or_create("g")->get_or_create("g-h")->get_or_create("h-i")->set("valueHI");
 		t["g"]["x"]["y1"] = "XYZ1";
 		t["g"]["x"]["y2"] = &t;
 		t["g"]["x"]["y3"] = time();
diff --git a/site/test/index.uce b/site/test/index.uce
index 856e027..e74002d 100644
--- a/site/test/index.uce
+++ b/site/test/index.uce
@@ -56,6 +56,7 @@ RENDER(Request& context)
 
 			
Advanced
+ diff --git a/site/test/json.uce b/site/test/json.uce index 11881f4..56ddf6f 100644 --- a/site/test/json.uce +++ b/site/test/json.uce @@ -15,13 +15,13 @@ RENDER(Request& context)
set("valueA");
-		t.key("b")->set("valueB");
-		t.key("c")->key("c-dt")->set_bool(true);
-		t.key("c")->key("c-df")->set_bool(false);
-		t.key("c")->key("c-e")->set("valueCE");
-		t.key("c")->key("c-f")->set(&t);
-		t.key("g")->key("g-h")->key("h-i")->set("valueHI");
+		t.get_or_create("a")->set("valueA");
+		t.get_or_create("b")->set("valueB");
+		t.get_or_create("c")->get_or_create("c-dt")->set_bool(true);
+		t.get_or_create("c")->get_or_create("c-df")->set_bool(false);
+		t.get_or_create("c")->get_or_create("c-e")->set("valueCE");
+		t.get_or_create("c")->get_or_create("c-f")->set(&t);
+		t.get_or_create("g")->get_or_create("g-h")->get_or_create("h-i")->set("valueHI");
 		t["g"]["x"]["y1"] = "XYZ1";
 		t["g"]["x"]["y2"] = &t;
 		t["g"]["x"]["y3"] = time();
diff --git a/site/test/parse_time.uce b/site/test/parse_time.uce
index 5bd87a1..2cfc662 100644
--- a/site/test/parse_time.uce
+++ b/site/test/parse_time.uce
@@ -3,6 +3,7 @@ RENDER(Request& context)
 {
 
 	String raw = first(context.post["raw"], "today");
+	u64 parsed = time_parse(raw);
 
 	<>
 		
@@ -21,7 +22,9 @@ RENDER(Request& context)
 		
 		
diff --git a/site/test/sharedunit.uce b/site/test/sharedunit.uce index 1ae5648..5e0162b 100644 --- a/site/test/sharedunit.uce +++ b/site/test/sharedunit.uce @@ -1,7 +1,7 @@ RENDER(Request& context) { - auto p = compiler_load_shared_unit(context, "post.uce"); + auto p = compiler_load_shared_unit(&context, "post.uce"); if(p) print(to_string(p)); } diff --git a/site/test/style.css b/site/test/style.css index 1584c59..4324a0b 100644 --- a/site/test/style.css +++ b/site/test/style.css @@ -1,5 +1,33 @@ /* UCE Test Suite — 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; @@ -19,8 +47,8 @@ --border-strong: rgba(255, 255, 255, 0.14); --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2); --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25); - --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); @@ -382,6 +410,223 @@ section h2 { padding: 0; } +/* ── Unit Browser ── */ + +.unit-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.unit-toolbar p { + margin: 0; + color: var(--text-dim); +} + +.unit-count { + font-family: var(--font-mono); + font-size: 0.82rem; + color: var(--text-dim); + padding: 6px 10px; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: 999px; +} + +.status-badge { + display: inline-flex; + align-items: center; + padding: 4px 10px; + border-radius: 999px; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.04em; + text-transform: uppercase; + border: 1px solid var(--border); + background: var(--bg-code); + white-space: nowrap; +} + +.status-ok { + color: #9ff0b3; + border-color: rgba(159, 240, 179, 0.25); + background: rgba(38, 105, 58, 0.22); +} + +.status-warn { + color: #ffd97a; + border-color: rgba(255, 217, 122, 0.26); + background: rgba(120, 88, 22, 0.25); +} + +.status-error { + color: #ff9e9e; + border-color: rgba(255, 158, 158, 0.3); + background: rgba(122, 34, 34, 0.28); +} + +.status-muted { + color: var(--text-dim); + border-color: var(--border); + background: var(--bg-code); +} + +.unit-browser-layout { + display: grid; + grid-template-columns: minmax(250px, 320px) minmax(0, 1fr); + gap: 18px; + align-items: start; +} + +.unit-sidebar, +.unit-detail-pane { + min-width: 0; +} + +.unit-sidebar-header, +.unit-detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + margin-bottom: 14px; +} + +.unit-sidebar-header h2, +.unit-detail-pane h2, +.unit-detail-header h3 { + margin: 0; +} + +.unit-list { + display: flex; + flex-direction: column; +} + +.unit-list-item { + display: block; + text-decoration: none; +} + +.unit-list-item:hover { + text-decoration: none; + background: rgba(255, 255, 255, 0.05); +} + +.unit-list-item.is-selected { + border-color: rgba(240, 196, 48, 0.32); + background: rgba(240, 196, 48, 0.08); + box-shadow: 0 0 0 1px rgba(240, 196, 48, 0.08), var(--shadow-sm); +} + +.unit-list-title-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + margin-bottom: 6px; +} + +.unit-list-title-row strong { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text); + word-break: break-word; +} + +.unit-list-path { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--text-muted); + word-break: break-word; + margin-bottom: 6px; + line-height: 1.45; +} + +.unit-list-meta { + display: flex; + gap: 8px; + flex-wrap: wrap; + font-size: 0.74rem; + color: var(--text-dim); +} + +.unit-list-meta span { + padding: 2px 6px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.unit-list-error { + margin-top: 8px; + font-size: 0.74rem; + color: #ffb7b7; + line-height: 1.4; +} + +.unit-actions { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.unit-actions a { + font-size: 0.82rem; + font-family: var(--font-mono); +} + +.unit-detail-path { + margin: 6px 0 0; + color: var(--text-dim); +} + +.detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.detail-card { + padding: 16px 18px; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); +} + +.detail-card strong { + display: block; + font-family: var(--font-mono); + font-size: 0.74rem; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 8px; +} + +.detail-card span, +.detail-card code { + font-size: 0.88rem; + word-break: break-word; +} + +@media (max-width: 920px) { + .unit-browser-layout { + grid-template-columns: 1fr; + } + + .unit-sidebar-header, + .unit-detail-header { + flex-direction: column; + } +} + /* ── Scrollbar ── */ ::-webkit-scrollbar { diff --git a/site/test/unit-browser.uce b/site/test/unit-browser.uce new file mode 100644 index 0000000..e239ffa --- /dev/null +++ b/site/test/unit-browser.uce @@ -0,0 +1,168 @@ +String unit_query(String selected_path, String compile_path = "") +{ + String result = "?"; + if(selected_path != "") + result += "selected=" + uri_encode(selected_path); + if(compile_path != "") + { + if(result.length() > 1) + result += "&"; + result += "compile=" + uri_encode(compile_path); + } + return(result); +} + +String unit_status_class(String compile_status, String error_status) +{ + if(trim(error_status) != "") + return("status-error"); + if(compile_status == "loaded" || compile_status == "compiled") + return("status-ok"); + if(compile_status == "stale") + return("status-warn"); + if(compile_status == "missing_source") + return("status-error"); + return("status-muted"); +} + +String unit_flag_label(DTree value) +{ + return(value.to_string() == "(true)" ? "✅" : "❌"); +} + +RENDER(Request& context) +{ + String compile_target = first(context.get["compile"], ""); + String selected_path = first(context.get["selected"], ""); + String compile_message = ""; + String compile_message_class = "status-muted"; + + if(compile_target != "") + { + bool compile_ok = unit_compile(compile_target); + selected_path = compile_target; + compile_message = (compile_ok ? "Compile succeeded for " : "Compile failed for ") + compile_target; + compile_message_class = compile_ok ? "status-ok" : "status-error"; + } + + auto unit_paths = units_list(); + if(selected_path == "" && unit_paths.size() > 0) + selected_path = unit_paths.front(); + + DTree selected_info = unit_info(selected_path); + String selected_compile_status = selected_info["compile_status"].to_string(); + String selected_error_status = selected_info["error_status"].to_string(); + String selected_status_class = unit_status_class(selected_compile_status, selected_error_status); + String selected_export_count = std::to_string(selected_info["exports"]._map.size()); + String selected_compile_link = unit_query(selected_path, selected_path); + + <> + + + + UCE Test: Unit Browser + + +

+ UCE Test: + Unit Browser +

+ + +
+

+
+ + +
+
+ + +
+

Unit Info

+ +

No unit metadata is available for the current selection.

+ +
+
+

+

+
+
+ + compile +
+
+ +
+
+ Error Status + +
+
+ Exports + +
+
+ Filesystem + source , compiled , metadata +
+
+ Runtime Flags + loaded , stale , current +
+
+ Artifacts + +
+
+ Timestamps + Source file:
+ Binary:
+ Meta info:
+
+
+ +
+ unit_info() payload +
+
+ +
+
+
+ + +} diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index fabd57d..45ea420 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -1,9 +1,11 @@ #include "compiler.h" #include "compiler-parser.h" +#include "hash.h" #include #include #include #include +#include namespace { @@ -17,11 +19,14 @@ struct SharedUnitFilesystemState { bool source_exists = false; bool metadata_exists = false; + bool compile_output_exists = false; bool metadata_parsed = false; bool abi_compatible = false; + bool input_signature_matches = false; time_t source_time = 0; time_t compiled_time = 0; time_t metadata_time = 0; + time_t compile_output_time = 0; time_t setup_template_time = 0; time_t compiler_header_time = 0; time_t compiler_source_time = 0; @@ -30,8 +35,51 @@ struct SharedUnitFilesystemState time_t required_time = 0; u64 metadata_abi_version = 0; u64 runtime_abi_version = UCE_UNIT_ABI_VERSION; + String metadata_content; + String compile_output_content; + String metadata_build_token; + String metadata_input_signature; + String current_input_signature; }; +struct SharedUnitCompileCheck +{ + bool source_missing = false; + bool needs_compile = false; +}; + +bool compiler_config_truthy(String raw, bool default_value) +{ + raw = to_lower(trim(raw)); + if(raw == "") + return(default_value); + if(raw == "1" || raw == "true" || raw == "yes" || raw == "on") + return(true); + if(raw == "0" || raw == "false" || raw == "no" || raw == "off") + return(false); + return(default_value); +} + +bool compiler_jit_compile_on_request_enabled(Request* context) +{ + if(!context || !context->server) + return(true); + return(compiler_config_truthy(context->server->config["JIT_COMPILE_ON_REQUEST"], true)); +} + +u64 compiler_failure_retry_seconds(Request* context) +{ + if(!context || !context->server) + return(10); + auto raw = trim(context->server->config["COMPILE_FAILURE_RETRY_SECONDS"]); + if(raw == "") + return(10); + auto value = int_val(raw); + if(value < 0) + return(0); + return((u64)value); +} + bool compiler_is_u64_string(String value) { value = trim(value); @@ -45,11 +93,12 @@ bool compiler_is_u64_string(String value) return(true); } -bool compiler_parse_unit_metadata_abi(String content, u64& abi_out) +StringMap compiler_parse_unit_metadata(String content) { + StringMap metadata; content = trim(content); if(content == "") - return(false); + return(metadata); auto lines = split(content, "\n"); for(auto& raw_line : lines) @@ -62,37 +111,97 @@ bool compiler_parse_unit_metadata_abi(String content, u64& abi_out) continue; auto key = trim(line.substr(0, split_pos)); auto value = trim(line.substr(split_pos + 1)); - if(key != "unit_abi_version" || !compiler_is_u64_string(value)) - continue; - abi_out = (u64)atoll(value.c_str()); - return(true); + if(key != "") + metadata[key] = value; } - return(false); + return(metadata); } -String compiler_unit_metadata_text() +String compiler_unit_input_signature(Request* context, SharedUnit* su) +{ + if(!context || !su || !file_exists(su->file_name)) + return(""); + + String setup_template = context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]; + return( + gen_sha1(file_get_contents(su->file_name)) + ":" + + gen_sha1(file_get_contents(setup_template)) + ":" + + std::to_string(UCE_UNIT_ABI_VERSION) + ); +} + +String compiler_unit_build_token() +{ + return( + std::to_string(getpid()) + ":" + + std::to_string((u64)(time_precise() * 1000000.0)) + ); +} + +String compiler_unit_metadata_text(Request* context, SharedUnit* su) { return( "format=uce-unit-metadata-v1\n" "unit_abi_version=" + std::to_string(UCE_UNIT_ABI_VERSION) + "\n" + "input_signature=" + compiler_unit_input_signature(context, su) + "\n" + "build_token=" + compiler_unit_build_token() + "\n" ); } -bool shared_unit_filesystem_requires_recompile(const SharedUnitFilesystemState& state) +SharedUnitCompileCheck shared_unit_compile_check(const SharedUnitFilesystemState& state) { - if(!state.source_exists) - return(true); - if(state.compiled_time == 0) - return(true); - if(state.compiled_time < state.required_time) - return(true); - if(!state.metadata_exists) - return(true); - if(!state.metadata_parsed) - return(true); - if(!state.abi_compatible) - return(true); - return(false); + SharedUnitCompileCheck result; + result.source_missing = !state.source_exists; + result.needs_compile = + !state.source_exists || + state.compiled_time == 0 || + state.compiled_time < state.required_time || + !state.metadata_exists || + !state.metadata_parsed || + !state.abi_compatible || + !state.input_signature_matches; + return(result); +} + +bool compiler_failure_retry_deferred(Request* context, SharedUnit* su, const SharedUnitFilesystemState& state) +{ + if(!context || !su) + return(false); + auto retry_seconds = compiler_failure_retry_seconds(context); + if(retry_seconds == 0) + return(false); + if(!state.compile_output_exists || state.compile_output_time == 0) + return(false); + if(state.source_time == 0) + return(false); + if(state.source_time > state.compile_output_time) + return(false); + return(time() < (u64)state.compile_output_time + retry_seconds); +} + +String compiler_failure_output_for_state(SharedUnit* su, const SharedUnitFilesystemState& state) +{ + if(!state.compile_output_exists) + return(""); + auto content = trim(state.compile_output_content); + if(content != "") + return(content); + if(su) + return(trim(su->compile_error_status)); + return(""); +} + +void compiler_restore_persisted_failure(SharedUnit* su, const SharedUnitFilesystemState& state, String status = "compile_error") +{ + if(!su) + return; + auto message = compiler_failure_output_for_state(su, state); + if(message == "") + message = "recent compilation failed"; + su->compiler_messages = message; + su->compile_status = status; + su->compile_error_status = message; + su->last_error = (state.compile_output_time != 0 ? state.compile_output_time : time()); } time_t compiler_runtime_abi_time(Request* context) @@ -196,6 +305,16 @@ auto compiler_with_registry_lock(Request* context, TCallback callback) -> declty return(result); } +bool compiler_has_known_unit_cached(Request* context, String file_name) +{ + if(!context) + return(false); + return(compiler_with_registry_lock(context, [&]() { + auto files = compiler_read_known_units_unlocked(context); + return(std::find(files.begin(), files.end(), file_name) != files.end()); + })); +} + SharedUnitFilesystemState inspect_shared_unit_filesystem(Request* context, SharedUnit* su) { SharedUnitFilesystemState state; @@ -211,27 +330,60 @@ SharedUnitFilesystemState inspect_shared_unit_filesystem(Request* context, Share state.runtime_binary_time = file_mtime(context->server->config["COMPILER_SYS_PATH"] + "/bin/uce_fastcgi.linux.bin"); state.compiler_abi_time = compiler_runtime_abi_time(context); state.metadata_time = file_mtime(su->meta_file_name); + state.compile_output_time = file_mtime(su->compile_output_file_name); + state.current_input_signature = compiler_unit_input_signature(context, su); state.metadata_exists = (state.metadata_time != 0); + state.compile_output_exists = (state.compile_output_time != 0); if(state.metadata_exists) - state.metadata_parsed = compiler_parse_unit_metadata_abi( - file_get_contents(su->meta_file_name), - state.metadata_abi_version - ); + { + state.metadata_content = file_get_contents(su->meta_file_name); + auto metadata = compiler_parse_unit_metadata(state.metadata_content); + auto abi_it = metadata.find("unit_abi_version"); + if(abi_it != metadata.end() && compiler_is_u64_string(abi_it->second)) + { + state.metadata_abi_version = (u64)atoll(abi_it->second.c_str()); + state.metadata_parsed = true; + } + auto input_it = metadata.find("input_signature"); + if(input_it != metadata.end()) + state.metadata_input_signature = input_it->second; + auto build_it = metadata.find("build_token"); + if(build_it != metadata.end()) + state.metadata_build_token = build_it->second; + } + if(state.compile_output_exists) + state.compile_output_content = file_get_contents(su->compile_output_file_name); state.abi_compatible = (state.metadata_parsed && state.metadata_abi_version == state.runtime_abi_version); + state.input_signature_matches = ( + state.metadata_parsed && + state.metadata_input_signature != "" && + state.current_input_signature != "" && + state.metadata_input_signature == state.current_input_signature + ); state.required_time = std::max({state.source_time, state.setup_template_time, state.compiler_abi_time}); state.compiled_time = file_mtime(su->so_name); return(state); } +void compiler_record_observed_filesystem_state(SharedUnit* su, const SharedUnitFilesystemState& state) +{ + if(!su) + return; + su->observed_compiled_time = state.compiled_time; + su->observed_metadata_content = state.metadata_content; +} + bool shared_unit_cache_is_stale(Request* context, SharedUnit* su) { if(!su) return(true); auto state = inspect_shared_unit_filesystem(context, su); - if(shared_unit_filesystem_requires_recompile(state)) + if(shared_unit_compile_check(state).needs_compile) return(true); - if(su->last_compiled != 0 && state.compiled_time != su->last_compiled) + if(state.compiled_time != su->observed_compiled_time) + return(true); + if(state.metadata_content != su->observed_metadata_content) return(true); return(false); } @@ -245,6 +397,45 @@ void release_shared_unit_cache_entry(Request* context, String file_name) context->server->units.erase(it); } +SharedUnit* compiler_cached_unit(Request* context, String file_name) +{ + auto it = context->server->units.find(file_name); + if(it == context->server->units.end()) + return(0); + return(it->second); +} + +bool compiler_cache_mode_matches(SharedUnit* su, bool opt_so_optional) +{ + return(su && su->opt_so_optional == opt_so_optional); +} + +bool compiler_cached_unit_is_reusable(Request* context, SharedUnit* su, bool opt_so_optional, bool force_recompile) +{ + return( + !force_recompile && + compiler_cache_mode_matches(su, opt_so_optional) && + !shared_unit_cache_is_stale(context, su) + ); +} + +SharedUnit* compiler_reusable_cached_unit(Request* context, String file_name, bool opt_so_optional, bool force_recompile) +{ + auto su = compiler_cached_unit(context, file_name); + if(compiler_cached_unit_is_reusable(context, su, opt_so_optional, force_recompile)) + return(su); + return(0); +} + +void compiler_release_cached_unit_if_needed(Request* context, String file_name, bool opt_so_optional, bool force_recompile) +{ + auto su = compiler_cached_unit(context, file_name); + if(!su) + return; + if(force_recompile || !compiler_cache_mode_matches(su, opt_so_optional) || shared_unit_cache_is_stale(context, su)) + release_shared_unit_cache_entry(context, file_name); +} + String compiler_current_unit_path(Request* context) { if(!context) @@ -354,11 +545,12 @@ String compiler_error_status(SharedUnit* su) String compiler_status_from_filesystem(const SharedUnitFilesystemState& state, SharedUnit* su = 0) { + auto compile_check = shared_unit_compile_check(state); if(!state.source_exists) return("missing_source"); if(state.compiled_time == 0 || !state.metadata_exists) return("not_compiled"); - if(shared_unit_filesystem_requires_recompile(state)) + if(compile_check.needs_compile) return("stale"); if(su && su->so_handle) return("loaded"); @@ -453,13 +645,12 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name) su->so_name = su->bin_path + "/" + su->bin_file_name; su->api_file_name = su->bin_path + "/" + su->src_file_name + ".exports.txt"; su->meta_file_name = su->bin_path + "/" + su->src_file_name + ".meta.txt"; + su->compile_output_file_name = su->bin_path + "/" + su->src_file_name + ".compile.txt"; //su->setup_file_name = su->bin_path + "/" + su->src_file_name + ".setup.h"; } -void load_shared_unit(Request* context, SharedUnit* su, String file_name) +void load_shared_unit(Request* context, SharedUnit* su) { - //setup_unit_paths(context, su, file_name); - su->on_render = 0; su->on_component = 0; su->on_websocket = 0; @@ -474,6 +665,12 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) su->compile_status = "not_compiled"; return; } + auto fs_state = inspect_shared_unit_filesystem(context, su); + if(compiler_failure_retry_deferred(context, su, fs_state)) + { + compiler_restore_persisted_failure(su, fs_state); + return; + } //printf("(i) unit file not found: %s\n", su->so_name.c_str()); su->compiler_messages = "unit file not found"; su->compile_status = "not_compiled"; @@ -525,14 +722,14 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name) return(result); }*/ -void compile_shared_unit(Request* context, SharedUnit* su, String file_name) +void compile_shared_unit(Request* context, SharedUnit* su) { - //setup_unit_paths(context, su, file_name); f64 comp_start = time_precise(); if(!file_exists(su->file_name)) { su->compiler_messages = "source file not found (" + su->file_name + ")"; + file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); compiler_untrack_known_unit(context, su->file_name); compiler_record_compile_result(su, time_precise() - comp_start, false, "missing_source", su->compiler_messages); return; @@ -554,14 +751,22 @@ void compile_shared_unit(Request* context, SharedUnit* su, String file_name) if(su->compiler_messages.length() > 0) { + file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", su->compiler_messages); printf("%s \n", su->compiler_messages.c_str()); } else { - load_shared_unit(context, su, file_name); + load_shared_unit(context, su); if(su->so_handle) - file_put_contents(su->meta_file_name, compiler_unit_metadata_text()); + { + file_put_contents(su->meta_file_name, compiler_unit_metadata_text(context, su)); + file_unlink(su->compile_output_file_name); + } + else if(trim(su->compiler_messages) != "") + { + file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); + } compiler_record_compile_result( su, time_precise() - comp_start, @@ -579,16 +784,11 @@ SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name { file_name = compiler_normalize_unit_path(context, file_name); - auto existing = context->server->units.find(file_name); - bool cache_mode_matches = ( - existing != context->server->units.end() && - existing->second->opt_so_optional == opt_so_optional - ); - if(!force_recompile && existing != context->server->units.end() && cache_mode_matches && !shared_unit_cache_is_stale(context, existing->second)) - return(existing->second); + auto cached = compiler_reusable_cached_unit(context, file_name, opt_so_optional, force_recompile); + if(cached) + return(cached); - if(existing != context->server->units.end()) - release_shared_unit_cache_entry(context, file_name); + compiler_release_cached_unit_if_needed(context, file_name, opt_so_optional, force_recompile); SharedUnit* su = new SharedUnit(); setup_unit_paths(context, su, file_name); @@ -598,38 +798,61 @@ SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name if(fdlock != -1) flock(fdlock, LOCK_EX); - existing = context->server->units.find(file_name); - cache_mode_matches = ( - existing != context->server->units.end() && - existing->second->opt_so_optional == opt_so_optional - ); - if(existing != context->server->units.end() && (force_recompile || !cache_mode_matches || shared_unit_cache_is_stale(context, existing->second))) - release_shared_unit_cache_entry(context, file_name); - existing = context->server->units.find(file_name); - cache_mode_matches = ( - existing != context->server->units.end() && - existing->second->opt_so_optional == opt_so_optional - ); - if(!force_recompile && existing != context->server->units.end() && cache_mode_matches && !shared_unit_cache_is_stale(context, existing->second)) + cached = compiler_reusable_cached_unit(context, file_name, opt_so_optional, force_recompile); + if(cached) { compiler_close_lock_file(fdlock); delete su; - return(existing->second); + return(cached); } + compiler_release_cached_unit_if_needed(context, file_name, opt_so_optional, force_recompile); + auto state = inspect_shared_unit_filesystem(context, su); - bool do_recompile = force_recompile || !state.source_exists || state.compiled_time == 0 || state.compiled_time < state.required_time; + auto compile_check = shared_unit_compile_check(state); + bool retry_deferred = compiler_failure_retry_deferred(context, su, state); + bool jit_enabled = compiler_jit_compile_on_request_enabled(context); + bool do_recompile = force_recompile || compile_check.needs_compile; if(do_recompile) { - compile_shared_unit(context, su, file_name); + if(!force_recompile && retry_deferred) + compiler_restore_persisted_failure(su, state); + else if(!force_recompile && !jit_enabled) + { + compiler_restore_persisted_failure(su, state, "jit_compile_disabled"); + if(trim(su->compiler_messages) == "") + { + su->compiler_messages = "JIT compilation on request is disabled"; + su->compile_error_status = su->compiler_messages; + } + } + else + compile_shared_unit(context, su); } else { - load_shared_unit(context, su, file_name); + load_shared_unit(context, su); if(!su->so_handle) - compile_shared_unit(context, su, file_name); + { + if(!force_recompile && retry_deferred) + compiler_restore_persisted_failure(su, state); + else if(!force_recompile && !jit_enabled) + { + compiler_restore_persisted_failure(su, state, "jit_compile_disabled"); + if(trim(su->compiler_messages) == "") + { + su->compiler_messages = "JIT compilation on request is disabled"; + su->compile_error_status = su->compiler_messages; + } + } + else + compile_shared_unit(context, su); + } } + auto observed_state = inspect_shared_unit_filesystem(context, su); + compiler_record_observed_filesystem_state(su, observed_state); + compiler_close_lock_file(fdlock); context->server->units[file_name] = su; @@ -728,13 +951,9 @@ StringList compiler_scan_site_units(Request* context) StringList compiler_list_known_units(Request* context) { - auto files = compiler_with_registry_lock(context, [&]() { + return(compiler_with_registry_lock(context, [&]() { return(compiler_read_known_units_unlocked(context)); - }); - context->server->known_unit_files.clear(); - for(auto& file_name : files) - context->server->known_unit_files[file_name] = true; - return(files); + })); } void compiler_set_known_units(Request* context, StringList files) @@ -744,24 +963,22 @@ void compiler_set_known_units(Request* context, StringList files) compiler_write_known_units_unlocked(context, files); return(0); }); - context->server->known_unit_files.clear(); - for(auto& file_name : files) - context->server->known_unit_files[file_name] = true; } void compiler_track_known_unit(Request* context, String file_name) { file_name = compiler_normalize_unit_path(context, file_name); - if(file_name == "" || !compiler_is_known_unit_file(file_name) || context->server->known_unit_files[file_name]) + if(file_name == "" || !compiler_is_known_unit_file(file_name)) return; compiler_with_registry_lock(context, [&]() { auto files = compiler_read_known_units_unlocked(context); + if(std::find(files.begin(), files.end(), file_name) != files.end()) + return(0); files.push_back(file_name); compiler_write_known_units_unlocked(context, files); return(0); }); - context->server->known_unit_files[file_name] = true; } void compiler_untrack_known_unit(Request* context, String file_name) @@ -776,7 +993,6 @@ void compiler_untrack_known_unit(Request* context, String file_name) compiler_write_known_units_unlocked(context, files); return(0); }); - context->server->known_unit_files.erase(file_name); } bool compiler_unit_needs_recompile(Request* context, String file_name, bool* source_missing) @@ -785,13 +1001,14 @@ bool compiler_unit_needs_recompile(Request* context, String file_name, bool* sou SharedUnit su; setup_unit_paths(context, &su, file_name); auto state = inspect_shared_unit_filesystem(context, &su); + auto compile_check = shared_unit_compile_check(state); if(source_missing) - *source_missing = !state.source_exists; - if(!state.source_exists) + *source_missing = compile_check.source_missing; + if(compile_check.source_missing) return(false); - if(state.compiled_time == 0) - return(true); - return(state.compiled_time < state.required_time); + if(compiler_failure_retry_deferred(context, &su, state)) + return(false); + return(compile_check.needs_compile); } DTree unit_info(String path) @@ -825,6 +1042,8 @@ DTree unit_info(String path) } auto fs_state = inspect_shared_unit_filesystem(context, su); + if(su->compile_status == "unknown" && compiler_failure_retry_deferred(context, su, fs_state)) + compiler_restore_persisted_failure(su, fs_state); auto exports_text = compiler_unit_exports_text(su); auto exports = compiler_unit_exports(su); if(exports.size() == 0 && exports_text != "") @@ -841,6 +1060,7 @@ DTree unit_info(String path) info["so_name"] = su->so_name; info["api_file_name"] = su->api_file_name; info["meta_file_name"] = su->meta_file_name; + info["compile_output_file_name"] = su->compile_output_file_name; info["compile_status"] = (su->compile_status != "unknown" ? su->compile_status : compiler_status_from_filesystem(fs_state, su)); info["compile_error_status"] = su->compile_error_status; info["runtime_error_status"] = su->runtime_error_status; @@ -867,11 +1087,15 @@ DTree unit_info(String path) info["source_mtime"] = (f64)fs_state.source_time; info["compiled_mtime"] = (f64)fs_state.compiled_time; info["metadata_mtime"] = (f64)fs_state.metadata_time; + info["compile_output_mtime"] = (f64)fs_state.compile_output_time; info["setup_template_mtime"] = (f64)fs_state.setup_template_time; info["required_mtime"] = (f64)fs_state.required_time; info["runtime_abi_version"] = (f64)fs_state.runtime_abi_version; info["metadata_abi_version"] = (f64)fs_state.metadata_abi_version; - compiler_tree_set_bool(info, "known", context->server->known_unit_files[resolved_path]); + info["current_input_signature"] = fs_state.current_input_signature; + info["metadata_input_signature"] = fs_state.metadata_input_signature; + info["metadata_build_token"] = fs_state.metadata_build_token; + compiler_tree_set_bool(info, "known", compiler_has_known_unit_cached(context, resolved_path)); compiler_tree_set_bool(info, "current_unit", resolved_path == compiler_current_unit_path(context)); compiler_tree_set_bool(info, "loaded", su->so_handle != 0); compiler_tree_set_bool(info, "source_exists", fs_state.source_exists); @@ -879,7 +1103,10 @@ DTree unit_info(String path) compiler_tree_set_bool(info, "metadata_exists", fs_state.metadata_exists); compiler_tree_set_bool(info, "metadata_parsed", fs_state.metadata_parsed); compiler_tree_set_bool(info, "abi_compatible", fs_state.abi_compatible); - compiler_tree_set_bool(info, "stale", shared_unit_cache_is_stale(context, su)); + compiler_tree_set_bool(info, "input_signature_matches", fs_state.input_signature_matches); + compiler_tree_set_bool(info, "stale", shared_unit_compile_check(fs_state).needs_compile && !compiler_failure_retry_deferred(context, su, fs_state)); + compiler_tree_set_bool(info, "retry_deferred", compiler_failure_retry_deferred(context, su, fs_state)); + compiler_tree_set_bool(info, "cache_stale", shared_unit_cache_is_stale(context, su)); compiler_tree_set_bool(info, "has_render", su->on_render != 0); compiler_tree_set_bool(info, "has_component", su->on_component != 0); compiler_tree_set_bool(info, "has_websocket", su->on_websocket != 0); diff --git a/src/lib/compiler.h b/src/lib/compiler.h index d0cdf9a..6a8c7c6 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -7,8 +7,8 @@ String preprocess_shared_unit(Request* context, SharedUnit* su); void setup_unit_paths(Request* context, SharedUnit* su, String file_name); -void load_shared_unit(Request* context, SharedUnit* su, String file_name); -void compile_shared_unit(Request* context, SharedUnit* su, String file_name); +void load_shared_unit(Request* context, SharedUnit* su); +void compile_shared_unit(Request* context, SharedUnit* su); SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_optional = false); void compiler_invoke(Request* context, String file_name); void compiler_invoke_websocket(Request* context, String file_name); diff --git a/src/lib/dtree.cpp b/src/lib/dtree.cpp index 8dc81c8..9156419 100644 --- a/src/lib/dtree.cpp +++ b/src/lib/dtree.cpp @@ -1,4 +1,8 @@ +#include +#include +#include + namespace { template @@ -30,6 +34,110 @@ bool dtree_key_is_index(String key, s64 expected_index = -1) return(true); } +String dtree_trim(String raw) +{ + if(raw == "") + return(raw); + + size_t start = 0; + while(start < raw.length() && isspace(raw[start])) + start += 1; + + size_t end = raw.length(); + while(end > start && isspace(raw[end - 1])) + end -= 1; + + return(raw.substr(start, end - start)); +} + +String dtree_lower(String raw) +{ + for(auto& c : raw) + c = (char)tolower(c); + return(raw); +} + +bool dtree_string_to_bool_value(String raw, bool& value_out) +{ + raw = dtree_lower(dtree_trim(raw)); + if(raw == "") + return(false); + if(raw == "1" || raw == "true" || raw == "(true)" || raw == "yes" || raw == "on") + { + value_out = true; + return(true); + } + if(raw == "0" || raw == "false" || raw == "(false)" || raw == "no" || raw == "off" || raw == "null") + { + value_out = false; + return(true); + } + return(false); +} + +bool dtree_string_to_f64_value(String raw, f64& value_out) +{ + raw = dtree_trim(raw); + if(raw == "") + return(false); + + bool bool_value = false; + if(dtree_string_to_bool_value(raw, bool_value)) + { + value_out = (bool_value ? 1.0 : 0.0); + return(true); + } + + char* end = 0; + value_out = strtod(raw.c_str(), &end); + if(end == raw.c_str()) + return(false); + while(end && *end != 0) + { + if(!isspace(*end)) + return(false); + end += 1; + } + return(std::isfinite(value_out)); +} + +const DTree* dtree_scalar_map_value(const DTree& tree) +{ + if(tree.type != 'M' || tree._map.size() != 1) + return(0); + auto it = tree._map.begin(); + if(it == tree._map.end()) + return(0); + return(&it->second.deref()); +} + +f64 dtree_clamp_to_f64_range(long double value) +{ + if(value > std::numeric_limits::max()) + return(std::numeric_limits::max()); + if(value < -std::numeric_limits::max()) + return(-std::numeric_limits::max()); + return((f64)value); +} + +s64 dtree_clamp_to_s64_range(long double value) +{ + if(value > (long double)std::numeric_limits::max()) + return(std::numeric_limits::max()); + if(value < (long double)std::numeric_limits::min()) + return(std::numeric_limits::min()); + return((s64)value); +} + +u64 dtree_clamp_to_u64_range(long double value) +{ + if(value <= 0) + return(0); + if(value > (long double)std::numeric_limits::max()) + return(std::numeric_limits::max()); + return((u64)value); +} + } void DTree::each(std::function f) @@ -98,6 +206,158 @@ String DTree::to_string() return(""); } +s64 DTree::to_s64() +{ + const DTree& target = deref(); + switch(target.type) + { + case('S'): + { + f64 value = 0; + if(!dtree_string_to_f64_value(target._String, value)) + return(0); + return(dtree_clamp_to_s64_range((long double)value)); + } + case('F'): + return(dtree_clamp_to_s64_range((long double)target._float)); + case('B'): + return(target._bool ? 1 : 0); + case('M'): + { + const DTree* item = dtree_scalar_map_value(target); + if(item) + return(const_cast(item)->to_s64()); + return(0); + } + case('P'): + return(dtree_clamp_to_s64_range((long double)(u64)target._ptr)); + case('R'): + return(0); + } + return(0); +} + +u64 DTree::to_u64() +{ + const DTree& target = deref(); + switch(target.type) + { + case('S'): + { + f64 value = 0; + if(!dtree_string_to_f64_value(target._String, value)) + return(0); + return(dtree_clamp_to_u64_range((long double)value)); + } + case('F'): + return(dtree_clamp_to_u64_range((long double)target._float)); + case('B'): + return(target._bool ? 1 : 0); + case('M'): + { + const DTree* item = dtree_scalar_map_value(target); + if(item) + return(const_cast(item)->to_u64()); + return(0); + } + case('P'): + return((u64)target._ptr); + case('R'): + return(0); + } + return(0); +} + +f64 DTree::to_f64() +{ + const DTree& target = deref(); + switch(target.type) + { + case('S'): + { + f64 value = 0; + if(!dtree_string_to_f64_value(target._String, value)) + return(0); + return(value); + } + case('F'): + return(target._float); + case('B'): + return(target._bool ? 1.0 : 0.0); + case('M'): + { + const DTree* item = dtree_scalar_map_value(target); + if(item) + return(const_cast(item)->to_f64()); + return(0); + } + case('P'): + return(dtree_clamp_to_f64_range((long double)(u64)target._ptr)); + case('R'): + return(0); + } + return(0); +} + +bool DTree::to_bool() +{ + const DTree& target = deref(); + switch(target.type) + { + case('S'): + { + bool value = false; + if(dtree_string_to_bool_value(target._String, value)) + return(value); + f64 numeric_value = 0; + if(dtree_string_to_f64_value(target._String, numeric_value)) + return(numeric_value != 0); + return(dtree_trim(target._String) != ""); + } + case('F'): + return(target._float != 0); + case('B'): + return(target._bool); + case('M'): + { + const DTree* item = dtree_scalar_map_value(target); + if(item) + return(const_cast(item)->to_bool()); + return(target._map.size() > 0); + } + case('P'): + return(target._ptr != 0); + case('R'): + return(false); + } + return(false); +} + +StringMap DTree::to_stringmap() +{ + const DTree& target = deref(); + StringMap result; + switch(target.type) + { + case('M'): + for(const auto& entry : target._map) + result[entry.first] = const_cast(entry.second.deref()).to_string(); + break; + case('S'): + if(dtree_trim(target._String) != "") + result["value"] = target._String; + break; + case('F'): + case('B'): + case('P'): + result["value"] = const_cast(target).to_string(); + break; + case('R'): + break; + } + return(result); +} + String DTree::to_json(char quote_char) { const DTree& target = deref(); @@ -350,6 +610,7 @@ void DTree::set(StringMap source) return; } set_type('M'); + _map.clear(); _array_index = 0; _list_mode = false; for (auto it = source.begin(); it != source.end(); ++it) @@ -378,12 +639,46 @@ void DTree::set_reference(DTree* target) _ptr = target; } +bool DTree::has(String s) const +{ + const DTree& target = deref(); + if(target.type != 'M') + return(false); + return(target._map.find(s) != target._map.end()); +} + DTree* DTree::key(String s) { DTree* target = reference_target(); if(target) return(target->key(s)); + if(type != 'M') + return(0); + auto it = _map.find(s); + if(it == _map.end()) + return(0); + return(&it->second); +} + +const DTree* DTree::key(String s) const +{ + const DTree& target = deref(); + if(target.type != 'M') + return(0); + auto it = target._map.find(s); + if(it == target._map.end()) + return(0); + return(&it->second); +} + +DTree* DTree::get_or_create(String s) +{ + DTree* target = reference_target(); + if(target) + return(target->get_or_create(s)); set_type('M'); + if(_list_mode && !dtree_key_is_index(s)) + _list_mode = false; return(&_map[s]); } @@ -391,10 +686,7 @@ DTree& DTree::operator [] (String s) { DTree* target = reference_target(); if(target) return((*target)[s]); - set_type('M'); - if(_list_mode && !dtree_key_is_index(s)) - _list_mode = false; - return(_map[s]); + return(*get_or_create(s)); } void DTree::operator = (String v) { set(v); } diff --git a/src/lib/dtree.h b/src/lib/dtree.h index 8e082e8..9bd080d 100644 --- a/src/lib/dtree.h +++ b/src/lib/dtree.h @@ -2,6 +2,11 @@ String json_escape(String s, char quote_char = '"'); +// DTree is UCE's general-purpose structured value container. +// It stores scalar values, nested map/list-like values, and internal references. +// Numeric and boolean reads are intentionally permissive so request data, +// JSON-decoded values, and metadata trees can be consumed without repetitive +// manual parsing at each call site. struct DTree { char type = 'S'; @@ -18,6 +23,11 @@ struct DTree { bool is_array(); bool is_list() const; String to_string(); + s64 to_s64(); + u64 to_u64(); + f64 to_f64(); + bool to_bool(); + StringMap to_stringmap(); String to_json(char quote_char = '"'); String get_type_name(); DTree get_by_path(String path, String delim = "/"); @@ -35,7 +45,10 @@ struct DTree { void set(StringMap source); void set_array(); void set_reference(DTree* target); + bool has(String s) const; DTree* key(String s); + const DTree* key(String s) const; + DTree* get_or_create(String s); DTree& operator [] (String s); void operator = (String v); void operator = (f64 v); diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 7b2886f..00df1d7 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -31,7 +31,7 @@ String capture_backtrace_string(u32 max_frames, u32 skip_frames) trace += "\n"; } free(symbols); - return(trace); + return(trace); } String signal_name(int sig) @@ -49,6 +49,40 @@ String signal_name(int sig) } } +namespace { + +String time_format_expand_delta_tokens(String format, u64 timestamp, u64 now_timestamp) +{ + u64 delta_seconds = 0; + if(now_timestamp > timestamp) + delta_seconds = now_timestamp - timestamp; + + format = replace(format, "%deltaY", std::to_string(delta_seconds / (60 * 60 * 24 * 365))); + format = replace(format, "%deltam", std::to_string(delta_seconds / (60 * 60 * 24 * 30))); + format = replace(format, "%deltad", std::to_string(delta_seconds / (60 * 60 * 24))); + format = replace(format, "%deltaH", std::to_string(delta_seconds / (60 * 60))); + format = replace(format, "%deltaM", std::to_string(delta_seconds / 60)); + format = replace(format, "%deltaS", std::to_string(delta_seconds)); + return(format); +} + +String time_format_shell(String format, u64 timestamp, bool use_utc) +{ + String ts; + String fmt; + u64 effective_timestamp = (timestamp > 0 ? timestamp : time()); + String expanded_format = time_format_expand_delta_tokens(format, effective_timestamp, time()); + + if(timestamp > 0) + ts = String("-d '@")+std::to_string(timestamp)+"'"; + if(expanded_format != "") + fmt = String("+'")+expanded_format+"'"; + + return(trim(shell_exec(String("date ") + (use_utc ? "-u " : "") + ts + " " + fmt))); +} + +} + String shell_exec(String cmd) { //printf("(i) shell_exec(%s)\n", cmd.c_str()); @@ -154,63 +188,117 @@ bool file_exists(String path) return(std::filesystem::exists(fp)); } -String file_get_contents(String file_name) +int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode) { - /*std::ifstream ifs(file_name); - printf("stream file desc %i\n", ifs.filedesc()); - String content( - (std::istreambuf_iterator(ifs) ), - (std::istreambuf_iterator() ) );*/ + int fd = open(file_name.c_str(), open_flags, create_mode); + if(fd == -1) + return(-1); + if(flock(fd, lock_type) == -1) + { + close(fd); + return(-1); + } + return(fd); +} + +void file_close_locked(int fd) +{ + if(fd == -1) + return; + flock(fd, LOCK_UN); + close(fd); +} + +String file_get_contents_locked_fd(int fd) +{ + if(fd == -1) + return(""); + char buf[512]; String content; - s32 fd = open(file_name.c_str(), O_RDONLY); + s64 bytes_read = 0; + lseek(fd, 0, SEEK_SET); + while((bytes_read = read(fd, buf, sizeof(buf))) > 0) + content.append(buf, bytes_read); + return(content); +} + +namespace { + +bool file_write_all(int fd, const char* data, size_t remaining) +{ + while(remaining > 0) + { + auto bytes_written = write(fd, data, remaining); + if(bytes_written < 0) + return(false); + data += bytes_written; + remaining -= bytes_written; + } + return(true); +} + +} + +bool file_put_contents_locked_fd(int fd, String content) +{ + if(fd == -1) + return(false); + lseek(fd, 0, SEEK_SET); + if(ftruncate(fd, 0) != 0) + return(false); + if(!file_write_all(fd, content.data(), content.length())) + return(false); + return(true); +} + +String file_get_contents(String file_name) +{ + s32 fd = file_open_locked(file_name, O_RDONLY, LOCK_SH); if(fd == -1) { printf("(!) Could not read %s\n", file_name.c_str()); return(""); } - flock(fd, LOCK_SH); - s64 bytes_read = 0; - //s64 size = lseek(fd, 0, SEEK_END); - //lseek(fd, 0, SEEK_SET); - //content.reserve(size+1); - - while((bytes_read = read(fd, buf, 512)) > 0) - { - content.append(buf, bytes_read); - } - - flock(fd, LOCK_UN); - close(fd); + String content = file_get_contents_locked_fd(fd); + file_close_locked(fd); return(content); } bool file_put_contents(String file_name, String content) { - s32 fd = open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); if(fd == -1) { printf("(!) Could not write %s\n", file_name.c_str()); return(false); } - flock(fd, LOCK_EX); - const char* data = content.data(); - size_t remaining = content.length(); - while(remaining > 0) + bool ok = file_put_contents_locked_fd(fd, content); + file_close_locked(fd); + if(!ok) { - auto bytes_written = write(fd, data, remaining); - if(bytes_written < 0) - { - flock(fd, LOCK_UN); - close(fd); - printf("(!) Could not fully write %s\n", file_name.c_str()); - return(false); - } - data += bytes_written; - remaining -= bytes_written; + printf("(!) Could not fully write %s\n", file_name.c_str()); + return(false); + } + return(true); +} + +bool file_append_contents(String file_name, String content) +{ + s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); + if(fd == -1) + { + printf("(!) Could not append %s\n", file_name.c_str()); + return(false); + } + lseek(fd, 0, SEEK_END); + bool ok = file_write_all(fd, content.data(), content.length()); + file_close_locked(fd); + if(!ok) + { + printf("(!) Could not fully append %s\n", file_name.c_str()); + return(false); } - flock(fd, LOCK_UN); - close(fd); return(true); } @@ -284,26 +372,34 @@ u64 time() String time_format_local(String format, u64 timestamp) { - String ts; - String fmt; - if(timestamp > 0) - ts = String("-d '@")+std::to_string(timestamp)+"'"; - if(format != "") - fmt = String("+'"+format+"'"); - return(trim(shell_exec("date "+ts+" "+fmt))); + return(time_format_shell(format, timestamp, false)); } String time_format_utc(String format, u64 timestamp) { - String ts; - String fmt; - if(timestamp > 0) - ts = String("-d '@")+std::to_string(timestamp)+"'"; if(format == "RFC1123") format = "%a, %d %b %Y %T GMT"; - if(format != "") - fmt = String("+'"+format+"'"); - return(trim(shell_exec("date -u "+ts+" "+fmt))); + return(time_format_shell(format, timestamp, true)); +} + +String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) +{ + u64 now_timestamp = time(); + u64 delta_seconds = 0; + if(now_timestamp > timestamp) + delta_seconds = now_timestamp - timestamp; + + format_very_recent = first(format_very_recent, "just now"); + medium_recency_seconds = (medium_recency_seconds > 0 ? medium_recency_seconds : 90); + format_medium_recent = first(format_medium_recent, "%deltaM minutes ago"); + not_recent_seconds = (not_recent_seconds > 0 ? not_recent_seconds : 90 * 60); + format_not_recent = first(format_not_recent, "%deltaH hours ago"); + + if(delta_seconds < medium_recency_seconds) + return(time_format_expand_delta_tokens(format_very_recent, timestamp, now_timestamp)); + if(delta_seconds < not_recent_seconds) + return(time_format_expand_delta_tokens(format_medium_recent, timestamp, now_timestamp)); + return(time_format_expand_delta_tokens(format_not_recent, timestamp, now_timestamp)); } u64 time_parse(String time_String) @@ -511,21 +607,29 @@ int task_kill(pid_t pid, int sig) pid_t task_pid(String key) { String status_file_name = context->server->config["BIN_DIRECTORY"] + "/task-" + key; + String lock_file_name = status_file_name + ".lock"; + int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); String status_file = file_get_contents(status_file_name); pid_t p = 0; if(status_file != "") { p = int_val(status_file); if(task_kill(p, 0) == 0) // process is still running + { + file_close_locked(lock_fd); return(p); + } file_unlink(status_file_name); } + file_close_locked(lock_fd); return(p); } pid_t task(String key, std::function exec_after_spawn, u64 timeout) { String status_file_name = context->server->config["BIN_DIRECTORY"] + "/task-" + key; + String lock_file_name = status_file_name + ".lock"; + int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); String status_file = file_get_contents(status_file_name); pid_t p; if(status_file != "") @@ -534,6 +638,7 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) if(task_kill(p, 0) == 0) // process is still running { printf("(P) worker process '%s' already running: PID %i\n", key.c_str(), p); + file_close_locked(lock_fd); return(p); } //printf("(P) worker process '%s' had crashed: PID %i\n", key.c_str(), p); @@ -542,27 +647,31 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) p = fork(); if(p == 0) { + file_close_locked(lock_fd); my_pid = getpid(); - file_put_contents(status_file_name, std::to_string(my_pid)); close(context->resources.client_socket); context->resources.client_socket = 0; //printf("(C) child procress started, PID:%i\n", my_pid); //prctl(PR_SET_PDEATHSIG, SIGHUP); exec_after_spawn(); + int exit_lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); file_unlink(status_file_name); + file_close_locked(exit_lock_fd); printf("(P) worker process '%s' terminated: PID %i\n", key.c_str(), my_pid); exit(0); } else { + file_put_contents(status_file_name, std::to_string(p)); + file_close_locked(lock_fd); printf("(P) worker process '%s' spawned: PID %i\n", key.c_str(), p); return(p); } } #include -pid_t task_repeat(String key, u64 interval, std::function exec_after_spawn, u64 timeout) +pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout) { auto repeater_function = [&]() { while (true) @@ -610,6 +719,9 @@ StringMap make_server_settings() cfg["COMPILER_SYS_PATH"] = "."; cfg["PRECOMPILE_FILES_IN"] = ""; cfg["SITE_DIRECTORY"] = "site"; + cfg["JIT_COMPILE_ON_REQUEST"] = "1"; + cfg["PROACTIVE_COMPILE_ENABLED"] = "1"; + cfg["COMPILE_FAILURE_RETRY_SECONDS"] = std::to_string(10); cfg["PROACTIVE_COMPILE_CHECK_INTERVAL"] = std::to_string(60); cfg["HTTP_PORT"] = std::to_string(8080); diff --git a/src/lib/sys.h b/src/lib/sys.h index 08de282..0d16460 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include String shell_exec(String cmd); String shell_escape(String raw); @@ -9,17 +11,19 @@ String dirname(String fn); String path_join(String base, String child); bool mkdir(String path); bool file_exists(String path); +int file_open_locked(String file_name, int open_flags, int lock_type = LOCK_SH, int create_mode = 0644); +void file_close_locked(int fd); +String file_get_contents_locked_fd(int fd); +bool file_put_contents_locked_fd(int fd, String content); String file_get_contents(String file_name); bool file_put_contents(String file_name, String content); -#include +bool file_append_contents(String file_name, String content); template bool file_append(String file_name, Ts... args) { - std::ofstream fout; - fout.open(file_name.c_str(), std::ios_base::app); - ((fout << args), ...); - fout.close(); - return(true); + std::ostringstream out; + ((out << args), ...); + return(file_append_contents(file_name, out.str())); } String cwd_get(); void cwd_set(String path); @@ -32,6 +36,7 @@ f64 time_precise(); u64 time(); String time_format_local(String format = "", u64 timestamp = 0); String time_format_utc(String format = "", u64 timestamp = 0); +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 = ""); u64 time_parse(String time_String); u64 socket_connect(String host, short port); diff --git a/src/lib/types.cpp b/src/lib/types.cpp index c627d84..03ede5c 100644 --- a/src/lib/types.cpp +++ b/src/lib/types.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -97,6 +98,11 @@ Request::~Request() delete stream; ob_stack.clear(); ob = 0; + if(session_lock_fd != -1) + { + flock(session_lock_fd, LOCK_UN); + close(session_lock_fd); + } for(auto& sockfd : resources.sockets) close(sockfd); } diff --git a/src/lib/types.h b/src/lib/types.h index 9903617..049c352 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -66,6 +66,7 @@ struct SharedUnit { String so_name; String api_file_name; String meta_file_name; + String compile_output_file_name; String setup_file_name; StringList api_declarations; std::map api_functions; @@ -89,9 +90,11 @@ struct SharedUnit { String compile_error_status = ""; String runtime_error_status = ""; time_t last_compiled = 0; + time_t observed_compiled_time = 0; time_t last_loaded = 0; time_t last_rendered = 0; time_t last_error = 0; + String observed_metadata_content = ""; u64 request_count = 0; u64 invoke_count = 0; @@ -125,7 +128,6 @@ struct UploadedFile { struct ServerState { std::map units; - std::map known_unit_files; StringMap config; u32 request_count = 0; @@ -153,6 +155,9 @@ struct Request { StringMap post; StringMap cookies; StringMap session; + String session_file_name = ""; + String session_serialized = ""; + int session_lock_fd = -1; DTree var; DTree cfg; diff --git a/src/lib/uri.cpp b/src/lib/uri.cpp index d386f59..f049dda 100644 --- a/src/lib/uri.cpp +++ b/src/lib/uri.cpp @@ -542,27 +542,70 @@ String session_file_path(String session_id) return(context->server->config["SESSION_PATH"] + "/" + session_id); } +namespace { + +void session_release_lock(Request* request) +{ + if(!request) + return; + if(request->session_lock_fd != -1) + file_close_locked(request->session_lock_fd); + request->session_lock_fd = -1; + request->session_file_name = ""; + request->session_serialized = ""; +} + +} + StringMap load_session_data(String session_id) { String session_path = session_file_path(session_id); if(session_path == "") return(StringMap()); + if(context && context->session_lock_fd != -1 && context->session_file_name == session_path) + return(parse_query(file_get_contents_locked_fd(context->session_lock_fd))); return(parse_query(file_get_contents(session_path))); } void save_session_data(String session_id, StringMap data) { String session_path = session_file_path(session_id); + String encoded = encode_query(data); if(session_path == "") { printf("(!) Refusing to save invalid session id\n"); return; } - file_put_contents(session_path, encode_query(data)); + if(context && context->session_lock_fd != -1 && context->session_file_name == session_path) + { + if(encoded == context->session_serialized) + return; + if(file_put_contents_locked_fd(context->session_lock_fd, encoded)) + context->session_serialized = encoded; + return; + } + int fd = file_open_locked(session_path, O_RDWR | O_CREAT, LOCK_EX, 0644); + if(fd == -1) + { + printf("(!) Refusing to save unreadable session file %s\n", session_path.c_str()); + return; + } + if(file_get_contents_locked_fd(fd) != encoded) + file_put_contents_locked_fd(fd, encoded); + file_close_locked(fd); } String session_start(String session_name) { + if(context->session_lock_fd != -1 && context->session_name == session_name && context->session_id != "") + return(context->session_id); + session_release_lock(context); + context->session.clear(); + context->session_serialized = ""; + context->session_file_name = ""; + context->session_id = ""; + context->session_name = ""; + String session_id = context->cookies[session_name]; if(!is_valid_session_id(session_id)) session_id = ""; @@ -574,7 +617,15 @@ String session_start(String session_name) } context->session_id = session_id; context->session_name = session_name; + context->session_file_name = session_file_path(context->session_id); + if(context->session_file_name != "") + { + context->session_lock_fd = file_open_locked(context->session_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); + if(context->session_lock_fd == -1) + printf("(!) Could not lock session file %s\n", context->session_file_name.c_str()); + } context->session = load_session_data(context->session_id); + context->session_serialized = encode_query(context->session); return(context->session_id); } @@ -585,6 +636,7 @@ void session_destroy(String session_name) set_cookie(session_name, "", time() - int_val(context->server->config["SESSION_TIME"])); context->session.clear(); save_session_data(context->session_id, context->session); + session_release_lock(context); context->session_id = ""; } } diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 2f131f7..e0f6c3e 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -145,6 +145,18 @@ String ws_message() return(context->call["message"].to_string()); } +bool config_truthy(String raw, bool default_value = true) +{ + raw = to_lower(trim(raw)); + if(raw == "") + return(default_value); + if(raw == "1" || raw == "true" || raw == "yes" || raw == "on") + return(true); + if(raw == "0" || raw == "false" || raw == "no" || raw == "off") + return(false); + return(default_value); +} + String ws_connection_id() { if(!context) @@ -419,17 +431,22 @@ void run_proactive_compiler() { Request background_context; StringList compile_queue; + background_context.server = &server_state; + if(!config_truthy(server_state.config["PROACTIVE_COMPILE_ENABLED"], true)) + return; f64 check_interval = float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]); f64 failure_retry_interval = 0; f64 next_scan_at = 0; std::map retry_after; if(check_interval < 1) check_interval = 1; - failure_retry_interval = std::max(check_interval, 60.0); + failure_retry_interval = std::max( + check_interval, + (f64)std::max((s64)10, (s64)int_val(server_state.config["COMPILE_FAILURE_RETRY_SECONDS"])) + ); my_pid = getpid(); context = &background_context; - background_context.server = &server_state; close_inherited_server_sockets(); signal(SIGSEGV, on_segfault); @@ -502,6 +519,8 @@ void run_proactive_compiler() { retry_after.erase(file_name); } + background_context.session.clear(); + background_context.session_serialized = ""; clear_shared_unit_cache(server_state); usleep(250000); continue; @@ -518,6 +537,8 @@ bool proactive_compiler_alive() void ensure_proactive_compiler() { + if(!config_truthy(server_state.config["PROACTIVE_COMPILE_ENABLED"], true)) + return; if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0) return;