diff --git a/site/test/call_file.uce b/site/demo/call_file.uce similarity index 100% rename from site/test/call_file.uce rename to site/demo/call_file.uce diff --git a/site/test/call_file_funcs.uce b/site/demo/call_file_funcs.uce similarity index 100% rename from site/test/call_file_funcs.uce rename to site/demo/call_file_funcs.uce diff --git a/site/test/components.uce b/site/demo/components.uce similarity index 100% rename from site/test/components.uce rename to site/demo/components.uce diff --git a/site/test/components/card.uce b/site/demo/components/card.uce similarity index 100% rename from site/test/components/card.uce rename to site/demo/components/card.uce diff --git a/site/test/components/markdown/code_block.uce b/site/demo/components/markdown/code_block.uce similarity index 100% rename from site/test/components/markdown/code_block.uce rename to site/demo/components/markdown/code_block.uce diff --git a/site/test/components/markdown/warning.uce b/site/demo/components/markdown/warning.uce similarity index 100% rename from site/test/components/markdown/warning.uce rename to site/demo/components/markdown/warning.uce diff --git a/site/test/cookie.uce b/site/demo/cookie.uce similarity index 100% rename from site/test/cookie.uce rename to site/demo/cookie.uce diff --git a/site/test/demo_guard.h b/site/demo/demo_guard.h similarity index 100% rename from site/test/demo_guard.h rename to site/demo/demo_guard.h diff --git a/site/test/dtree.uce b/site/demo/dtree.uce similarity index 100% rename from site/test/dtree.uce rename to site/demo/dtree.uce diff --git a/site/test/empty.uce b/site/demo/empty.uce similarity index 100% rename from site/test/empty.uce rename to site/demo/empty.uce diff --git a/site/test/error-reporting.uce b/site/demo/error-reporting.uce similarity index 100% rename from site/test/error-reporting.uce rename to site/demo/error-reporting.uce diff --git a/site/test/file_append.uce b/site/demo/file_append.uce similarity index 100% rename from site/test/file_append.uce rename to site/demo/file_append.uce diff --git a/site/test/fileio.uce b/site/demo/fileio.uce similarity index 100% rename from site/test/fileio.uce rename to site/demo/fileio.uce diff --git a/site/test/header.uce b/site/demo/header.uce similarity index 100% rename from site/test/header.uce rename to site/demo/header.uce diff --git a/site/test/hello.uce b/site/demo/hello.uce similarity index 100% rename from site/test/hello.uce rename to site/demo/hello.uce diff --git a/site/test/inc-test.cpp b/site/demo/inc-test.cpp similarity index 100% rename from site/test/inc-test.cpp rename to site/demo/inc-test.cpp diff --git a/site/test/index.uce b/site/demo/index.uce similarity index 96% rename from site/test/index.uce rename to site/demo/index.uce index df513a1..e2a4b1d 100644 --- a/site/test/index.uce +++ b/site/demo/index.uce @@ -27,6 +27,9 @@ RENDER(Request& context) API Docs →
+
Coverage
+ +
Basics
diff --git a/site/test/json.uce b/site/demo/json.uce similarity index 100% rename from site/test/json.uce rename to site/demo/json.uce diff --git a/site/test/markdown-example.md b/site/demo/markdown-example.md similarity index 100% rename from site/test/markdown-example.md rename to site/demo/markdown-example.md diff --git a/site/test/markdown.uce b/site/demo/markdown.uce similarity index 100% rename from site/test/markdown.uce rename to site/demo/markdown.uce diff --git a/site/test/memcached.uce b/site/demo/memcached.uce similarity index 100% rename from site/test/memcached.uce rename to site/demo/memcached.uce diff --git a/site/test/mysql.uce b/site/demo/mysql.uce similarity index 100% rename from site/test/mysql.uce rename to site/demo/mysql.uce diff --git a/site/test/parse_time.uce b/site/demo/parse_time.uce similarity index 100% rename from site/test/parse_time.uce rename to site/demo/parse_time.uce diff --git a/site/test/post-multipart.uce b/site/demo/post-multipart.uce similarity index 100% rename from site/test/post-multipart.uce rename to site/demo/post-multipart.uce diff --git a/site/test/post.uce b/site/demo/post.uce similarity index 100% rename from site/test/post.uce rename to site/demo/post.uce diff --git a/site/test/preprocessor-comments.uce b/site/demo/preprocessor-comments.uce similarity index 100% rename from site/test/preprocessor-comments.uce rename to site/demo/preprocessor-comments.uce diff --git a/site/test/random.uce b/site/demo/random.uce similarity index 100% rename from site/test/random.uce rename to site/demo/random.uce diff --git a/site/test/script-example1.usp b/site/demo/script-example1.usp similarity index 100% rename from site/test/script-example1.usp rename to site/demo/script-example1.usp diff --git a/site/test/script.h b/site/demo/script.h similarity index 100% rename from site/test/script.h rename to site/demo/script.h diff --git a/site/test/script.uce b/site/demo/script.uce similarity index 100% rename from site/test/script.uce rename to site/demo/script.uce diff --git a/site/test/session.uce b/site/demo/session.uce similarity index 100% rename from site/test/session.uce rename to site/demo/session.uce diff --git a/site/test/sharedunit.uce b/site/demo/sharedunit.uce similarity index 100% rename from site/test/sharedunit.uce rename to site/demo/sharedunit.uce diff --git a/site/test/shell.uce b/site/demo/shell.uce similarity index 100% rename from site/test/shell.uce rename to site/demo/shell.uce diff --git a/site/test/str_replace.uce b/site/demo/str_replace.uce similarity index 100% rename from site/test/str_replace.uce rename to site/demo/str_replace.uce diff --git a/site/test/string.uce b/site/demo/string.uce similarity index 100% rename from site/test/string.uce rename to site/demo/string.uce diff --git a/site/test/style.css b/site/demo/style.css similarity index 100% rename from site/test/style.css rename to site/demo/style.css diff --git a/site/test/task-status.uce b/site/demo/task-status.uce similarity index 100% rename from site/test/task-status.uce rename to site/demo/task-status.uce diff --git a/site/test/task.uce b/site/demo/task.uce similarity index 100% rename from site/test/task.uce rename to site/demo/task.uce diff --git a/site/test/task_repeat.uce b/site/demo/task_repeat.uce similarity index 100% rename from site/test/task_repeat.uce rename to site/demo/task_repeat.uce diff --git a/site/test/test2/working-dir-test.uce b/site/demo/test2/working-dir-test.uce similarity index 100% rename from site/test/test2/working-dir-test.uce rename to site/demo/test2/working-dir-test.uce diff --git a/site/test/unit-browser.uce b/site/demo/unit-browser.uce similarity index 100% rename from site/test/unit-browser.uce rename to site/demo/unit-browser.uce diff --git a/site/test/unit-info.uce b/site/demo/unit-info.uce similarity index 100% rename from site/test/unit-info.uce rename to site/demo/unit-info.uce diff --git a/site/test/uri.uce b/site/demo/uri.uce similarity index 100% rename from site/test/uri.uce rename to site/demo/uri.uce diff --git a/site/test/utf8.uce b/site/demo/utf8.uce similarity index 100% rename from site/test/utf8.uce rename to site/demo/utf8.uce diff --git a/site/test/websockets.ws.uce b/site/demo/websockets.ws.uce similarity index 100% rename from site/test/websockets.ws.uce rename to site/demo/websockets.ws.uce diff --git a/site/test/working-dir.uce b/site/demo/working-dir.uce similarity index 100% rename from site/test/working-dir.uce rename to site/demo/working-dir.uce diff --git a/site/info/config/settings.uce b/site/info/config/settings.uce new file mode 100644 index 0000000..ce371f6 --- /dev/null +++ b/site/info/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/info/index.uce b/site/info/index.uce new file mode 100644 index 0000000..a0549d0 --- /dev/null +++ b/site/info/index.uce @@ -0,0 +1,920 @@ +void render_nav_link(String href, String label) +{ + <> +} + +void render_feature_card(String eyebrow, String title, String body) +{ + <>
+
+

+

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

+

+
+ +
+
+
+
+} + +RENDER(Request& context) +{ + String page_title = "UCE: make dynamic websites like it's 2006"; + String generated_at = time_format_utc("%Y-%m-%d %H:%M:%S UTC"); + + String snippet_minimal = + "RENDER(Request& context)\n" + "{\n" + "\tString name = first(context.get[\"name\"].to_string(), \"guest\");\n" + "\t<>\n" + "\t\t

Hello, .

\n" + "\t\t

Rendered at

\n" + "\t\n" + "}\n"; + + String snippet_forms = + "RENDER(Request& context)\n" + "{\n" + "\tif(context.post.has(\"email\"))\n" + "\t\tcontext.session[\"flash\"] = \"Saved \" + context.post[\"email\"].to_string();\n" + "\t<>\n" + "\t\t
\n" + "\t\t\t\n" + "\t\t\t\n" + "\t\t
\n" + "\t\t\n" + "\t\t\t

\n" + "\t\t\n" + "\t\n" + "}\n"; + + String snippet_components = + "COMPONENT(Request& context)\n" + "{\n" + "\t<>\n" + "\t\t
\n" + "\t\t\t

\n" + "\t\t\t

\n" + "\t\t
\n" + "\t\n" + "}\n\n" + "RENDER(Request& context)\n" + "{\n" + "\tDTree props;\n" + "\tprops[\"title\"] = \"Shipping note\";\n" + "\tprops[\"body\"] = \"Components are just .uce files with structured props.\";\n" + "\t<>\n" + "\t\t\n" + "\t\n" + "}\n"; + + String snippet_markdown = + "RENDER(Request& context)\n" + "{\n" + "\tString src = \"# Release Notes\\n\\n- fast path\\n- docs\\n- demos\";\n" + "\t<>\n" + "\t\t
\n" + "\t\t\t\n" + "\t\t
\n" + "\t\n" + "}\n"; + + String snippet_websocket = + "RENDER(Request& context)\n" + "{\n" + "\t<>\n" + "\t\t\n" + "\t\n" + "}\n\n" + "WS(Request& context)\n" + "{\n" + "\tDTree payload = json_decode(ws_message());\n" + "\tcontext.connection[\"name\"] = payload[\"name\"];\n" + "\tws_send(json_encode(payload));\n" + "}\n"; + + String snippet_nginx = + "location ~ \\.ws\\.uce$ {\n" + "\terror_page 418 = @uce_websocket;\n" + "\tif ($http_upgrade = \"websocket\") { return 418; }\n" + "\tinclude fastcgi_params;\n" + "\tfastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n" + "\tfastcgi_pass unix:/run/uce/fastcgi.sock;\n" + "}\n\n" + "location @uce_websocket {\n" + "\tproxy_http_version 1.1;\n" + "\tproxy_set_header Upgrade $http_upgrade;\n" + "\tproxy_set_header Connection $connection_upgrade;\n" + "\tproxy_pass http://127.0.0.1:8080;\n" + "}\n"; + + <> + + + + + <?= page_title ?> + + + + +
+
+ UCE + + Docs +
+ +
+
+
+
PHP energy, C++ runtime
+

Make dynamic websites like it's 2006.

+

+ UCE is a deliberately direct web runtime: files are pages, templates live next to control flow, + nginx fronts the whole thing, and a request can compile into a shared object on first hit. + It is built in the image of PHP's non-architecture, but with an explicit request object, + component rendering, and a built-in WebSocket broker. +

+ +
+ + + +
+
+ +
+ +
+
+
Why UCE
+

The old web had the right instincts.

+

+ Put code where the page lives. Let runtime state die with the request. Keep deployment boring. + Treat server-rendered HTML as the default, and add richer behavior only where it pays for itself. +

+
+
+ + + + +
+
+ +
+
+
Non-architecture
+

It is not trying to save you from the web.

+
+
+
+

What it leans into

+

Files on disk, explicit request handlers, template tags, reusable partials, request-scoped state, and operationally plain deployment.

+
+
+

What it resists

+

Mandatory client-side hydration, giant application shells, hidden routing layers, and pretending every website is a desktop app in a trench coat.

+
+
+

What it adds

+

Built-in components, markdown rendering, current request data in one place, and a broker-backed WebSocket story that stays inside the same runtime.

+
+
+
+ +
+
+
Code first
+

Common things in UCE

+

+ The examples below are modeled on the current runtime API and the published demo pages. + The point is not abstraction depth. The point is that the code stays obvious. +

+
+
+ + + + + + +
+
+ +
+
+
Deployment
+

Put nginx in front. Keep the runtime focused.

+
+
+
+

The intended shape

+
    +
  • nginx serves static files directly from `site/`
  • +
  • ordinary `.uce` and plain `.ws.uce` page loads go through FastCGI
  • +
  • real websocket upgrade traffic goes to the built-in HTTP listener
  • +
  • systemd manages the runtime binary and restart policy
  • +
+
+
+

The practical settings

+
    +
  • `FCGI_SOCKET_PATH=/run/uce/fastcgi.sock` for normal requests
  • +
  • `HTTP_PORT=8080` for `.ws.uce` websocket upgrades
  • +
  • `scripts/systemd/manage-uce-service.sh` for build and service control
  • +
  • published root should be `site/`, not the repo root
  • +
+
+
+
+ +
+
+
Explore
+

Go straight to the useful parts.

+
+ +
+ +
+
+
Reality check
+

What this is and what it is not

+
+
+ Is UCE production ready? +

The runtime is still experimental. The point of this site is to explain the model, show the current surface area, and make it easy to evaluate the demos and docs.

+
+
+ Why compare it to PHP? +

Because the design instinct is similar: dynamic pages as files, shallow deployment, and a bias toward solving the request in front of you instead of building an application architecture altar.

+
+
+ What makes it different from old PHP? +

The request object is explicit, components and named handlers are native, markdown support is built in, and WebSockets use a broker-owned connection model inside the runtime.

+
+
+
+ +
+

UCE is a web runtime for people who still think pages, handlers, sockets, and deployment scripts should be understandable.

+

Demo / Docs / Starter

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

U-Howler.js Demo

+ +
+
+

Basic Controls

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

Global Controls

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

API Overview

+ +
+

Howler (Global)

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

Howl Constructor

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

Core Methods

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

Examples

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

Audio Sprites

+
+

Audio Sprites Demo

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

Audio Sprites API

+ +
+

Sprite Methods

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

Sprite Configuration

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

Audio Effects

+
+

Filters

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

Rate Control

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

Fade Effects

+
+ + +
+
+
+
+ +
+

Effects API

+ +
+

Effect Methods

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

Audio Effects

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

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

Frequency Analysis

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

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

3D Spatial Audio

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

Spatial API

+ +
+

3D Spatial Audio

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

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

Environment Presets

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

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

Spatial Examples

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

Library Info

+
+
Loading library information...
+ +

Supported Codecs:

+
+
+
+ +
+

Events & Utilities

+ +
+

Event Listeners

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

Utility Methods

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

Events

+
load() - Audio loaded successfully
+
loaderror(id, error) - Failed to load audio
+
play(id) - Sound started playing
+
pause(id) - Sound paused
+
stop(id) - Sound stopped
+
end(id) - Sound finished playing
+
fade(id) - Fade effect completed
+
volume(id) - Volume changed
+
rate(id) - Playback rate changed
+
seek(id) - Playback position changed
+
mute(id) - Mute state changed
+
unlock() - Audio context unlocked (user interaction)
+
playerror(id, error) - Playback error occurred
+
+
+
+
+ + + + + diff --git a/site/info/js/u-macrobars-demo.html b/site/info/js/u-macrobars-demo.html new file mode 100755 index 0000000..63dc607 --- /dev/null +++ b/site/info/js/u-macrobars-demo.html @@ -0,0 +1,977 @@ + + + + + + U-Macrobars.js Demo + + + +
+

U-Macrobars.js Demo

+ +
+
+

Basic Field Output

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

Number Formatting

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

Default Values & Safety

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

Field Output API

+ +
+

Basic Syntax

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

Number Formatting

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

Examples

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

Control Structures

+
+

Conditionals

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

Loops & Iteration

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

Equality & Lookup

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

Control Flow API

+ +
+

Conditionals

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

Loops

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

Comparison & Lookup

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

Examples

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

Advanced Features

+
+

Event Binding

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

Code Blocks

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

Components

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

Advanced API

+ +
+

Event Binding

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

Code Execution

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

Components & Compilation

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

Examples

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

Template Editor

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

Template Information

+
Ready.
+
+ +
+
+ +
+

Compilation & Debugging

+ +
+

Compilation Options

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

Debugging Properties

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

Utility Functions

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

Example Templates

+
+// Complete example
+const template = Macrobars.compile(`
+<div class="user-card">
+  <h3>{{name or "Unknown User"}}</h3>
+  {{#if profile.verified}}✅ Verified{{/if}}
+  <p>Balance: ${{%balance}}</p>
+  {{#each achievements}}
+    <span class="badge">{{data}}</span>
+  {{/each}}
+</div>
+`, { decimals: 2 });
+
+const result = template(userData);
+
+
+
+
+
+ + + + + diff --git a/site/info/js/u-popup-menu.js b/site/info/js/u-popup-menu.js new file mode 100644 index 0000000..e30498b --- /dev/null +++ b/site/info/js/u-popup-menu.js @@ -0,0 +1,84 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.showPopupMenu = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + let showPopupMenu = (x, y, items, prop = {}) => { + document.querySelectorAll('.vp-popup-menu').forEach(menu => { + if(menu.parentNode) menu.parentNode.removeChild(menu); + }); + + let container = prop.container || document.body; + let menu = $('
').css({ + position: 'absolute', + left: (x|0) + 'px', + top: (y|0) + 'px' + })[0]; + + let elems = []; + if(!items || !items.length) { + $(menu).append(`
${prop.emptyText || 'No items'}
`); + } else { + items.forEach((it, idx) => { + let label = typeof it === 'string' ? it : (it.label || it.screen || it.name || JSON.stringify(it)); + let $item = $(`
${label}
`) + .attr('data-idx', idx) + .on('click', ev => { ev.stopPropagation(); try { (prop.onSelect || (()=>{}))(it); } catch(e){ console.error(e); } removeMenu(); }) + .on('keydown', e => { if(e.key === 'Enter') { e.preventDefault(); $item[0].click(); } }); + $(menu).append($item[0]); + elems.push({el: $item[0], data: it}); + }); + } + + let focusedIndex = elems.length ? 0 : -1; + let focusAt = i => { + elems.forEach((it, idx) => { $(it.el).removeClass('focused'); if(idx === i) { $(it.el).addClass('focused'); try{ it.el.focus(); }catch(e){} } }); + focusedIndex = i; + }; + + let removeMenu = () => { + if(menu.parentNode) menu.parentNode.removeChild(menu); + $(document).off('mousedown', onDoc).off('keydown', onKey); + $(window).off('resize', onResize); + }; + let onDoc = ev => { if(!menu.contains(ev.target)) removeMenu(); }; + let onResize = () => reposition(); + let onKey = ev => { + if(!elems.length) { if(ev.key === 'Escape') removeMenu(); return; } + if(ev.key === 'Escape') { ev.preventDefault(); removeMenu(); return; } + if(ev.key === 'ArrowDown') { ev.preventDefault(); focusAt((focusedIndex + 1) % elems.length); return; } + if(ev.key === 'ArrowUp') { ev.preventDefault(); focusAt((focusedIndex - 1 + elems.length) % elems.length); return; } + if(ev.key === 'Home') { ev.preventDefault(); focusAt(0); return; } + if(ev.key === 'End') { ev.preventDefault(); focusAt(elems.length - 1); return; } + if(ev.key === 'Enter') { ev.preventDefault(); if(focusedIndex >= 0) elems[focusedIndex].el.click(); } + }; + + container.appendChild(menu); + let reposition = () => { + let mRect = menu.getBoundingClientRect(); + let winW = window.innerWidth, winH = window.innerHeight; + let left = parseInt($(menu).css('left'),10) || 0, top = parseInt($(menu).css('top'),10) || 0; + if(mRect.right > winW) left = Math.max(4, winW - Math.ceil(mRect.width) - 8); + if(mRect.bottom > winH) top = Math.max(4, winH - Math.ceil(mRect.height) - 8); + if(left < 4) left = 4; if(top < 4) top = 4; + $(menu).css({left: left + 'px', top: top + 'px'}); + }; + + $(document).on('mousedown', onDoc).on('keydown', onKey); + $(window).on('resize', onResize); + if(elems.length) { focusAt(0); } + setTimeout(reposition, 0); + return menu; + }; + + return showPopupMenu; +})); \ No newline at end of file diff --git a/site/info/js/u-query-demo.html b/site/info/js/u-query-demo.html new file mode 100755 index 0000000..ce48d39 --- /dev/null +++ b/site/info/js/u-query-demo.html @@ -0,0 +1,976 @@ + + + + + + U-Query.js Demo + + + +
+

U-Query.js Demo

+ +
+
+

Element Selection & DOM Manipulation

+
+

DOM Queries

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

DOM Manipulation

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

CSS & Classes

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

Selection API

+ +
+

Core Selectors

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

DOM Manipulation

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

CSS & Classes

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

Examples

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

Event Handling & Visibility

+
+

Event System

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

Show/Hide/Toggle

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

Method Chaining

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

Events & Visibility API

+ +
+

Event Methods

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

Visibility Control

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

Method Chaining

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

AJAX & Global Events

+
+

AJAX Utilities

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

Global Event System

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

Utility Functions

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

AJAX & Utilities API

+ +
+

AJAX Methods

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

Global Event System

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

Utility Functions

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

Examples

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

Browser Info

+
+
Loading library information...
+ +

This browser supports

+
+ +
+ + +
+
+
+ +
+

Advanced Features

+ +
+

Advanced Selectors

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

Data Attributes

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

Traversal Methods

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

DOM Diff Updates

+
+ u-query can make use of morphdom.js for efficient DOM diff updates. +
+
$.options.alwaysDoDifferentialUpdate = true
+
+ Global flag for morphdom. +
+
+ +
+
+
+ + + + + diff --git a/site/info/js/u-query.js b/site/info/js/u-query.js new file mode 100755 index 0000000..907e8ba --- /dev/null +++ b/site/info/js/u-query.js @@ -0,0 +1,589 @@ +(function (root, factory) { + // this is the stupid UMD pattern, but eh we lost that battle a long time ago + if (typeof exports === 'object' && typeof module !== 'undefined') { + var exports_obj = factory(); + module.exports = exports_obj.$; + // Also export individual components + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + exports[key] = exports_obj[key]; + } + } + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + var exports_obj = factory(); + root.$ = exports_obj.$; + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + root[key] = exports_obj[key]; + } + } + } +}(typeof self !== 'undefined' ? self : this, function () { + +// a collection of DOM utility functions that I thought were cool in jQuery + +class EventEmitter { + constructor() { + /** Map> */ + this._topics = new Map(); + } + + /** + * Subscribe to `topic`. If `slot_key` is provided (truthy), + * it dedupes by that key; otherwise a unique Symbol is used. + * Returns an unsubscribe fn. + */ + on(topic, handler, slot_key = null) { + let map = this._topics.get(topic); + if (!map) { + map = new Map(); + this._topics.set(topic, map); + } + // use the provided slot_key or a fresh Symbol() + const key = slot_key != null ? slot_key : Symbol(); + map.set(key, handler); + return () => this.off(topic, key); + } + + /** + * Unsubscribe by handler function or by slot_key. + */ + off(topic, handlerOrSlotKey) { + const map = this._topics.get(topic); + if (!map) return; + + // if it matches a slotKey directly, remove it + if (map.has(handlerOrSlotKey)) { + map.delete(handlerOrSlotKey); + } else { + // otherwise assume it's a function: remove all matching fns + for (const [key, fn] of map.entries()) { + if (fn === handlerOrSlotKey) { + map.delete(key); + } + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + } + + /** + * Emit to all handlers on `topic`. Handlers returning + * 'remove_handler' are auto-removed. + * Returns the number of handlers invoked. + */ + emit(topic, ...args) { + let count = 0; + const map = this._topics.get(topic); + if (!map) return count; + + for (const [key, fn] of Array.from(map.entries())) { + const res = fn(...args); + count++; + if (res === 'remove_handler') { + map.delete(key); + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + return count; + } +} + +class QueryWrapper extends Array { + constructor(elements) { + super(); + if (elements) { + this.push(...(Array.isArray(elements) ? elements : [elements])); + } + } +} + +$ = function(selector_or_element) { + let elements; + if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) { + elements = [selector_or_element]; + } else if (typeof selector_or_element === 'string') { + // Check if string looks like HTML (starts with < and ends with >) + if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') { + // Create element from HTML string + let temp = document.createElement('div'); + temp.innerHTML = selector_or_element.trim(); + elements = Array.from(temp.children); + } else { + // Treat as CSS selector + elements = Array.from(document.querySelectorAll(selector_or_element)); + } + } else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) { + elements = Array.from(selector_or_element); + } else { + elements = [selector_or_element]; + } + return new QueryWrapper(elements); +}; + +$.options = { + alwaysDoDifferentialUpdate: true, +}; + +$.events = new EventEmitter(); +$.on = $.events.on.bind($.events); +$.off = $.events.off.bind($.events); +$.emit = $.events.emit.bind($.events); + +$.each = function(selector, callback) { + let elements; + if (typeof selector === 'string') { + elements = document.querySelectorAll(selector); + } else if (selector instanceof QueryWrapper) { + elements = selector; + } else { + elements = selector; + } + for (let i = 0; i < elements.length; i++) { + callback(elements[i], i); + } +} + +$.post = function(url, data, callback) { + return $.ajax({ + method: 'POST', + url: url, + data: data, + success: callback, + error: function(xhr) { + console.error('POST request failed:', xhr); + } + }); +} + +$.get = function(url, callback) { + return $.ajax({ + method: 'GET', + url: url, + success: callback, + error: function(xhr) { + console.error('GET request failed:', xhr); + } + }); +} + +$.ajax = function(options) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open(options.method || 'GET', options.url, true); + + // Set default headers + if (options.method === 'POST' || options.data) { + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + } + + // Set custom headers + if (options.headers) { + for (var key in options.headers) { + xhr.setRequestHeader(key, options.headers[key]); + } + } + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + var response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + // If JSON parsing fails, use raw response + response = xhr.responseText; + } + + if (xhr.status >= 200 && xhr.status < 300) { + if (options.success) options.success(response); + resolve(response); + } else { + if (options.error) options.error(xhr); + reject(xhr); + } + } + }; + + var data = null; + if (options.data) { + data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data); + } + + xhr.send(data); + }); +} + +$.ready = function(callback) { + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } +} + +// Helper function to execute scripts in HTML content +function executeScripts(htmlContent) { + // Create a temporary container to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // Find all script tags and disable them temporarily + const scripts = tempDiv.querySelectorAll('script'); + const scriptData = []; + + scripts.forEach((script) => { + // Store script data for later execution + scriptData.push({ + content: script.textContent || script.innerText || '', + src: script.src, + type: script.type, + nonce: script.nonce, + async: script.async, + defer: script.defer + }); + + // Disable the script by changing its type + script.type = 'text/disabled-script'; + }); + + // Return the modified HTML and script data + return { + html: tempDiv.innerHTML, + scripts: scriptData + }; +} + +// Helper function to execute a single script +function executeScript(scriptInfo) { + if (scriptInfo.src) { + // External script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + if (scriptInfo.async) newScript.async = scriptInfo.async; + if (scriptInfo.defer) newScript.defer = scriptInfo.defer; + newScript.src = scriptInfo.src; + + document.head.appendChild(newScript); + } else if (scriptInfo.content.trim()) { + // Inline script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + newScript.text = scriptInfo.content; + + // Execute by inserting and immediately removing + document.head.appendChild(newScript); + document.head.removeChild(newScript); + } +} + +// Add all the jQuery-like methods to QueryWrapper +QueryWrapper.prototype.parent = function() { + const parents = Array.from(this).map(el => el.parentNode).filter(el => el); + return new QueryWrapper(parents); +} + +QueryWrapper.prototype.load = function(url, opt = {}) { + return new Promise((resolve, reject) => { + const ajaxOptions = { + method: opt.method || (opt.postData ? 'POST' : 'GET'), + url: url, + success: (response) => { + if(opt.replace) + this.replaceWith(response, opt); + else + this.html(response, opt); + if (opt.onLoad) opt.onLoad(response, null); + resolve(this); + }, + error: (xhr) => { + if (opt.onError) opt.onError(xhr); + reject(new Error('Failed to load: ' + url)); + } + }; + + if (opt.postData) { + ajaxOptions.data = opt.postData; + } + + $.ajax(ajaxOptions); + }); +} + +QueryWrapper.prototype.html = function(opt_content = false, prop = {}) { + if (opt_content === false) { + return Array.from(this).map(el => el.innerHTML).join(''); + } + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + const temp = document.createElement('div'); + temp.innerHTML = opt_content; + this.forEach(el => { + morphdom(el, temp, { childrenOnly: true }); + }); + } else { + this.forEach(el => { + const result = executeScripts(opt_content); + el.innerHTML = result.html; + result.scripts.forEach(executeScript); + }); + } + return this; +} + +QueryWrapper.prototype.text = function(opt_content = false) { + if (opt_content === false) { + return Array.from(this).map(el => el.textContent).join(''); + } + this.forEach(el => { + el.textContent = opt_content; + }); + return this; +} + +QueryWrapper.prototype.replaceWith = function(content, prop = {}) { + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + this.forEach(el => { + morphdom(el, content); + }); + } else { + if(typeof content === 'string') { + this.forEach(el => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content; + el.parentNode.replaceChild(tempDiv.firstChild, el); + }); + } else if(content instanceof Element) { + this.forEach(el => { + el.parentNode.replaceChild(content.cloneNode(true), el); + }); + } + } + return this; +} + +QueryWrapper.prototype.query = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.find = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.on = function(event, handler) { + this.forEach(function(el) { + el.addEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.off = function(event, handler) { + this.forEach(function(el) { + el.removeEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.hide = function() { + return this.css({ display: 'none' }); +} + +QueryWrapper.prototype.show = function() { + return this.css({ display: '' }); +} + +QueryWrapper.prototype.toggle = function() { + this.forEach(function(el) { + el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none'; + }); + return this; +} + +QueryWrapper.prototype.attr = function(name, value) { + if (value === undefined) { + return this.length > 0 ? this[0].getAttribute(name) : null; + } + this.forEach(function(el) { + el.setAttribute(name, value); + }); + return this; +} + +QueryWrapper.prototype.addClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.add(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.removeClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.remove(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.toggleClass = function(classNameOrList, force) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + if (typeof force !== 'undefined') { + el.classList.toggle(className, force); + } else { + el.classList.toggle(className); + } + }); + }); + }); + return this; +} + +QueryWrapper.prototype.empty = function() { + this.forEach(function(el) { + el.innerHTML = ''; + }); + return this; +} + +QueryWrapper.prototype.css = function(styles, optOrValue) { + // If no arguments or first argument is a string (getter mode) + if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) { + if (this.length === 0) return undefined; + var el = this[0]; + if (typeof styles === 'string') { + // Get computed style for a specific property + return window.getComputedStyle(el)[styles]; + } else { + // Return computed styles object (not commonly used) + return window.getComputedStyle(el); + } + } + + // Setter mode + this.forEach(function(el) { + if (typeof styles === 'string' && arguments.length >= 2) { + // Single property setter: .css('prop', 'value') + el.style[styles] = optOrValue; + } else if (typeof styles === 'object') { + // Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'}) + for (var key in styles) { + el.style[key] = styles[key]; + } + } + }); + return this; +} + +QueryWrapper.prototype.each = function(callback) { + this.forEach(function(el, index) { + callback(el, index); + }); + return this; +} + +QueryWrapper.prototype.append = function(child_or_html) { + this.forEach(function(el) { + if (typeof child_or_html === 'string' && el.insertAdjacentHTML) { + if (child_or_html.includes(' max) return max; + return v; +} + +function pick_entry_from_range(array, value) { + if (!array) return {}; + let result = {}; + for (const [pv] of Object.entries(array)) { + if (value >= pv.from && value <= pv.to) result = pv; + } + return result; +} + +return { + $: $, + EventEmitter: EventEmitter, + QueryWrapper: QueryWrapper, + first: first, + clamp: clamp, + pick_entry_from_range: pick_entry_from_range +}; + +})); \ No newline at end of file diff --git a/site/info/js/u-tiling-viewport.css b/site/info/js/u-tiling-viewport.css new file mode 100644 index 0000000..63976c7 --- /dev/null +++ b/site/info/js/u-tiling-viewport.css @@ -0,0 +1,165 @@ +:root { + --vp-color-primary: rgba(255, 125, 55, 1); + --vp-color-primary-focus: rgba(255, 120, 60, 0.4); + --vp-background: rgba(0,0,0,0.2); + + --vp-color-black: #000; + --vp-color-gray: #666; + --vp-color-white: #fff; + --vp-space-s: 4px; + --vp-space-l: 6px; + + --vp-size-icon: 12px; + --vp-size-nav-height: 48px; + + --vp-opacity: 0.4; + --vp-opacity-active: 0.9; + + --vp-z-pane: 2; + --vp-z-toolbar: 3; + --vp-z-popup: 9999; + --vp-duration: 0.25s; + --vp-easing: ease; +} + +#fabric { + position: absolute; + top: var(--vp-size-nav-height); + left: 0; right: 0; bottom: 0; + display: flex; + overflow: hidden; +} + +.vp-split { + flex: 1 1 auto; + display: flex; + position: relative; + overflow: hidden; +} + +.vp-split.row { flex-direction: row; } +.vp-split.col { flex-direction: column; } + +.viewport-pane { + flex: 1 1 0; + position: relative; + border: var(--vp-space-s) solid var(--color-border); + background: var(--vp-background); + padding: var(--vp-space-l); + overflow: auto; + font-family: console; + transition: + border-color var(--vp-duration) var(--vp-easing), + box-shadow var(--vp-duration) var(--vp-easing), + background var(--vp-duration) var(--vp-easing); +} + +.vp-split > .viewport-pane { + transition: + flex var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); +} + +.vp-no-transition, +.vp-no-transition * { + transition: none !important; +} + +.viewport-pane:before { + content: attr(data-title); + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + opacity: var(--vp-opacity-medium); + pointer-events: none; +} + +.viewport-pane.focused { + border-color: var(--vp-color-primary-focus); + z-index: var(--vp-z-pane); +} + +.viewport-pane.focused:before { + opacity: var(--vp-opacity-active); + color: var(--color-highlight); +} + +.vp-divider { + background: var(--color-border); + opacity: var(--vp-opacity); + position: relative; + flex: 0 0 auto; +} + +.vp-divider.row { + width: var(--vp-space-l); + cursor: col-resize; +} + +.vp-divider.col { + height: var(--vp-space-l); + cursor: row-resize; +} + +.vp-divider:after { + content:''; + position: absolute; + inset: 0; + transition: + background var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); + background: var(--color-border); +} + +.vp-divider.row:after { + background: var(--color-border); +} + +.vp-divider:hover:after { + background: var(--color-highlight); + opacity: var(--vp-opacity); +} + +.vp-divider.dragging:after { + background: var(--color-highlight); + opacity: var(--vp-opacity-active); +} + +.vp-help-overlay { + position: absolute; + top: var(--vp-space-s); + left: var(--vp-space-s); + opacity: var(--vp-opacity); + pointer-events: none; +} + +.vp-pane-toolbar { + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + display: flex; + gap: var(--vp-space-s); + z-index: var(--vp-z-toolbar); +} + +.vp-pane-toolbar button { + padding: 0 var(--vp-space-l); + border: 1px solid var(--color-border); + background: var(--vp-background); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.vp-pane-toolbar button:hover { + background: var(--color-highlight); + color: var(--vp-color-black); + text-shadow: none; +} + +.vp-icon { + width: var(--vp-size-icon); + height: var(--vp-size-icon); + display: block; +} \ No newline at end of file diff --git a/site/info/js/u-tiling-viewport.js b/site/info/js/u-tiling-viewport.js new file mode 100644 index 0000000..3762033 --- /dev/null +++ b/site/info/js/u-tiling-viewport.js @@ -0,0 +1,388 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.TilingViewportManager = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + // Note: This module expects showPopupMenu to be available globally + // or you may need to adjust the dependency list above to include it + + let TilingViewportManager = function(container, options = {}) { + + let idCounter = 1, rootNode = null, focusedPane = null, containerEl = null; + let nextId = () => 'vp_' + (idCounter++); + let paneMeta = new Map(); + let pendingViewLoads = []; + let defaultoptions = { + minPaneSize: 100, + keyboard: true, + showHelp: true, + closeTransitionTimeout: 400, + toolbarButtons: ['split-v','split-h','views','close'], + standardViewList: [], + cssVars: { '--vp-duration': '.22s', '--vp-easing': 'ease' } + }; + options = Object.assign({}, defaultoptions, options); + + let createPane = (content, props = {}) => { + let id = nextId(); + let $el = $('
').attr('data-id', id); + if(props.title) $el.attr('data-title', props.title); + $el.html(content || `
Use Alt+<H V O X ↑ ← → ↓>
`) + .on('mousedown', () => focusPaneByEl($el[0])); + attachPaneToolbar($el[0]); + paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props}); + if(props.view?.screen) pendingViewLoads.push({id, view: props.view}); + return {type: 'pane', id, el: $el[0]}; + }; + + let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); }; + let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); }; + let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id'))); + let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r); + let getNodeById = id => findNodeById(rootNode, id) || null; + let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r); + let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc); + let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); }; + let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); }; + + let split = (direction, newPaneProps = {}) => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canSplit === false) return; + + let paneRect = focusedPane.el.getBoundingClientRect(); + let availableSpace = direction === 'row' ? paneRect.width : paneRect.height; + let requiredSpace = 2 * options.minPaneSize; + if(availableSpace < requiredSpace) { + //console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`); + return; + } + + let parentPath = findParentPath(rootNode, focusedPane.id); + let newPane = createPane(undefined, newPaneProps); + let animatedSplit = null, newIndex = -1; + if(!parentPath?.length) { + rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + animatedSplit = rootNode; newIndex = 1; + } else { + let parent = parentPath.at(-1); + if(parent.type === 'split' && parent.direction === direction) { + let idx = parent.children.indexOf(focusedPane); + parent.children.splice(idx+1, 0, newPane); + parent.sizes = parent.sizes || parent.children.map(()=>1); + parent.sizes.splice(idx+1, 0, 0); + renormalizeSizes(parent); + animatedSplit = parent; newIndex = idx+1; + } else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) { + parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0]; + animatedSplit = parent; newIndex = parent.children.length-1; + } else { + let idx = parent.children.indexOf(focusedPane); + let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + parent.children.splice(idx, 1, created); + animatedSplit = created; newIndex = 1; + } + } + render(); + if(animatedSplit && newIndex >= 0) { + requestAnimationFrame(() => requestAnimationFrame(() => { + animatedSplit.sizes = animatedSplit.children.map(()=>1); + renormalizeSizes(animatedSplit); + animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`)); + })); + } + focusPane(newPane); return newPane; + }; + + let splitVertical = () => split('row'); + let splitHorizontal = () => split('col'); + + let closeFocused = () => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canClose === false) return; + if(rootNode === focusedPane) { buildInitial(); return; } + if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id); + if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id); + let parentPath = findParentPath(rootNode, focusedPane.id); + if(!parentPath?.length) return; + let parent = parentPath.at(-1); + if(parent.type !== 'split') return; + let idx = parent.children.indexOf(focusedPane); + + let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice(); + let oldSize = oldSizes[idx] || 0; + let remaining = 1 - oldSize; + let newSizes = oldSizes.slice(); + if(remaining > 0) { + for(let i=0;i ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`)); + if(focusedPane.el) { + focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease'; + focusedPane.el.style.opacity = '0'; + } + let done = false; + let cleanup = () => { + if(done) return; done = true; + parent.children.splice(idx,1); + parent.sizes?.splice(idx,1); + paneMeta.delete(focusedPane.id); + if(parent.children.length === 1) { + let only = parent.children[0]; + parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only); + } else if(parent.sizes) renormalizeSizes(parent); + render(); focusPane(paneList(rootNode)[0]); + }; + let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } }; + if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd); + setTimeout(() => cleanup(), 350); + }; + + let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; }; + let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); }; + let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); }; + + let serialize = (node=rootNode) => { + let ser = n => !n ? null : n.type==='pane' ? + {type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} : + {type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)}; + return {focus: focusedPane?.id, layout: ser(node)}; + }; + + let restore = data => { + if(!data) return; + let wrapper = data.layout ? data : {layout:data}; + paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0; + let idNums = []; + let build = l => l && l.type==='pane' ? + (() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view}); + if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); } + let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); } + return pane; })() : + l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null; + rootNode = build(wrapper.layout) || createPane(); + if(idNums.length) idCounter = Math.max(...idNums) + 1; + render(); + pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0; + focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]); + }; + + let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); }; + + let renderNode = node => { + if(node.type==='pane') return node.el; + node.el = node.el || $('
')[0]; + $(node.el).empty().addClass(`vp-split ${node.direction}`); + if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); } + node.children.forEach((ch,i) => { + let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl); + if(i < node.children.length-1) { let div = $('
').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); } + }); + return node.el; + }; + + let setupDivider = (div, splitNode, leftIdx) => { + $(div).on('mousedown', e => { + e.preventDefault(); $(div).addClass('dragging'); + $(containerEl).addClass('vp-no-transition'); + let isRow = splitNode.direction === 'row'; + let startPos = isRow ? e.clientX : e.clientY; + let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect()); + let prop = isRow ? 'width' : 'height'; + let startPixels = childRects.map(r => r[prop]); + let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]]; + let totalPixels = startPixels.reduce((a,b)=>a+b,0); + let onMove = ev => { + let curPos = isRow ? ev.clientX : ev.clientY; + let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos]; + if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize]; + if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize]; + startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx; + splitNode.sizes = startPixels.map(p => p / totalPixels); + splitNode.children.forEach((ch,i)=> { + if(ch.el) { + let flexValue = `${splitNode.sizes[i]} 1 0`; + $(ch.el).css('flex', flexValue); + } + }); + }; + let onUp = () => { + $(div).removeClass('dragging'); + $(containerEl).removeClass('vp-no-transition'); + $(document).off('mousemove', onMove).off('mouseup', onUp); + }; + $(document).on('mousemove', onMove).on('mouseup', onUp); + }); + $(div).on('dblclick', () => { + $(containerEl).addClass('vp-no-transition'); + let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len); + splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`)); + setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0); + }); + }; + + let attachPaneToolbar = el => { + let bar = $('
').addClass('vp-pane-toolbar').html(` + + + + `)[0]; + el.appendChild(bar); + $(bar).on('mousedown', ev => ev.stopPropagation()); + $(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el); + let button = ev.target.closest('button[data-act]'); + let act = button ? button.getAttribute('data-act') : null; + if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused(); + else if(act==='views') { + // compute button position + let rect = button.getBoundingClientRect(); + let items = options.standardViewList.slice(); + showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => { + // support string or object {screen} + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(el.dataset.id, view); + }, container: document.body, emptyText: 'No views available' }); + } + }); + refreshToolbarState(el.dataset.id); + }; + + let refreshToolbarState = paneId => { + let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; + if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return; + let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false]; + ['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } }); + let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); } + }; + + let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props); + if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; } + if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload; + paneMeta.set(paneId, meta); refreshToolbarState(paneId); }; + let getPaneProps = paneId => paneMeta.get(paneId); + + let focusDirection = (dx, dy) => { + if(!focusedPane) return; + let panes = Array.from(containerEl.querySelectorAll('.viewport-pane')); + let cur = focusedPane.el.getBoundingClientRect(); + let candidates = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); + if(dx) { + if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null; + let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top)); + let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.height - overlap)}; + } + if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null; + let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left)); + let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.width - overlap)}; + }).filter(Boolean); + if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el); + let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2]; + let fallback = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2]; + let [vx, vy] = [px-cx, py-cy]; + if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null; + return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))}; + }).filter(Boolean); + if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el); + }; + + let injectHelpOverlay = () => { + if(!options.showHelp) return; + if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``})); + }; + + let getPaneEl = id => findNodeById(rootNode, id)?.el || null; + + let openViewsPopupForPane = (paneId) => { + let node = findNodeById(rootNode, paneId || focusedPane?.id); + if(!node || !node.el) return; + let rect = node.el.getBoundingClientRect(); + let x = Math.max(8, rect.right - 80); + let y = rect.top + 24; + let items = options.standardViewList.slice(); + showPopupMenu(x, y, items, { onSelect: it => { + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(node.id, view); + }, container: document.body, emptyText: 'No views available' }); + }; + + let loadView = (paneId, view) => { + let node = findNodeById(rootNode, paneId); + if(!node || !node.el) return; // nothing to load into + let meta = paneMeta.get(paneId); + try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); } + if(meta) meta.view = view; + node.el.innerHTML = `
Loading ${view.screen||'...'}...
`; + $(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})}); + }; + + let handleKeys = e => { + if(!e.altKey || !options.keyboard) return; + let actions = { + 'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(), + 'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(), + 'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(), + 'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(), + 'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext() + }; + if(actions[e.code] || actions[e.key]) { + (actions[e.code] || actions[e.key])(); + e.preventDefault(); + } + }; + + containerEl = typeof container === 'string' ? document.querySelector(container) : container; + if(!containerEl) throw new Error('TilingViewportManager: container not found'); + if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k])); + buildInitial(); + if(options.keyboard) document.addEventListener('keydown', handleKeys); + + this.splitVertical = splitVertical; + this.splitHorizontal = splitHorizontal; + this.closeFocused = closeFocused; + this.focusNext = focusNext; + this.focusPrev = focusPrev; + this.serialize = serialize; + this.restore = restore; + this.setPaneProps = setPaneProps; + this.getPaneProps = getPaneProps; + this.options = options; + this.loadView = loadView; + this.getPaneEl = getPaneEl; + this.getNodeById = getNodeById; + + Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) }); + Object.defineProperty(this, 'focused', { get: () => focusedPane?.id }); + }; + + return TilingViewportManager; +})); \ No newline at end of file diff --git a/site/info/js/u-workspace-shell.js b/site/info/js/u-workspace-shell.js new file mode 100644 index 0000000..90fbd31 --- /dev/null +++ b/site/info/js/u-workspace-shell.js @@ -0,0 +1,68 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UWorkspaceShell = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getElement(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target && target.nodeType === 1 ? target : null; + } + + function bindShell(options) { + const sidebar = getElement(options.sidebarId || options.sidebar); + const overlay = getElement(options.overlayId || options.overlay); + const toggle = getElement(options.toggleButtonId || options.toggle); + const closeOnNav = options.closeOnNav !== false; + + if (!sidebar || !overlay) return null; + + function setOpen(open) { + sidebar.classList.toggle('is-open', !!open); + overlay.classList.toggle('is-open', !!open); + } + + function toggleOpen() { + setOpen(!sidebar.classList.contains('is-open')); + } + + if (toggle) { + toggle.addEventListener('click', toggleOpen); + } + + overlay.addEventListener('click', function () { + setOpen(false); + }); + + if (closeOnNav) { + sidebar.querySelectorAll('a').forEach(function (link) { + link.addEventListener('click', function () { + setOpen(false); + }); + }); + } + + globalScope.addEventListener('resize', function () { + if (globalScope.innerWidth > 860) { + setOpen(false); + } + }); + + return { + open: function () { setOpen(true); }, + close: function () { setOpen(false); }, + toggle: toggleOpen, + }; + } + + return { + init: bindShell, + }; +})); \ No newline at end of file diff --git a/site/info/js/u-wsconnection.js b/site/info/js/u-wsconnection.js new file mode 100755 index 0000000..9c3b40f --- /dev/null +++ b/site/info/js/u-wsconnection.js @@ -0,0 +1,111 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.Connection = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + +var Connection = { + + auto_reconnect : false, + established : false, + + update_indicator : (status) => { + var status_colors = { + 'offline' : 'red', + 'online' : 'lightgreen', + 'error' : 'DarkOrange', + }; + $('#connection-status').text(status).css('color', status_colors[status] || 'gray'); + }, + + debug : true, + auto_reconnect : true, + cmd_waiting_rels : {}, + + server_url : '', + + init : () => { + new EventSystem(Connection); + }, + + start : (url = null) => { + + if(url) Connection.server_url = url; + + Connection.update_indicator('offline'); + if(Connection.socket) Connection.socket.close(); + Connection.socket = new WebSocket(Connection.server_url); + Connection.socket.onmessage = function(rawmsg) { + var msg = JSON.parse(rawmsg.data); + if(Connection.debug) console.log('CONNECTION MSG', msg); + if(msg.type) Connection.trigger(msg.type, msg); + Connection.trigger('message', msg); + } + Connection.socket.onclose = function() { + Connection.update_indicator('offline'); + if(Connection.debug) console.log('CONNECTION CLOSED'); + Connection.established = false; + Connection.trigger('close', {}); + } + Connection.socket.onerror = function(error) { + Connection.update_indicator('error'); + console.error('CONNECTION', error); + Connection.established = false; + } + Connection.socket.onopen = function() { + Connection.update_indicator('online'); + if(Connection.debug) console.log('CONNECTION ESTABLISHED'); + Connection.established = true; + Connection.trigger('open', {}); + } + setTimeout(Connection.reconnect, 2000); + + }, + + deauth : () => { + Game.session = {}; + }, + + reconnect : () => { + if(!Connection.established && Connection.auto_reconnect) + Connection.start(); + else + setTimeout(Connection.reconnect, 2000); + }, + + queue : [], + + dequeue : () => { + var dq = Connection.queue; + Connection.queue = []; + dq.forEach(function(fm) { + Connection.send(fm); + }); + }, + + send : (msg) => { + if(Connection.established) { + if(typeof msg == 'function') + msg(); + else + Connection.socket.send(JSON.stringify(msg)); + } else { + Connection.queue.push(msg); + } + }, + + close : () => { + Connection.auto_reconnect = false; + Connection.socket.close(); + }, + +} + +return Connection; + +})); + diff --git a/site/info/style.css b/site/info/style.css new file mode 100644 index 0000000..486434d --- /dev/null +++ b/site/info/style.css @@ -0,0 +1,743 @@ +@font-face { + font-family: 'default_sans'; + src: url('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('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('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('themes/common/fonts/b612/b612-mono-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +@font-face { + font-family: 'display_condensed'; + src: url('themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +:root { + --bg: #113399; + --bg-deep: #0c2370; + --bg-grad: linear-gradient(160deg, #1d44b5 0%, #113399 42%, #0b225f 100%); + --surface: rgba(255, 255, 255, 0.065); + --surface-strong: rgba(255, 255, 255, 0.1); + --surface-hover: rgba(255, 255, 255, 0.13); + --surface-inset: rgba(7, 17, 52, 0.8); + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.16); + --text: #e7edf8; + --text-dim: rgba(215, 226, 245, 0.72); + --text-muted: rgba(215, 226, 245, 0.48); + --accent: #f0c430; + --accent-soft: rgba(240, 196, 48, 0.12); + --accent-hover: #ffe07c; + --shadow-sm: 0 10px 28px rgba(0, 0, 0, 0.18); + --shadow-md: 0 22px 48px rgba(0, 0, 0, 0.24); + --shadow-lg: 0 30px 80px rgba(0, 0, 0, 0.34); + --radius: 16px; + --radius-lg: 26px; + --font-sans: 'default_sans', sans-serif; + --font-mono: 'default_mono', monospace; + --font-display: 'display_condensed', 'default_sans', sans-serif; + --ease: cubic-bezier(0.22, 1, 0.36, 1); +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +* { + font: inherit; + color: inherit; + line-height: inherit; +} + +html { + font-size: 16px; + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(255, 255, 255, 0.08), transparent 32%), + radial-gradient(circle at 85% 12%, rgba(240, 196, 48, 0.13), transparent 22%), + var(--bg-grad); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.65; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); + background-size: 24px 24px; + opacity: 0.18; + pointer-events: none; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.4), transparent 88%); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 160ms var(--ease), transform 160ms var(--ease), border-color 160ms var(--ease), background 160ms var(--ease); +} + +a:hover { + color: var(--accent-hover); +} + +code, +pre, +.brand, +.eyebrow, +.link-meta, +.signal-card strong, +.deploy-card li, +.topbar nav a { + font-family: var(--font-mono); +} + +.page-shell { + width: min(1180px, calc(100% - 32px)); + margin: 0 auto; + padding: 20px 0 56px; + position: relative; + z-index: 1; +} + +.topbar { + position: sticky; + top: 14px; + z-index: 10; + display: flex; + align-items: center; + gap: 16px; + padding: 14px 18px; + margin-bottom: 24px; + background: rgba(8, 20, 66, 0.7); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: 999px; + box-shadow: var(--shadow-sm); +} + +.brand { + font-size: 1.05rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text); + padding-right: 8px; +} + +.topbar nav { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.topbar nav a, +.topbar-link { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid transparent; + color: var(--text-dim); + font-size: 0.88rem; +} + +.topbar nav a:hover, +.topbar-link:hover { + border-color: var(--border-strong); + background: rgba(255, 255, 255, 0.06); + color: var(--text); + transform: translateY(-1px); +} + +.topbar-link { + margin-left: auto; + background: var(--accent-soft); + color: var(--accent); + border-color: rgba(240, 196, 48, 0.24); +} + +main { + display: grid; + gap: 22px; +} + +.hero-panel, +.section, +.page-footer { + position: relative; + overflow: hidden; + background: var(--surface); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + padding: 28px; +} + +.hero-panel::after, +.section::after { + content: ''; + position: absolute; + inset: auto -10% -40% auto; + width: 280px; + height: 280px; + border-radius: 50%; + background: radial-gradient(circle, rgba(240, 196, 48, 0.12), transparent 70%); + pointer-events: none; +} + +.hero-panel { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.8fr); + gap: 24px; + align-items: start; + padding: 34px; + box-shadow: var(--shadow-lg); +} + +.hero-copy, +.hero-aside, +.section-heading, +.feature-grid, +.code-grid, +.link-grid, +.deploy-grid, +.manifesto-grid, +.signal-grid { + position: relative; + z-index: 1; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + margin-bottom: 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: var(--accent); + font-size: 0.74rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1, +h2, +h3 { + line-height: 1.08; + letter-spacing: -0.03em; +} + +h1, +h2 { + font-family: var(--font-display); + text-transform: uppercase; + margin-bottom: 16px; +} + +h1 { + font-size: clamp(3rem, 8vw, 6rem); + max-width: 10ch; + text-wrap: balance; +} + +h2 { + font-size: clamp(1.9rem, 3.2vw, 3rem); + max-width: 14ch; +} + +h3 { + font-family: var(--font-mono); + font-size: 1.08rem; + margin-bottom: 10px; +} + +p, +li, +summary, +.code-note, +.manual-card span, +.signal-card span, +.link-card span { + color: var(--text-dim); +} + +p + p { + margin-top: 12px; +} + +ul { + padding-left: 18px; +} + +li + li { + margin-top: 8px; +} + +.hero-lead, +.section-heading p { + max-width: 70ch; + font-size: 1.05rem; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin: 26px 0 28px; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 18px; + border-radius: 999px; + border: 1px solid var(--border-strong); + font-family: var(--font-mono); + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.03em; + text-decoration: none; + box-shadow: var(--shadow-sm); +} + +.button:hover { + text-decoration: none; + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.button-primary { + background: var(--accent); + color: #1a2247; + border-color: rgba(255, 224, 124, 0.42); +} + +.button-primary:hover { + color: #1a2247; + background: var(--accent-hover); +} + +.button-secondary { + background: rgba(255, 255, 255, 0.06); + color: var(--text); +} + +.signal-grid, +.feature-grid, +.manifesto-grid, +.link-grid, +.deploy-grid { + display: grid; + gap: 14px; +} + +.signal-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 8px; +} + +.feature-grid, +.link-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 22px; +} + +.manifesto-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 10px; +} + +.deploy-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 14px; +} + +.signal-card, +.feature-card, +.manual-card, +.manifesto-card, +.deploy-card, +.link-card, +.code-card { + background: rgba(6, 16, 54, 0.52); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + transition: transform 180ms var(--ease), border-color 180ms var(--ease), background 180ms var(--ease), box-shadow 180ms var(--ease); +} + +.signal-card, +.feature-card, +.manual-card, +.manifesto-card, +.deploy-card { + padding: 18px; +} + +.signal-card strong, +.link-card strong { + font-size: 0.9rem; + color: var(--text); + display: block; + margin-bottom: 8px; +} + +.feature-card:hover, +.link-card:hover, +.code-card:hover, +.manual-card:hover, +.manifesto-card:hover, +.deploy-card:hover, +.signal-card:hover { + transform: translateY(-2px); + border-color: var(--border-strong); + background: rgba(10, 24, 75, 0.72); + box-shadow: var(--shadow-md); +} + +.hero-aside { + display: grid; + gap: 14px; +} + +.manual-card.compact p + p { + margin-top: 10px; +} + +.section-heading { + margin-bottom: 8px; +} + +.manifesto-card.emphasis { + background: linear-gradient(180deg, rgba(240, 196, 48, 0.14), rgba(255, 255, 255, 0.06)); + border-color: rgba(240, 196, 48, 0.2); +} + +.code-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-top: 20px; +} + +.code-card { + padding: 20px; + overflow: hidden; +} + +.code-card p { + min-height: 4.6em; + margin-bottom: 14px; +} + +.code-toolbar { + display: flex; + justify-content: flex-end; + margin-bottom: 8px; +} + +.code-lang { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + font-family: var(--font-mono); + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); +} + +.syntax-block { + overflow-x: auto; + padding: 18px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: + linear-gradient(180deg, rgba(3, 9, 31, 0.97), rgba(8, 16, 48, 0.98)), + radial-gradient(circle at top right, rgba(240, 196, 48, 0.08), transparent 34%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); + font-size: 0.88rem; + line-height: 1.6; + color: #f3f6fc; +} + +.syntax-code { + display: block; + white-space: pre; + min-width: max-content; + font-family: var(--font-mono); + color: #f3f6fc; + tab-size: 2; + -moz-tab-size: 2; + font-variant-ligatures: none; +} + +.tok-comment { + color: #7f8db7; + font-style: italic; +} + +.tok-keyword, +.tok-delimiter { + color: #ff8ea1; + font-weight: 700; +} + +.tok-type { + color: #8bd9ff; +} + +.tok-builtin { + color: #7ce7d5; + font-weight: 700; +} + +.tok-call { + color: #f7d774; +} + +.tok-variable { + color: #b8a7ff; +} + +.tok-field { + color: #b6c9ff; +} + +.tok-string { + color: #8ce6b4; +} + +.tok-number { + color: #ffbf7a; +} + +.tok-operator, +.tok-punct { + color: #d8e1ff; +} + +.tok-ident { + color: #dfe7fb; +} + +.tok-markup-tag { + color: #7ee3ff; + font-weight: 700; +} + +.tok-markup-attr { + color: #ffd27c; +} + +.tok-markup-text { + color: #c9d5f4; +} + +.code-note { + margin-top: 12px; + font-size: 0.92rem; +} + +.link-card { + display: block; + padding: 20px; + text-decoration: none; +} + +.link-card:hover { + text-decoration: none; +} + +.link-meta { + font-size: 0.76rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 10px; +} + +.deployment ul, +.manual-card ul, +.deploy-card ul { + list-style: none; + padding-left: 0; +} + +.deployment li, +.manual-card li, +.deploy-card li { + position: relative; + padding-left: 18px; +} + +.deployment li::before, +.manual-card li::before, +.deploy-card li::before { + content: '+'; + position: absolute; + left: 0; + color: var(--accent); +} + +details { + padding: 18px 20px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(6, 16, 54, 0.48); +} + +details + details { + margin-top: 12px; +} + +summary { + cursor: pointer; + list-style: none; + font-family: var(--font-mono); + font-size: 0.96rem; + color: var(--text); +} + +summary::-webkit-details-marker { + display: none; +} + +details p { + margin-top: 12px; +} + +.page-footer { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: center; + margin-top: 4px; + padding-top: 20px; + padding-bottom: 20px; + background: rgba(7, 18, 56, 0.64); + border-radius: 20px; +} + +.page-footer p { + max-width: 55ch; + font-size: 0.94rem; +} + +.page-footer span { + opacity: 0.42; + padding: 0 8px; +} + +@media (max-width: 1024px) { + .hero-panel, + .signal-grid, + .feature-grid, + .manifesto-grid, + .code-grid, + .link-grid, + .deploy-grid, + .page-footer { + grid-template-columns: 1fr; + } + + .hero-panel { + padding: 28px; + } + + .page-footer { + display: grid; + } +} + +@media (max-width: 760px) { + .page-shell { + width: min(100% - 20px, 1180px); + padding-top: 10px; + } + + .topbar { + border-radius: 24px; + padding: 14px; + flex-wrap: wrap; + } + + .topbar-link { + margin-left: 0; + } + + .hero-panel, + .section, + .page-footer { + padding: 22px; + } + + h1 { + font-size: 2.8rem; + } + + h2 { + font-size: 2rem; + } + + .button { + width: 100%; + } + + pre { + padding: 16px; + font-size: 0.82rem; + } +} \ No newline at end of file diff --git a/site/info/themes/common/css/gauges.css b/site/info/themes/common/css/gauges.css new file mode 100644 index 0000000..1c465da --- /dev/null +++ b/site/info/themes/common/css/gauges.css @@ -0,0 +1,348 @@ +.gauge-demo-row { + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: stretch; + margin-bottom: 1.5rem; +} + +.gauge-control-panel { + flex: 1 1 18rem; + min-width: 16rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-control-panel h4 { + margin: 0 0 0.75rem; + color: var(--text-primary); +} + +.gauge-slider-group { + display: grid; + gap: 0.35rem; + margin-bottom: 0.9rem; +} + +.gauge-slider-group label { + color: var(--text-secondary); + font-weight: 600; + font-size: 0.92rem; +} + +.gauge-slider-group input[type="range"] { + width: 100%; + accent-color: var(--primary); +} + +.gauge-set, +.arcgauge-set { + flex: 1 1 24rem; + min-width: 20rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-set-header, +.arcgauge-set-header { + margin-bottom: 1rem; +} + +.gauge-set-header h3, +.arcgauge-set-header h3 { + margin: 0 0 0.3rem; + color: var(--text-primary); +} + +.gauge-set-header p, +.arcgauge-set-header p { + margin: 0; + color: var(--text-secondary); +} + +.gauge-card, +.arcgauge-card { + display: flex; + flex-direction: column; + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-elevated, var(--surface)); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: relative; + overflow: hidden; +} + +.gauge-card::before, +.arcgauge-card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.5; +} + +.gauge-metric-label { + color: var(--text-secondary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.gauge-metric-value { + color: var(--text-primary); + font-size: 1.1rem; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); + letter-spacing: -0.03em; +} + +.gauge-metric-meta { + color: var(--text-muted); + font-size: 0.84rem; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.progressbar-set-horizontal, +.progressbar-set-vertical { + align-self: stretch; +} + +.progressbar-grid-horizontal { + display: grid; + gap: 0.85rem; +} + +.progressbar-grid-vertical { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + align-items: stretch; +} + +.progressbar-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.7rem; +} + +.progressbar-card-horizontal { + gap: 0.7rem; +} + +.progressbar-card-vertical { + align-items: center; + justify-content: flex-end; + text-align: center; + gap: 0.7rem; +} + +.progressbar-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.progressbar-background { + position: relative; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent 30%); + background: color-mix(in srgb, var(--bg-color) 72%, var(--surface) 28%); + display: flex; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); +} + +.progressbar-background-horizontal { + height: 0.9rem; + border-radius: 999px; +} + +.progressbar-background-vertical { + height: var(--progressbar-height, 240px); + width: 100%; + max-width: 4rem; + margin: 0 auto; + border-radius: 1.2rem; + justify-content: flex-end; + padding: 0.35rem; + flex-direction: column; +} + + +.progressbar-bar { + transition: all 0.35s ease; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12), 0 0 24px color-mix(in srgb, currentColor 28%, transparent 72%); +} + +.progressbar-background-horizontal .progressbar-bar { + height: 100%; + border-radius: 999px; +} + +.progressbar-background-vertical .progressbar-bar { + width: 100%; + border-radius: 0.95rem; +} + +.progressbar-marker { + position: absolute; + z-index: 10; + opacity: 0.72; + background: var(--text-color, var(--text-primary, #333)); + box-shadow: 0 0 0 1px rgba(255,255,255,0.16); +} + +.progressbar-marker:hover { + opacity: 1; +} + +.progressbar-background-horizontal .progressbar-marker { + height: 100%; + top: 0; + width: 3px; + transform: translateX(-50%); +} + +.progressbar-background-vertical .progressbar-marker { + width: 100%; + left: 0; + height: 3px; + transform: translateY(50%); +} + +.needlegauge-svg .needle { + transition: all 0.3s ease; +} + +.progressbar-meta { + margin-top: 0.1rem; +} + +.needlegauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); +} + +.needlegauge-card { + align-items: center; + text-align: center; + gap: 0.55rem; +} + +.needlegauge-label-head { + align-self: flex-start; +} + +.needlegauge-visual { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.needlegauge-svg { + max-width: 100%; + overflow: visible; +} + + +.needlegauge-svg .ticks line { + opacity: 0.8; +} + +.needlegauge-svg .ticks text { + font-family: var(--font-mono, ui-monospace, monospace); + fill: var(--text-muted); +} + +.needlegauge-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.arcgauge-label { + margin-bottom: 0.35rem; +} + +.arcgauge-svg { + width: 130px; + height: 72px; + margin: 0.1rem 0 0.35rem; + overflow: visible; +} + +.arcgauge-track { + stroke: color-mix(in srgb, var(--border) 70%, transparent 30%); +} + +.arcgauge-arc-dyn { + transition: stroke-dasharray 0.6s ease, stroke 0.6s ease; +} + +.arcgauge-watermark { + stroke-width: 1.5; + stroke-linecap: round; +} + +.arcgauge-watermark-lo { + stroke: var(--primary); +} + +.arcgauge-watermark-hi { + stroke: var(--accent); +} + +.arcgauge-value { + fill: var(--text-primary); + font-size: 17px; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.arcgauge-caption { + fill: var(--text-muted); + font-size: 7px; + letter-spacing: 1.5px; + font-family: var(--font-sans, system-ui, sans-serif); +} + +.arcgauge-meta { + width: 100%; + text-align: center; +} + +.arcgauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); +} + +.arcgauge-card { + align-items: center; + text-align: center; +} + +@media (max-width: 768px) { + .gauge-demo-row { + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/info/themes/common/css/workspace.css b/site/info/themes/common/css/workspace.css new file mode 100644 index 0000000..b0575cb --- /dev/null +++ b/site/info/themes/common/css/workspace.css @@ -0,0 +1,522 @@ +.ws-app { + --ws-sidebar-width: 280px; + --ws-frame-bg: var(--surface); + --ws-sidebar-bg: color-mix(in srgb, var(--bg-secondary) 80%, var(--surface) 20%); + --ws-main-bg: var(--surface); + --ws-surface-alt: color-mix(in srgb, var(--bg-secondary) 72%, var(--surface) 28%); + --ws-shadow: var(--shadow-md); + display: flex; + width: 100%; + min-height: min(76vh, 900px); + overflow: hidden; + position: relative; + background: var(--ws-frame-bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--ws-shadow); +} + +.ws-sidebar { + width: var(--ws-sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + min-height: 0; + background: var(--ws-sidebar-bg); + border-right: 1px solid var(--border); + position: relative; + z-index: 2; +} + +.ws-main { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--ws-main-bg); + padding: 0; +} + +.ws-sidebar-top, +.ws-panel-header, +.ws-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.ws-sidebar-top { + padding: 0.9rem; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + flex-direction: column; + align-items: stretch; + background: color-mix(in srgb, var(--surface) 78%, var(--bg-secondary) 22%); +} + +.ws-search-wrap { + position: relative; +} + +.ws-search-icon { + position: absolute; + left: 0.7rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 0.8rem; + pointer-events: none; +} + +.ws-search-input { + width: 100%; + min-height: 36px; + padding: 0 0.8rem 0 2rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + font-size: 0.9rem; + color: var(--text-primary); +} + +.ws-search-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent 80%); +} + +.ws-panel-title-group, +.ws-header-actions { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.ws-panel-title-group { + flex-direction: column; + align-items: flex-start; +} + +.ws-panel-title-group h2, +.ws-section-head h3, +.ws-empty-state h2 { + margin: 0; +} + +.ws-subtitle { + margin: 0; + color: var(--text-secondary); + font-size: 0.92rem; +} + +.ws-sidebar-overlay { + display: none; +} + +.ws-mobile-bar { + display: none; + align-items: center; + gap: 0.7rem; + padding: 0.9rem 1rem; + border-bottom: 1px solid var(--border); + background: color-mix(in srgb, var(--surface) 92%, var(--bg-secondary) 8%); + position: sticky; + top: 0; + z-index: 1; +} + +.ws-mobile-title { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); +} + +.ws-empty-state { + min-height: 320px; + display: grid; + place-items: center; + align-content: center; + gap: 0.75rem; + text-align: center; + padding: 2rem; + max-width: 40rem; + margin: auto; +} + +.ws-empty-state p { + max-width: 32rem; + color: var(--text-secondary); + margin: 0; +} + +.ws-empty-icon { + width: 64px; + height: 64px; + display: grid; + place-items: center; + border-radius: var(--radius); + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: #fff; + font-size: 24px; + box-shadow: var(--shadow-sm); +} + +.ws-panel { + min-height: 0; + display: flex; + flex-direction: column; + padding: 1.1rem 1.25rem; + gap: 1rem; +} + +.ws-section { + display: flex; + flex-direction: column; + gap: 0.8rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--ws-surface-alt); +} + +.ws-section + .ws-section { + margin-top: 0.25rem; +} + +.ws-icon-btn { + min-width: 34px; + height: 34px; + padding: 0 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + flex-shrink: 0; + background: var(--bg-secondary); + color: var(--text-secondary); + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease, border-color 0.2s ease; + text-decoration: none; +} + +.ws-icon-btn:hover { + background: var(--surface-hover); + border-color: var(--border-hover); + color: var(--text-primary); + text-decoration: none; +} + +.ws-icon-btn:active { + transform: translateY(1px); +} + +.ws-primary-btn, +.ws-sidebar-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + width: 100%; + min-height: 38px; + padding: 0.55rem 0.85rem; + border: none; + border-radius: var(--radius-sm); + background: var(--primary); + color: #fff; + font-size: 0.92rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; +} + +.ws-primary-btn:hover, +.ws-sidebar-action-btn:hover { + background: var(--primary-dark); + text-decoration: none; + color: #fff; +} + +.ws-primary-btn:active, +.ws-sidebar-action-btn:active { + transform: translateY(1px); +} + +.ws-list-state { + padding: 1rem; + color: var(--text-secondary); + font-size: 0.86rem; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + border-top: 1px solid var(--border); +} + +.ws-nav-group-label { + padding: 0.7rem 0.95rem 0.35rem; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + user-select: none; +} + +.ws-nav-list { + display: flex; + flex-direction: column; + padding: 0.3rem 0.4rem 0.9rem; + gap: 0.2rem; + overflow: auto; + min-height: 0; + flex: 1; +} + +.ws-nav-item { + display: block; + padding: 0 0.2rem; + text-decoration: none; + color: inherit; +} + +.ws-nav-item-inner { + display: flex; + align-items: center; + gap: 0.55rem; + width: 100%; + padding: 0.58rem 0.65rem; + border-radius: var(--radius-sm); + min-width: 0; + transition: background 0.2s ease, border-color 0.2s ease, padding-left 0.2s ease; + border-left: 3px solid transparent; +} + +.ws-nav-item:hover .ws-nav-item-inner { + background: color-mix(in srgb, var(--surface-hover) 85%, transparent 15%); +} + +.ws-nav-item.active .ws-nav-item-inner, +.ws-nav-item.is-active .ws-nav-item-inner { + background: color-mix(in srgb, var(--primary) 12%, transparent 88%); + border-left-color: var(--primary); + padding-left: calc(0.65rem - 3px); +} + +.ws-nav-icon { + flex-shrink: 0; + width: 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; +} + +.ws-nav-item.active .ws-nav-icon, +.ws-nav-item.is-active .ws-nav-icon { + color: var(--primary); +} + +.ws-nav-item-text { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.45rem; + overflow: hidden; +} + +.ws-nav-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.9rem; + color: var(--text-primary); + line-height: 1.3; +} + +.ws-nav-item.active .ws-nav-title, +.ws-nav-item.is-active .ws-nav-title { + font-weight: 700; + color: var(--primary-dark); +} + +.ws-nav-meta { + flex-shrink: 0; + font-size: 0.72rem; + color: var(--text-muted); + transition: opacity 0.2s ease; +} + +.ws-status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0.2rem 0.6rem; + border-radius: 999px; + border: 1px solid transparent; + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.ws-status-pill-neutral { + background: color-mix(in srgb, var(--text-muted) 16%, transparent 84%); + border-color: color-mix(in srgb, var(--text-muted) 28%, transparent 72%); + color: var(--text-secondary); +} + +.ws-status-pill-success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +.ws-status-pill-warn { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + +.ws-status-pill-danger { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); +} + +.ws-status-pill-info { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); +} + +.ws-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.85rem; +} + +.ws-stat-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.ws-stat-card-label { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + color: var(--text-secondary); +} + +.ws-stat-card-value { + font-size: 1.45rem; + font-weight: 700; + line-height: 1.1; + font-variant-numeric: tabular-nums; +} + +.ws-stat-card-meta, +.ws-section-copy, +.ws-inline-note, +.ws-detail-copy { + margin: 0; + color: var(--text-secondary); +} + +.ws-detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.85rem; +} + +.ws-detail-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--bg-secondary) 85%, var(--surface) 15%); + min-height: 100%; +} + +.ws-detail-card h4 { + margin: 0 0 0.35rem; + font-size: 0.95rem; +} + +.ws-detail-card p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.ws-demo-sidebar-copy { + padding: 0 1rem 1rem; + font-size: 0.84rem; + color: var(--text-secondary); +} + +.ws-demo-sidebar-copy p { + margin: 0; +} + +@media (max-width: 860px) { + .ws-app { + display: block; + min-height: 72vh; + } + + .ws-sidebar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: min(88vw, max(var(--ws-sidebar-width), 320px)); + transform: translateX(-100%); + transition: transform 160ms ease; + box-shadow: var(--shadow-lg); + z-index: 10; + } + + .ws-sidebar.is-open { + transform: translateX(0); + } + + .ws-sidebar-overlay.is-open { + display: block; + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.38); + z-index: 5; + } + + .ws-mobile-bar { + display: flex; + } + + .ws-panel { + padding: 1rem; + } + + .ws-stat-grid, + .ws-detail-grid { + grid-template-columns: 1fr; + } + + .ws-section-head, + .ws-panel-header { + align-items: flex-start; + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/info/themes/common/fontawesome/LICENSE.txt b/site/info/themes/common/fontawesome/LICENSE.txt new file mode 100755 index 0000000..f31bef9 --- /dev/null +++ b/site/info/themes/common/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/site/info/themes/common/fontawesome/css/all.min.css b/site/info/themes/common/fontawesome/css/all.min.css new file mode 100755 index 0000000..0f922d8 --- /dev/null +++ b/site/info/themes/common/fontawesome/css/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/site/info/themes/common/fontawesome/css/fontawesome.min.css b/site/info/themes/common/fontawesome/css/fontawesome.min.css new file mode 100755 index 0000000..ab52c55 --- /dev/null +++ b/site/info/themes/common/fontawesome/css/fontawesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/site/info/themes/common/fontawesome/webfonts/fa-brands-400.eot b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.eot new file mode 100755 index 0000000..30a2784 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.eot differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-brands-400.svg b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.svg new file mode 100755 index 0000000..47bb690 --- /dev/null +++ b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.svg @@ -0,0 +1,3451 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/info/themes/common/fontawesome/webfonts/fa-brands-400.ttf b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.ttf new file mode 100755 index 0000000..a7ab4d4 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-brands-400.woff b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.woff new file mode 100755 index 0000000..ec5b613 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.woff differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-brands-400.woff2 b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100755 index 0000000..df11cea Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-regular-400.eot b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.eot new file mode 100755 index 0000000..b5440c9 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.eot differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-regular-400.svg b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.svg new file mode 100755 index 0000000..5ec81a8 --- /dev/null +++ b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/info/themes/common/fontawesome/webfonts/fa-regular-400.ttf b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.ttf new file mode 100755 index 0000000..87693a8 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-regular-400.woff b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.woff new file mode 100755 index 0000000..917bf73 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-regular-400.woff2 b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100755 index 0000000..0f10115 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-solid-900.eot b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.eot new file mode 100755 index 0000000..305fc64 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.eot differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-solid-900.svg b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.svg new file mode 100755 index 0000000..eb47f6a --- /dev/null +++ b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,4649 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:21 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/info/themes/common/fontawesome/webfonts/fa-solid-900.ttf b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.ttf new file mode 100755 index 0000000..8fb4d53 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-solid-900.woff b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.woff new file mode 100755 index 0000000..69f4474 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.woff differ diff --git a/site/info/themes/common/fontawesome/webfonts/fa-solid-900.woff2 b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100755 index 0000000..20e4ce2 Binary files /dev/null and b/site/info/themes/common/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/site/info/themes/common/fonts/b612/b612-bold-italic.ttf b/site/info/themes/common/fonts/b612/b612-bold-italic.ttf new file mode 100755 index 0000000..049965e Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-bold-italic.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-bold.ttf b/site/info/themes/common/fonts/b612/b612-bold.ttf new file mode 100755 index 0000000..3c92e2e Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-bold.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-italic.ttf b/site/info/themes/common/fonts/b612/b612-italic.ttf new file mode 100755 index 0000000..01ec3c5 Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-italic.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-mono-bold-italic.ttf b/site/info/themes/common/fonts/b612/b612-mono-bold-italic.ttf new file mode 100755 index 0000000..ec2a812 Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-mono-bold-italic.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-mono-bold.ttf b/site/info/themes/common/fonts/b612/b612-mono-bold.ttf new file mode 100755 index 0000000..fb26114 Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-mono-bold.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-mono-italic.ttf b/site/info/themes/common/fonts/b612/b612-mono-italic.ttf new file mode 100755 index 0000000..af20348 Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-mono-italic.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-mono-regular.ttf b/site/info/themes/common/fonts/b612/b612-mono-regular.ttf new file mode 100755 index 0000000..aaac636 Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-mono-regular.ttf differ diff --git a/site/info/themes/common/fonts/b612/b612-regular.ttf b/site/info/themes/common/fonts/b612/b612-regular.ttf new file mode 100755 index 0000000..c685a23 Binary files /dev/null and b/site/info/themes/common/fonts/b612/b612-regular.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf new file mode 100755 index 0000000..4c0ee56 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf new file mode 100755 index 0000000..fa51576 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf new file mode 100755 index 0000000..3cb9269 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf new file mode 100755 index 0000000..010ed45 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf new file mode 100755 index 0000000..8550ed9 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf new file mode 100755 index 0000000..2e93072 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf new file mode 100755 index 0000000..592caec Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf new file mode 100755 index 0000000..1f42361 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf new file mode 100755 index 0000000..ff1769f Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf new file mode 100755 index 0000000..dcd2c27 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf new file mode 100755 index 0000000..18da8c6 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf new file mode 100755 index 0000000..0af4e38 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf new file mode 100755 index 0000000..09cc053 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf differ diff --git a/site/info/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf b/site/info/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf new file mode 100755 index 0000000..e25a540 Binary files /dev/null and b/site/info/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf differ diff --git a/site/info/themes/common/fonts/press-start-2p/OFL.txt b/site/info/themes/common/fonts/press-start-2p/OFL.txt new file mode 100644 index 0000000..22796df --- /dev/null +++ b/site/info/themes/common/fonts/press-start-2p/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/site/info/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf b/site/info/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/site/info/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Black.ttf b/site/info/themes/common/fonts/roboto/Roboto-Black.ttf new file mode 100755 index 0000000..689fe5c Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Black.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-BlackItalic.ttf b/site/info/themes/common/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100755 index 0000000..0b4e0ee Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Bold.ttf b/site/info/themes/common/fonts/roboto/Roboto-Bold.ttf new file mode 100755 index 0000000..d3f01ad Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Bold.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-BoldItalic.ttf b/site/info/themes/common/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100755 index 0000000..41cc1e7 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Italic.ttf b/site/info/themes/common/fonts/roboto/Roboto-Italic.ttf new file mode 100755 index 0000000..6a1cee5 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Italic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Light.ttf b/site/info/themes/common/fonts/roboto/Roboto-Light.ttf new file mode 100755 index 0000000..219063a Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Light.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-LightItalic.ttf b/site/info/themes/common/fonts/roboto/Roboto-LightItalic.ttf new file mode 100755 index 0000000..0e81e87 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Medium.ttf b/site/info/themes/common/fonts/roboto/Roboto-Medium.ttf new file mode 100755 index 0000000..1a7f3b0 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Medium.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-MediumItalic.ttf b/site/info/themes/common/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100755 index 0000000..0030295 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Regular.ttf b/site/info/themes/common/fonts/roboto/Roboto-Regular.ttf new file mode 100755 index 0000000..2c97eea Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Regular.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-Thin.ttf b/site/info/themes/common/fonts/roboto/Roboto-Thin.ttf new file mode 100755 index 0000000..b74a4fd Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-Thin.ttf differ diff --git a/site/info/themes/common/fonts/roboto/Roboto-ThinItalic.ttf b/site/info/themes/common/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100755 index 0000000..dd0ddb8 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf b/site/info/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf new file mode 100755 index 0000000..fc28868 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf differ diff --git a/site/info/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf b/site/info/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf new file mode 100755 index 0000000..e1a648f Binary files /dev/null and b/site/info/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf b/site/info/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf new file mode 100755 index 0000000..97ff9f1 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/RobotoCondensed-Light.ttf b/site/info/themes/common/fonts/roboto/RobotoCondensed-Light.ttf new file mode 100755 index 0000000..2dae31e Binary files /dev/null and b/site/info/themes/common/fonts/roboto/RobotoCondensed-Light.ttf differ diff --git a/site/info/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf b/site/info/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf new file mode 100755 index 0000000..da108d3 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf differ diff --git a/site/info/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf b/site/info/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf new file mode 100755 index 0000000..c2304c1 Binary files /dev/null and b/site/info/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf differ diff --git a/site/info/themes/common/page.blank.uce b/site/info/themes/common/page.blank.uce new file mode 100644 index 0000000..40d48f7 --- /dev/null +++ b/site/info/themes/common/page.blank.uce @@ -0,0 +1,7 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + print(starter_page_main_html(context)); +} diff --git a/site/info/themes/common/page.json.uce b/site/info/themes/common/page.json.uce new file mode 100644 index 0000000..975a1a8 --- /dev/null +++ b/site/info/themes/common/page.json.uce @@ -0,0 +1,14 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + context.header["Content-Type"] = "application/json"; + context.header["Cache-Control"] = "no-cache, no-store, must-revalidate"; + if(context.call["json"].get_type_name() == "array") + print(json_encode(context.call["json"])); + else if(context.var["starter"]["json"].get_type_name() == "array") + print(json_encode(context.var["starter"]["json"])); + else + print(starter_page_main_html(context)); +} diff --git a/site/info/themes/dark/css/style.css b/site/info/themes/dark/css/style.css new file mode 100755 index 0000000..0b45533 --- /dev/null +++ b/site/info/themes/dark/css/style.css @@ -0,0 +1,516 @@ +:root { + /* Dark theme color palette */ + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --bg-gradient: linear-gradient(135deg, #1e3a8a 0%, #3730a3 100%); + --surface: #1e293b; + --surface-elevated: #334155; + --surface-hover: #475569; + --primary: #60a5fa; + --primary-dark: #3b82f6; + --primary-light: #93c5fd; + --secondary: #a78bfa; + --accent: #22d3ee; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --border: #334155; + --border-hover: #475569; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + + /* Dark theme specific variables */ + --glass-bg: rgba(30, 41, 59, 0.8); + --glass-border: rgba(148, 163, 184, 0.1); + --overlay: rgba(0, 0, 0, 0.6); + --success: #10b981; + --success-bg: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.3); + --success-text: #10b981; + --warning: #f59e0b; + --warning-bg: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.3); + --warning-text: #f59e0b; + --error: #ef4444; + --error-bg: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.3); + --error-text: #ef4444; + --info: #3b82f6; + --info-bg: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.3); + --info-text: #3b82f6; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Links */ +a { + color: var(--primary); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--primary-light); + text-decoration: underline; +} + +/* Modern Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +nav > a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +nav.nav-scrolled { + background: rgba(30, 41, 59, 0.95) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} + +h1::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: var(--bg-gradient); + border-radius: 3px; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px var(--success-bg); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px var(--error-bg); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #8b5cf6; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: var(--bg-color); +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/info/themes/dark/icon.png b/site/info/themes/dark/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/info/themes/dark/icon.png differ diff --git a/site/info/themes/dark/page.html.uce b/site/info/themes/dark/page.html.uce new file mode 100644 index 0000000..8645bb0 --- /dev/null +++ b/site/info/themes/dark/page.html.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > + + +
> + +
+ + + + +} diff --git a/site/info/themes/light/css/style.css b/site/info/themes/light/css/style.css new file mode 100755 index 0000000..b872b44 --- /dev/null +++ b/site/info/themes/light/css/style.css @@ -0,0 +1,540 @@ +:root { + /* Light theme color palette */ + --bg-color: #f8fafc; + --bg-secondary: #ffffff; + --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --surface: #ffffff; + --surface-elevated: #ffffff; + --surface-hover: #f1f5f9; + --primary: #3b82f6; + --primary-dark: #1d4ed8; + --primary-light: #93c5fd; + --secondary: #8b5cf6; + --accent: #06b6d4; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --border: #e2e8f0; + --border-hover: #cbd5e1; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + + /* Light theme specific variables */ + --glass-bg: rgba(255, 255, 255, 0.95); + --glass-border: rgba(0, 0, 0, 0.1); + --overlay: rgba(0, 0, 0, 0.3); + --success: #10b981; + --success-bg: #dcfce7; + --success-border: #16a34a; + --success-text: #065f46; + --warning: #f59e0b; + --warning-bg: #fef3c7; + --warning-border: #d97706; + --warning-text: #92400e; + --error: #ef4444; + --error-bg: #fee2e2; + --error-border: #dc2626; + --error-text: #991b1b; + --info: #3b82f6; + --info-bg: #dbeafe; + --info-border: #2563eb; + --info-text: #1e40af; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* Links */ +a { + color: var(--primary); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--primary-dark); + text-decoration: underline; +} + +/* Modern Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +nav > a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + margin-right: 0.5rem; + font-size: 0.95rem; +} + +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} + +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary-dark); +} + +/* Navigation scroll state */ +nav.nav-scrolled { + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +/* Main Content Area */ +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2.5rem; + background: var(--surface); + padding: 1.75rem 2rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--border); + position: relative; + overflow: hidden; +} + +h1::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--bg-gradient); +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #7c3aed; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/info/themes/light/icon.png b/site/info/themes/light/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/info/themes/light/icon.png differ diff --git a/site/info/themes/light/img/favicon.png b/site/info/themes/light/img/favicon.png new file mode 100755 index 0000000..6653d41 Binary files /dev/null and b/site/info/themes/light/img/favicon.png differ diff --git a/site/info/themes/light/page.html.uce b/site/info/themes/light/page.html.uce new file mode 100644 index 0000000..495d3e0 --- /dev/null +++ b/site/info/themes/light/page.html.uce @@ -0,0 +1,28 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > + + +
> + +
+ + + + +} diff --git a/site/info/themes/localfirst/css/style.css b/site/info/themes/localfirst/css/style.css new file mode 100644 index 0000000..088ce45 --- /dev/null +++ b/site/info/themes/localfirst/css/style.css @@ -0,0 +1,382 @@ +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.28); } + 50% { opacity: 0.6; box-shadow: 0 0 4px var(--accent), 0 0 8px rgba(255,107,26,0.16); } +} + +:root { + --brand-cyan: #00b7f9; + --brand-cyan-soft: #58d2ff; + --brand-cyan-bright: #b6eeff; + --brand-cyan-deep: #0088bb; + --brand-orange: #ff6b1a; + --brand-orange-soft: #ff9f6a; + --brand-orange-bright: #ffd0b3; + --bg-color: #020913; + --bg-secondary: #07131f; + --surface: rgba(7, 19, 31, 0.9); + --surface-elevated: rgba(10, 28, 43, 0.96); + --surface-hover: rgba(0, 183, 249, 0.08); + --card: rgba(7, 19, 31, 0.84); + --text-primary: var(--brand-cyan-soft); + --text-secondary: rgba(139, 222, 255, 0.82); + --text-muted: rgba(101, 161, 188, 0.78); + --primary: var(--brand-cyan); + --primary-light: rgba(0, 183, 249, 0.18); + --primary-dark: var(--brand-cyan-deep); + --secondary: var(--brand-cyan-soft); + --accent: var(--brand-orange); + --success: #34d399; + --success-bg: rgba(52, 211, 153, 0.1); + --success-border: rgba(52, 211, 153, 0.3); + --success-text: #7ef0bb; + --warning: var(--brand-orange); + --warning-bg: rgba(255, 107, 26, 0.12); + --warning-border: rgba(255, 107, 26, 0.32); + --warning-text: #ffc29f; + --error: #fb7185; + --error-bg: rgba(251, 113, 133, 0.12); + --error-border: rgba(251, 113, 133, 0.3); + --error-text: #fecdd3; + --info: var(--brand-cyan); + --info-bg: rgba(0, 183, 249, 0.1); + --info-border: rgba(0, 183, 249, 0.28); + --info-text: var(--brand-cyan-bright); + --border: rgba(67, 149, 182, 0.22); + --border-hover: rgba(98, 205, 246, 0.36); + --shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.18); + --shadow-md: 0 14px 32px rgba(0, 0, 0, 0.24); + --shadow-lg: 0 18px 38px rgba(0, 0, 0, 0.28); + --shadow-xl: 0 22px 48px rgba(0, 0, 0, 0.34); + --radius: 10px; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 20px; + --font-sans: 'B612', 'Segoe UI', sans-serif; + --font-mono: 'B612 Mono', ui-monospace, monospace; +} + +* { box-sizing: border-box; margin: 0; } +html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +body { + font-family: var(--font-sans); + font-size: 14px; + background: var(--bg-color); + color: var(--text-primary); + min-height: 100vh; +} +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 10% 0%, rgba(0,183,249,0.12) 0%, transparent 60%), + radial-gradient(ellipse 60% 50% at 90% 100%, rgba(255,107,26,0.08) 0%, transparent 60%); + pointer-events: none; + z-index: 0; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +body.admin-page { overflow: hidden; height: 100vh; } +.admin-shell { display: flex; height: 100vh; overflow: hidden; position: relative; z-index: 1; } +.admin-nav { + width: 210px; + flex-shrink: 0; + background: + linear-gradient(180deg, rgba(5, 16, 26, 0.98), rgba(2, 10, 18, 0.98)), + linear-gradient(180deg, rgba(0, 174, 240, 0.06), rgba(255, 107, 26, 0.04)); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0 0 16px; +} +.admin-nav-header { + border-bottom: 1px solid var(--border); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 9px; + padding: 10px 10px 12px; +} +.admin-nav-title { display: block; width: 100%; } +.admin-nav-logo-img { display: block; width: 100%; height: auto; } +.admin-nav-item { + display: block; + padding: 9px 16px 9px 14px; + text-decoration: none; + color: var(--text-secondary); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; + border-left: 3px solid transparent; +} +.admin-nav-item:hover { + color: var(--brand-cyan-bright); + background: linear-gradient(90deg, rgba(0, 174, 240, 0.12), rgba(255, 107, 26, 0.03)); + text-decoration: none; +} +.admin-nav-item.active { + color: var(--accent); + border-left-color: var(--accent); + background: linear-gradient(90deg, rgba(255, 107, 26, 0.16), rgba(255, 107, 26, 0.04)); + box-shadow: inset 0 0 0 1px rgba(255, 107, 26, 0.08); +} + +.admin-main { + flex: 1; + min-width: 0; + position: relative; + display: flex; + flex-direction: column; +} + +.admin-toolbar { + position: fixed; + top: 16px; + right: 20px; + z-index: 40; + display: flex; + justify-content: flex-end; + pointer-events: none; +} + +.admin-account-card { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(7, 19, 31, 0.78); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} +.admin-account-name { + color: var(--brand-cyan-bright); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} +.admin-account-links { + display: inline-flex; + gap: 10px; + align-items: center; +} +.admin-account-links a { + color: var(--accent); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} + +.admin-content { + flex: 1; + overflow-y: auto; + padding: 16px 20px 28px; + position: relative; + min-width: 0; +} + +h1 { + font-size: 18px; + font-weight: 600; + letter-spacing: 1.4px; + text-transform: uppercase; + color: var(--brand-cyan-bright); + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 14px; +} +h1::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background: var(--accent); + border-radius: 2px; + box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.24); + animation: pulse-glow 2.5s ease-in-out infinite; +} +h2, h3, h4, h5, h6 { + color: var(--brand-cyan-bright); + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} +p { margin: 0 0 12px; color: var(--text-secondary); } + +.card, +.block, +.dashboard-panel, +#content > div:not(.ws-app):not(.demo-section) { + background: var(--card); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + position: relative; + transition: border-color 0.25s ease, box-shadow 0.25s ease; + margin-bottom: 14px; +} +.card::before, +.block::before, +.dashboard-panel::before, +#content > div:not(.ws-app):not(.demo-section)::before { + content: ''; + position: absolute; + top: 0; + left: 12px; + right: 12px; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.55; +} +.card:hover, +.block:hover, +.dashboard-panel:hover, +#content > div:not(.ws-app):not(.demo-section):hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-md); +} + +form { max-width: 760px; } +form > div { display: grid; gap: 6px; margin-bottom: 12px; } +label { + color: var(--text-muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} +input, select, textarea { + width: 100%; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(3, 7, 18, 0.55); + color: var(--brand-cyan-bright); + padding: 10px 12px; + font: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--border-hover); + box-shadow: 0 0 0 3px rgba(0, 183, 249, 0.12); +} +button, .btn, input[type="submit"] { + border: 1px solid var(--border); + background: rgba(255, 107, 26, 0.12); + color: var(--brand-orange-bright); + border-radius: var(--radius); + padding: 10px 14px; + font: inherit; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: rgba(255, 107, 26, 0.18); + border-color: rgba(255, 107, 26, 0.42); + text-decoration: none; +} +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: var(--radius); + padding: 10px 12px; + border: 1px solid var(--border); + background: rgba(0, 183, 249, 0.08); + color: var(--text-secondary); + margin-bottom: 12px; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: rgba(3, 7, 18, 0.42); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + background: rgba(0, 183, 249, 0.08); +} +tr:last-child td { border-bottom: none; } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 12px; + background: rgba(3, 7, 18, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +@media (max-width: 980px) { + .admin-shell { flex-direction: column; height: auto; min-height: 100vh; } + .admin-nav { + width: 100%; + flex-direction: row; + overflow-x: auto; + padding-bottom: 0; + } + .admin-nav-header { min-width: 180px; margin-bottom: 0; border-bottom: 0; border-right: 1px solid var(--border); } + .admin-nav-item { white-space: nowrap; border-left: 0; border-bottom: 3px solid transparent; } + .admin-nav-item.active { border-bottom-color: var(--accent); border-left-color: transparent; } + .admin-toolbar { position: static; justify-content: flex-start; margin: 16px 20px 0; } + .admin-content { padding-top: 12px; } +} \ No newline at end of file diff --git a/site/info/themes/localfirst/icon.png b/site/info/themes/localfirst/icon.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/info/themes/localfirst/icon.png differ diff --git a/site/info/themes/localfirst/img/local_first_logo.png b/site/info/themes/localfirst/img/local_first_logo.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/info/themes/localfirst/img/local_first_logo.png differ diff --git a/site/info/themes/localfirst/page.html.uce b/site/info/themes/localfirst/page.html.uce new file mode 100644 index 0000000..5a7771c --- /dev/null +++ b/site/info/themes/localfirst/page.html.uce @@ -0,0 +1,54 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + String current_path = context.var["starter"]["route"]["l_path"].to_string(); + + DTree global_props; + global_props["cookie_consent"].set_bool(false); + + DTree account_props; + account_props["wrapper_class"] = "admin-account-card"; + account_props["links_wrapper_class"] = "admin-account-links"; + account_props["name_class"] = "admin-account-name"; + + <> + + "> + + + + "> + +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + +} diff --git a/site/info/themes/portal-dark/css/style.css b/site/info/themes/portal-dark/css/style.css new file mode 100644 index 0000000..90001c3 --- /dev/null +++ b/site/info/themes/portal-dark/css/style.css @@ -0,0 +1,313 @@ +:root { + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --surface: #1e293b; + --surface-elevated: #334155; + --surface-hover: #475569; + --primary: #60a5fa; + --primary-dark: #3b82f6; + --primary-light: #93c5fd; + --secondary: #a78bfa; + --accent: #22d3ee; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --border: #334155; + --border-hover: #475569; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + --glass-bg: rgba(30, 41, 59, 0.8); + --success: #10b981; + --success-bg: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.3); + --success-text: #10b981; + --warning: #f59e0b; + --warning-bg: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.3); + --warning-text: #f59e0b; + --error: #ef4444; + --error-bg: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.3); + --error-text: #ef4444; + --info: #3b82f6; + --info-bg: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.3); + --info-text: #3b82f6; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'SFMono-Regular', Consolas, monospace; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; } +body { + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +a { color: var(--primary); text-decoration: none; transition: all 0.2s ease; } +a:hover { color: var(--primary-light); text-decoration: underline; } + +::selection { + background: rgba(96, 165, 250, 0.3); + color: var(--text-primary); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 50% at 20% -10%, rgba(96, 165, 250, 0.05) 0%, transparent 60%), + radial-gradient(ellipse 60% 40% at 85% 110%, rgba(34, 211, 238, 0.04) 0%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} +nav::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.3; +} + +.nav-menu { display: flex; align-items: center; gap: 0.25rem; } +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding: 1rem 1.5rem; + margin: 0.5rem 0.25rem; +} +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary); +} +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + font-size: 0.95rem; + margin-right: 0.5rem; +} +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid var(--border); +} +.nav-account .nav-avatar { width: 30px; height: 30px; } +.profile-avatar { width: 96px; height: 96px; } + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 3px; +} +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } + +#content > div:not(.ws-app), .block, .card, .dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover, .dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +form { max-width: 600px; margin: 0 auto; } +form > div { display: flex; flex-direction: column; margin-bottom: 1.5rem; gap: 0.5rem; } +label { font-weight: 500; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; } +input, select, textarea { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); + text-decoration: none; +} + +.banner { + border-radius: var(--radius); + padding: 0.8rem 1rem; + border: 1px solid var(--border); + margin-bottom: 1rem; + background: rgba(255,255,255,0.03); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--border); + background: rgba(255,255,255,0.02); + border-radius: var(--radius); + overflow: hidden; +} +th, td { padding: 0.8rem 0.9rem; border-bottom: 1px solid var(--border); text-align: left; } +th { background: var(--surface-elevated); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(255,255,255,0.02); } +code, pre { font-family: var(--font-mono); } +pre { + padding: 1rem; + background: rgba(15, 23, 42, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + padding: 1.25rem 1rem; + color: var(--text-muted); + font-size: 0.875rem; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-secondary); + outline: none; + border: none; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} + +@media (max-width: 860px) { + nav { flex-wrap: wrap; padding-bottom: 0.6rem; } + .nav-account { width: 100%; margin-left: 0; justify-content: flex-start; } + #content { margin-top: 7.5rem; padding: 1rem 0.75rem; } + #content > div:not(.ws-app), .block, .card, .dashboard-panel { padding: 1.25rem; margin-bottom: 1rem; } + h1 { font-size: 2rem; padding: 1.25rem; } +} \ No newline at end of file diff --git a/site/info/themes/portal-dark/icon.png b/site/info/themes/portal-dark/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/info/themes/portal-dark/icon.png differ diff --git a/site/info/themes/portal-dark/page.html.uce b/site/info/themes/portal-dark/page.html.uce new file mode 100644 index 0000000..3f353a7 --- /dev/null +++ b/site/info/themes/portal-dark/page.html.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + "> + + +
> + +
+ + + + +} diff --git a/site/info/themes/portal-light/css/style.css b/site/info/themes/portal-light/css/style.css new file mode 100644 index 0000000..141b15f --- /dev/null +++ b/site/info/themes/portal-light/css/style.css @@ -0,0 +1,392 @@ +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-italic.ttf") format("truetype"); + font-style: italic; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +:root { + --kg-blue: #0f68ad; + --kg-blue-dark: #0b4f85; + --kg-blue-light: #2d86ca; + --kg-bg: #e9e9ea; + --kg-surface: #ffffff; + --kg-border: rgba(0, 0, 0, 0.2); + --kg-text: #121417; + --kg-muted: #6c737b; + --kg-orange: #f08a00; + --kg-success: #149b2e; + --kg-danger: #e11818; + --topbar-height: 56px; + --bg-color: var(--kg-bg); + --bg-secondary: #ffffff; + --surface: var(--kg-surface); + --surface-elevated: #ffffff; + --surface-hover: #e8eef3; + --primary: var(--kg-blue); + --primary-dark: var(--kg-blue-dark); + --primary-light: var(--kg-blue-light); + --secondary: #596572; + --accent: var(--kg-orange); + --text-primary: var(--kg-text); + --text-secondary: #4b5560; + --text-muted: var(--kg-muted); + --border: var(--kg-border); + --border-hover: rgba(15, 104, 173, 0.3); + --success: var(--kg-success); + --success-bg: #e7f5ea; + --success-border: #86c88f; + --success-text: #0f6e22; + --warning: #c97a14; + --warning-bg: #fff4e4; + --warning-border: #f0c07b; + --warning-text: #92550a; + --error: var(--kg-danger); + --error-bg: #ffe7e7; + --error-border: #ea9999; + --error-text: #b80f0f; + --info: var(--primary); + --info-bg: #e8f2ff; + --info-border: #b9d4f3; + --info-text: #0b4f85; + --shadow-sm: 0 1px 2px rgba(18, 20, 23, 0.06); + --shadow-md: 0 8px 22px rgba(18, 20, 23, 0.06); + --shadow-lg: 0 16px 34px rgba(18, 20, 23, 0.1); + --shadow-xl: 0 20px 40px rgba(18, 20, 23, 0.14); + --radius: 8px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --font-sans: "B612", "Segoe UI", sans-serif; + --font-mono: "B612 Mono", "SFMono-Regular", Consolas, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body { min-height: 100%; } +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.4; + color: var(--text-primary); + background: var(--bg-color); +} +a { color: var(--primary-dark); text-decoration: none; } +a:hover { text-decoration: underline; } + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--topbar-height); + display: flex; + align-items: stretch; + background: var(--primary); + border-bottom: 1px solid var(--primary-dark); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); + z-index: 60; +} + +.nav-menu { + display: flex; + align-items: stretch; + overflow-x: auto; + white-space: nowrap; + flex: 1; +} + +.nav-menu > a { + display: inline-flex; + align-items: center; + padding: 0 0.9rem; + color: #fff; + border-right: 1px solid rgba(255, 255, 255, 0.16); + font-size: 14px; + font-weight: 700; + text-decoration: none; + background: transparent; +} + +.nav-menu > a:first-child { + font-size: 18px; + min-width: 250px; + background: rgba(0, 0, 0, 0.08); +} + +.nav-menu > a:hover, +.nav-menu > a:focus { + background: var(--primary-dark); + text-decoration: none; +} + +.nav-account { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0 0.7rem; + border-left: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.12); + flex-shrink: 0; +} + +.nav-account .account-name { + color: #d8f8df; + font-size: 14px; + font-weight: 700; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid rgba(255, 255, 255, 0.4); +} + +.nav-account .nav-avatar { + width: 30px; + height: 30px; +} + +.profile-avatar { + width: 96px; + height: 96px; + border-color: var(--border); +} + +.nav-account a { + color: #fff; + font-size: 14px; + font-weight: 700; + padding: 0.22rem 0.5rem; + border-radius: 3px; +} + +.nav-account a:hover { + background: rgba(255, 255, 255, 0.16); + text-decoration: none; +} + +#content { + margin-top: var(--topbar-height); + padding: 1rem; + min-height: calc(100vh - var(--topbar-height)); +} + +#content > div:not(.ws-app), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: 1.25rem; + margin-bottom: 1rem; + transition: box-shadow 0.2s ease, border-color 0.2s ease; +} + +#content > div:not(.ws-app):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: rgba(15, 104, 173, 0.2); +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + color: #252b31; + margin-bottom: 0.7rem; +} +h1 { font-size: 22px; padding-bottom: 0.5rem; border-bottom: 2px solid #d2d3d7; position: relative; } +h1::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 50px; + height: 2px; + background: var(--primary); + border-radius: 2px; +} +h2 { font-size: 18px; } +h3 { font-size: 16px; } +h4, h5, h6 { font-size: 14px; } + +p { margin: 0.4rem 0 0.85rem; } + +form { max-width: 780px; } +form > div { margin-bottom: 0.8rem; } +label { + display: block; + font-weight: 700; + font-size: 14px; + color: #3f464f; + margin-bottom: 0.3rem; +} +input, select, textarea { + width: 100%; + min-height: 36px; + padding: 0 0.7rem; + border: 1px solid #a8aaaf; + border-radius: 4px; + background: #fff; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} +textarea { min-height: 100px; padding: 0.5rem 0.7rem; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-light); + box-shadow: 0 0 0 1px var(--primary-light); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 0.9rem; + border: 1px solid #7f8084; + border-radius: 4px; + background: #f7f7f7; + color: #1a1e23; + font-size: 14px; + font-weight: 700; + cursor: pointer; + font-family: inherit; +} +button:hover, .btn:hover, input[type="submit"]:hover { background: #ececef; text-decoration: none; } +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: 4px; + padding: 0.65rem 0.8rem; + border: 1px solid #b4b5ba; + margin-bottom: 0.8rem; + background: #fafafa; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 0.7rem 0.8rem; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { background: #e7e7e8; font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 0, 0, 0.02); } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.9rem; + background: #fff; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + background: #ffffff; + color: var(--text-muted); + padding: 1rem 1rem 1.5rem; + font-size: 13px; +} +.footer-inner { max-width: 1200px; margin: 0 auto; } + +input[type="range"] { + height: 6px; + padding: 0; + min-height: auto; + border: none; + background: #d4d5d9; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +::selection { + background: rgba(15, 104, 173, 0.2); + color: var(--text-primary); +} + +@media (max-width: 860px) { + nav { + height: auto; + flex-direction: column; + } + .nav-menu { + flex-wrap: wrap; + } + .nav-menu > a:first-child { + min-width: 0; + } + .nav-account { + justify-content: flex-start; + padding: 0.6rem 0.7rem; + } + #content { + margin-top: 104px; + padding: 0.75rem; + } +} \ No newline at end of file diff --git a/site/info/themes/portal-light/icon.png b/site/info/themes/portal-light/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/info/themes/portal-light/icon.png differ diff --git a/site/info/themes/portal-light/page.html.uce b/site/info/themes/portal-light/page.html.uce new file mode 100644 index 0000000..d125df0 --- /dev/null +++ b/site/info/themes/portal-light/page.html.uce @@ -0,0 +1,29 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + footer_props["inner_class"] = "footer-inner"; + + <> + + "> + + + + "> + + +
> + +
+ + + + +} diff --git a/site/info/themes/retro-gaming/css/style.css b/site/info/themes/retro-gaming/css/style.css new file mode 100644 index 0000000..f187fa6 --- /dev/null +++ b/site/info/themes/retro-gaming/css/style.css @@ -0,0 +1,523 @@ +/* ──────────────────────────────────────────────────────── + RETRO GAMING THEME + CRT scanlines · pixel font · neon glow · 8-bit palette + ──────────────────────────────────────────────────────── */ + +@font-face { + font-family: 'Press Start 2P'; + src: url('../../common/fonts/press-start-2p/press-start-2p-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} + +/* ── tokens ─────────────────────────────────────────── */ +:root { + /* background */ + --bg-color: #0a0a1a; + --bg-secondary: #10102a; + --surface: #12122e; + --surface-elevated: #1a1a3e; + --surface-hover: #22224e; + + /* neon palette */ + --primary: #00ff88; + --primary-dark: #00cc6a; + --primary-light: #66ffbb; + --secondary: #ff00ff; + --accent: #ffff00; + --text-primary: #e0e0ff; + --text-secondary: #a0a0cc; + --text-muted: #6868a0; + + /* borders, shadows */ + --border: #2a2a5a; + --border-hover: #3a3a7a; + --shadow-sm: 0 2px 0 #000; + --shadow-md: 0 4px 0 #000, 0 0 12px rgba(0, 255, 136, 0.08); + --shadow-lg: 0 6px 0 #000, 0 0 24px rgba(0, 255, 136, 0.12); + --shadow-xl: 0 8px 0 #000, 0 0 40px rgba(0, 255, 136, 0.16); + + /* semantic */ + --success: #00ff88; + --success-bg: rgba(0, 255, 136, 0.1); + --success-border: rgba(0, 255, 136, 0.3); + --success-text: #00ff88; + --warning: #ffff00; + --warning-bg: rgba(255, 255, 0, 0.1); + --warning-border: rgba(255, 255, 0, 0.3); + --warning-text: #ffff00; + --error: #ff3366; + --error-bg: rgba(255, 51, 102, 0.1); + --error-border: rgba(255, 51, 102, 0.3); + --error-text: #ff3366; + --info: #00ccff; + --info-bg: rgba(0, 204, 255, 0.1); + --info-border: rgba(0, 204, 255, 0.3); + --info-text: #00ccff; + + /* geometry */ + --radius: 0px; + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 2px; + --radius-xl: 4px; + + /* typography */ + --font-sans: 'Press Start 2P', monospace; + --font-mono: 'Press Start 2P', monospace; +} + +/* ── reset ──────────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; image-rendering: pixelated; } + +body { + font-family: var(--font-sans); + font-size: 10px; + line-height: 2; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: none; + -moz-osx-font-smoothing: unset; +} + +/* ── CRT scanline overlay ───────────────────────────── */ +body::before { + content: ''; + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.15) 2px, + rgba(0, 0, 0, 0.15) 4px + ); + pointer-events: none; + z-index: 9000; +} + +/* subtle vignette */ +body::after { + content: ''; + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.5) 100%); + pointer-events: none; + z-index: 8999; +} + +::selection { + background: var(--primary); + color: var(--bg-color); +} + +/* ── links ──────────────────────────────────────────── */ +a { + color: var(--primary); + text-decoration: none; + transition: color 0.1s step-end; +} +a:hover { + color: var(--accent); + text-decoration: none; + text-shadow: 0 0 6px var(--accent); +} + +/* ── navigation ─────────────────────────────────────── */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--bg-secondary); + border-bottom: 3px solid var(--primary); + padding: 0 0.5rem; + display: flex; + align-items: center; + box-shadow: 0 3px 0 #000, 0 6px 20px rgba(0, 255, 136, 0.15); +} + +.nav-menu { + display: flex; + align-items: center; + gap: 0; + flex: 1; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; +} +.nav-menu::-webkit-scrollbar { display: none; } + +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.75rem 0.8rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 8px; + letter-spacing: 0.5px; + text-transform: uppercase; + border-right: 1px solid var(--border); + transition: background 0.1s step-end, color 0.1s step-end; +} + +.nav-menu > a:first-child { + color: var(--primary); + text-shadow: 0 0 8px rgba(0, 255, 136, 0.5); + font-size: 10px; + padding: 0.75rem 1rem; + border-right: 2px solid var(--primary); +} + +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--accent); + text-shadow: 0 0 6px var(--accent); + text-decoration: none; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0; + flex: 0 0 auto; + overflow: visible; +} +.nav-account a { + border-right: none; + border-left: 1px solid var(--border); +} + +.nav-account .account-name { + color: var(--primary); + font-size: 8px; + padding: 0 0.5rem; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 0; + border: 2px solid var(--primary); + image-rendering: pixelated; +} +.nav-account .nav-avatar { width: 24px; height: 24px; } +.profile-avatar { width: 64px; height: 64px; } + +/* ── main content ───────────────────────────────────── */ +#content { + margin-top: 3.5rem; + min-height: calc(100vh - 7rem); + padding: 1.5rem 1rem; + max-width: 1100px; + margin-left: auto; + margin-right: auto; + position: relative; + z-index: 1; +} + +/* ── typography ─────────────────────────────────────── */ +h1, h2, h3, h4, h5, h6 { + font-weight: 400; + line-height: 2; + margin-bottom: 0.75rem; + color: var(--primary); + text-shadow: 0 0 10px rgba(0, 255, 136, 0.35); + text-transform: uppercase; +} + +h1 { + font-size: 16px; + padding: 0.75rem 0; + border-bottom: 3px double var(--primary); + margin-bottom: 1.25rem; + position: relative; +} +h1::before { + content: '►'; + margin-right: 0.5rem; + animation: blink-cursor 1s step-end infinite; +} +h2 { font-size: 12px; color: var(--info-text); text-shadow: 0 0 8px rgba(0, 204, 255, 0.3); } +h3 { font-size: 10px; color: var(--secondary); text-shadow: 0 0 8px rgba(255, 0, 255, 0.3); } +h4, h5, h6 { font-size: 10px; } + +p { margin: 0 0 0.75rem; } + +@keyframes blink-cursor { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* ── cards / blocks ─────────────────────────────────── */ +#content > div:not(.ws-app):not(.demo-section), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 2px solid var(--border); + padding: 1rem; + margin-bottom: 1.25rem; + box-shadow: var(--shadow-md); + position: relative; + transition: border-color 0.1s step-end; +} + +#content > div:not(.ws-app):not(.demo-section)::before, +.block::before, +.card::before, +.dashboard-panel::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, var(--primary), var(--secondary), var(--accent)); +} + +#content > div:not(.ws-app):not(.demo-section):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-lg); +} + +/* ── forms ──────────────────────────────────────────── */ +form { max-width: 640px; } +form > div { display: grid; gap: 0.35rem; margin-bottom: 1rem; } + +label { + color: var(--accent); + font-size: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +input, select, textarea { + width: 100%; + padding: 0.6rem 0.7rem; + border: 2px solid var(--border); + background: var(--bg-color); + color: var(--primary); + font-family: var(--font-mono); + font-size: 10px; + caret-color: var(--primary); +} +textarea { min-height: 100px; } + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary), 0 0 12px rgba(0, 255, 136, 0.2); +} + +input::placeholder, textarea::placeholder { + color: var(--text-muted); + opacity: 1; +} + +/* ── buttons ────────────────────────────────────────── */ +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1.2rem; + border: 2px solid var(--primary); + background: transparent; + color: var(--primary); + font-family: var(--font-sans); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.1s step-end; + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--primary); +} + +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary); + color: var(--bg-color); + text-shadow: none; + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--primary); + transform: translate(2px, 2px); + text-decoration: none; +} + +button:active, .btn:active, input[type="submit"]:active { + box-shadow: none; + transform: translate(4px, 4px); +} + +.btn-secondary { + border-color: var(--secondary); + color: var(--secondary); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--secondary); +} +.btn-secondary:hover { + background: var(--secondary); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--secondary); +} + +.btn-outline { + border-color: var(--accent); + color: var(--accent); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--accent); +} +.btn-outline:hover { + background: var(--accent); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--accent); +} + +/* ── range slider ───────────────────────────────────── */ +input[type="range"] { + height: 8px; + padding: 0; + border: 2px solid var(--border); + background: var(--bg-color); + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} +input[type="range"]::-moz-range-thumb { + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + border-radius: 0; + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} + +/* ── banners ────────────────────────────────────────── */ +.banner { + padding: 0.6rem 0.8rem; + border: 2px solid var(--border); + margin-bottom: 0.75rem; + background: var(--surface); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +/* ── tables ─────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border: 2px solid var(--border); +} +th, td { + padding: 0.5rem 0.65rem; + border-bottom: 1px solid var(--border); + text-align: left; + font-size: 9px; +} +th { + background: var(--surface-elevated); + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid var(--accent); +} +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 255, 136, 0.04); } + +/* ── code ───────────────────────────────────────────── */ +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.75rem; + background: var(--bg-color); + border: 2px solid var(--border); + overflow: auto; + color: var(--primary); +} + +/* ── footer ─────────────────────────────────────────── */ +footer { + border-top: 3px solid var(--primary); + padding: 1rem; + color: var(--text-muted); + font-size: 8px; + background: var(--bg-secondary); + box-shadow: 0 -3px 0 #000; + position: relative; + z-index: 1; +} +footer::before { + content: '// '; + color: var(--text-muted); +} + +/* ── pixel decoration classes (for page template) ──── */ +.retro-stars { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(1px 1px at 10% 15%, var(--primary), transparent), + radial-gradient(1px 1px at 30% 45%, var(--accent), transparent), + radial-gradient(1px 1px at 55% 20%, var(--secondary), transparent), + radial-gradient(1px 1px at 70% 65%, var(--info-text), transparent), + radial-gradient(1px 1px at 85% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 15% 75%, var(--accent), transparent), + radial-gradient(1px 1px at 45% 85%, var(--secondary), transparent), + radial-gradient(1px 1px at 90% 80%, var(--primary), transparent), + radial-gradient(1px 1px at 25% 55%, var(--info-text), transparent), + radial-gradient(1px 1px at 60% 50%, var(--accent), transparent), + radial-gradient(1px 1px at 5% 40%, var(--error-text), transparent), + radial-gradient(1px 1px at 75% 10%, var(--accent), transparent), + radial-gradient(1px 1px at 40% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 95% 55%, var(--secondary), transparent), + radial-gradient(1px 1px at 50% 70%, var(--primary), transparent), + radial-gradient(1px 1px at 20% 90%, var(--info-text), transparent); + animation: twinkle 4s ease-in-out infinite alternate; +} +@keyframes twinkle { + 0% { opacity: 0.4; } + 100% { opacity: 0.8; } +} + +/* ── responsive ─────────────────────────────────────── */ +@media (max-width: 860px) { + nav { flex-wrap: wrap; } + .nav-menu { overflow-x: auto; white-space: nowrap; } + .nav-account { + width: 100%; + margin-left: 0; + border-top: 1px solid var(--border); + justify-content: flex-start; + } + #content { margin-top: 5rem; padding: 1rem 0.75rem; } + h1 { font-size: 12px; } + h2 { font-size: 10px; } +} + +/* ── focus accessibility ────────────────────────────── */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* ── print ──────────────────────────────────────────── */ +@media print { + nav, footer, .retro-stars { display: none; } + body::before, body::after { display: none; } + #content { margin-top: 0; } + * { background: white !important; color: black !important; box-shadow: none !important; text-shadow: none !important; } +} diff --git a/site/info/themes/retro-gaming/icon.png b/site/info/themes/retro-gaming/icon.png new file mode 100644 index 0000000..12dfe2c Binary files /dev/null and b/site/info/themes/retro-gaming/icon.png differ diff --git a/site/info/themes/retro-gaming/page.html.uce b/site/info/themes/retro-gaming/page.html.uce new file mode 100644 index 0000000..c0a53da --- /dev/null +++ b/site/info/themes/retro-gaming/page.html.uce @@ -0,0 +1,32 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > +
+ + +
> + +
+ + + + +} diff --git a/site/info/uce-starter/README.md b/site/info/uce-starter/README.md new file mode 100644 index 0000000..991a46a --- /dev/null +++ b/site/info/uce-starter/README.md @@ -0,0 +1,29 @@ +# UCE Starter + +`site/examples/uce-starter/` is a UCE-native port of the PHP `web-app-starter/`. + +It keeps the PHP starter around as reference while mirroring the same broad structure: + +- `index.uce` as the front controller +- `views/` for routed page content +- `components/` for reusable UI building blocks +- `themes/`, `js/`, and `img/` for theme templates and static assets + +This port intentionally leans on UCE's component layer rather than treating components as a compatibility shim. The page shell, nav/footer chrome, theme switcher, dashboard blocks, workspace primitives, and marketing sections are all rendered through `component()`. + +Theme rendering is split the same way as the PHP starter: + +- `themes/common/page.blank.uce` +- `themes/common/page.json.uce` +- `themes//page.html.uce` + +Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes//page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation. + +The example uses query-string routing in the same style as the PHP starter: + +- `index.uce` +- `index.uce?page1` +- `index.uce?themes&theme=portal-dark` +- `index.uce?workspace/projects` + +The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state. diff --git a/site/info/uce-starter/components/auth/oauth-client.uce b/site/info/uce-starter/components/auth/oauth-client.uce new file mode 100644 index 0000000..7439a78 --- /dev/null +++ b/site/info/uce-starter/components/auth/oauth-client.uce @@ -0,0 +1,100 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + String title = first(context.call["title"].to_string(), "Sign in with OAuth"); + String subtitle = first(context.call["subtitle"].to_string(), "Choose your preferred authentication method"); + String callback_url = first(context.call["callback_url"].to_string(), starter_link("auth/callback", context)); + + DTree services = context.call["services"]; + if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "") + { + services["google"]["name"] = "Google"; + services["google"]["color"] = "#4285f4"; + services["google"]["icon"] = "fab fa-google"; + services["google"]["scope"] = "openid email profile"; + services["google"]["auth_url"] = "https://accounts.google.com/o/oauth2/v2/auth"; + + services["github"]["name"] = "GitHub"; + services["github"]["color"] = "#333333"; + services["github"]["icon"] = "fab fa-github"; + services["github"]["scope"] = "user:email"; + services["github"]["auth_url"] = "https://github.com/login/oauth/authorize"; + + services["discord"]["name"] = "Discord"; + services["discord"]["color"] = "#5865f2"; + services["discord"]["icon"] = "fab fa-discord"; + services["discord"]["scope"] = "identify email"; + services["discord"]["auth_url"] = "https://discord.com/api/oauth2/authorize"; + + services["twitch"]["name"] = "Twitch"; + services["twitch"]["color"] = "#9146ff"; + services["twitch"]["icon"] = "fab fa-twitch"; + services["twitch"]["scope"] = "user:read:email"; + services["twitch"]["auth_url"] = "https://id.twitch.tv/oauth2/authorize"; + } + + <> +
+
+

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

+

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

+

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

+

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

+

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

Why Choose Our Framework?

+

Discover the powerful features that make development a breeze

+
+
+
+
+

+

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

Trusted by Developers Worldwide

+

Join thousands of developers who have chosen our framework

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

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

What Developers Say

+

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

+
+
+
+
+
"
+

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

Simple Pricing

+

Choose the plan that fits your product stage.

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

+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + +} diff --git a/site/info/uce-starter/components/example/theme-switcher.uce b/site/info/uce-starter/components/example/theme-switcher.uce new file mode 100644 index 0000000..28f7de7 --- /dev/null +++ b/site/info/uce-starter/components/example/theme-switcher.uce @@ -0,0 +1,153 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + String current_theme = context.cfg.get_by_path("theme/key").to_string(); + String current_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme); + String route_path = context.var["starter"]["route"]["l_path"].to_string(); + + <> +
+ + + +
+ + + +} diff --git a/site/info/uce-starter/components/gauges/arcgauge.uce b/site/info/uce-starter/components/gauges/arcgauge.uce new file mode 100644 index 0000000..a9b406a --- /dev/null +++ b/site/info/uce-starter/components/gauges/arcgauge.uce @@ -0,0 +1,91 @@ +#load "helpers.uce" + +COMPONENT(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "arcgauge-" + std::to_string((u64)time())); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + String style = context.call["style"].to_string(); + DTree scale = context.call["scale"]; + + <> +
+ +
+

+

+
+ +
+ 0.0 ? gauge_clamp_value((value / max_value) * 100.0, 0.0, 100.0) : 0.0; + f64 arc_length = round((pct / 100.0) * 157.08 * 10.0) / 10.0; + String resolved_color = ""; + if(merged["color"].get_type_name() == "array") + resolved_color = gauge_range_color(merged["color"], value); + else + resolved_color = merged["color"].to_string(); + if(resolved_color == "") + { + if(pct >= 85.0) + resolved_color = "var(--error, #ef4444)"; + else if(pct >= 60.0) + resolved_color = "var(--warning, #f59e0b)"; + else + resolved_color = "var(--success, #10b981)"; + } + String display_value = gauge_format_number(value, precision) + merged["unit"].to_string(); + String watermark_prefix = merged["watermark_prefix"].to_string(); + ?>
+
+ +
+
+
+
+ + + + +} diff --git a/site/info/uce-starter/components/gauges/common.css b/site/info/uce-starter/components/gauges/common.css new file mode 100755 index 0000000..e45daa7 --- /dev/null +++ b/site/info/uce-starter/components/gauges/common.css @@ -0,0 +1,95 @@ + .horizontal .progressbar-item { + display: flex; + min-width: 100%; + padding: 5px; + } + + .progressbar-container.vertical { + display: flex; + } + + .vertical .progressbar-item { + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: 5px; + text-align: center; + } + + .progressbar-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; + } + + .horizontal .progressbar-label, .horizontal .progressbar-value { + flex: 0 0 auto; + margin-right: 10px; + } + + .horizontal .progressbar-label { + min-width: 50px; + } + + .vertical .progressbar-label { + text-align: center; + } + + .horizontal .progressbar-value { + min-width: 50px; + text-align: right; + } + + .progressbar-background { + flex: 1; + background: var(--bg-color); + padding: 4px; + display: flex; + } + + .vertical .progressbar-background { + flex-direction: column; + justify-content: flex-end; + } + + .progressbar-bar { + transition: all 0.3s ease; + } + + .progressbar-marker { + position: absolute; + z-index: 10; + opacity: 0.5; + background: var(--text-color, #333); + } + + .progressbar-marker:hover { + opacity: 1; + } + + .horizontal .progressbar-marker { + border-width: 0 2px 0 2px; + height: 100%; + top: 0; + min-width: 4px; + } + + .vertical .progressbar-marker { + border-width: 2px 0 2px 0; + width: 100%; + left: 0; + min-height: 4px; + } + + .progressbar-background { + position: relative; + } + + .needlegauge-svg .needle { + transition: all 0.3s ease; + } + + .needlegauge-item { + display: inline-block; + } \ No newline at end of file diff --git a/site/info/uce-starter/components/gauges/common.js b/site/info/uce-starter/components/gauges/common.js new file mode 100755 index 0000000..76ab786 --- /dev/null +++ b/site/info/uce-starter/components/gauges/common.js @@ -0,0 +1,121 @@ +// common code for gauges components + +window.GaugeComponents = window.GaugeComponents || {}; + +Object.assign(window.GaugeComponents, { // as a namespace + + // utility functions + clampValue: function(value, min, max) { + return Math.min(max, Math.max(min, value)); + }, + + pickEntryFromRange: function(ranges, value) { + if (!Array.isArray(ranges)) return null; + for (const entry of ranges) { + if (value >= entry.from && value <= entry.to) return entry; + } + return null; + }, + + /** + * Creates an SVG element with namespace + */ + createSVGElement: function(tagName, attributes = {}) { + const element = document.createElementNS('http://www.w3.org/2000/svg', tagName); + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + return element; + }, + + /** + * Gets CSS custom property value from computed styles + */ + getCSSVar: function(varName) { + return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); + }, + + resolveColor: function(colorSpec, value, pct) { + if (Array.isArray(colorSpec)) { + const match = this.pickEntryFromRange(colorSpec, value); + if (match && match.color) return match.color; + } + if (typeof colorSpec === 'string' && colorSpec !== '') return colorSpec; + if (pct < 60) return this.getCSSVar('--success') || '#10b981'; + if (pct < 85) return this.getCSSVar('--warning') || '#f59e0b'; + return this.getCSSVar('--error') || '#ef4444'; + }, + + formatValue: function(value, precision, suffix) { + const numericValue = Number(value); + const normalizedPrecision = Number.isFinite(precision) ? precision : 1; + if (!Number.isFinite(numericValue)) return '--'; + return numericValue.toFixed(normalizedPrecision) + (suffix || ''); + }, + + gaugeArcPoint: function(pct, radius = 50) { + const angle = Math.PI - (pct / 100) * Math.PI; + return { + x: 60 + radius * Math.cos(angle), + y: 60 - radius * Math.sin(angle), + }; + }, + + updateWatermark: function(prefix, pct) { + const now = Date.now(); + this._watermarks = this._watermarks || {}; + let watermark = this._watermarks[prefix]; + const resetWindow = 10 * 60 * 1000; + if (!watermark || (now - watermark.resetTs) > resetWindow) { + watermark = { lo: pct, hi: pct, resetTs: now }; + this._watermarks[prefix] = watermark; + } else { + if (pct < watermark.lo) watermark.lo = pct; + if (pct > watermark.hi) watermark.hi = pct; + } + return watermark; + }, + + renderWatermarkTick: function(lineId, pct) { + const line = document.getElementById(lineId); + if (!line) return; + if (pct == null) { + line.setAttribute('opacity', '0'); + return; + } + const outer = this.gaugeArcPoint(pct, 53); + const inner = this.gaugeArcPoint(pct, 43); + line.setAttribute('x1', outer.x.toFixed(1)); + line.setAttribute('y1', outer.y.toFixed(1)); + line.setAttribute('x2', inner.x.toFixed(1)); + line.setAttribute('y2', inner.y.toFixed(1)); + line.setAttribute('opacity', '0.7'); + }, + + updateArcGauge: function(options) { + const value = Number(options.value); + const max = Number(options.max || 100); + const normalizedValue = Number.isFinite(value) ? value : 0; + const pct = this.clampValue(max === 0 ? 0 : (normalizedValue / max) * 100, 0, 100); + const arcLength = (pct / 100) * 157.08; + const arc = document.getElementById(options.arcId); + const text = document.getElementById(options.textId); + const meta = options.metaId ? document.getElementById(options.metaId) : null; + if (arc) { + arc.setAttribute('stroke-dasharray', arcLength.toFixed(1) + ' 157.08'); + arc.setAttribute('stroke', this.resolveColor(options.color, normalizedValue, pct)); + } + if (text) { + text.textContent = this.formatValue(normalizedValue, options.precision, options.suffix); + } + if (meta && options.meta != null) { + meta.textContent = options.meta; + } + if (options.watermarkPrefix) { + const watermark = this.updateWatermark(options.watermarkPrefix, pct); + this.renderWatermarkTick(options.watermarkPrefix + 'WmLo', watermark.lo); + this.renderWatermarkTick(options.watermarkPrefix + 'WmHi', watermark.hi); + } + } + +}); \ No newline at end of file diff --git a/site/info/uce-starter/components/gauges/helpers.uce b/site/info/uce-starter/components/gauges/helpers.uce new file mode 100644 index 0000000..5fb6421 --- /dev/null +++ b/site/info/uce-starter/components/gauges/helpers.uce @@ -0,0 +1,71 @@ +#load "../../lib/app.uce" +#include + +f64 gauge_clamp_value(f64 value, f64 min_value, f64 max_value) +{ + return(std::max(min_value, std::min(max_value, value))); +} + +f64 gauge_pi() +{ + return(3.14159265358979323846); +} + +String gauge_string_attr(String value) +{ + return(html_escape(value)); +} + +String gauge_attr_id(String value) +{ + return(ascii_safe_name(value)); +} + +String gauge_range_color(DTree ranges, f64 value) +{ + String matched = ""; + ranges.each([&](DTree range, String key) { + f64 from_value = float_val(first(range["from"].to_string(), "-999999999")); + f64 to_value = float_val(first(range["to"].to_string(), "999999999")); + if(value >= from_value && value <= to_value && matched == "") + matched = range["color"].to_string(); + }); + return(matched); +} + +String gauge_arc_path(f64 cx, f64 cy, f64 radius, f64 start_angle, f64 end_angle) +{ + f64 start_x = cx + cos(start_angle) * radius; + f64 start_y = cy + sin(start_angle) * radius; + f64 end_x = cx + cos(end_angle) * radius; + f64 end_y = cy + sin(end_angle) * radius; + s32 large_arc = fabs(end_angle - start_angle) > gauge_pi() ? 1 : 0; + s32 sweep = end_angle >= start_angle ? 1 : 0; + return( + "M " + std::to_string(start_x) + " " + std::to_string(start_y) + + " A " + std::to_string(radius) + " " + std::to_string(radius) + + " 0 " + std::to_string(large_arc) + " " + std::to_string(sweep) + + " " + std::to_string(end_x) + " " + std::to_string(end_y) + ); +} + +String gauge_circle_segment_svg(f64 cx, f64 cy, f64 radius, f64 start_angle, f64 end_angle, String stroke, f64 stroke_width, String fill, String style) +{ + String path_d = gauge_arc_path(cx, cy, radius, start_angle, end_angle); + return( + "" + ); +} + +String gauge_format_number(f64 value, s32 precision) +{ + char buf[64]; + snprintf(buf, sizeof(buf), ("%." + std::to_string(std::max(0, precision)) + "f").c_str(), value); + return(String(buf)); +} diff --git a/site/info/uce-starter/components/gauges/needlegauge.uce b/site/info/uce-starter/components/gauges/needlegauge.uce new file mode 100644 index 0000000..25ed3f1 --- /dev/null +++ b/site/info/uce-starter/components/gauges/needlegauge.uce @@ -0,0 +1,144 @@ +#load "helpers.uce" + +COMPONENT(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time())); + String style = context.call["style"].to_string(); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + f64 size = float_val(first(context.call["size"].to_string(), "200")); + DTree scale = context.call["scale"]; + f64 scale_angle_start = float_val(first(scale["angle_start"].to_string(), std::to_string(-gauge_pi()))); + f64 scale_angle_end = float_val(first(scale["angle_end"].to_string(), "0")); + f64 img_height = float_val(first(context.call["img_height"].to_string(), std::to_string(size * std::max(cos(scale_angle_start), cos(scale_angle_end))))); + + <> +
+ +
+

+

+
+ +
+ "; + } + for(f64 v = min_value; v <= max_value + 0.0001; v += label_interval) + { + f64 angle = angle_start + ((v - min_value) / vrange) * (angle_end - angle_start); + f64 x1 = size / 2.0 + cos(angle) * size * 0.35; + f64 y1 = size / 2.0 + sin(angle) * size * 0.35; + f64 x2 = size / 2.0 + cos(angle) * size * 0.41; + f64 y2 = size / 2.0 + sin(angle) * size * 0.41; + ticks_html += ""; + f64 lx = size / 2.0 + cos(angle) * size * 0.45; + f64 ly = size / 2.0 + sin(angle) * size * 0.45; + String label_value = std::to_string((s64)round(v)); + ticks_html += "" + + html_escape(label_value) + ""; + } + String segments_html = ""; + if(merged["color"].get_type_name() == "array") + { + merged["color"].each([&](DTree range, String key) { + f64 range_from = std::max(float_val(first(range["from"].to_string(), std::to_string(min_value))), min_value); + f64 range_to = std::min(float_val(first(range["to"].to_string(), std::to_string(max_value))), max_value); + f64 angle_from = angle_start + ((range_from - min_value) / vrange) * (angle_end - angle_start); + f64 angle_to = angle_start + ((range_to - min_value) / vrange) * (angle_end - angle_start); + segments_html += gauge_circle_segment_svg(size / 2.0, size / 2.0, size * 0.4, angle_from, angle_to, first(range["color"].to_string(), "rgba(120,120,120,0.5)"), 8.0, "rgba(0,0,0,0)", "opacity:0.25"); + }); + } + else + { + segments_html = gauge_circle_segment_svg(size / 2.0, size / 2.0, size * 0.4, angle_start, angle_end, tick_color, 8.0, "rgba(0,0,0,0)", "opacity:0.25"); + } + ?>
+
+
+ + + + + + +
+
+
+
+
+
+
+
+ + + + +} diff --git a/site/info/uce-starter/components/gauges/progressbar.uce b/site/info/uce-starter/components/gauges/progressbar.uce new file mode 100644 index 0000000..1410243 --- /dev/null +++ b/site/info/uce-starter/components/gauges/progressbar.uce @@ -0,0 +1,168 @@ +#load "helpers.uce" + +COMPONENT(Request& context) +{ + starter_register_js("components/gauges/common.js", context); + starter_register_css("themes/common/css/gauges.css", context); + + String gauge_id = first(context.call["id"].to_string(), "progressbar-" + std::to_string((u64)time())); + String style = context.call["style"].to_string(); + String item_style = context.call["item-style"].to_string(); + String label_style = context.call["label-style"].to_string(); + String value_style = context.call["value-style"].to_string(); + String bar_style = context.call["bar-style"].to_string(); + String title = context.call["title"].to_string(); + String subtitle = context.call["subtitle"].to_string(); + String layout = first(context.call["layout"].to_string(), "horizontal"); + String bar_color_default = context.call["bar-color"].to_string(); + DTree scale = context.call["scale"]; + DTree markers = context.call["markers"]; + + StringList default_palette; + default_palette.push_back("var(--success, #10b981)"); + default_palette.push_back("var(--primary, #60a5fa)"); + default_palette.push_back("var(--accent, #22d3ee)"); + default_palette.push_back("var(--warning, #f59e0b)"); + u32 auto_color_counter = 0; + + <> +
+ +
+

+

+
+ + +
+
+ +
+
+
+
+
+
+
" + style="left: %; background: ;">
+
+
+ +
+
+ +
px;"> +
+ +
+
+
+
" + style="bottom: %; background: ;">
+
+
+
+ +
+
+ +
+ + + + +} diff --git a/site/info/uce-starter/components/theme/account_links.uce b/site/info/uce-starter/components/theme/account_links.uce new file mode 100644 index 0000000..96018c2 --- /dev/null +++ b/site/info/uce-starter/components/theme/account_links.uce @@ -0,0 +1,32 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + StarterUser users(context); + DTree user = users.current(); + bool signed_in = user["email"].to_string() != ""; + String theme_key = context.cfg.get_by_path("theme/key").to_string(); + String wrapper_class = first(context.call["wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu"); + String links_class = first(context.call["links_wrapper_class"].to_string(), theme_key == "localfirst" ? "admin-account-links" : ""); + String name_class = first(context.call["name_class"].to_string(), theme_key == "localfirst" ? "admin-account-name" : "account-name"); + + <> +
+ + + + + + +
+ +} diff --git a/site/info/uce-starter/components/theme/footer.uce b/site/info/uce-starter/components/theme/footer.uce new file mode 100644 index 0000000..1a81513 --- /dev/null +++ b/site/info/uce-starter/components/theme/footer.uce @@ -0,0 +1,22 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + if(embed_mode) + return; + + String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string()); + String inner_class = first(context.call["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : ""); + + <> +
+ +

+ +

+ +
+ +} diff --git a/site/info/uce-starter/components/theme/global_controls.uce b/site/info/uce-starter/components/theme/global_controls.uce new file mode 100644 index 0000000..9af2162 --- /dev/null +++ b/site/info/uce-starter/components/theme/global_controls.uce @@ -0,0 +1,18 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + if(starter_page_embed_mode(context)) + return; + + String cookie_value = context.call["cookie_consent"].to_string(); + bool cookie_consent = !(cookie_value == "0" || cookie_value == "false" || cookie_value == "FALSE" || cookie_value == "no" || cookie_value == "NO"); + + <> + + + + + +} diff --git a/site/info/uce-starter/components/theme/head.uce b/site/info/uce-starter/components/theme/head.uce new file mode 100644 index 0000000..77ddaa7 --- /dev/null +++ b/site/info/uce-starter/components/theme/head.uce @@ -0,0 +1,22 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string()); + String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example"); + String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a"); + String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context); + + <> + + <?= title + " | " + context.cfg.get_by_path("site/name").to_string() ?> + + + + + + + + +} diff --git a/site/info/uce-starter/components/theme/page_shell.uce b/site/info/uce-starter/components/theme/page_shell.uce new file mode 100644 index 0000000..890f2c2 --- /dev/null +++ b/site/info/uce-starter/components/theme/page_shell.uce @@ -0,0 +1,20 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + if(context.call["main_html"].to_string() != "") + context.var["starter"]["fragments"]["main"] = context.call["main_html"]; + if(context.call["json"].get_type_name() == "array") + context.var["starter"]["json"] = context.call["json"]; + if(context.call["page_type"].to_string() != "") + context.var["starter"]["page_type"] = context.call["page_type"]; + if(context.call["embed_mode"].to_string() != "") + context.var["starter"]["embed_mode"] = context.call["embed_mode"]; + starter_render_page(context); +} + +COMPONENT:HEAD(Request& context) +{ + print(component("head", context.call, context)); +} diff --git a/site/info/uce-starter/components/theme/standard_nav.uce b/site/info/uce-starter/components/theme/standard_nav.uce new file mode 100644 index 0000000..587548a --- /dev/null +++ b/site/info/uce-starter/components/theme/standard_nav.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + String nav_class = context.call["nav_class"].to_string(); + if(nav_class == "" && (context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming")) + nav_class = "nav-shell"; + + DTree account_props; + if(context.call["account_wrapper_class"].to_string() != "") + account_props["wrapper_class"] = context.call["account_wrapper_class"]; + if(context.call["links_wrapper_class"].to_string() != "") + account_props["links_wrapper_class"] = context.call["links_wrapper_class"]; + if(context.call["name_class"].to_string() != "") + account_props["name_class"] = context.call["name_class"]; + + <> + > + + + + +} diff --git a/site/info/uce-starter/components/workspace/primitives.uce b/site/info/uce-starter/components/workspace/primitives.uce new file mode 100644 index 0000000..f63a2c3 --- /dev/null +++ b/site/info/uce-starter/components/workspace/primitives.uce @@ -0,0 +1,122 @@ +#load "../../lib/app.uce" + +COMPONENT:APP_FRAME(Request& context) +{ + starter_register_css("themes/common/css/workspace.css", context); + starter_register_js("js/u-workspace-shell.js", context); + String id = context.call["id"].to_string(); + String overlay_id = context.call["overlay_id"].to_string(); + String class_name = context.call["class"].to_string(); + <> + class="ws-app"> + + class="ws-sidebar-overlay">
+
+
+ +} + +COMPONENT:EMPTY_STATE(Request& context) +{ + <> +
"> +
">
+

+

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

+

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

+
+
+ +} + +COMPONENT:SECTION(Request& context) +{ + <> + class="ws-section"> + + + + +} + +COMPONENT:SIDEBAR_SHELL(Request& context) +{ + <> + class="ws-sidebar"> + + + + +} + +COMPONENT:SIDEBAR_TOOLBAR(Request& context) +{ + <> +
"> + +
+ + name="" class="ws-search-input" placeholder="" autocomplete="off"> +
+
+ +} + +COMPONENT:STATUS_PILL(Request& context) +{ + String variant = to_lower(first(context.call["variant"].to_string(), "neutral")); + <> + "> + +} diff --git a/site/info/uce-starter/config/settings.uce b/site/info/uce-starter/config/settings.uce new file mode 100644 index 0000000..ce371f6 --- /dev/null +++ b/site/info/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/info/uce-starter/img/cat01.jpg b/site/info/uce-starter/img/cat01.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/info/uce-starter/img/cat01.jpg differ diff --git a/site/info/uce-starter/img/cat02.jpg b/site/info/uce-starter/img/cat02.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/info/uce-starter/img/cat02.jpg differ diff --git a/site/info/uce-starter/img/cat03.jpg b/site/info/uce-starter/img/cat03.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/info/uce-starter/img/cat03.jpg differ diff --git a/site/info/uce-starter/img/cat04.jpg b/site/info/uce-starter/img/cat04.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/info/uce-starter/img/cat04.jpg differ diff --git a/site/info/uce-starter/img/cat05.jpg b/site/info/uce-starter/img/cat05.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/info/uce-starter/img/cat05.jpg differ diff --git a/site/info/uce-starter/img/cat06.jpg b/site/info/uce-starter/img/cat06.jpg new file mode 100755 index 0000000..39c227a Binary files /dev/null and b/site/info/uce-starter/img/cat06.jpg differ diff --git a/site/info/uce-starter/index.uce b/site/info/uce-starter/index.uce new file mode 100644 index 0000000..6c5513f --- /dev/null +++ b/site/info/uce-starter/index.uce @@ -0,0 +1,29 @@ +#load "lib/app.uce" + +RENDER(Request& context) +{ + app_init(context); + + DTree resolved = app_resolve_view(context); + + ob_start(); + if(resolved["file"].to_string() != "") + { + if(resolved["param"].to_string() != "") + context.var["app"]["route"]["param"] = resolved["param"]; + unit_render(resolved["file"].to_string(), context); + } + else + { + app_not_found("The requested page does not exist.", context); + <> +
+

404 Not Found

+

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

U-Howler.js Demo

+ +
+
+

Basic Controls

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

Global Controls

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

API Overview

+ +
+

Howler (Global)

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

Howl Constructor

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

Core Methods

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

Examples

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

Audio Sprites

+
+

Audio Sprites Demo

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

Audio Sprites API

+ +
+

Sprite Methods

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

Sprite Configuration

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

Audio Effects

+
+

Filters

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

Rate Control

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

Fade Effects

+
+ + +
+
+
+
+ +
+

Effects API

+ +
+

Effect Methods

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

Audio Effects

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

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

Frequency Analysis

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

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

3D Spatial Audio

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

Spatial API

+ +
+

3D Spatial Audio

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

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

Environment Presets

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

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

Spatial Examples

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

Library Info

+
+
Loading library information...
+ +

Supported Codecs:

+
+
+
+ +
+

Events & Utilities

+ +
+

Event Listeners

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

Utility Methods

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

Events

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

U-Macrobars.js Demo

+ +
+
+

Basic Field Output

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

Number Formatting

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

Default Values & Safety

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

Field Output API

+ +
+

Basic Syntax

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

Number Formatting

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

Examples

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

Control Structures

+
+

Conditionals

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

Loops & Iteration

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

Equality & Lookup

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

Control Flow API

+ +
+

Conditionals

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

Loops

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

Comparison & Lookup

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

Examples

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

Advanced Features

+
+

Event Binding

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

Code Blocks

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

Components

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

Advanced API

+ +
+

Event Binding

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

Code Execution

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

Components & Compilation

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

Examples

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

Template Editor

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

Template Information

+
Ready.
+
+ +
+
+ +
+

Compilation & Debugging

+ +
+

Compilation Options

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

Debugging Properties

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

Utility Functions

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

Example Templates

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

{{title}}

{{content}}

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

U-Query.js Demo

+ +
+
+

Element Selection & DOM Manipulation

+
+

DOM Queries

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

DOM Manipulation

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

CSS & Classes

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

Selection API

+ +
+

Core Selectors

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

DOM Manipulation

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

CSS & Classes

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

Examples

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

Event Handling & Visibility

+
+

Event System

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

Show/Hide/Toggle

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

Method Chaining

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

Events & Visibility API

+ +
+

Event Methods

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

Visibility Control

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

Method Chaining

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

AJAX & Global Events

+
+

AJAX Utilities

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

Global Event System

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

Utility Functions

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

AJAX & Utilities API

+ +
+

AJAX Methods

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

Global Event System

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

Utility Functions

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

Examples

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

Browser Info

+
+
Loading library information...
+ +

This browser supports

+
+ +
+ + +
+
+
+ +
+

Advanced Features

+ +
+

Advanced Selectors

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

Data Attributes

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

Traversal Methods

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

DOM Diff Updates

+
+ u-query can make use of morphdom.js for efficient DOM diff updates. +
+
$.options.alwaysDoDifferentialUpdate = true
+
+ Global flag for morphdom. +
+
+ +
+
+
+ + + + + diff --git a/site/info/uce-starter/js/u-query.js b/site/info/uce-starter/js/u-query.js new file mode 100755 index 0000000..907e8ba --- /dev/null +++ b/site/info/uce-starter/js/u-query.js @@ -0,0 +1,589 @@ +(function (root, factory) { + // this is the stupid UMD pattern, but eh we lost that battle a long time ago + if (typeof exports === 'object' && typeof module !== 'undefined') { + var exports_obj = factory(); + module.exports = exports_obj.$; + // Also export individual components + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + exports[key] = exports_obj[key]; + } + } + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + var exports_obj = factory(); + root.$ = exports_obj.$; + for (var key in exports_obj) { + if (key !== '$' && exports_obj.hasOwnProperty(key)) { + root[key] = exports_obj[key]; + } + } + } +}(typeof self !== 'undefined' ? self : this, function () { + +// a collection of DOM utility functions that I thought were cool in jQuery + +class EventEmitter { + constructor() { + /** Map> */ + this._topics = new Map(); + } + + /** + * Subscribe to `topic`. If `slot_key` is provided (truthy), + * it dedupes by that key; otherwise a unique Symbol is used. + * Returns an unsubscribe fn. + */ + on(topic, handler, slot_key = null) { + let map = this._topics.get(topic); + if (!map) { + map = new Map(); + this._topics.set(topic, map); + } + // use the provided slot_key or a fresh Symbol() + const key = slot_key != null ? slot_key : Symbol(); + map.set(key, handler); + return () => this.off(topic, key); + } + + /** + * Unsubscribe by handler function or by slot_key. + */ + off(topic, handlerOrSlotKey) { + const map = this._topics.get(topic); + if (!map) return; + + // if it matches a slotKey directly, remove it + if (map.has(handlerOrSlotKey)) { + map.delete(handlerOrSlotKey); + } else { + // otherwise assume it's a function: remove all matching fns + for (const [key, fn] of map.entries()) { + if (fn === handlerOrSlotKey) { + map.delete(key); + } + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + } + + /** + * Emit to all handlers on `topic`. Handlers returning + * 'remove_handler' are auto-removed. + * Returns the number of handlers invoked. + */ + emit(topic, ...args) { + let count = 0; + const map = this._topics.get(topic); + if (!map) return count; + + for (const [key, fn] of Array.from(map.entries())) { + const res = fn(...args); + count++; + if (res === 'remove_handler') { + map.delete(key); + } + } + + if (map.size === 0) { + this._topics.delete(topic); + } + return count; + } +} + +class QueryWrapper extends Array { + constructor(elements) { + super(); + if (elements) { + this.push(...(Array.isArray(elements) ? elements : [elements])); + } + } +} + +$ = function(selector_or_element) { + let elements; + if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) { + elements = [selector_or_element]; + } else if (typeof selector_or_element === 'string') { + // Check if string looks like HTML (starts with < and ends with >) + if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') { + // Create element from HTML string + let temp = document.createElement('div'); + temp.innerHTML = selector_or_element.trim(); + elements = Array.from(temp.children); + } else { + // Treat as CSS selector + elements = Array.from(document.querySelectorAll(selector_or_element)); + } + } else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) { + elements = Array.from(selector_or_element); + } else { + elements = [selector_or_element]; + } + return new QueryWrapper(elements); +}; + +$.options = { + alwaysDoDifferentialUpdate: true, +}; + +$.events = new EventEmitter(); +$.on = $.events.on.bind($.events); +$.off = $.events.off.bind($.events); +$.emit = $.events.emit.bind($.events); + +$.each = function(selector, callback) { + let elements; + if (typeof selector === 'string') { + elements = document.querySelectorAll(selector); + } else if (selector instanceof QueryWrapper) { + elements = selector; + } else { + elements = selector; + } + for (let i = 0; i < elements.length; i++) { + callback(elements[i], i); + } +} + +$.post = function(url, data, callback) { + return $.ajax({ + method: 'POST', + url: url, + data: data, + success: callback, + error: function(xhr) { + console.error('POST request failed:', xhr); + } + }); +} + +$.get = function(url, callback) { + return $.ajax({ + method: 'GET', + url: url, + success: callback, + error: function(xhr) { + console.error('GET request failed:', xhr); + } + }); +} + +$.ajax = function(options) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open(options.method || 'GET', options.url, true); + + // Set default headers + if (options.method === 'POST' || options.data) { + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + } + + // Set custom headers + if (options.headers) { + for (var key in options.headers) { + xhr.setRequestHeader(key, options.headers[key]); + } + } + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + var response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + // If JSON parsing fails, use raw response + response = xhr.responseText; + } + + if (xhr.status >= 200 && xhr.status < 300) { + if (options.success) options.success(response); + resolve(response); + } else { + if (options.error) options.error(xhr); + reject(xhr); + } + } + }; + + var data = null; + if (options.data) { + data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data); + } + + xhr.send(data); + }); +} + +$.ready = function(callback) { + if (document.readyState !== 'loading') { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } +} + +// Helper function to execute scripts in HTML content +function executeScripts(htmlContent) { + // Create a temporary container to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // Find all script tags and disable them temporarily + const scripts = tempDiv.querySelectorAll('script'); + const scriptData = []; + + scripts.forEach((script) => { + // Store script data for later execution + scriptData.push({ + content: script.textContent || script.innerText || '', + src: script.src, + type: script.type, + nonce: script.nonce, + async: script.async, + defer: script.defer + }); + + // Disable the script by changing its type + script.type = 'text/disabled-script'; + }); + + // Return the modified HTML and script data + return { + html: tempDiv.innerHTML, + scripts: scriptData + }; +} + +// Helper function to execute a single script +function executeScript(scriptInfo) { + if (scriptInfo.src) { + // External script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + if (scriptInfo.async) newScript.async = scriptInfo.async; + if (scriptInfo.defer) newScript.defer = scriptInfo.defer; + newScript.src = scriptInfo.src; + + document.head.appendChild(newScript); + } else if (scriptInfo.content.trim()) { + // Inline script + const newScript = document.createElement('script'); + if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') { + newScript.type = scriptInfo.type; + } + if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce; + newScript.text = scriptInfo.content; + + // Execute by inserting and immediately removing + document.head.appendChild(newScript); + document.head.removeChild(newScript); + } +} + +// Add all the jQuery-like methods to QueryWrapper +QueryWrapper.prototype.parent = function() { + const parents = Array.from(this).map(el => el.parentNode).filter(el => el); + return new QueryWrapper(parents); +} + +QueryWrapper.prototype.load = function(url, opt = {}) { + return new Promise((resolve, reject) => { + const ajaxOptions = { + method: opt.method || (opt.postData ? 'POST' : 'GET'), + url: url, + success: (response) => { + if(opt.replace) + this.replaceWith(response, opt); + else + this.html(response, opt); + if (opt.onLoad) opt.onLoad(response, null); + resolve(this); + }, + error: (xhr) => { + if (opt.onError) opt.onError(xhr); + reject(new Error('Failed to load: ' + url)); + } + }; + + if (opt.postData) { + ajaxOptions.data = opt.postData; + } + + $.ajax(ajaxOptions); + }); +} + +QueryWrapper.prototype.html = function(opt_content = false, prop = {}) { + if (opt_content === false) { + return Array.from(this).map(el => el.innerHTML).join(''); + } + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + const temp = document.createElement('div'); + temp.innerHTML = opt_content; + this.forEach(el => { + morphdom(el, temp, { childrenOnly: true }); + }); + } else { + this.forEach(el => { + const result = executeScripts(opt_content); + el.innerHTML = result.html; + result.scripts.forEach(executeScript); + }); + } + return this; +} + +QueryWrapper.prototype.text = function(opt_content = false) { + if (opt_content === false) { + return Array.from(this).map(el => el.textContent).join(''); + } + this.forEach(el => { + el.textContent = opt_content; + }); + return this; +} + +QueryWrapper.prototype.replaceWith = function(content, prop = {}) { + if(typeof prop.diff == 'undefined') + prop.diff = $.options.alwaysDoDifferentialUpdate; + if(prop.diff === true) { + this.forEach(el => { + morphdom(el, content); + }); + } else { + if(typeof content === 'string') { + this.forEach(el => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content; + el.parentNode.replaceChild(tempDiv.firstChild, el); + }); + } else if(content instanceof Element) { + this.forEach(el => { + el.parentNode.replaceChild(content.cloneNode(true), el); + }); + } + } + return this; +} + +QueryWrapper.prototype.query = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.find = function(selector) { + const found = Array.from(this).reduce((acc, el) => { + const results = el.querySelectorAll(selector); + return acc.concat(Array.from(results)); + }, []); + return new QueryWrapper(found); +} + +QueryWrapper.prototype.on = function(event, handler) { + this.forEach(function(el) { + el.addEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.off = function(event, handler) { + this.forEach(function(el) { + el.removeEventListener(event, handler); + }); + return this; +} + +QueryWrapper.prototype.hide = function() { + return this.css({ display: 'none' }); +} + +QueryWrapper.prototype.show = function() { + return this.css({ display: '' }); +} + +QueryWrapper.prototype.toggle = function() { + this.forEach(function(el) { + el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none'; + }); + return this; +} + +QueryWrapper.prototype.attr = function(name, value) { + if (value === undefined) { + return this.length > 0 ? this[0].getAttribute(name) : null; + } + this.forEach(function(el) { + el.setAttribute(name, value); + }); + return this; +} + +QueryWrapper.prototype.addClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.add(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.removeClass = function(classNameOrList) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + el.classList.remove(className); + }); + }); + }); + return this; +} + +QueryWrapper.prototype.toggleClass = function(classNameOrList, force) { + var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList]; + this.forEach(function(el) { + classList.forEach(function(classNames) { + // Split space-separated class names + var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; }); + classes.forEach(function(className) { + if (typeof force !== 'undefined') { + el.classList.toggle(className, force); + } else { + el.classList.toggle(className); + } + }); + }); + }); + return this; +} + +QueryWrapper.prototype.empty = function() { + this.forEach(function(el) { + el.innerHTML = ''; + }); + return this; +} + +QueryWrapper.prototype.css = function(styles, optOrValue) { + // If no arguments or first argument is a string (getter mode) + if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) { + if (this.length === 0) return undefined; + var el = this[0]; + if (typeof styles === 'string') { + // Get computed style for a specific property + return window.getComputedStyle(el)[styles]; + } else { + // Return computed styles object (not commonly used) + return window.getComputedStyle(el); + } + } + + // Setter mode + this.forEach(function(el) { + if (typeof styles === 'string' && arguments.length >= 2) { + // Single property setter: .css('prop', 'value') + el.style[styles] = optOrValue; + } else if (typeof styles === 'object') { + // Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'}) + for (var key in styles) { + el.style[key] = styles[key]; + } + } + }); + return this; +} + +QueryWrapper.prototype.each = function(callback) { + this.forEach(function(el, index) { + callback(el, index); + }); + return this; +} + +QueryWrapper.prototype.append = function(child_or_html) { + this.forEach(function(el) { + if (typeof child_or_html === 'string' && el.insertAdjacentHTML) { + if (child_or_html.includes(' max) return max; + return v; +} + +function pick_entry_from_range(array, value) { + if (!array) return {}; + let result = {}; + for (const [pv] of Object.entries(array)) { + if (value >= pv.from && value <= pv.to) result = pv; + } + return result; +} + +return { + $: $, + EventEmitter: EventEmitter, + QueryWrapper: QueryWrapper, + first: first, + clamp: clamp, + pick_entry_from_range: pick_entry_from_range +}; + +})); \ No newline at end of file diff --git a/site/info/uce-starter/js/u-sortable-table.js b/site/info/uce-starter/js/u-sortable-table.js new file mode 100644 index 0000000..e9fee4f --- /dev/null +++ b/site/info/uce-starter/js/u-sortable-table.js @@ -0,0 +1,168 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.USortableTable = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getTable(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target.nodeType === 1 ? target : null; + } + + function getStorageKey(table, options) { + if (options && options.storageKey) return options.storageKey; + if (table && table.id) return `u-sortable-table.${table.id}`; + return null; + } + + function loadSort(table, options) { + const storageKey = getStorageKey(table, options); + if (!storageKey || !globalScope.localStorage) return null; + try { + const raw = globalScope.localStorage.getItem(storageKey); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (!parsed || !Number.isInteger(parsed.column) || (parsed.direction !== 'asc' && parsed.direction !== 'desc')) { + return null; + } + return parsed; + } catch (error) { + return null; + } + } + + function saveSort(table, column, direction, options) { + const storageKey = getStorageKey(table, options); + if (!storageKey || !globalScope.localStorage) return; + try { + globalScope.localStorage.setItem(storageKey, JSON.stringify({ column, direction })); + } catch (error) { + // Ignore storage errors. + } + } + + function parseSortValue(text) { + const normalized = String(text || '').trim(); + if (!normalized) return { type: 'text', value: '' }; + + if (globalScope.UFormat && typeof globalScope.UFormat.parseUnitNumber === 'function') { + const unitValue = globalScope.UFormat.parseUnitNumber(normalized); + if (unitValue != null) { + return { type: 'number', value: unitValue }; + } + } + + const timestamp = Date.parse(normalized); + if (!Number.isNaN(timestamp)) { + return { type: 'number', value: timestamp }; + } + + return { type: 'text', value: normalized.toLowerCase() }; + } + + function setHeaderState(table, columnIndex, direction) { + Array.from(table.querySelectorAll('thead th')).forEach((header, index) => { + header.classList.remove('sorted-asc', 'sorted-desc'); + header.setAttribute('aria-sort', 'none'); + if (index === columnIndex) { + header.classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc'); + header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending'); + } + }); + } + + function sortTable(table, columnIndex, direction, options, persist) { + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.children.length > 0 && !row.hasAttribute('data-empty-row')); + if (!rows.length) return; + + rows.sort((rowA, rowB) => { + const cellA = rowA.children[columnIndex]; + const cellB = rowB.children[columnIndex]; + const rawA = cellA ? (cellA.getAttribute('data-sort-value') || cellA.textContent) : ''; + const rawB = cellB ? (cellB.getAttribute('data-sort-value') || cellB.textContent) : ''; + const valueA = parseSortValue(rawA); + const valueB = parseSortValue(rawB); + + let comparison = 0; + if (valueA.type === 'number' && valueB.type === 'number') { + comparison = valueA.value - valueB.value; + } else { + comparison = String(valueA.value).localeCompare(String(valueB.value), undefined, { + numeric: true, + sensitivity: 'base', + }); + } + return direction === 'asc' ? comparison : -comparison; + }); + + rows.forEach((row) => tbody.appendChild(row)); + setHeaderState(table, columnIndex, direction); + if (persist !== false) { + saveSort(table, columnIndex, direction, options); + } + } + + function getInitialSort(table, options) { + const saved = loadSort(table, options); + if (saved) return saved; + if (options && options.initialSort) { + return { + column: Number(options.initialSort.column || 0), + direction: options.initialSort.direction === 'desc' ? 'desc' : 'asc', + }; + } + return null; + } + + function attachHeader(header, table, columnIndex, options) { + if (header.getAttribute('data-sortable') === 'false') return; + header.classList.add('sortable'); + header.tabIndex = 0; + header.addEventListener('click', function () { + const current = loadSort(table, options); + const nextDirection = current && current.column === columnIndex && current.direction === 'asc' ? 'desc' : 'asc'; + sortTable(table, columnIndex, nextDirection, options, true); + }); + header.addEventListener('keydown', function (event) { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + header.click(); + }); + } + + function init(target, options = {}) { + const table = getTable(target); + if (!table || table.dataset.sortableTableInit === '1') return table; + + Array.from(table.querySelectorAll('thead th')).forEach((header, index) => attachHeader(header, table, index, options)); + table.dataset.sortableTableInit = '1'; + + const initialSort = getInitialSort(table, options); + if (initialSort) { + sortTable(table, initialSort.column, initialSort.direction, options, false); + } + + return table; + } + + function initAll(selector = '.u-sortable-table') { + Array.from(document.querySelectorAll(selector)).forEach((table) => init(table)); + } + + return { + init, + initAll, + parseSortValue, + sortTable, + }; +})); \ No newline at end of file diff --git a/site/info/uce-starter/js/u-tiling-viewport.css b/site/info/uce-starter/js/u-tiling-viewport.css new file mode 100644 index 0000000..63976c7 --- /dev/null +++ b/site/info/uce-starter/js/u-tiling-viewport.css @@ -0,0 +1,165 @@ +:root { + --vp-color-primary: rgba(255, 125, 55, 1); + --vp-color-primary-focus: rgba(255, 120, 60, 0.4); + --vp-background: rgba(0,0,0,0.2); + + --vp-color-black: #000; + --vp-color-gray: #666; + --vp-color-white: #fff; + --vp-space-s: 4px; + --vp-space-l: 6px; + + --vp-size-icon: 12px; + --vp-size-nav-height: 48px; + + --vp-opacity: 0.4; + --vp-opacity-active: 0.9; + + --vp-z-pane: 2; + --vp-z-toolbar: 3; + --vp-z-popup: 9999; + --vp-duration: 0.25s; + --vp-easing: ease; +} + +#fabric { + position: absolute; + top: var(--vp-size-nav-height); + left: 0; right: 0; bottom: 0; + display: flex; + overflow: hidden; +} + +.vp-split { + flex: 1 1 auto; + display: flex; + position: relative; + overflow: hidden; +} + +.vp-split.row { flex-direction: row; } +.vp-split.col { flex-direction: column; } + +.viewport-pane { + flex: 1 1 0; + position: relative; + border: var(--vp-space-s) solid var(--color-border); + background: var(--vp-background); + padding: var(--vp-space-l); + overflow: auto; + font-family: console; + transition: + border-color var(--vp-duration) var(--vp-easing), + box-shadow var(--vp-duration) var(--vp-easing), + background var(--vp-duration) var(--vp-easing); +} + +.vp-split > .viewport-pane { + transition: + flex var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); +} + +.vp-no-transition, +.vp-no-transition * { + transition: none !important; +} + +.viewport-pane:before { + content: attr(data-title); + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + opacity: var(--vp-opacity-medium); + pointer-events: none; +} + +.viewport-pane.focused { + border-color: var(--vp-color-primary-focus); + z-index: var(--vp-z-pane); +} + +.viewport-pane.focused:before { + opacity: var(--vp-opacity-active); + color: var(--color-highlight); +} + +.vp-divider { + background: var(--color-border); + opacity: var(--vp-opacity); + position: relative; + flex: 0 0 auto; +} + +.vp-divider.row { + width: var(--vp-space-l); + cursor: col-resize; +} + +.vp-divider.col { + height: var(--vp-space-l); + cursor: row-resize; +} + +.vp-divider:after { + content:''; + position: absolute; + inset: 0; + transition: + background var(--vp-duration) var(--vp-easing), + opacity var(--vp-duration) var(--vp-easing); + background: var(--color-border); +} + +.vp-divider.row:after { + background: var(--color-border); +} + +.vp-divider:hover:after { + background: var(--color-highlight); + opacity: var(--vp-opacity); +} + +.vp-divider.dragging:after { + background: var(--color-highlight); + opacity: var(--vp-opacity-active); +} + +.vp-help-overlay { + position: absolute; + top: var(--vp-space-s); + left: var(--vp-space-s); + opacity: var(--vp-opacity); + pointer-events: none; +} + +.vp-pane-toolbar { + position: absolute; + top: var(--vp-space-s); + right: var(--vp-space-s); + display: flex; + gap: var(--vp-space-s); + z-index: var(--vp-z-toolbar); +} + +.vp-pane-toolbar button { + padding: 0 var(--vp-space-l); + border: 1px solid var(--color-border); + background: var(--vp-background); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.vp-pane-toolbar button:hover { + background: var(--color-highlight); + color: var(--vp-color-black); + text-shadow: none; +} + +.vp-icon { + width: var(--vp-size-icon); + height: var(--vp-size-icon); + display: block; +} \ No newline at end of file diff --git a/site/info/uce-starter/js/u-tiling-viewport.js b/site/info/uce-starter/js/u-tiling-viewport.js new file mode 100644 index 0000000..3762033 --- /dev/null +++ b/site/info/uce-starter/js/u-tiling-viewport.js @@ -0,0 +1,388 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + root.TilingViewportManager = factory(root.$); + } +}(typeof self !== 'undefined' ? self : this, function ($) { + 'use strict'; + + // Note: This module expects showPopupMenu to be available globally + // or you may need to adjust the dependency list above to include it + + let TilingViewportManager = function(container, options = {}) { + + let idCounter = 1, rootNode = null, focusedPane = null, containerEl = null; + let nextId = () => 'vp_' + (idCounter++); + let paneMeta = new Map(); + let pendingViewLoads = []; + let defaultoptions = { + minPaneSize: 100, + keyboard: true, + showHelp: true, + closeTransitionTimeout: 400, + toolbarButtons: ['split-v','split-h','views','close'], + standardViewList: [], + cssVars: { '--vp-duration': '.22s', '--vp-easing': 'ease' } + }; + options = Object.assign({}, defaultoptions, options); + + let createPane = (content, props = {}) => { + let id = nextId(); + let $el = $('
').attr('data-id', id); + if(props.title) $el.attr('data-title', props.title); + $el.html(content || `
Use Alt+<H V O X ↑ ← → ↓>
`) + .on('mousedown', () => focusPaneByEl($el[0])); + attachPaneToolbar($el[0]); + paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props}); + if(props.view?.screen) pendingViewLoads.push({id, view: props.view}); + return {type: 'pane', id, el: $el[0]}; + }; + + let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); }; + let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); }; + let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id'))); + let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r); + let getNodeById = id => findNodeById(rootNode, id) || null; + let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r); + let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc); + let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); }; + let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); }; + + let split = (direction, newPaneProps = {}) => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canSplit === false) return; + + let paneRect = focusedPane.el.getBoundingClientRect(); + let availableSpace = direction === 'row' ? paneRect.width : paneRect.height; + let requiredSpace = 2 * options.minPaneSize; + if(availableSpace < requiredSpace) { + //console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`); + return; + } + + let parentPath = findParentPath(rootNode, focusedPane.id); + let newPane = createPane(undefined, newPaneProps); + let animatedSplit = null, newIndex = -1; + if(!parentPath?.length) { + rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + animatedSplit = rootNode; newIndex = 1; + } else { + let parent = parentPath.at(-1); + if(parent.type === 'split' && parent.direction === direction) { + let idx = parent.children.indexOf(focusedPane); + parent.children.splice(idx+1, 0, newPane); + parent.sizes = parent.sizes || parent.children.map(()=>1); + parent.sizes.splice(idx+1, 0, 0); + renormalizeSizes(parent); + animatedSplit = parent; newIndex = idx+1; + } else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) { + parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0]; + animatedSplit = parent; newIndex = parent.children.length-1; + } else { + let idx = parent.children.indexOf(focusedPane); + let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]}; + parent.children.splice(idx, 1, created); + animatedSplit = created; newIndex = 1; + } + } + render(); + if(animatedSplit && newIndex >= 0) { + requestAnimationFrame(() => requestAnimationFrame(() => { + animatedSplit.sizes = animatedSplit.children.map(()=>1); + renormalizeSizes(animatedSplit); + animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`)); + })); + } + focusPane(newPane); return newPane; + }; + + let splitVertical = () => split('row'); + let splitHorizontal = () => split('col'); + + let closeFocused = () => { + if(!focusedPane) return; + let meta = paneMeta.get(focusedPane.id); + if(meta?.canClose === false) return; + if(rootNode === focusedPane) { buildInitial(); return; } + if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id); + if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id); + let parentPath = findParentPath(rootNode, focusedPane.id); + if(!parentPath?.length) return; + let parent = parentPath.at(-1); + if(parent.type !== 'split') return; + let idx = parent.children.indexOf(focusedPane); + + let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice(); + let oldSize = oldSizes[idx] || 0; + let remaining = 1 - oldSize; + let newSizes = oldSizes.slice(); + if(remaining > 0) { + for(let i=0;i ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`)); + if(focusedPane.el) { + focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease'; + focusedPane.el.style.opacity = '0'; + } + let done = false; + let cleanup = () => { + if(done) return; done = true; + parent.children.splice(idx,1); + parent.sizes?.splice(idx,1); + paneMeta.delete(focusedPane.id); + if(parent.children.length === 1) { + let only = parent.children[0]; + parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only); + } else if(parent.sizes) renormalizeSizes(parent); + render(); focusPane(paneList(rootNode)[0]); + }; + let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } }; + if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd); + setTimeout(() => cleanup(), 350); + }; + + let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; }; + let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); }; + let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); }; + + let serialize = (node=rootNode) => { + let ser = n => !n ? null : n.type==='pane' ? + {type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} : + {type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)}; + return {focus: focusedPane?.id, layout: ser(node)}; + }; + + let restore = data => { + if(!data) return; + let wrapper = data.layout ? data : {layout:data}; + paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0; + let idNums = []; + let build = l => l && l.type==='pane' ? + (() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view}); + if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); } + let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); } + return pane; })() : + l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null; + rootNode = build(wrapper.layout) || createPane(); + if(idNums.length) idCounter = Math.max(...idNums) + 1; + render(); + pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0; + focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]); + }; + + let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); }; + + let renderNode = node => { + if(node.type==='pane') return node.el; + node.el = node.el || $('
')[0]; + $(node.el).empty().addClass(`vp-split ${node.direction}`); + if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); } + node.children.forEach((ch,i) => { + let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl); + if(i < node.children.length-1) { let div = $('
').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); } + }); + return node.el; + }; + + let setupDivider = (div, splitNode, leftIdx) => { + $(div).on('mousedown', e => { + e.preventDefault(); $(div).addClass('dragging'); + $(containerEl).addClass('vp-no-transition'); + let isRow = splitNode.direction === 'row'; + let startPos = isRow ? e.clientX : e.clientY; + let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect()); + let prop = isRow ? 'width' : 'height'; + let startPixels = childRects.map(r => r[prop]); + let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]]; + let totalPixels = startPixels.reduce((a,b)=>a+b,0); + let onMove = ev => { + let curPos = isRow ? ev.clientX : ev.clientY; + let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos]; + if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize]; + if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize]; + startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx; + splitNode.sizes = startPixels.map(p => p / totalPixels); + splitNode.children.forEach((ch,i)=> { + if(ch.el) { + let flexValue = `${splitNode.sizes[i]} 1 0`; + $(ch.el).css('flex', flexValue); + } + }); + }; + let onUp = () => { + $(div).removeClass('dragging'); + $(containerEl).removeClass('vp-no-transition'); + $(document).off('mousemove', onMove).off('mouseup', onUp); + }; + $(document).on('mousemove', onMove).on('mouseup', onUp); + }); + $(div).on('dblclick', () => { + $(containerEl).addClass('vp-no-transition'); + let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len); + splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`)); + setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0); + }); + }; + + let attachPaneToolbar = el => { + let bar = $('
').addClass('vp-pane-toolbar').html(` + + + + `)[0]; + el.appendChild(bar); + $(bar).on('mousedown', ev => ev.stopPropagation()); + $(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el); + let button = ev.target.closest('button[data-act]'); + let act = button ? button.getAttribute('data-act') : null; + if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused(); + else if(act==='views') { + // compute button position + let rect = button.getBoundingClientRect(); + let items = options.standardViewList.slice(); + showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => { + // support string or object {screen} + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(el.dataset.id, view); + }, container: document.body, emptyText: 'No views available' }); + } + }); + refreshToolbarState(el.dataset.id); + }; + + let refreshToolbarState = paneId => { + let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; + if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return; + let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false]; + ['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } }); + let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); } + }; + + let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props); + if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; } + if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload; + paneMeta.set(paneId, meta); refreshToolbarState(paneId); }; + let getPaneProps = paneId => paneMeta.get(paneId); + + let focusDirection = (dx, dy) => { + if(!focusedPane) return; + let panes = Array.from(containerEl.querySelectorAll('.viewport-pane')); + let cur = focusedPane.el.getBoundingClientRect(); + let candidates = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); + if(dx) { + if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null; + let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top)); + let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.height - overlap)}; + } + if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null; + let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left)); + let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom; + if(primaryDist < -1) return null; + return {el:p, score: primaryDist*1000 + (cur.width - overlap)}; + }).filter(Boolean); + if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el); + let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2]; + let fallback = panes.filter(p => p !== focusedPane.el).map(p => { + let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2]; + let [vx, vy] = [px-cx, py-cy]; + if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null; + return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))}; + }).filter(Boolean); + if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el); + }; + + let injectHelpOverlay = () => { + if(!options.showHelp) return; + if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``})); + }; + + let getPaneEl = id => findNodeById(rootNode, id)?.el || null; + + let openViewsPopupForPane = (paneId) => { + let node = findNodeById(rootNode, paneId || focusedPane?.id); + if(!node || !node.el) return; + let rect = node.el.getBoundingClientRect(); + let x = Math.max(8, rect.right - 80); + let y = rect.top + 24; + let items = options.standardViewList.slice(); + showPopupMenu(x, y, items, { onSelect: it => { + let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it}); + loadView(node.id, view); + }, container: document.body, emptyText: 'No views available' }); + }; + + let loadView = (paneId, view) => { + let node = findNodeById(rootNode, paneId); + if(!node || !node.el) return; // nothing to load into + let meta = paneMeta.get(paneId); + try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); } + if(meta) meta.view = view; + node.el.innerHTML = `
Loading ${view.screen||'...'}...
`; + $(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})}); + }; + + let handleKeys = e => { + if(!e.altKey || !options.keyboard) return; + let actions = { + 'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(), + 'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(), + 'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(), + 'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(), + 'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext() + }; + if(actions[e.code] || actions[e.key]) { + (actions[e.code] || actions[e.key])(); + e.preventDefault(); + } + }; + + containerEl = typeof container === 'string' ? document.querySelector(container) : container; + if(!containerEl) throw new Error('TilingViewportManager: container not found'); + if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k])); + buildInitial(); + if(options.keyboard) document.addEventListener('keydown', handleKeys); + + this.splitVertical = splitVertical; + this.splitHorizontal = splitHorizontal; + this.closeFocused = closeFocused; + this.focusNext = focusNext; + this.focusPrev = focusPrev; + this.serialize = serialize; + this.restore = restore; + this.setPaneProps = setPaneProps; + this.getPaneProps = getPaneProps; + this.options = options; + this.loadView = loadView; + this.getPaneEl = getPaneEl; + this.getNodeById = getNodeById; + + Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) }); + Object.defineProperty(this, 'focused', { get: () => focusedPane?.id }); + }; + + return TilingViewportManager; +})); \ No newline at end of file diff --git a/site/info/uce-starter/js/u-timeseries-chart.js b/site/info/uce-starter/js/u-timeseries-chart.js new file mode 100644 index 0000000..b19f3eb --- /dev/null +++ b/site/info/uce-starter/js/u-timeseries-chart.js @@ -0,0 +1,354 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UTimeSeriesChart = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + class TimeSeriesChart { + constructor(canvasOrId, options = {}) { + this.canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId; + if (!this.canvas || !this.canvas.getContext) { + throw new Error('UTimeSeriesChart requires a canvas element'); + } + + this.ctx = this.canvas.getContext('2d'); + const theme = this.resolveThemeColors(); + this.options = Object.assign({ + xAxisLabel: 'Time', + yAxisLeftLabel: 'Value', + yAxisRightLabel: '', + yAxisLeftFormat: 'number', + yAxisRightFormat: 'number', + padding: { top: 18, right: 62, bottom: 34, left: 58 }, + gridColor: theme.gridColor, + axisColor: theme.axisColor, + textColor: theme.textColor, + legendBackground: theme.legendBackground, + legendTextColor: theme.legendTextColor, + tooltipBackground: theme.tooltipBackground, + tooltipBorder: theme.tooltipBorder, + tooltipTitleColor: theme.tooltipTitleColor, + tooltipTextColor: theme.tooltipTextColor, + hoverLineColor: 'rgba(255,255,255,0.35)', + }, options); + + this.series = []; + this.xLabels = []; + this.hoverIndex = null; + this.hoverX = 0; + this.width = 0; + this.height = 0; + + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + this.onResize = this.onResize.bind(this); + + this.canvas.addEventListener('mousemove', this.onMouseMove); + this.canvas.addEventListener('mouseleave', this.onMouseLeave); + globalScope.addEventListener('resize', this.onResize); + if (typeof ResizeObserver !== 'undefined') { + this.resizeObserver = new ResizeObserver(this.onResize); + this.resizeObserver.observe(this.canvas); + } + + this.resize(); + } + + resolveThemeColors() { + const styles = globalScope.getComputedStyle ? globalScope.getComputedStyle(document.documentElement) : null; + const pick = function (name, fallback) { + if (!styles) return fallback; + const value = styles.getPropertyValue(name).trim(); + return value || fallback; + }; + return { + gridColor: pick('--border', 'rgba(148, 163, 184, 0.20)'), + axisColor: pick('--border-hover', pick('--border', 'rgba(148, 163, 184, 0.42)')), + textColor: pick('--text-secondary', '#64748b'), + legendBackground: pick('--surface', 'rgba(15, 23, 42, 0.72)'), + legendTextColor: pick('--text-primary', '#0f172a'), + tooltipBackground: pick('--surface', '#0f172a'), + tooltipBorder: pick('--border', 'rgba(148, 163, 184, 0.30)'), + tooltipTitleColor: pick('--primary', '#3b82f6'), + tooltipTextColor: pick('--text-primary', '#0f172a'), + }; + } + + destroy() { + this.canvas.removeEventListener('mousemove', this.onMouseMove); + this.canvas.removeEventListener('mouseleave', this.onMouseLeave); + globalScope.removeEventListener('resize', this.onResize); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + } + + onResize() { + this.resize(); + } + + onMouseLeave() { + this.hoverIndex = null; + this.draw(); + } + + resize() { + const rect = this.canvas.getBoundingClientRect(); + const cssWidth = Math.max(280, Math.round(rect.width || this.canvas.width || 640)); + const cssHeight = Math.max(180, Math.round(rect.height || this.canvas.height || 320)); + const pixelRatio = globalScope.devicePixelRatio || 1; + + this.width = cssWidth; + this.height = cssHeight; + this.canvas.width = Math.round(cssWidth * pixelRatio); + this.canvas.height = Math.round(cssHeight * pixelRatio); + this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + this.draw(); + } + + setData(series, xLabels = []) { + this.series = (Array.isArray(series) ? series : []).map((item) => ({ + key: item.key || '', + label: item.label || 'Series', + color: item.color || '#60a5fa', + axis: item.axis === 'right' ? 'right' : 'left', + unit: item.unit || '', + format: item.format || '', + maxHint: Number(item.maxHint || 0), + decimals: Number.isInteger(item.decimals) ? item.decimals : 2, + values: Array.isArray(item.values) ? item.values.map((value) => Number(value || 0)) : [], + })); + this.xLabels = Array.isArray(xLabels) ? xLabels : []; + this.draw(); + } + + getPlotRect() { + const padding = this.options.padding; + return { + x: padding.left, + y: padding.top, + w: this.width - padding.left - padding.right, + h: this.height - padding.top - padding.bottom, + }; + } + + getMaxForAxis(axis) { + const relevant = this.series.filter((item) => item.axis === axis); + if (!relevant.length) return 1; + let max = 1; + relevant.forEach((item) => { + const localMax = Math.max(...(item.values.length ? item.values : [0]), item.maxHint || 0, 1); + if (localMax > max) max = localMax; + }); + return max; + } + + xForIndex(index, pointCount, plot) { + if (pointCount <= 1) return plot.x; + return plot.x + (index / (pointCount - 1)) * plot.w; + } + + yForValue(value, axisMax, plot) { + const clamped = Math.max(0, Number(value || 0)); + return plot.y + plot.h - ((clamped / axisMax) * plot.h); + } + + formatValue(value, format, unit, decimals) { + const number = Number(value || 0); + if (globalScope.UFormat) { + if (format === 'bytes' && globalScope.UFormat.formatBytes) return globalScope.UFormat.formatBytes(number); + if (format === 'disk-bytes' && globalScope.UFormat.formatDiskBytes) return globalScope.UFormat.formatDiskBytes(number); + if (format === 'count' && globalScope.UFormat.formatCount) return globalScope.UFormat.formatCount(number); + if (format === 'duration-ms' && globalScope.UFormat.formatDurationMs) return globalScope.UFormat.formatDurationMs(number); + } + return `${number.toFixed(decimals)}${unit || ''}`; + } + + drawGridAndAxes(plot, maxLeft, maxRight) { + const ctx = this.ctx; + ctx.strokeStyle = this.options.gridColor; + ctx.lineWidth = 1; + + for (let index = 0; index <= 4; index += 1) { + const y = plot.y + (plot.h / 4) * index; + ctx.beginPath(); + ctx.moveTo(plot.x, y); + ctx.lineTo(plot.x + plot.w, y); + ctx.stroke(); + + const leftValue = maxLeft - ((maxLeft / 4) * index); + const leftTick = this.formatValue(leftValue, this.options.yAxisLeftFormat, '', 1); + ctx.fillStyle = this.options.textColor; + ctx.font = '11px Inter, sans-serif'; + const leftWidth = ctx.measureText(leftTick).width; + ctx.fillText(leftTick, Math.max(4, plot.x - leftWidth - 8), y + 4); + + if (this.options.yAxisRightLabel) { + const rightValue = maxRight - ((maxRight / 4) * index); + const rightTick = this.formatValue(rightValue, this.options.yAxisRightFormat, '', 1); + ctx.fillText(rightTick, plot.x + plot.w + 8, y + 4); + } + } + + ctx.strokeStyle = this.options.axisColor; + ctx.beginPath(); + ctx.moveTo(plot.x, plot.y); + ctx.lineTo(plot.x, plot.y + plot.h); + ctx.lineTo(plot.x + plot.w, plot.y + plot.h); + ctx.stroke(); + + ctx.fillStyle = this.options.textColor; + ctx.font = '12px Inter, sans-serif'; + ctx.save(); + ctx.translate(14, plot.y + (plot.h / 2)); + ctx.rotate(-Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.yAxisLeftLabel, 0, 0); + ctx.restore(); + + if (this.options.yAxisRightLabel) { + ctx.save(); + ctx.translate(this.width - 14, plot.y + (plot.h / 2)); + ctx.rotate(Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.yAxisRightLabel, 0, 0); + ctx.restore(); + } + + const xLabel = this.options.xAxisLabel; + const xLabelWidth = ctx.measureText(xLabel).width; + ctx.fillText(xLabel, plot.x + plot.w - xLabelWidth, this.height - 10); + } + + drawSeries(plot, maxLeft, maxRight) { + const ctx = this.ctx; + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) return; + + this.series.forEach((item) => { + if (!item.values.length) return; + const axisMax = item.axis === 'right' ? maxRight : maxLeft; + ctx.beginPath(); + item.values.forEach((value, index) => { + const x = this.xForIndex(index, item.values.length, plot); + const y = this.yForValue(value, axisMax, plot); + if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); + }); + ctx.strokeStyle = item.color; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + if (this.series.length > 1) { + ctx.fillStyle = this.options.legendBackground; + ctx.fillRect(plot.x + 4, plot.y + 4, Math.min(plot.w - 8, 320), 24); + let offsetX = plot.x + 12; + this.series.forEach((item) => { + ctx.fillStyle = item.color; + ctx.fillRect(offsetX, plot.y + 13, 14, 3); + offsetX += 20; + ctx.fillStyle = this.options.legendTextColor; + ctx.font = '11px Inter, sans-serif'; + ctx.fillText(item.label, offsetX, plot.y + 17); + offsetX += ctx.measureText(item.label).width + 18; + }); + } + } + + drawHover(plot, maxLeft, maxRight) { + if (this.hoverIndex == null) return; + const ctx = this.ctx; + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) return; + + const index = Math.max(0, Math.min(pointCount - 1, this.hoverIndex)); + const x = this.xForIndex(index, pointCount, plot); + + ctx.strokeStyle = this.options.hoverLineColor; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, plot.y); + ctx.lineTo(x, plot.y + plot.h); + ctx.stroke(); + + const lines = []; + lines.push(this.xLabels[index] || `Sample ${index + 1}`); + + this.series.forEach((item) => { + if (index >= item.values.length) return; + const axisMax = item.axis === 'right' ? maxRight : maxLeft; + const value = item.values[index] ?? 0; + const y = this.yForValue(value, axisMax, plot); + ctx.fillStyle = item.color; + ctx.beginPath(); + ctx.arc(x, y, 3.5, 0, Math.PI * 2); + ctx.fill(); + lines.push(`${item.label}: ${this.formatValue(value, item.format || (item.axis === 'right' ? this.options.yAxisRightFormat : this.options.yAxisLeftFormat), item.unit || '', item.decimals)}`); + }); + + ctx.font = '11px Inter, sans-serif'; + const padding = 8; + const lineHeight = 14; + const tooltipWidth = Math.max(...lines.map((line) => ctx.measureText(line).width)) + padding * 2; + const tooltipHeight = lines.length * lineHeight + padding * 2 - 2; + let tooltipX = this.hoverX + 12; + let tooltipY = plot.y + 8; + + if (tooltipX + tooltipWidth > this.width - 4) tooltipX = this.hoverX - tooltipWidth - 12; + if (tooltipX < 4) tooltipX = 4; + if (tooltipY + tooltipHeight > this.height - 4) tooltipY = this.height - tooltipHeight - 4; + + ctx.fillStyle = this.options.tooltipBackground; + ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight); + ctx.strokeStyle = this.options.tooltipBorder; + ctx.strokeRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight); + + lines.forEach((line, lineIndex) => { + ctx.fillStyle = lineIndex === 0 ? this.options.tooltipTitleColor : this.options.tooltipTextColor; + ctx.fillText(line, tooltipX + padding, tooltipY + padding + 10 + lineIndex * lineHeight); + }); + } + + onMouseMove(event) { + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + this.hoverX = x; + + const plot = this.getPlotRect(); + const pointCount = Math.max(...this.series.map((item) => item.values.length), 0); + if (pointCount <= 0) { + this.hoverIndex = null; + this.draw(); + return; + } + + const relative = Math.max(0, Math.min(plot.w, x - plot.x)); + this.hoverIndex = pointCount <= 1 ? 0 : Math.round((relative / plot.w) * (pointCount - 1)); + this.draw(); + } + + draw() { + const ctx = this.ctx; + ctx.clearRect(0, 0, this.width, this.height); + + const plot = this.getPlotRect(); + if (plot.w <= 0 || plot.h <= 0) return; + const maxLeft = this.getMaxForAxis('left'); + const maxRight = this.getMaxForAxis('right'); + + this.drawGridAndAxes(plot, maxLeft, maxRight); + this.drawSeries(plot, maxLeft, maxRight); + this.drawHover(plot, maxLeft, maxRight); + } + } + + return TimeSeriesChart; +})); \ No newline at end of file diff --git a/site/info/uce-starter/js/u-workspace-shell.js b/site/info/uce-starter/js/u-workspace-shell.js new file mode 100644 index 0000000..90fbd31 --- /dev/null +++ b/site/info/uce-starter/js/u-workspace-shell.js @@ -0,0 +1,68 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.UWorkspaceShell = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + + function getElement(target) { + if (!target) return null; + if (typeof target === 'string') return document.getElementById(target); + return target && target.nodeType === 1 ? target : null; + } + + function bindShell(options) { + const sidebar = getElement(options.sidebarId || options.sidebar); + const overlay = getElement(options.overlayId || options.overlay); + const toggle = getElement(options.toggleButtonId || options.toggle); + const closeOnNav = options.closeOnNav !== false; + + if (!sidebar || !overlay) return null; + + function setOpen(open) { + sidebar.classList.toggle('is-open', !!open); + overlay.classList.toggle('is-open', !!open); + } + + function toggleOpen() { + setOpen(!sidebar.classList.contains('is-open')); + } + + if (toggle) { + toggle.addEventListener('click', toggleOpen); + } + + overlay.addEventListener('click', function () { + setOpen(false); + }); + + if (closeOnNav) { + sidebar.querySelectorAll('a').forEach(function (link) { + link.addEventListener('click', function () { + setOpen(false); + }); + }); + } + + globalScope.addEventListener('resize', function () { + if (globalScope.innerWidth > 860) { + setOpen(false); + } + }); + + return { + open: function () { setOpen(true); }, + close: function () { setOpen(false); }, + toggle: toggleOpen, + }; + } + + return { + init: bindShell, + }; +})); \ No newline at end of file diff --git a/site/info/uce-starter/js/u-wsconnection.js b/site/info/uce-starter/js/u-wsconnection.js new file mode 100755 index 0000000..9c3b40f --- /dev/null +++ b/site/info/uce-starter/js/u-wsconnection.js @@ -0,0 +1,111 @@ +(function (root, factory) { + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define([], factory); + } else { + root.Connection = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + +var Connection = { + + auto_reconnect : false, + established : false, + + update_indicator : (status) => { + var status_colors = { + 'offline' : 'red', + 'online' : 'lightgreen', + 'error' : 'DarkOrange', + }; + $('#connection-status').text(status).css('color', status_colors[status] || 'gray'); + }, + + debug : true, + auto_reconnect : true, + cmd_waiting_rels : {}, + + server_url : '', + + init : () => { + new EventSystem(Connection); + }, + + start : (url = null) => { + + if(url) Connection.server_url = url; + + Connection.update_indicator('offline'); + if(Connection.socket) Connection.socket.close(); + Connection.socket = new WebSocket(Connection.server_url); + Connection.socket.onmessage = function(rawmsg) { + var msg = JSON.parse(rawmsg.data); + if(Connection.debug) console.log('CONNECTION MSG', msg); + if(msg.type) Connection.trigger(msg.type, msg); + Connection.trigger('message', msg); + } + Connection.socket.onclose = function() { + Connection.update_indicator('offline'); + if(Connection.debug) console.log('CONNECTION CLOSED'); + Connection.established = false; + Connection.trigger('close', {}); + } + Connection.socket.onerror = function(error) { + Connection.update_indicator('error'); + console.error('CONNECTION', error); + Connection.established = false; + } + Connection.socket.onopen = function() { + Connection.update_indicator('online'); + if(Connection.debug) console.log('CONNECTION ESTABLISHED'); + Connection.established = true; + Connection.trigger('open', {}); + } + setTimeout(Connection.reconnect, 2000); + + }, + + deauth : () => { + Game.session = {}; + }, + + reconnect : () => { + if(!Connection.established && Connection.auto_reconnect) + Connection.start(); + else + setTimeout(Connection.reconnect, 2000); + }, + + queue : [], + + dequeue : () => { + var dq = Connection.queue; + Connection.queue = []; + dq.forEach(function(fm) { + Connection.send(fm); + }); + }, + + send : (msg) => { + if(Connection.established) { + if(typeof msg == 'function') + msg(); + else + Connection.socket.send(JSON.stringify(msg)); + } else { + Connection.queue.push(msg); + } + }, + + close : () => { + Connection.auto_reconnect = false; + Connection.socket.close(); + }, + +} + +return Connection; + +})); + diff --git a/site/info/uce-starter/lib/app.uce b/site/info/uce-starter/lib/app.uce new file mode 100644 index 0000000..0a2d92f --- /dev/null +++ b/site/info/uce-starter/lib/app.uce @@ -0,0 +1,418 @@ +#load "../config/settings.uce" +#load "user.class.h" + +String app_fs_root(Request& context) +{ + String root = context.var["app"]["fs_root"].to_string(); + if(root == "") + root = cwd_get(); + return(root); +} + +String app_script_url(Request& context) +{ + String url = context.var["app"]["script_url"].to_string(); + if(url == "") + url = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]); + return(url); +} + +String app_base_url(Request& context) +{ + String base = context.var["app"]["base_url"].to_string(); + if(base == "") + { + base = dirname(app_script_url(context)); + if(base == "") + base = "/"; + if(base[base.length() - 1] != '/') + base.append(1, '/'); + } + return(base); +} + +String app_fs_path(String relative, Request& context) +{ + return(path_join(app_fs_root(context), relative)); +} + +String app_link(String path, Request& context); +String app_link(String path, StringMap params, Request& context); +void app_init(Request& context); + +String app_first_route_segment(Request& context) +{ + String query = context.params["QUERY_STRING"]; + for(auto part : split(query, "&")) + { + if(part == "") + continue; + if(part.find("=") == String::npos) + return(uri_decode(part)); + } + return(""); +} + +DTree app_make_route(Request& context) +{ + DTree route; + String path = app_first_route_segment(context); + path = trim(path); + while(path.length() > 0 && path[0] == '/') + path = path.substr(1); + while(path.length() > 0 && path[path.length() - 1] == '/') + path = path.substr(0, path.length() - 1); + if(path == "") + path = "index"; + route["l_path"] = path; + route["page"] = nibble(path, "/"); + if(route["page"].to_string() == "") + route["page"] = "index"; + return(route); +} + +DTree app_resolve_view(Request& context, String base_dir = "views") +{ + DTree result; + 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] == '/') + base = base.substr(0, base.length() - 1); + + String exact = base + "/" + lpath + ".uce"; + if(file_exists(exact)) + { + result["file"] = exact; + return(result); + } + + String dir_index = base + "/" + lpath + "/index.uce"; + if(file_exists(dir_index)) + { + result["file"] = dir_index; + return(result); + } + + auto parts = split(lpath, "/"); + if(parts.size() > 1) + { + String param = parts.back(); + parts.pop_back(); + String parent = join(parts, "/"); + String parent_index = base + "/" + parent + "/index.uce"; + if(file_exists(parent_index)) + { + result["file"] = parent_index; + result["param"] = param; + return(result); + } + } + + return(result); +} + +void app_register_css(String path, Request& context) +{ + context.var["app"]["assets"]["css"][path] = path; +} + +void app_register_js(String path, Request& context) +{ + context.var["app"]["assets"]["js"][path] = path; +} + +String app_asset_url(String path, Request& 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 app_render_registered_css(Request& context) +{ + context.var["app"]["assets"]["css"].each([&](DTree item, String key) { + String path = item.to_string(); + if(path != "") + { + <> + } + }); +} + +void app_render_registered_js(Request& context) +{ + context.var["app"]["assets"]["js"].each([&](DTree item, String key) { + String path = item.to_string(); + if(path != "") + { + <> + } + }); +} + +String app_link(String path, Request& context) +{ + StringMap params; + return(app_link(path, params, context)); +} + +String app_link(String path, StringMap params, Request& context) +{ + String query = ""; + path = trim(path); + if(path != "") + query = uri_encode(path); + String extra = encode_query(params); + if(query != "" && extra != "") + query += "&" + extra; + else if(query == "") + query = extra; + String url = app_script_url(context); + if(query != "") + url += "?" + query; + return(url); +} + +void app_redirect(String path, Request& context) +{ + context.set_status(302, "Found"); + context.header["Location"] = app_link(path, context); +} + +void app_not_found(String message, Request& context) +{ + context.set_status(404, "Not Found"); + context.var["app"]["error"] = message; + context.var["app"]["page_title"] = "404 Not Found"; +} + +String app_menu_href(String menu_key, DTree menu_item, Request& context) +{ + if(menu_item["external"].to_string() != "") + return("/" + menu_key); + return(app_link(menu_key, context)); +} + +String app_html_class(Request& context) +{ + String classes = "no-js"; + if(context.cfg.get_by_path("theme/mode").to_string() == "dark") + classes += " dark-theme"; + return(classes); +} + +String app_page_main_html(Request& context) +{ + String main_html = context.call["main_html"].to_string(); + if(main_html == "") + main_html = context.var["app"]["fragments"]["main"].to_string(); + return(main_html); +} + +bool app_bool_value(DTree value, bool fallback = false) +{ + String raw = trim(value.to_string()); + if(raw == "") + return(fallback); + return( + raw != "0" && + raw != "false" && + raw != "FALSE" && + raw != "False" && + raw != "(false)" && + raw != "no" && + raw != "NO" && + raw != "No" + ); +} + +bool app_request_embed_mode(Request& context) +{ + return(app_bool_value(context.var["app"]["embed_mode"])); +} + +bool app_page_embed_mode(Request& context) +{ + String embed_value = context.call["embed_mode"].to_string(); + if(embed_value != "") + return(app_bool_value(context.call["embed_mode"])); + return(app_request_embed_mode(context)); +} + +String app_theme_page_component(Request& context) +{ + 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") + return("themes/common/page.json.uce"); + + String theme_path = context.cfg.get_by_path("theme/path").to_string(); + if(theme_path == "") + return(""); + if(theme_path[theme_path.length() - 1] != '/') + theme_path.append(1, '/'); + return(theme_path + "page.html.uce"); +} + +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["app"]["fragments"]["main"]; + if(context.var["app"]["json"].get_type_name() == "array") + page_props["json"] = context.var["app"]["json"]; + + String page_component = app_theme_page_component(context); + if(page_component != "" && file_exists(page_component)) + { + print(component(page_component, page_props, context)); + return; + } + + context.set_status(500, "Internal Server Error"); + context.header["Content-Type"] = "text/plain; charset=utf-8"; + 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) +{ + 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["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["app"]["script_url"].to_string()); + if(base_url == "") + base_url = "/"; + if(base_url[base_url.length() - 1] != '/') + base_url.append(1, '/'); + context.var["app"]["base_url"] = base_url; + + 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(); + + config["theme"]["options"][requested_theme].each([&](DTree item, String key) { + config["theme"][key] = item; + }); + config["theme"]["key"] = requested_theme; + context.cfg = config; + context.var["cfg"].set_reference(&context.cfg); + 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("app_theme", requested_theme, time() + (86400 * 180)); + context.cookies["app_theme"] = requested_theme; + } + + session_start(); + AppUser(context).is_signed_in(); + + 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/info/uce-starter/lib/user.class.h b/site/info/uce-starter/lib/user.class.h new file mode 100644 index 0000000..6932641 --- /dev/null +++ b/site/info/uce-starter/lib/user.class.h @@ -0,0 +1,186 @@ +#pragma once + +struct AppUser +{ + + Request& context; + + explicit AppUser(Request& request) + : context(request) + { + } + + static String session_key() + { + return("app_user_id"); + } + + static String normalize_email(String email) + { + return(to_lower(trim(email))); + } + + static String hash_id(String raw) + { + raw = normalize_email(raw); + 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-app-data")); + } + + String dir(String email) + { + String bucket = hash_id(email); + return(path_join(root_path(), "users/" + bucket.substr(0, 2) + "/" + bucket)); + } + + String file_name(String email) + { + return(path_join(dir(email), "account.json")); + } + + static String password_hash(String password, String salt) + { + String hash = "uce-app:" + password + ":" + salt; + for(s32 i = 0; i < 2048; i++) + hash = gen_sha1(hash + ":" + std::to_string(i)); + return(hash); + } + + static DTree read_json_file(String file_name) + { + DTree result; + if(!file_exists(file_name)) + return(result); + String raw = trim(file_get_contents(file_name)); + if(raw == "") + return(result); + return(json_decode(raw)); + } + + static bool write_json_file(String file_name, DTree data) + { + mkdir(dirname(file_name)); + return(file_put_contents(file_name, json_encode(data))); + } + + DTree load(String email) + { + DTree user = read_json_file(file_name(email)); + if(user.get_type_name() != "array") + return(DTree()); + user["id"] = normalize_email(email); + return(user); + } + + DTree create(String email, String password) + { + DTree result; + email = normalize_email(email); + password = trim(password); + result["message"] = ""; + result["result"].set_bool(false); + + if(email == "" || password == "") + { + result["message"] = "email_and_password_required"; + return(result); + } + if(email.find("@") == String::npos) + { + result["message"] = "invalid_email"; + return(result); + } + + DTree existing = load(email); + if(existing.get_type_name() == "array" && existing["email"].to_string() != "") + { + result["message"] = "user_exists"; + return(result); + } + + String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + ":" + session_id_create()).substr(0, 24); + DTree user; + user["email"] = email; + user["salt"] = salt; + user["password_hash"] = password_hash(password, salt); + user["created"] = std::to_string((u64)time()); + DTree role; + role = "user"; + user["roles"].push(role); + + write_json_file(file_name(email), user); + user["id"] = email; + result["result"].set_bool(true); + result["id"] = email; + result["profile"] = user; + return(result); + } + + DTree sign_in(String email, String password) + { + DTree result; + result["result"].set_bool(false); + email = normalize_email(email); + password = trim(password); + + DTree user = load(email); + if(user.get_type_name() != "array" || user["email"].to_string() == "") + { + result["message"] = "no_such_user"; + return(result); + } + if(user["password_hash"].to_string() == "") + { + result["message"] = "no_password_set"; + return(result); + } + + String expected = password_hash(password, user["salt"].to_string()); + if(expected != user["password_hash"].to_string()) + { + result["message"] = "invalid_password"; + return(result); + } + + session_start(); + context.session[session_key()] = email; + context.var["app"]["current_user"] = user; + result["result"].set_bool(true); + result["profile"] = user; + return(result); + } + + bool is_signed_in() + { + if(context.var["app"]["current_user"]["email"].to_string() != "") + return(true); + String user_id = context.session[session_key()]; + if(user_id == "") + return(false); + DTree user = load(user_id); + if(user["email"].to_string() == "") + { + context.session.erase(session_key()); + return(false); + } + context.var["app"]["current_user"] = user; + return(true); + } + + DTree current() + { + if(is_signed_in()) + return(context.var["app"]["current_user"]); + return(DTree()); + } + + void logout() + { + context.session.erase(session_key()); + context.var["app"]["current_user"].clear(); + } +}; diff --git a/site/info/uce-starter/themes/common/css/gauges.css b/site/info/uce-starter/themes/common/css/gauges.css new file mode 100644 index 0000000..1c465da --- /dev/null +++ b/site/info/uce-starter/themes/common/css/gauges.css @@ -0,0 +1,348 @@ +.gauge-demo-row { + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: stretch; + margin-bottom: 1.5rem; +} + +.gauge-control-panel { + flex: 1 1 18rem; + min-width: 16rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-control-panel h4 { + margin: 0 0 0.75rem; + color: var(--text-primary); +} + +.gauge-slider-group { + display: grid; + gap: 0.35rem; + margin-bottom: 0.9rem; +} + +.gauge-slider-group label { + color: var(--text-secondary); + font-weight: 600; + font-size: 0.92rem; +} + +.gauge-slider-group input[type="range"] { + width: 100%; + accent-color: var(--primary); +} + +.gauge-set, +.arcgauge-set { + flex: 1 1 24rem; + min-width: 20rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.gauge-set-header, +.arcgauge-set-header { + margin-bottom: 1rem; +} + +.gauge-set-header h3, +.arcgauge-set-header h3 { + margin: 0 0 0.3rem; + color: var(--text-primary); +} + +.gauge-set-header p, +.arcgauge-set-header p { + margin: 0; + color: var(--text-secondary); +} + +.gauge-card, +.arcgauge-card { + display: flex; + flex-direction: column; + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface-elevated, var(--surface)); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: relative; + overflow: hidden; +} + +.gauge-card::before, +.arcgauge-card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.5; +} + +.gauge-metric-label { + color: var(--text-secondary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.gauge-metric-value { + color: var(--text-primary); + font-size: 1.1rem; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); + letter-spacing: -0.03em; +} + +.gauge-metric-meta { + color: var(--text-muted); + font-size: 0.84rem; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.progressbar-set-horizontal, +.progressbar-set-vertical { + align-self: stretch; +} + +.progressbar-grid-horizontal { + display: grid; + gap: 0.85rem; +} + +.progressbar-grid-vertical { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + align-items: stretch; +} + +.progressbar-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.7rem; +} + +.progressbar-card-horizontal { + gap: 0.7rem; +} + +.progressbar-card-vertical { + align-items: center; + justify-content: flex-end; + text-align: center; + gap: 0.7rem; +} + +.progressbar-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.progressbar-background { + position: relative; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent 30%); + background: color-mix(in srgb, var(--bg-color) 72%, var(--surface) 28%); + display: flex; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); +} + +.progressbar-background-horizontal { + height: 0.9rem; + border-radius: 999px; +} + +.progressbar-background-vertical { + height: var(--progressbar-height, 240px); + width: 100%; + max-width: 4rem; + margin: 0 auto; + border-radius: 1.2rem; + justify-content: flex-end; + padding: 0.35rem; + flex-direction: column; +} + + +.progressbar-bar { + transition: all 0.35s ease; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12), 0 0 24px color-mix(in srgb, currentColor 28%, transparent 72%); +} + +.progressbar-background-horizontal .progressbar-bar { + height: 100%; + border-radius: 999px; +} + +.progressbar-background-vertical .progressbar-bar { + width: 100%; + border-radius: 0.95rem; +} + +.progressbar-marker { + position: absolute; + z-index: 10; + opacity: 0.72; + background: var(--text-color, var(--text-primary, #333)); + box-shadow: 0 0 0 1px rgba(255,255,255,0.16); +} + +.progressbar-marker:hover { + opacity: 1; +} + +.progressbar-background-horizontal .progressbar-marker { + height: 100%; + top: 0; + width: 3px; + transform: translateX(-50%); +} + +.progressbar-background-vertical .progressbar-marker { + width: 100%; + left: 0; + height: 3px; + transform: translateY(50%); +} + +.needlegauge-svg .needle { + transition: all 0.3s ease; +} + +.progressbar-meta { + margin-top: 0.1rem; +} + +.needlegauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); +} + +.needlegauge-card { + align-items: center; + text-align: center; + gap: 0.55rem; +} + +.needlegauge-label-head { + align-self: flex-start; +} + +.needlegauge-visual { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.needlegauge-svg { + max-width: 100%; + overflow: visible; +} + + +.needlegauge-svg .ticks line { + opacity: 0.8; +} + +.needlegauge-svg .ticks text { + font-family: var(--font-mono, ui-monospace, monospace); + fill: var(--text-muted); +} + +.needlegauge-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.arcgauge-label { + margin-bottom: 0.35rem; +} + +.arcgauge-svg { + width: 130px; + height: 72px; + margin: 0.1rem 0 0.35rem; + overflow: visible; +} + +.arcgauge-track { + stroke: color-mix(in srgb, var(--border) 70%, transparent 30%); +} + +.arcgauge-arc-dyn { + transition: stroke-dasharray 0.6s ease, stroke 0.6s ease; +} + +.arcgauge-watermark { + stroke-width: 1.5; + stroke-linecap: round; +} + +.arcgauge-watermark-lo { + stroke: var(--primary); +} + +.arcgauge-watermark-hi { + stroke: var(--accent); +} + +.arcgauge-value { + fill: var(--text-primary); + font-size: 17px; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); +} + +.arcgauge-caption { + fill: var(--text-muted); + font-size: 7px; + letter-spacing: 1.5px; + font-family: var(--font-sans, system-ui, sans-serif); +} + +.arcgauge-meta { + width: 100%; + text-align: center; +} + +.arcgauge-grid { + display: grid; + gap: 0.85rem; + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); +} + +.arcgauge-card { + align-items: center; + text-align: center; +} + +@media (max-width: 768px) { + .gauge-demo-row { + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/info/uce-starter/themes/common/css/workspace.css b/site/info/uce-starter/themes/common/css/workspace.css new file mode 100644 index 0000000..b0575cb --- /dev/null +++ b/site/info/uce-starter/themes/common/css/workspace.css @@ -0,0 +1,522 @@ +.ws-app { + --ws-sidebar-width: 280px; + --ws-frame-bg: var(--surface); + --ws-sidebar-bg: color-mix(in srgb, var(--bg-secondary) 80%, var(--surface) 20%); + --ws-main-bg: var(--surface); + --ws-surface-alt: color-mix(in srgb, var(--bg-secondary) 72%, var(--surface) 28%); + --ws-shadow: var(--shadow-md); + display: flex; + width: 100%; + min-height: min(76vh, 900px); + overflow: hidden; + position: relative; + background: var(--ws-frame-bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--ws-shadow); +} + +.ws-sidebar { + width: var(--ws-sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + min-height: 0; + background: var(--ws-sidebar-bg); + border-right: 1px solid var(--border); + position: relative; + z-index: 2; +} + +.ws-main { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--ws-main-bg); + padding: 0; +} + +.ws-sidebar-top, +.ws-panel-header, +.ws-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.ws-sidebar-top { + padding: 0.9rem; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + flex-direction: column; + align-items: stretch; + background: color-mix(in srgb, var(--surface) 78%, var(--bg-secondary) 22%); +} + +.ws-search-wrap { + position: relative; +} + +.ws-search-icon { + position: absolute; + left: 0.7rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 0.8rem; + pointer-events: none; +} + +.ws-search-input { + width: 100%; + min-height: 36px; + padding: 0 0.8rem 0 2rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + font-size: 0.9rem; + color: var(--text-primary); +} + +.ws-search-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent 80%); +} + +.ws-panel-title-group, +.ws-header-actions { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.ws-panel-title-group { + flex-direction: column; + align-items: flex-start; +} + +.ws-panel-title-group h2, +.ws-section-head h3, +.ws-empty-state h2 { + margin: 0; +} + +.ws-subtitle { + margin: 0; + color: var(--text-secondary); + font-size: 0.92rem; +} + +.ws-sidebar-overlay { + display: none; +} + +.ws-mobile-bar { + display: none; + align-items: center; + gap: 0.7rem; + padding: 0.9rem 1rem; + border-bottom: 1px solid var(--border); + background: color-mix(in srgb, var(--surface) 92%, var(--bg-secondary) 8%); + position: sticky; + top: 0; + z-index: 1; +} + +.ws-mobile-title { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); +} + +.ws-empty-state { + min-height: 320px; + display: grid; + place-items: center; + align-content: center; + gap: 0.75rem; + text-align: center; + padding: 2rem; + max-width: 40rem; + margin: auto; +} + +.ws-empty-state p { + max-width: 32rem; + color: var(--text-secondary); + margin: 0; +} + +.ws-empty-icon { + width: 64px; + height: 64px; + display: grid; + place-items: center; + border-radius: var(--radius); + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: #fff; + font-size: 24px; + box-shadow: var(--shadow-sm); +} + +.ws-panel { + min-height: 0; + display: flex; + flex-direction: column; + padding: 1.1rem 1.25rem; + gap: 1rem; +} + +.ws-section { + display: flex; + flex-direction: column; + gap: 0.8rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--ws-surface-alt); +} + +.ws-section + .ws-section { + margin-top: 0.25rem; +} + +.ws-icon-btn { + min-width: 34px; + height: 34px; + padding: 0 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + flex-shrink: 0; + background: var(--bg-secondary); + color: var(--text-secondary); + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease, border-color 0.2s ease; + text-decoration: none; +} + +.ws-icon-btn:hover { + background: var(--surface-hover); + border-color: var(--border-hover); + color: var(--text-primary); + text-decoration: none; +} + +.ws-icon-btn:active { + transform: translateY(1px); +} + +.ws-primary-btn, +.ws-sidebar-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + width: 100%; + min-height: 38px; + padding: 0.55rem 0.85rem; + border: none; + border-radius: var(--radius-sm); + background: var(--primary); + color: #fff; + font-size: 0.92rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; +} + +.ws-primary-btn:hover, +.ws-sidebar-action-btn:hover { + background: var(--primary-dark); + text-decoration: none; + color: #fff; +} + +.ws-primary-btn:active, +.ws-sidebar-action-btn:active { + transform: translateY(1px); +} + +.ws-list-state { + padding: 1rem; + color: var(--text-secondary); + font-size: 0.86rem; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + border-top: 1px solid var(--border); +} + +.ws-nav-group-label { + padding: 0.7rem 0.95rem 0.35rem; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + user-select: none; +} + +.ws-nav-list { + display: flex; + flex-direction: column; + padding: 0.3rem 0.4rem 0.9rem; + gap: 0.2rem; + overflow: auto; + min-height: 0; + flex: 1; +} + +.ws-nav-item { + display: block; + padding: 0 0.2rem; + text-decoration: none; + color: inherit; +} + +.ws-nav-item-inner { + display: flex; + align-items: center; + gap: 0.55rem; + width: 100%; + padding: 0.58rem 0.65rem; + border-radius: var(--radius-sm); + min-width: 0; + transition: background 0.2s ease, border-color 0.2s ease, padding-left 0.2s ease; + border-left: 3px solid transparent; +} + +.ws-nav-item:hover .ws-nav-item-inner { + background: color-mix(in srgb, var(--surface-hover) 85%, transparent 15%); +} + +.ws-nav-item.active .ws-nav-item-inner, +.ws-nav-item.is-active .ws-nav-item-inner { + background: color-mix(in srgb, var(--primary) 12%, transparent 88%); + border-left-color: var(--primary); + padding-left: calc(0.65rem - 3px); +} + +.ws-nav-icon { + flex-shrink: 0; + width: 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; +} + +.ws-nav-item.active .ws-nav-icon, +.ws-nav-item.is-active .ws-nav-icon { + color: var(--primary); +} + +.ws-nav-item-text { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.45rem; + overflow: hidden; +} + +.ws-nav-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.9rem; + color: var(--text-primary); + line-height: 1.3; +} + +.ws-nav-item.active .ws-nav-title, +.ws-nav-item.is-active .ws-nav-title { + font-weight: 700; + color: var(--primary-dark); +} + +.ws-nav-meta { + flex-shrink: 0; + font-size: 0.72rem; + color: var(--text-muted); + transition: opacity 0.2s ease; +} + +.ws-status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0.2rem 0.6rem; + border-radius: 999px; + border: 1px solid transparent; + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.ws-status-pill-neutral { + background: color-mix(in srgb, var(--text-muted) 16%, transparent 84%); + border-color: color-mix(in srgb, var(--text-muted) 28%, transparent 72%); + color: var(--text-secondary); +} + +.ws-status-pill-success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +.ws-status-pill-warn { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); +} + +.ws-status-pill-danger { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); +} + +.ws-status-pill-info { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); +} + +.ws-stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.85rem; +} + +.ws-stat-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.ws-stat-card-label { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + color: var(--text-secondary); +} + +.ws-stat-card-value { + font-size: 1.45rem; + font-weight: 700; + line-height: 1.1; + font-variant-numeric: tabular-nums; +} + +.ws-stat-card-meta, +.ws-section-copy, +.ws-inline-note, +.ws-detail-copy { + margin: 0; + color: var(--text-secondary); +} + +.ws-detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.85rem; +} + +.ws-detail-card { + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--bg-secondary) 85%, var(--surface) 15%); + min-height: 100%; +} + +.ws-detail-card h4 { + margin: 0 0 0.35rem; + font-size: 0.95rem; +} + +.ws-detail-card p { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.ws-demo-sidebar-copy { + padding: 0 1rem 1rem; + font-size: 0.84rem; + color: var(--text-secondary); +} + +.ws-demo-sidebar-copy p { + margin: 0; +} + +@media (max-width: 860px) { + .ws-app { + display: block; + min-height: 72vh; + } + + .ws-sidebar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: min(88vw, max(var(--ws-sidebar-width), 320px)); + transform: translateX(-100%); + transition: transform 160ms ease; + box-shadow: var(--shadow-lg); + z-index: 10; + } + + .ws-sidebar.is-open { + transform: translateX(0); + } + + .ws-sidebar-overlay.is-open { + display: block; + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.38); + z-index: 5; + } + + .ws-mobile-bar { + display: flex; + } + + .ws-panel { + padding: 1rem; + } + + .ws-stat-grid, + .ws-detail-grid { + grid-template-columns: 1fr; + } + + .ws-section-head, + .ws-panel-header { + align-items: flex-start; + flex-direction: column; + } +} \ No newline at end of file diff --git a/site/info/uce-starter/themes/common/fontawesome/LICENSE.txt b/site/info/uce-starter/themes/common/fontawesome/LICENSE.txt new file mode 100755 index 0000000..f31bef9 --- /dev/null +++ b/site/info/uce-starter/themes/common/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/site/info/uce-starter/themes/common/fontawesome/css/all.min.css b/site/info/uce-starter/themes/common/fontawesome/css/all.min.css new file mode 100755 index 0000000..0f922d8 --- /dev/null +++ b/site/info/uce-starter/themes/common/fontawesome/css/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/site/info/uce-starter/themes/common/fontawesome/css/fontawesome.min.css b/site/info/uce-starter/themes/common/fontawesome/css/fontawesome.min.css new file mode 100755 index 0000000..ab52c55 --- /dev/null +++ b/site/info/uce-starter/themes/common/fontawesome/css/fontawesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.10.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-wizard:before{content:"\f6e8"}.fa-haykal:before{content:"\f666"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot new file mode 100755 index 0000000..30a2784 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.eot differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg new file mode 100755 index 0000000..47bb690 --- /dev/null +++ b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.svg @@ -0,0 +1,3451 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf new file mode 100755 index 0000000..a7ab4d4 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff new file mode 100755 index 0000000..ec5b613 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100755 index 0000000..df11cea Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot new file mode 100755 index 0000000..b5440c9 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.eot differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg new file mode 100755 index 0000000..5ec81a8 --- /dev/null +++ b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:22 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf new file mode 100755 index 0000000..87693a8 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff new file mode 100755 index 0000000..917bf73 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100755 index 0000000..0f10115 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot new file mode 100755 index 0000000..305fc64 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.eot differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg new file mode 100755 index 0000000..eb47f6a --- /dev/null +++ b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,4649 @@ + + + + + +Created by FontForge 20190112 at Mon Jul 29 09:54:21 2019 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf new file mode 100755 index 0000000..8fb4d53 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff new file mode 100755 index 0000000..69f4474 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff differ diff --git a/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100755 index 0000000..20e4ce2 Binary files /dev/null and b/site/info/uce-starter/themes/common/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-bold-italic.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-bold-italic.ttf new file mode 100755 index 0000000..049965e Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-bold-italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-bold.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-bold.ttf new file mode 100755 index 0000000..3c92e2e Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-bold.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-italic.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-italic.ttf new file mode 100755 index 0000000..01ec3c5 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf new file mode 100755 index 0000000..ec2a812 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-bold-italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf new file mode 100755 index 0000000..fb26114 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-bold.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-mono-italic.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-italic.ttf new file mode 100755 index 0000000..af20348 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf new file mode 100755 index 0000000..aaac636 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-mono-regular.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/b612/b612-regular.ttf b/site/info/uce-starter/themes/common/fonts/b612/b612-regular.ttf new file mode 100755 index 0000000..c685a23 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/b612/b612-regular.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf new file mode 100755 index 0000000..4c0ee56 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Black.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf new file mode 100755 index 0000000..fa51576 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BlackItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf new file mode 100755 index 0000000..3cb9269 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Bold.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf new file mode 100755 index 0000000..010ed45 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-BoldItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf new file mode 100755 index 0000000..8550ed9 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLight.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf new file mode 100755 index 0000000..2e93072 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ExtraLightItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf new file mode 100755 index 0000000..592caec Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf new file mode 100755 index 0000000..1f42361 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Light.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf new file mode 100755 index 0000000..ff1769f Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-LightItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf new file mode 100755 index 0000000..dcd2c27 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Medium.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf new file mode 100755 index 0000000..18da8c6 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-MediumItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf new file mode 100755 index 0000000..0af4e38 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Regular.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf new file mode 100755 index 0000000..09cc053 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-Thin.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf new file mode 100755 index 0000000..e25a540 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/input-sans/InputSansCondensed-ThinItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/press-start-2p/OFL.txt b/site/info/uce-starter/themes/common/fonts/press-start-2p/OFL.txt new file mode 100644 index 0000000..22796df --- /dev/null +++ b/site/info/uce-starter/themes/common/fonts/press-start-2p/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/site/info/uce-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf b/site/info/uce-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/press-start-2p/press-start-2p-regular.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Black.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Black.ttf new file mode 100755 index 0000000..689fe5c Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Black.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100755 index 0000000..0b4e0ee Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Bold.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Bold.ttf new file mode 100755 index 0000000..d3f01ad Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Bold.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100755 index 0000000..41cc1e7 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Italic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Italic.ttf new file mode 100755 index 0000000..6a1cee5 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Light.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Light.ttf new file mode 100755 index 0000000..219063a Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Light.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf new file mode 100755 index 0000000..0e81e87 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Medium.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Medium.ttf new file mode 100755 index 0000000..1a7f3b0 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Medium.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100755 index 0000000..0030295 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Regular.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Regular.ttf new file mode 100755 index 0000000..2c97eea Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Regular.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Thin.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Thin.ttf new file mode 100755 index 0000000..b74a4fd Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-Thin.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100755 index 0000000..dd0ddb8 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf new file mode 100755 index 0000000..fc28868 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Bold.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf new file mode 100755 index 0000000..e1a648f Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-BoldItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf new file mode 100755 index 0000000..97ff9f1 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Italic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf new file mode 100755 index 0000000..2dae31e Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Light.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf new file mode 100755 index 0000000..da108d3 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-LightItalic.ttf differ diff --git a/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf new file mode 100755 index 0000000..c2304c1 Binary files /dev/null and b/site/info/uce-starter/themes/common/fonts/roboto/RobotoCondensed-Regular.ttf differ diff --git a/site/info/uce-starter/themes/common/page.blank.uce b/site/info/uce-starter/themes/common/page.blank.uce new file mode 100644 index 0000000..40d48f7 --- /dev/null +++ b/site/info/uce-starter/themes/common/page.blank.uce @@ -0,0 +1,7 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + print(starter_page_main_html(context)); +} diff --git a/site/info/uce-starter/themes/common/page.json.uce b/site/info/uce-starter/themes/common/page.json.uce new file mode 100644 index 0000000..975a1a8 --- /dev/null +++ b/site/info/uce-starter/themes/common/page.json.uce @@ -0,0 +1,14 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + context.header["Content-Type"] = "application/json"; + context.header["Cache-Control"] = "no-cache, no-store, must-revalidate"; + if(context.call["json"].get_type_name() == "array") + print(json_encode(context.call["json"])); + else if(context.var["starter"]["json"].get_type_name() == "array") + print(json_encode(context.var["starter"]["json"])); + else + print(starter_page_main_html(context)); +} diff --git a/site/info/uce-starter/themes/dark/css/style.css b/site/info/uce-starter/themes/dark/css/style.css new file mode 100755 index 0000000..0b45533 --- /dev/null +++ b/site/info/uce-starter/themes/dark/css/style.css @@ -0,0 +1,516 @@ +:root { + /* Dark theme color palette */ + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --bg-gradient: linear-gradient(135deg, #1e3a8a 0%, #3730a3 100%); + --surface: #1e293b; + --surface-elevated: #334155; + --surface-hover: #475569; + --primary: #60a5fa; + --primary-dark: #3b82f6; + --primary-light: #93c5fd; + --secondary: #a78bfa; + --accent: #22d3ee; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --border: #334155; + --border-hover: #475569; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + + /* Dark theme specific variables */ + --glass-bg: rgba(30, 41, 59, 0.8); + --glass-border: rgba(148, 163, 184, 0.1); + --overlay: rgba(0, 0, 0, 0.6); + --success: #10b981; + --success-bg: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.3); + --success-text: #10b981; + --warning: #f59e0b; + --warning-bg: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.3); + --warning-text: #f59e0b; + --error: #ef4444; + --error-bg: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.3); + --error-text: #ef4444; + --info: #3b82f6; + --info-bg: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.3); + --info-text: #3b82f6; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Links */ +a { + color: var(--primary); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--primary-light); + text-decoration: underline; +} + +/* Modern Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +nav > a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +nav.nav-scrolled { + background: rgba(30, 41, 59, 0.95) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} + +h1::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: var(--bg-gradient); + border-radius: 3px; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px var(--success-bg); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px var(--error-bg); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #8b5cf6; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: var(--bg-color); +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/info/uce-starter/themes/dark/icon.png b/site/info/uce-starter/themes/dark/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/info/uce-starter/themes/dark/icon.png differ diff --git a/site/info/uce-starter/themes/dark/page.html.uce b/site/info/uce-starter/themes/dark/page.html.uce new file mode 100644 index 0000000..8645bb0 --- /dev/null +++ b/site/info/uce-starter/themes/dark/page.html.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > + + +
> + +
+ + + + +} diff --git a/site/info/uce-starter/themes/light/css/style.css b/site/info/uce-starter/themes/light/css/style.css new file mode 100755 index 0000000..b872b44 --- /dev/null +++ b/site/info/uce-starter/themes/light/css/style.css @@ -0,0 +1,540 @@ +:root { + /* Light theme color palette */ + --bg-color: #f8fafc; + --bg-secondary: #ffffff; + --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --surface: #ffffff; + --surface-elevated: #ffffff; + --surface-hover: #f1f5f9; + --primary: #3b82f6; + --primary-dark: #1d4ed8; + --primary-light: #93c5fd; + --secondary: #8b5cf6; + --accent: #06b6d4; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --border: #e2e8f0; + --border-hover: #cbd5e1; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + + /* Light theme specific variables */ + --glass-bg: rgba(255, 255, 255, 0.95); + --glass-border: rgba(0, 0, 0, 0.1); + --overlay: rgba(0, 0, 0, 0.3); + --success: #10b981; + --success-bg: #dcfce7; + --success-border: #16a34a; + --success-text: #065f46; + --warning: #f59e0b; + --warning-bg: #fef3c7; + --warning-border: #d97706; + --warning-text: #92400e; + --error: #ef4444; + --error-bg: #fee2e2; + --error-border: #dc2626; + --error-text: #991b1b; + --info: #3b82f6; + --info-bg: #dbeafe; + --info-border: #2563eb; + --info-text: #1e40af; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* Links */ +a { + color: var(--primary); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--primary-dark); + text-decoration: underline; +} + +/* Modern Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} + +nav > a, +.nav-menu a { + display: inline-flex; + align-items: center; + padding: 0.625rem 1rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + position: relative; + border-radius: var(--radius); + margin: 0.35rem 0.15rem; +} + +nav > a:first-child, +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +nav > a:hover, +.nav-menu a:hover { + background: var(--surface-hover); + color: var(--primary); + text-decoration: none; +} + +nav > a::after, +.nav-menu a::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: var(--primary); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +nav > a:hover::after, +.nav-menu a:hover::after { + width: 60%; +} + +/* Account area inside the nav */ +.nav-menu { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + margin-right: 0.5rem; + font-size: 0.95rem; +} + +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} + +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary-dark); +} + +/* Navigation scroll state */ +nav.nav-scrolled { + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(20px) !important; + -webkit-backdrop-filter: blur(20px) !important; + box-shadow: var(--shadow-md) !important; +} + +/* Main Content Area */ +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} + +h1 { + font-size: 2.5rem; + background: var(--surface); + padding: 1.75rem 2rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--border); + position: relative; + overflow: hidden; +} + +h1::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--bg-gradient); +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +/* Modern Cards/Blocks */ +#content > div:not(.ws-app), .block, .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +/* Modern Forms */ +form { + max-width: 600px; + margin: 0 auto; +} + +form > div { + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + gap: 0.5rem; +} + +form > div > label { + font-weight: 500; + color: var(--text-secondary); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all 0.2s ease; +} + +form > div > input, form > div > textarea, form > div > select { + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s ease; +} + +form > div > input:focus, form > div > textarea:focus, form > div > select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Enhanced form validation states */ +form > div > input.valid, form > div > textarea.valid { + border-color: var(--success); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); +} + +form > div > input.invalid, form > div > textarea.invalid { + border-color: var(--error); + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +/* Floating label effect */ +form > div.focused > label { + transform: translateY(-0.5rem) scale(0.85); + color: var(--primary); +} + +/* Modern Buttons */ +button, .btn, form > div > input[type=submit], input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + font-weight: 500; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + position: relative; + overflow: hidden; +} + +button:hover, .btn:hover, form > div > input[type=submit]:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +button:active, .btn:active { + transform: translateY(0); +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); +} + +.btn-secondary:hover { + background: #7c3aed; +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-outline:hover { + background: var(--primary); + color: white; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-color); + outline: none; + border-radius: 3px; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: var(--shadow-sm); +} + +/* Utility Classes */ +.banner { + padding: 1rem 1.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; +} + +.banner.success { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success); +} + +.banner.warning { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning); +} + +.banner.error { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error); +} + +.error { + color: var(--error); +} + +/* Footer */ +footer { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; + margin-top: 4rem; + border-top: 1px solid var(--border); + background: var(--surface); +} + +/* Mobile Responsive Design */ +@media (max-width: 768px) { + nav { + padding: 0 0.5rem; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + } + + nav::-webkit-scrollbar { + display: none; + } + + nav > a { + padding: 0.75rem 1rem; + margin: 0.25rem 0.125rem; + font-size: 0.875rem; + display: inline-flex; + flex-shrink: 0; + } + + #content { + margin-top: 4rem; + padding: 1rem 0.5rem; + } + + h1 { + font-size: 1.875rem; + padding: 1.5rem; + } + + #content > div, .block, .card { + padding: 1.5rem; + margin-bottom: 1rem; + } + + form > div { + margin-bottom: 1rem; + } + + button, .btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav > a { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + #content { + padding: 1rem 0.25rem; + } + + h1 { + font-size: 1.5rem; + padding: 1rem; + } + + #content > div, .block, .card { + padding: 1rem; + border-radius: var(--radius); + } +} + +/* Smooth scrolling and improved animations */ +* { + scroll-behavior: smooth; +} + +/* Loading animation for better perceived performance */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading { + animation: pulse 2s ease-in-out infinite; +} + +/* Focus states for accessibility */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Print styles */ +@media print { + nav, footer, .cta-section { + display: none; + } + + #content { + margin-top: 0; + } + + * { + background: white !important; + color: black !important; + box-shadow: none !important; + } +} diff --git a/site/info/uce-starter/themes/light/icon.png b/site/info/uce-starter/themes/light/icon.png new file mode 100755 index 0000000..8a42581 Binary files /dev/null and b/site/info/uce-starter/themes/light/icon.png differ diff --git a/site/info/uce-starter/themes/light/img/favicon.png b/site/info/uce-starter/themes/light/img/favicon.png new file mode 100755 index 0000000..6653d41 Binary files /dev/null and b/site/info/uce-starter/themes/light/img/favicon.png differ diff --git a/site/info/uce-starter/themes/light/page.html.uce b/site/info/uce-starter/themes/light/page.html.uce new file mode 100644 index 0000000..495d3e0 --- /dev/null +++ b/site/info/uce-starter/themes/light/page.html.uce @@ -0,0 +1,28 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > + + +
> + +
+ + + + +} diff --git a/site/info/uce-starter/themes/localfirst/css/style.css b/site/info/uce-starter/themes/localfirst/css/style.css new file mode 100644 index 0000000..088ce45 --- /dev/null +++ b/site/info/uce-starter/themes/localfirst/css/style.css @@ -0,0 +1,382 @@ +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612'; + src: url('../../common/fonts/b612/b612-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'B612 Mono'; + src: url('../../common/fonts/b612/b612-mono-bold.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.28); } + 50% { opacity: 0.6; box-shadow: 0 0 4px var(--accent), 0 0 8px rgba(255,107,26,0.16); } +} + +:root { + --brand-cyan: #00b7f9; + --brand-cyan-soft: #58d2ff; + --brand-cyan-bright: #b6eeff; + --brand-cyan-deep: #0088bb; + --brand-orange: #ff6b1a; + --brand-orange-soft: #ff9f6a; + --brand-orange-bright: #ffd0b3; + --bg-color: #020913; + --bg-secondary: #07131f; + --surface: rgba(7, 19, 31, 0.9); + --surface-elevated: rgba(10, 28, 43, 0.96); + --surface-hover: rgba(0, 183, 249, 0.08); + --card: rgba(7, 19, 31, 0.84); + --text-primary: var(--brand-cyan-soft); + --text-secondary: rgba(139, 222, 255, 0.82); + --text-muted: rgba(101, 161, 188, 0.78); + --primary: var(--brand-cyan); + --primary-light: rgba(0, 183, 249, 0.18); + --primary-dark: var(--brand-cyan-deep); + --secondary: var(--brand-cyan-soft); + --accent: var(--brand-orange); + --success: #34d399; + --success-bg: rgba(52, 211, 153, 0.1); + --success-border: rgba(52, 211, 153, 0.3); + --success-text: #7ef0bb; + --warning: var(--brand-orange); + --warning-bg: rgba(255, 107, 26, 0.12); + --warning-border: rgba(255, 107, 26, 0.32); + --warning-text: #ffc29f; + --error: #fb7185; + --error-bg: rgba(251, 113, 133, 0.12); + --error-border: rgba(251, 113, 133, 0.3); + --error-text: #fecdd3; + --info: var(--brand-cyan); + --info-bg: rgba(0, 183, 249, 0.1); + --info-border: rgba(0, 183, 249, 0.28); + --info-text: var(--brand-cyan-bright); + --border: rgba(67, 149, 182, 0.22); + --border-hover: rgba(98, 205, 246, 0.36); + --shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.18); + --shadow-md: 0 14px 32px rgba(0, 0, 0, 0.24); + --shadow-lg: 0 18px 38px rgba(0, 0, 0, 0.28); + --shadow-xl: 0 22px 48px rgba(0, 0, 0, 0.34); + --radius: 10px; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 20px; + --font-sans: 'B612', 'Segoe UI', sans-serif; + --font-mono: 'B612 Mono', ui-monospace, monospace; +} + +* { box-sizing: border-box; margin: 0; } +html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +body { + font-family: var(--font-sans); + font-size: 14px; + background: var(--bg-color); + color: var(--text-primary); + min-height: 100vh; +} +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 10% 0%, rgba(0,183,249,0.12) 0%, transparent 60%), + radial-gradient(ellipse 60% 50% at 90% 100%, rgba(255,107,26,0.08) 0%, transparent 60%); + pointer-events: none; + z-index: 0; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +body.admin-page { overflow: hidden; height: 100vh; } +.admin-shell { display: flex; height: 100vh; overflow: hidden; position: relative; z-index: 1; } +.admin-nav { + width: 210px; + flex-shrink: 0; + background: + linear-gradient(180deg, rgba(5, 16, 26, 0.98), rgba(2, 10, 18, 0.98)), + linear-gradient(180deg, rgba(0, 174, 240, 0.06), rgba(255, 107, 26, 0.04)); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0 0 16px; +} +.admin-nav-header { + border-bottom: 1px solid var(--border); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 9px; + padding: 10px 10px 12px; +} +.admin-nav-title { display: block; width: 100%; } +.admin-nav-logo-img { display: block; width: 100%; height: auto; } +.admin-nav-item { + display: block; + padding: 9px 16px 9px 14px; + text-decoration: none; + color: var(--text-secondary); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; + border-left: 3px solid transparent; +} +.admin-nav-item:hover { + color: var(--brand-cyan-bright); + background: linear-gradient(90deg, rgba(0, 174, 240, 0.12), rgba(255, 107, 26, 0.03)); + text-decoration: none; +} +.admin-nav-item.active { + color: var(--accent); + border-left-color: var(--accent); + background: linear-gradient(90deg, rgba(255, 107, 26, 0.16), rgba(255, 107, 26, 0.04)); + box-shadow: inset 0 0 0 1px rgba(255, 107, 26, 0.08); +} + +.admin-main { + flex: 1; + min-width: 0; + position: relative; + display: flex; + flex-direction: column; +} + +.admin-toolbar { + position: fixed; + top: 16px; + right: 20px; + z-index: 40; + display: flex; + justify-content: flex-end; + pointer-events: none; +} + +.admin-account-card { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(7, 19, 31, 0.78); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} +.admin-account-name { + color: var(--brand-cyan-bright); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} +.admin-account-links { + display: inline-flex; + gap: 10px; + align-items: center; +} +.admin-account-links a { + color: var(--accent); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} + +.admin-content { + flex: 1; + overflow-y: auto; + padding: 16px 20px 28px; + position: relative; + min-width: 0; +} + +h1 { + font-size: 18px; + font-weight: 600; + letter-spacing: 1.4px; + text-transform: uppercase; + color: var(--brand-cyan-bright); + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 14px; +} +h1::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background: var(--accent); + border-radius: 2px; + box-shadow: 0 0 8px var(--accent), 0 0 16px rgba(255,107,26,0.24); + animation: pulse-glow 2.5s ease-in-out infinite; +} +h2, h3, h4, h5, h6 { + color: var(--brand-cyan-bright); + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} +p { margin: 0 0 12px; color: var(--text-secondary); } + +.card, +.block, +.dashboard-panel, +#content > div:not(.ws-app):not(.demo-section) { + background: var(--card); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + position: relative; + transition: border-color 0.25s ease, box-shadow 0.25s ease; + margin-bottom: 14px; +} +.card::before, +.block::before, +.dashboard-panel::before, +#content > div:not(.ws-app):not(.demo-section)::before { + content: ''; + position: absolute; + top: 0; + left: 12px; + right: 12px; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.55; +} +.card:hover, +.block:hover, +.dashboard-panel:hover, +#content > div:not(.ws-app):not(.demo-section):hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-md); +} + +form { max-width: 760px; } +form > div { display: grid; gap: 6px; margin-bottom: 12px; } +label { + color: var(--text-muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; +} +input, select, textarea { + width: 100%; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(3, 7, 18, 0.55); + color: var(--brand-cyan-bright); + padding: 10px 12px; + font: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--border-hover); + box-shadow: 0 0 0 3px rgba(0, 183, 249, 0.12); +} +button, .btn, input[type="submit"] { + border: 1px solid var(--border); + background: rgba(255, 107, 26, 0.12); + color: var(--brand-orange-bright); + border-radius: var(--radius); + padding: 10px 14px; + font: inherit; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: rgba(255, 107, 26, 0.18); + border-color: rgba(255, 107, 26, 0.42); + text-decoration: none; +} +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: var(--radius); + padding: 10px 12px; + border: 1px solid var(--border); + background: rgba(0, 183, 249, 0.08); + color: var(--text-secondary); + margin-bottom: 12px; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: rgba(3, 7, 18, 0.42); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + background: rgba(0, 183, 249, 0.08); +} +tr:last-child td { border-bottom: none; } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 12px; + background: rgba(3, 7, 18, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +@media (max-width: 980px) { + .admin-shell { flex-direction: column; height: auto; min-height: 100vh; } + .admin-nav { + width: 100%; + flex-direction: row; + overflow-x: auto; + padding-bottom: 0; + } + .admin-nav-header { min-width: 180px; margin-bottom: 0; border-bottom: 0; border-right: 1px solid var(--border); } + .admin-nav-item { white-space: nowrap; border-left: 0; border-bottom: 3px solid transparent; } + .admin-nav-item.active { border-bottom-color: var(--accent); border-left-color: transparent; } + .admin-toolbar { position: static; justify-content: flex-start; margin: 16px 20px 0; } + .admin-content { padding-top: 12px; } +} \ No newline at end of file diff --git a/site/info/uce-starter/themes/localfirst/icon.png b/site/info/uce-starter/themes/localfirst/icon.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/info/uce-starter/themes/localfirst/icon.png differ diff --git a/site/info/uce-starter/themes/localfirst/img/local_first_logo.png b/site/info/uce-starter/themes/localfirst/img/local_first_logo.png new file mode 100644 index 0000000..22994d8 Binary files /dev/null and b/site/info/uce-starter/themes/localfirst/img/local_first_logo.png differ diff --git a/site/info/uce-starter/themes/localfirst/page.html.uce b/site/info/uce-starter/themes/localfirst/page.html.uce new file mode 100644 index 0000000..5a7771c --- /dev/null +++ b/site/info/uce-starter/themes/localfirst/page.html.uce @@ -0,0 +1,54 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + String current_path = context.var["starter"]["route"]["l_path"].to_string(); + + DTree global_props; + global_props["cookie_consent"].set_bool(false); + + DTree account_props; + account_props["wrapper_class"] = "admin-account-card"; + account_props["links_wrapper_class"] = "admin-account-links"; + account_props["name_class"] = "admin-account-name"; + + <> + + "> + + + + "> + +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + +} diff --git a/site/info/uce-starter/themes/portal-dark/css/style.css b/site/info/uce-starter/themes/portal-dark/css/style.css new file mode 100644 index 0000000..90001c3 --- /dev/null +++ b/site/info/uce-starter/themes/portal-dark/css/style.css @@ -0,0 +1,313 @@ +:root { + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --surface: #1e293b; + --surface-elevated: #334155; + --surface-hover: #475569; + --primary: #60a5fa; + --primary-dark: #3b82f6; + --primary-light: #93c5fd; + --secondary: #a78bfa; + --accent: #22d3ee; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --border: #334155; + --border-hover: #475569; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); + --radius: 8px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-sm: 4px; + --glass-bg: rgba(30, 41, 59, 0.8); + --success: #10b981; + --success-bg: rgba(16, 185, 129, 0.1); + --success-border: rgba(16, 185, 129, 0.3); + --success-text: #10b981; + --warning: #f59e0b; + --warning-bg: rgba(245, 158, 11, 0.1); + --warning-border: rgba(245, 158, 11, 0.3); + --warning-text: #f59e0b; + --error: #ef4444; + --error-bg: rgba(239, 68, 68, 0.1); + --error-border: rgba(239, 68, 68, 0.3); + --error-text: #ef4444; + --info: #3b82f6; + --info-bg: rgba(59, 130, 246, 0.1); + --info-border: rgba(59, 130, 246, 0.3); + --info-text: #3b82f6; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'SFMono-Regular', Consolas, monospace; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; } +body { + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.6; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +a { color: var(--primary); text-decoration: none; transition: all 0.2s ease; } +a:hover { color: var(--primary-light); text-decoration: underline; } + +::selection { + background: rgba(96, 165, 250, 0.3); + color: var(--text-primary); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 80% 50% at 20% -10%, rgba(96, 165, 250, 0.05) 0%, transparent 60%), + radial-gradient(ellipse 60% 40% at 85% 110%, rgba(34, 211, 238, 0.04) 0%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 1rem; + display: flex; + align-items: center; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; +} +nav::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary), var(--accent), transparent); + opacity: 0.3; +} + +.nav-menu { display: flex; align-items: center; gap: 0.25rem; } +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; +} +.nav-menu > a:first-child { + font-weight: 700; + color: var(--primary); + padding: 1rem 1.5rem; + margin: 0.5rem 0.25rem; +} +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--primary); +} +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} +.nav-account .account-name { + color: var(--text-secondary); + font-weight: 500; + font-size: 0.95rem; + margin-right: 0.5rem; +} +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid var(--border); +} +.nav-account .nav-avatar { width: 30px; height: 30px; } +.profile-avatar { width: 96px; height: 96px; } + +#content { + margin-top: 5rem; + min-height: calc(100vh - 10rem); + padding: 2rem 1rem; + max-width: 1200px; + margin-left: auto; + margin-right: auto; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; + color: var(--text-primary); +} +h1 { + font-size: 2rem; + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 1.25rem; +} +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 3px; +} +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } + +#content > div:not(.ws-app), .block, .card, .dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.75rem; + margin-bottom: 1.5rem; + box-shadow: var(--shadow-sm); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +#content > div:not(.ws-app):hover, .block:hover, .card:hover, .dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: var(--border-hover); +} + +form { max-width: 600px; margin: 0 auto; } +form > div { display: flex; flex-direction: column; margin-bottom: 1.5rem; gap: 0.5rem; } +label { font-weight: 500; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; } +input, select, textarea { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: var(--radius); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; +} +textarea { min-height: 120px; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); + background: var(--surface); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: var(--bg-color); + border: none; + border-radius: var(--radius); + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; +} +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); + text-decoration: none; +} + +.banner { + border-radius: var(--radius); + padding: 0.8rem 1rem; + border: 1px solid var(--border); + margin-bottom: 1rem; + background: rgba(255,255,255,0.03); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--border); + background: rgba(255,255,255,0.02); + border-radius: var(--radius); + overflow: hidden; +} +th, td { padding: 0.8rem 0.9rem; border-bottom: 1px solid var(--border); text-align: left; } +th { background: var(--surface-elevated); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(255,255,255,0.02); } +code, pre { font-family: var(--font-mono); } +pre { + padding: 1rem; + background: rgba(15, 23, 42, 0.55); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + padding: 1.25rem 1rem; + color: var(--text-muted); + font-size: 0.875rem; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-secondary); + outline: none; + border: none; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + cursor: pointer; + border-radius: 50%; + border: none; + box-shadow: 0 0 6px rgba(96, 165, 250, 0.3); +} + +@media (max-width: 860px) { + nav { flex-wrap: wrap; padding-bottom: 0.6rem; } + .nav-account { width: 100%; margin-left: 0; justify-content: flex-start; } + #content { margin-top: 7.5rem; padding: 1rem 0.75rem; } + #content > div:not(.ws-app), .block, .card, .dashboard-panel { padding: 1.25rem; margin-bottom: 1rem; } + h1 { font-size: 2rem; padding: 1.25rem; } +} \ No newline at end of file diff --git a/site/info/uce-starter/themes/portal-dark/icon.png b/site/info/uce-starter/themes/portal-dark/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/info/uce-starter/themes/portal-dark/icon.png differ diff --git a/site/info/uce-starter/themes/portal-dark/page.html.uce b/site/info/uce-starter/themes/portal-dark/page.html.uce new file mode 100644 index 0000000..3f353a7 --- /dev/null +++ b/site/info/uce-starter/themes/portal-dark/page.html.uce @@ -0,0 +1,31 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + "> + + +
> + +
+ + + + +} diff --git a/site/info/uce-starter/themes/portal-light/css/style.css b/site/info/uce-starter/themes/portal-light/css/style.css new file mode 100644 index 0000000..141b15f --- /dev/null +++ b/site/info/uce-starter/themes/portal-light/css/style.css @@ -0,0 +1,392 @@ +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-italic.ttf") format("truetype"); + font-style: italic; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612"; + src: url("../../common/fonts/b612/b612-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "B612 Mono"; + src: url("../../common/fonts/b612/b612-mono-bold.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +:root { + --kg-blue: #0f68ad; + --kg-blue-dark: #0b4f85; + --kg-blue-light: #2d86ca; + --kg-bg: #e9e9ea; + --kg-surface: #ffffff; + --kg-border: rgba(0, 0, 0, 0.2); + --kg-text: #121417; + --kg-muted: #6c737b; + --kg-orange: #f08a00; + --kg-success: #149b2e; + --kg-danger: #e11818; + --topbar-height: 56px; + --bg-color: var(--kg-bg); + --bg-secondary: #ffffff; + --surface: var(--kg-surface); + --surface-elevated: #ffffff; + --surface-hover: #e8eef3; + --primary: var(--kg-blue); + --primary-dark: var(--kg-blue-dark); + --primary-light: var(--kg-blue-light); + --secondary: #596572; + --accent: var(--kg-orange); + --text-primary: var(--kg-text); + --text-secondary: #4b5560; + --text-muted: var(--kg-muted); + --border: var(--kg-border); + --border-hover: rgba(15, 104, 173, 0.3); + --success: var(--kg-success); + --success-bg: #e7f5ea; + --success-border: #86c88f; + --success-text: #0f6e22; + --warning: #c97a14; + --warning-bg: #fff4e4; + --warning-border: #f0c07b; + --warning-text: #92550a; + --error: var(--kg-danger); + --error-bg: #ffe7e7; + --error-border: #ea9999; + --error-text: #b80f0f; + --info: var(--primary); + --info-bg: #e8f2ff; + --info-border: #b9d4f3; + --info-text: #0b4f85; + --shadow-sm: 0 1px 2px rgba(18, 20, 23, 0.06); + --shadow-md: 0 8px 22px rgba(18, 20, 23, 0.06); + --shadow-lg: 0 16px 34px rgba(18, 20, 23, 0.1); + --shadow-xl: 0 20px 40px rgba(18, 20, 23, 0.14); + --radius: 8px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --font-sans: "B612", "Segoe UI", sans-serif; + --font-mono: "B612 Mono", "SFMono-Regular", Consolas, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body { min-height: 100%; } +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.4; + color: var(--text-primary); + background: var(--bg-color); +} +a { color: var(--primary-dark); text-decoration: none; } +a:hover { text-decoration: underline; } + +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--topbar-height); + display: flex; + align-items: stretch; + background: var(--primary); + border-bottom: 1px solid var(--primary-dark); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); + z-index: 60; +} + +.nav-menu { + display: flex; + align-items: stretch; + overflow-x: auto; + white-space: nowrap; + flex: 1; +} + +.nav-menu > a { + display: inline-flex; + align-items: center; + padding: 0 0.9rem; + color: #fff; + border-right: 1px solid rgba(255, 255, 255, 0.16); + font-size: 14px; + font-weight: 700; + text-decoration: none; + background: transparent; +} + +.nav-menu > a:first-child { + font-size: 18px; + min-width: 250px; + background: rgba(0, 0, 0, 0.08); +} + +.nav-menu > a:hover, +.nav-menu > a:focus { + background: var(--primary-dark); + text-decoration: none; +} + +.nav-account { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0 0.7rem; + border-left: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.12); + flex-shrink: 0; +} + +.nav-account .account-name { + color: #d8f8df; + font-size: 14px; + font-weight: 700; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 999px; + object-fit: cover; + border: 1px solid rgba(255, 255, 255, 0.4); +} + +.nav-account .nav-avatar { + width: 30px; + height: 30px; +} + +.profile-avatar { + width: 96px; + height: 96px; + border-color: var(--border); +} + +.nav-account a { + color: #fff; + font-size: 14px; + font-weight: 700; + padding: 0.22rem 0.5rem; + border-radius: 3px; +} + +.nav-account a:hover { + background: rgba(255, 255, 255, 0.16); + text-decoration: none; +} + +#content { + margin-top: var(--topbar-height); + padding: 1rem; + min-height: calc(100vh - var(--topbar-height)); +} + +#content > div:not(.ws-app), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: 1.25rem; + margin-bottom: 1rem; + transition: box-shadow 0.2s ease, border-color 0.2s ease; +} + +#content > div:not(.ws-app):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + box-shadow: var(--shadow-md); + border-color: rgba(15, 104, 173, 0.2); +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + color: #252b31; + margin-bottom: 0.7rem; +} +h1 { font-size: 22px; padding-bottom: 0.5rem; border-bottom: 2px solid #d2d3d7; position: relative; } +h1::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 50px; + height: 2px; + background: var(--primary); + border-radius: 2px; +} +h2 { font-size: 18px; } +h3 { font-size: 16px; } +h4, h5, h6 { font-size: 14px; } + +p { margin: 0.4rem 0 0.85rem; } + +form { max-width: 780px; } +form > div { margin-bottom: 0.8rem; } +label { + display: block; + font-weight: 700; + font-size: 14px; + color: #3f464f; + margin-bottom: 0.3rem; +} +input, select, textarea { + width: 100%; + min-height: 36px; + padding: 0 0.7rem; + border: 1px solid #a8aaaf; + border-radius: 4px; + background: #fff; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} +textarea { min-height: 100px; padding: 0.5rem 0.7rem; } +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary-light); + box-shadow: 0 0 0 1px var(--primary-light); +} +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + padding: 0 0.9rem; + border: 1px solid #7f8084; + border-radius: 4px; + background: #f7f7f7; + color: #1a1e23; + font-size: 14px; + font-weight: 700; + cursor: pointer; + font-family: inherit; +} +button:hover, .btn:hover, input[type="submit"]:hover { background: #ececef; text-decoration: none; } +button:active, .btn:active { transform: translateY(1px); } + +.banner { + border-radius: 4px; + padding: 0.65rem 0.8rem; + border: 1px solid #b4b5ba; + margin-bottom: 0.8rem; + background: #fafafa; +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +table { + width: 100%; + border-collapse: collapse; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +th, td { + padding: 0.7rem 0.8rem; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} +th { background: #e7e7e8; font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 0, 0, 0.02); } + +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.9rem; + background: #fff; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: auto; +} + +footer { + border-top: 1px solid var(--border); + background: #ffffff; + color: var(--text-muted); + padding: 1rem 1rem 1.5rem; + font-size: 13px; +} +.footer-inner { max-width: 1200px; margin: 0 auto; } + +input[type="range"] { + height: 6px; + padding: 0; + min-height: auto; + border: none; + background: #d4d5d9; + border-radius: 999px; + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +::selection { + background: rgba(15, 104, 173, 0.2); + color: var(--text-primary); +} + +@media (max-width: 860px) { + nav { + height: auto; + flex-direction: column; + } + .nav-menu { + flex-wrap: wrap; + } + .nav-menu > a:first-child { + min-width: 0; + } + .nav-account { + justify-content: flex-start; + padding: 0.6rem 0.7rem; + } + #content { + margin-top: 104px; + padding: 0.75rem; + } +} \ No newline at end of file diff --git a/site/info/uce-starter/themes/portal-light/icon.png b/site/info/uce-starter/themes/portal-light/icon.png new file mode 100644 index 0000000..8a42581 Binary files /dev/null and b/site/info/uce-starter/themes/portal-light/icon.png differ diff --git a/site/info/uce-starter/themes/portal-light/page.html.uce b/site/info/uce-starter/themes/portal-light/page.html.uce new file mode 100644 index 0000000..d125df0 --- /dev/null +++ b/site/info/uce-starter/themes/portal-light/page.html.uce @@ -0,0 +1,29 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + footer_props["inner_class"] = "footer-inner"; + + <> + + "> + + + + "> + + +
> + +
+ + + + +} diff --git a/site/info/uce-starter/themes/retro-gaming/css/style.css b/site/info/uce-starter/themes/retro-gaming/css/style.css new file mode 100644 index 0000000..f187fa6 --- /dev/null +++ b/site/info/uce-starter/themes/retro-gaming/css/style.css @@ -0,0 +1,523 @@ +/* ──────────────────────────────────────────────────────── + RETRO GAMING THEME + CRT scanlines · pixel font · neon glow · 8-bit palette + ──────────────────────────────────────────────────────── */ + +@font-face { + font-family: 'Press Start 2P'; + src: url('../../common/fonts/press-start-2p/press-start-2p-regular.ttf') format('truetype'); + font-style: normal; + font-weight: 400; + font-display: swap; +} + +/* ── tokens ─────────────────────────────────────────── */ +:root { + /* background */ + --bg-color: #0a0a1a; + --bg-secondary: #10102a; + --surface: #12122e; + --surface-elevated: #1a1a3e; + --surface-hover: #22224e; + + /* neon palette */ + --primary: #00ff88; + --primary-dark: #00cc6a; + --primary-light: #66ffbb; + --secondary: #ff00ff; + --accent: #ffff00; + --text-primary: #e0e0ff; + --text-secondary: #a0a0cc; + --text-muted: #6868a0; + + /* borders, shadows */ + --border: #2a2a5a; + --border-hover: #3a3a7a; + --shadow-sm: 0 2px 0 #000; + --shadow-md: 0 4px 0 #000, 0 0 12px rgba(0, 255, 136, 0.08); + --shadow-lg: 0 6px 0 #000, 0 0 24px rgba(0, 255, 136, 0.12); + --shadow-xl: 0 8px 0 #000, 0 0 40px rgba(0, 255, 136, 0.16); + + /* semantic */ + --success: #00ff88; + --success-bg: rgba(0, 255, 136, 0.1); + --success-border: rgba(0, 255, 136, 0.3); + --success-text: #00ff88; + --warning: #ffff00; + --warning-bg: rgba(255, 255, 0, 0.1); + --warning-border: rgba(255, 255, 0, 0.3); + --warning-text: #ffff00; + --error: #ff3366; + --error-bg: rgba(255, 51, 102, 0.1); + --error-border: rgba(255, 51, 102, 0.3); + --error-text: #ff3366; + --info: #00ccff; + --info-bg: rgba(0, 204, 255, 0.1); + --info-border: rgba(0, 204, 255, 0.3); + --info-text: #00ccff; + + /* geometry */ + --radius: 0px; + --radius-sm: 0px; + --radius-md: 0px; + --radius-lg: 2px; + --radius-xl: 4px; + + /* typography */ + --font-sans: 'Press Start 2P', monospace; + --font-mono: 'Press Start 2P', monospace; +} + +/* ── reset ──────────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 16px; scroll-behavior: smooth; image-rendering: pixelated; } + +body { + font-family: var(--font-sans); + font-size: 10px; + line-height: 2; + color: var(--text-primary); + background: var(--bg-color); + -webkit-font-smoothing: none; + -moz-osx-font-smoothing: unset; +} + +/* ── CRT scanline overlay ───────────────────────────── */ +body::before { + content: ''; + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.15) 2px, + rgba(0, 0, 0, 0.15) 4px + ); + pointer-events: none; + z-index: 9000; +} + +/* subtle vignette */ +body::after { + content: ''; + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.5) 100%); + pointer-events: none; + z-index: 8999; +} + +::selection { + background: var(--primary); + color: var(--bg-color); +} + +/* ── links ──────────────────────────────────────────── */ +a { + color: var(--primary); + text-decoration: none; + transition: color 0.1s step-end; +} +a:hover { + color: var(--accent); + text-decoration: none; + text-shadow: 0 0 6px var(--accent); +} + +/* ── navigation ─────────────────────────────────────── */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--bg-secondary); + border-bottom: 3px solid var(--primary); + padding: 0 0.5rem; + display: flex; + align-items: center; + box-shadow: 0 3px 0 #000, 0 6px 20px rgba(0, 255, 136, 0.15); +} + +.nav-menu { + display: flex; + align-items: center; + gap: 0; + flex: 1; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; +} +.nav-menu::-webkit-scrollbar { display: none; } + +.nav-menu a, +.nav-account a { + display: inline-flex; + align-items: center; + padding: 0.75rem 0.8rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 8px; + letter-spacing: 0.5px; + text-transform: uppercase; + border-right: 1px solid var(--border); + transition: background 0.1s step-end, color 0.1s step-end; +} + +.nav-menu > a:first-child { + color: var(--primary); + text-shadow: 0 0 8px rgba(0, 255, 136, 0.5); + font-size: 10px; + padding: 0.75rem 1rem; + border-right: 2px solid var(--primary); +} + +.nav-menu a:hover, +.nav-account a:hover { + background: var(--surface-hover); + color: var(--accent); + text-shadow: 0 0 6px var(--accent); + text-decoration: none; +} + +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 0; + flex: 0 0 auto; + overflow: visible; +} +.nav-account a { + border-right: none; + border-left: 1px solid var(--border); +} + +.nav-account .account-name { + color: var(--primary); + font-size: 8px; + padding: 0 0.5rem; +} + +.nav-account .nav-avatar, +.profile-avatar { + border-radius: 0; + border: 2px solid var(--primary); + image-rendering: pixelated; +} +.nav-account .nav-avatar { width: 24px; height: 24px; } +.profile-avatar { width: 64px; height: 64px; } + +/* ── main content ───────────────────────────────────── */ +#content { + margin-top: 3.5rem; + min-height: calc(100vh - 7rem); + padding: 1.5rem 1rem; + max-width: 1100px; + margin-left: auto; + margin-right: auto; + position: relative; + z-index: 1; +} + +/* ── typography ─────────────────────────────────────── */ +h1, h2, h3, h4, h5, h6 { + font-weight: 400; + line-height: 2; + margin-bottom: 0.75rem; + color: var(--primary); + text-shadow: 0 0 10px rgba(0, 255, 136, 0.35); + text-transform: uppercase; +} + +h1 { + font-size: 16px; + padding: 0.75rem 0; + border-bottom: 3px double var(--primary); + margin-bottom: 1.25rem; + position: relative; +} +h1::before { + content: '►'; + margin-right: 0.5rem; + animation: blink-cursor 1s step-end infinite; +} +h2 { font-size: 12px; color: var(--info-text); text-shadow: 0 0 8px rgba(0, 204, 255, 0.3); } +h3 { font-size: 10px; color: var(--secondary); text-shadow: 0 0 8px rgba(255, 0, 255, 0.3); } +h4, h5, h6 { font-size: 10px; } + +p { margin: 0 0 0.75rem; } + +@keyframes blink-cursor { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* ── cards / blocks ─────────────────────────────────── */ +#content > div:not(.ws-app):not(.demo-section), +.block, +.card, +.dashboard-panel { + background: var(--surface); + border: 2px solid var(--border); + padding: 1rem; + margin-bottom: 1.25rem; + box-shadow: var(--shadow-md); + position: relative; + transition: border-color 0.1s step-end; +} + +#content > div:not(.ws-app):not(.demo-section)::before, +.block::before, +.card::before, +.dashboard-panel::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, var(--primary), var(--secondary), var(--accent)); +} + +#content > div:not(.ws-app):not(.demo-section):hover, +.block:hover, +.card:hover, +.dashboard-panel:hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-lg); +} + +/* ── forms ──────────────────────────────────────────── */ +form { max-width: 640px; } +form > div { display: grid; gap: 0.35rem; margin-bottom: 1rem; } + +label { + color: var(--accent); + font-size: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +input, select, textarea { + width: 100%; + padding: 0.6rem 0.7rem; + border: 2px solid var(--border); + background: var(--bg-color); + color: var(--primary); + font-family: var(--font-mono); + font-size: 10px; + caret-color: var(--primary); +} +textarea { min-height: 100px; } + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary), 0 0 12px rgba(0, 255, 136, 0.2); +} + +input::placeholder, textarea::placeholder { + color: var(--text-muted); + opacity: 1; +} + +/* ── buttons ────────────────────────────────────────── */ +button, .btn, input[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1.2rem; + border: 2px solid var(--primary); + background: transparent; + color: var(--primary); + font-family: var(--font-sans); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.1s step-end; + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--primary); +} + +button:hover, .btn:hover, input[type="submit"]:hover { + background: var(--primary); + color: var(--bg-color); + text-shadow: none; + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--primary); + transform: translate(2px, 2px); + text-decoration: none; +} + +button:active, .btn:active, input[type="submit"]:active { + box-shadow: none; + transform: translate(4px, 4px); +} + +.btn-secondary { + border-color: var(--secondary); + color: var(--secondary); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--secondary); +} +.btn-secondary:hover { + background: var(--secondary); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--secondary); +} + +.btn-outline { + border-color: var(--accent); + color: var(--accent); + box-shadow: 3px 3px 0 var(--bg-color), 4px 4px 0 var(--accent); +} +.btn-outline:hover { + background: var(--accent); + color: var(--bg-color); + box-shadow: 1px 1px 0 var(--bg-color), 2px 2px 0 var(--accent); +} + +/* ── range slider ───────────────────────────────────── */ +input[type="range"] { + height: 8px; + padding: 0; + border: 2px solid var(--border); + background: var(--bg-color); + -webkit-appearance: none; + appearance: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} +input[type="range"]::-moz-range-thumb { + width: 14px; + height: 14px; + background: var(--primary); + border: 2px solid var(--bg-color); + border-radius: 0; + cursor: pointer; + box-shadow: 0 0 6px rgba(0, 255, 136, 0.4); +} + +/* ── banners ────────────────────────────────────────── */ +.banner { + padding: 0.6rem 0.8rem; + border: 2px solid var(--border); + margin-bottom: 0.75rem; + background: var(--surface); +} +.banner.success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); } +.banner.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); } +.banner.error, .error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); } + +/* ── tables ─────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border: 2px solid var(--border); +} +th, td { + padding: 0.5rem 0.65rem; + border-bottom: 1px solid var(--border); + text-align: left; + font-size: 9px; +} +th { + background: var(--surface-elevated); + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid var(--accent); +} +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(0, 255, 136, 0.04); } + +/* ── code ───────────────────────────────────────────── */ +code, pre { font-family: var(--font-mono); } +pre { + padding: 0.75rem; + background: var(--bg-color); + border: 2px solid var(--border); + overflow: auto; + color: var(--primary); +} + +/* ── footer ─────────────────────────────────────────── */ +footer { + border-top: 3px solid var(--primary); + padding: 1rem; + color: var(--text-muted); + font-size: 8px; + background: var(--bg-secondary); + box-shadow: 0 -3px 0 #000; + position: relative; + z-index: 1; +} +footer::before { + content: '// '; + color: var(--text-muted); +} + +/* ── pixel decoration classes (for page template) ──── */ +.retro-stars { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(1px 1px at 10% 15%, var(--primary), transparent), + radial-gradient(1px 1px at 30% 45%, var(--accent), transparent), + radial-gradient(1px 1px at 55% 20%, var(--secondary), transparent), + radial-gradient(1px 1px at 70% 65%, var(--info-text), transparent), + radial-gradient(1px 1px at 85% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 15% 75%, var(--accent), transparent), + radial-gradient(1px 1px at 45% 85%, var(--secondary), transparent), + radial-gradient(1px 1px at 90% 80%, var(--primary), transparent), + radial-gradient(1px 1px at 25% 55%, var(--info-text), transparent), + radial-gradient(1px 1px at 60% 50%, var(--accent), transparent), + radial-gradient(1px 1px at 5% 40%, var(--error-text), transparent), + radial-gradient(1px 1px at 75% 10%, var(--accent), transparent), + radial-gradient(1px 1px at 40% 30%, var(--primary), transparent), + radial-gradient(1px 1px at 95% 55%, var(--secondary), transparent), + radial-gradient(1px 1px at 50% 70%, var(--primary), transparent), + radial-gradient(1px 1px at 20% 90%, var(--info-text), transparent); + animation: twinkle 4s ease-in-out infinite alternate; +} +@keyframes twinkle { + 0% { opacity: 0.4; } + 100% { opacity: 0.8; } +} + +/* ── responsive ─────────────────────────────────────── */ +@media (max-width: 860px) { + nav { flex-wrap: wrap; } + .nav-menu { overflow-x: auto; white-space: nowrap; } + .nav-account { + width: 100%; + margin-left: 0; + border-top: 1px solid var(--border); + justify-content: flex-start; + } + #content { margin-top: 5rem; padding: 1rem 0.75rem; } + h1 { font-size: 12px; } + h2 { font-size: 10px; } +} + +/* ── focus accessibility ────────────────────────────── */ +button:focus, .btn:focus, input:focus, textarea:focus, select:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* ── print ──────────────────────────────────────────── */ +@media print { + nav, footer, .retro-stars { display: none; } + body::before, body::after { display: none; } + #content { margin-top: 0; } + * { background: white !important; color: black !important; box-shadow: none !important; text-shadow: none !important; } +} diff --git a/site/info/uce-starter/themes/retro-gaming/icon.png b/site/info/uce-starter/themes/retro-gaming/icon.png new file mode 100644 index 0000000..12dfe2c Binary files /dev/null and b/site/info/uce-starter/themes/retro-gaming/icon.png differ diff --git a/site/info/uce-starter/themes/retro-gaming/page.html.uce b/site/info/uce-starter/themes/retro-gaming/page.html.uce new file mode 100644 index 0000000..c0a53da --- /dev/null +++ b/site/info/uce-starter/themes/retro-gaming/page.html.uce @@ -0,0 +1,32 @@ +#load "../../lib/app.uce" + +COMPONENT(Request& context) +{ + starter_boot(context); + bool embed_mode = starter_page_embed_mode(context); + String main_html = starter_page_main_html(context); + + DTree nav_props; + nav_props["account_wrapper_class"] = "nav-account nav-menu"; + + DTree footer_props; + footer_props["embed_mode"].set_bool(embed_mode); + + <> + + "> + + + + > +
+ + +
> + +
+ + + + +} diff --git a/site/info/uce-starter/views/account/login.uce b/site/info/uce-starter/views/account/login.uce new file mode 100644 index 0000000..89d5682 --- /dev/null +++ b/site/info/uce-starter/views/account/login.uce @@ -0,0 +1,28 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + context.var["starter"]["page_title"] = "Login"; + StarterUser users(context); + DTree result; + if(context.params["REQUEST_METHOD"] == "POST") + { + result = users.sign_in(context.post["email"], context.post["password"]); + if(result["result"].to_string() != "") + { + starter_redirect("account/profile", context); + return; + } + } + <> +

Login

+ +
+
+
+ +
+

">Register

+ +} diff --git a/site/info/uce-starter/views/account/logout.uce b/site/info/uce-starter/views/account/logout.uce new file mode 100644 index 0000000..cfee83b --- /dev/null +++ b/site/info/uce-starter/views/account/logout.uce @@ -0,0 +1,8 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + StarterUser(context).logout(); + starter_redirect("account/login", context); +} diff --git a/site/info/uce-starter/views/account/profile.uce b/site/info/uce-starter/views/account/profile.uce new file mode 100644 index 0000000..17fbc14 --- /dev/null +++ b/site/info/uce-starter/views/account/profile.uce @@ -0,0 +1,27 @@ +#load "../../lib/app.uce" + +RENDER(Request& context) +{ + starter_boot(context); + StarterUser users(context); + if(!users.is_signed_in()) + { + starter_redirect("account/login", context); + return; + } + context.var["starter"]["page_title"] = "Profile"; + DTree user = users.current(); + String roles = ""; + user["roles"].each([&](DTree role, String key) { + if(roles != "") + roles += ", "; + roles += role.to_string(); + }); + <> +

Profile

+

Email:

+

Roles:

+

Created:

+

">Logout

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

Register

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

Authentication Successful

+

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

+
+

Service:

+

Code: ...

+

State:

+
+ +
+ +
+
+

Invalid Callback

+

Missing required OAuth parameters.

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

Authentication Demo

+
+

OAuth Authentication

+

This component provides secure OAuth authentication with popular identity providers.

+ +
+
+

Current Session Status

+ +
+

Logged In

+

User ID:

+
+ +
+

Not Logged In

+

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

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

Dashboard Primitives

+

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

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

What Was Backported

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

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

Features

+

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

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

Gauge Components Demo

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

Event Binding

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

Arc Gauge Controls

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

Demo

+

Try our unbelievable components in action

+
+
+

Modern Forms

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

Button Variations

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

Theme Families

+

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

+
+
+

+

+ ">Preview Theme +
+
+

Starter Themes

+

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

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

Component Development Guidelines

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

Simple Data Table (ag-Grid)

+

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

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

Component System

+
+

Component Declaration

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

Example Components

+
+

marketing_blocks:HERO_SECTION

Landing page hero with CTA buttons

+

marketing_blocks:FEATURES_GRID

Feature showcase grid

+

widgets:DATA_TABLE

ag-Grid table wrapper

+

widgets:SORTABLE_TABLE

Lightweight sortable HTML table

+

primitives:APP_FRAME

Workspace shell wrapper

+

gauges:ARCGAUGE

SVG KPI gauge component

+
+
+ +
+

Usage Examples

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

Ajax Demo

+
+

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

+

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

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

+

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

+ +
+
+
+

Tokens at a Glance

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

System Message States

+ + + +
+
+

Form Controls

+
+
+
+
+
+
+
+
+

Dense Content Block

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

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

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

Shell layout

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

+

Semantic primitives

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

+

Starter-friendly

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

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

" + html_escape(description) + "

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

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

"; + main_body += component("../../components/workspace/primitives:SECTION", section_props, context); + + if(section == "activity") + { + DTree empty_props; + empty_props["icon_class"] = "fas fa-clock-rotate-left"; + empty_props["title"] = "No live stream wired yet"; + empty_props["text"] = "The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one."; + empty_props["action_html"] = "Open dashboard demo"; + main_body += component("../../components/workspace/primitives:EMPTY_STATE", empty_props, context); + } + + DTree panel_props; + panel_props["header_html"] = header; + panel_props["body_html"] = main_body; + String main = mobile_bar + component("../../components/workspace/primitives:PANEL", panel_props, context); + + DTree app_props; + app_props["id"] = "workspace-demo-shell"; + app_props["overlay_id"] = "workspace-demo-overlay"; + app_props["sidebar_html"] = sidebar; + app_props["main_html"] = main; + print(component("../../components/workspace/primitives:APP_FRAME", app_props, context)); + <> + + +} diff --git a/site/tests/call_helpers.uce b/site/tests/call_helpers.uce new file mode 100644 index 0000000..d79bda4 --- /dev/null +++ b/site/tests/call_helpers.uce @@ -0,0 +1,10 @@ +EXPORT DTree* emit_marker(DTree* call_param) +{ + print("UNIT_CALL_EXPORT_OK"); + return(0); +} + +RENDER(Request& context) +{ + print("
UNIT_RENDER_FIXTURE
"); +} \ No newline at end of file diff --git a/site/tests/components.uce b/site/tests/components.uce new file mode 100644 index 0000000..1a2e041 --- /dev/null +++ b/site/tests/components.uce @@ -0,0 +1,50 @@ +#include "testlib.h" + +RENDER(Request& context) +{ + u64 passed = 0; + u64 failed = 0; + u64 skipped = 0; + + auto check = [&](String name, bool ok, String detail) + { + site_tests_case(name, ok ? "pass" : "fail", detail); + if(ok) + passed++; + else + failed++; + }; + + DTree props; + props["title"] = "Component Output"; + props["body"] = "This body comes from component()"; + props["footer"] = "Named footer render"; + + bool exists = component_exists("components/panel"); + String resolved = component_resolve("components/panel"); + String panel_markup = component("components/panel", props, context); + + ob_start(); + component_render("components/panel:FOOTER", props, context); + String footer_markup = ob_get_close(); + + ob_start(); + print("buffered-text"); + String buffer_markup = ob_get_close(); + + site_tests_page_start("Components", "Component helpers, named handlers, and output buffering coverage."); + + check("component_exists()", exists, exists ? "components/panel resolved as existing" : "components/panel missing"); + check("component_resolve()", resolved != "", resolved); + check("component()", panel_markup.find("Component Output") != String::npos && panel_markup.find("This body comes from component()") != String::npos, panel_markup); + check("component_render() named handler", footer_markup.find("Named footer render") != String::npos, footer_markup); + check("ob_start() / ob_get_close()", buffer_markup == "buffered-text", buffer_markup); + + ?>
+

Rendered Component

+ +
+} \ No newline at end of file diff --git a/site/tests/components/markdown/warning.uce b/site/tests/components/markdown/warning.uce new file mode 100644 index 0000000..da19993 --- /dev/null +++ b/site/tests/components/markdown/warning.uce @@ -0,0 +1,4 @@ +COMPONENT(Request& context) +{ + <>
+} \ No newline at end of file diff --git a/site/tests/components/panel.uce b/site/tests/components/panel.uce new file mode 100644 index 0000000..7f6f731 --- /dev/null +++ b/site/tests/components/panel.uce @@ -0,0 +1,23 @@ +COMPONENT(Request& context) +{ + <>
+ + + +
+} + +COMPONENT:HEADER(Request& context) +{ + <>

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

+} + +COMPONENT:FOOTER(Request& context) +{ + <> +} \ No newline at end of file diff --git a/site/tests/core.uce b/site/tests/core.uce new file mode 100644 index 0000000..4a82731 --- /dev/null +++ b/site/tests/core.uce @@ -0,0 +1,53 @@ +#include "testlib.h" + +RENDER(Request& context) +{ + u64 passed = 0; + u64 failed = 0; + u64 skipped = 0; + + auto check = [&](String name, bool ok, String detail) + { + site_tests_case(name, ok ? "pass" : "fail", detail); + if(ok) + passed++; + else + failed++; + }; + + site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DTree, and JSON encoding/decoding."); + + auto comma_parts = split("alpha,beta,gamma", ","); + check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta", join(comma_parts, " | ")); + + auto spaced_parts = split_space(" one two three "); + check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / ")); + + check("trim()", trim(" padded value ") == "padded value", trim(" padded value ")); + check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done")); + check("substr() + strpos()", strpos("component suite", "suite") == 10 && substr("component suite", 10) == "suite", "strpos=10 substr='" + substr("component suite", 10) + "'"); + check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket"); + check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce"); + check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD")); + + String utf8_sample = "A\xC3\xA9"; + auto utf8_parts = split_utf8(utf8_sample); + check("split_utf8()", utf8_parts.size() == 2, "count=" + std::to_string(utf8_parts.size())); + + DTree payload; + payload["name"] = "uce"; + payload["count"] = (f64)3; + payload["kind"] = "core"; + String payload_json = json_encode(payload); + DTree decoded = json_decode(payload_json); + check("json_encode() / json_decode()", decoded["name"].to_string() == "uce" && int_val(decoded["count"].to_string()) == 3, payload_json); + + DTree tree; + tree["suite"] = "core"; + tree["nested"]["api"] = "dtree"; + tree["nested"]["ok"].set_bool(true); + check("DTree map access", tree["suite"].to_string() == "core" && tree["nested"]["api"].to_string() == "dtree", json_encode(tree)); + + site_tests_summary(passed, failed, skipped, "These assertions intentionally stay pure and side-effect free so they remain safe on the public site."); + site_tests_page_end(); +} \ No newline at end of file diff --git a/site/tests/http.uce b/site/tests/http.uce new file mode 100644 index 0000000..259e263 --- /dev/null +++ b/site/tests/http.uce @@ -0,0 +1,60 @@ +#include "testlib.h" + +RENDER(Request& context) +{ + u64 passed = 0; + u64 failed = 0; + u64 skipped = 0; + + auto check = [&](String name, bool ok, String detail) + { + site_tests_case(name, ok ? "pass" : "fail", detail); + if(ok) + passed++; + else + failed++; + }; + + String action = first(context.get["action"], "touch"); + String session_id = session_start("uce-site-tests"); + if(action == "destroy") + session_destroy(); + else + context.session["suite"] = "http"; + + set_cookie("site-tests-cookie", "cookie-value", time() + 300); + context.header["X-Site-Tests"] = "http-suite"; + + String query_src = "alpha=1&beta=two%20words&gamma=ok"; + StringMap query = parse_query(query_src); + String uri_input = "alpha beta/?x=1&y=2"; + String encoded = uri_encode(uri_input); + String decoded = uri_decode(encoded); + String generated_session = session_id_create(); + String query_dump = var_dump(query); + String set_cookie_dump = var_dump(context.set_cookies); + String header_dump = var_dump(context.header); + String session_dump = var_dump(context.session); + + site_tests_page_start("HTTP And Session", "Request helpers, cookies, headers, URI parsing, and session lifecycle checks."); + + check("uri_encode() / uri_decode()", decoded == uri_input, encoded + " => " + decoded); + check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words", query_dump); + check("set_cookie()", set_cookie_dump.find("site-tests-cookie") != String::npos && set_cookie_dump.find("cookie-value") != String::npos, set_cookie_dump); + check("response header mutation", context.header["X-Site-Tests"] == "http-suite", header_dump); + check("session_start()", session_id != "", "session_id=" + session_id); + check("session_id_create()", generated_session.length() >= 16, generated_session); + + if(action == "destroy") + check("session_destroy()", session_dump.find("suite") == String::npos, session_dump); + else + check("context.session writes", context.session["suite"] == "http", session_dump); + + ?>
Local-network access detected. The full suite, including filesystem, service, and task coverage pages, is available.
Public access detected. Local-only pages remain linked here but will return a restricted message instead of mutating server state.
220 ? 220 : ast_json.length()); + + site_tests_page_start("Markdown", "Markdown parser coverage with component-backed directive and node hooks."); + + check("markdown_to_ast()", ast_json.find("heading") != String::npos || ast_json.find("code_block") != String::npos, ast_excerpt); + check("markdown_to_html()", html.find("
+

Rendered HTML

+ +
220 ? 220 : response.length()) + ); + } + else + { + mark("socket_connect() / socket_write() / socket_read()", "fail", "socket_connect(localhost, 80) returned 0"); + } + + u64 memfd = memcache_connect(); + if(memfd == 0) + { + mark("memcache_*", "skip", "memcache_connect() returned 0"); + } + else + { + memcache_set(memfd, "site-tests-key", "value-1"); + String mem_value = memcache_get(memfd, "site-tests-key"); + memcache_delete(memfd, "site-tests-key"); + mark( + "memcache_connect() / memcache_set() / memcache_get() / memcache_delete()", + mem_value == "value-1" ? "pass" : "fail", + "memcache value=" + mem_value + ); + } + + MySQL mysql; + mysql.connect("localhost", "root", ""); + String mysql_error_text = trim(mysql.error()); + if(mysql_error_text != "") + { + mark("mysql connect/query", "skip", mysql_error_text); + } + else + { + String dbs = var_dump(mysql.query("SHOW DATABASES")); + mark( + "mysql connect/query", + (dbs.find("information_schema") != String::npos || dbs.find("mysql") != String::npos) ? "pass" : "fail", + dbs.substr(0, dbs.length() > 220 ? 220 : dbs.length()) + ); + } + + site_tests_summary(passed, failed, skipped, "Memcache or MySQL checks are allowed to skip cleanly when the corresponding service is not available on the development host."); + site_tests_page_end(); +} \ No newline at end of file diff --git a/site/tests/tasks.uce b/site/tests/tasks.uce new file mode 100644 index 0000000..6238944 --- /dev/null +++ b/site/tests/tasks.uce @@ -0,0 +1,69 @@ +#include "testlib.h" + +RENDER(Request& context) +{ + if(!test_demo_request_allowed(context)) + { + site_tests_restricted(context, "Tasks", "spawn and inspect background worker processes"); + return; + } + + u64 passed = 0; + u64 failed = 0; + u64 skipped = 0; + + auto check = [&](String name, bool ok, String detail) + { + site_tests_case(name, ok ? "pass" : "fail", detail); + if(ok) + passed++; + else + failed++; + }; + + String mode = first(context.get["mode"], "run"); + pid_t repeat_existing = task_pid("site-tests-repeat"); + if(mode == "stop" && repeat_existing != 0) + task_kill(repeat_existing, 15); + + pid_t short_pid = 0; + pid_t repeat_pid = 0; + if(mode != "stop") + { + short_pid = task("site-tests-short", []() { + sleep(2); + }); + + repeat_pid = task_repeat("site-tests-repeat", 1.0, []() { + file_put_contents("/tmp/uce-site-tests-repeat.txt", time_format_utc("%Y-%m-%d %H:%M:%S")); + }, 4); + } + + pid_t seen_short_pid = task_pid("site-tests-short"); + pid_t seen_repeat_pid = task_pid("site-tests-repeat"); + int short_alive = seen_short_pid == 0 ? -1 : task_kill(seen_short_pid, 0); + int repeat_alive = seen_repeat_pid == 0 ? -1 : task_kill(seen_repeat_pid, 0); + + site_tests_page_start("Tasks", "Local-only checks for one-shot and repeating background task helpers."); + + if(mode == "stop") + { + site_tests_case("repeat worker stop request", "skip", "stop mode requested; no new tasks were started on this request"); + skipped++; + } + else + { + check("task()", short_pid != 0 && seen_short_pid != 0, "started=" + std::to_string(short_pid) + " seen=" + std::to_string(seen_short_pid)); + check("task_pid() + task_kill(pid, 0)", short_alive == 0, "kill(0) result=" + std::to_string(short_alive)); + check("task_repeat()", repeat_pid != 0 && seen_repeat_pid != 0, "started=" + std::to_string(repeat_pid) + " seen=" + std::to_string(seen_repeat_pid)); + check("repeat worker liveness", repeat_alive == 0, "kill(0) result=" + std::to_string(repeat_alive)); + } + + ?>"); + print(""); + print(""); + print(""); + print("

UCE Site TestsAPI Docs →

"); + print("

", html_escape(title), "

"); + if(description != "") + print("

", html_escape(description), "

"); +} + +void site_tests_page_end() +{ + print(""); +} + +void site_tests_card(String href, String title, String description, String tags = "") +{ + print(""); + print("", html_escape(title), ""); + print("", html_escape(description), ""); + if(tags != "") + print("
", html_escape(tags), "
"); + print("
"); +} + +void site_tests_summary(u64 passed, u64 failed, u64 skipped, String note = "") +{ + print("
"); + print("passed ", std::to_string(passed), ""); + print("failed ", std::to_string(failed), ""); + print("skipped ", std::to_string(skipped), ""); + print("
"); + if(note != "") + print("
", html_escape(note), "
"); +} + +void site_tests_case(String name, String status, String detail) +{ + String css = site_tests_status_class(status); + String label = site_tests_status_label(status); + print("
"); + print("", html_escape(name), ""); + print("", html_escape(label), ""); + print("
", html_escape(detail), "
"); +} + +void site_tests_restricted(Request& context, String title, String risk) +{ + test_demo_render_restricted_html(context, title, risk, "../demo/style.css", "index.uce"); +} \ No newline at end of file diff --git a/site/tests/units.uce b/site/tests/units.uce new file mode 100644 index 0000000..e50beca --- /dev/null +++ b/site/tests/units.uce @@ -0,0 +1,38 @@ +#include "testlib.h" + +RENDER(Request& context) +{ + u64 passed = 0; + u64 failed = 0; + u64 skipped = 0; + + auto check = [&](String name, bool ok, String detail) + { + site_tests_case(name, ok ? "pass" : "fail", detail); + if(ok) + passed++; + else + failed++; + }; + + auto unit_paths = units_list(); + DTree info = unit_info("call_helpers.uce"); + + ob_start(); + unit_call("call_helpers.uce", "emit_marker"); + String call_output = ob_get_close(); + + ob_start(); + unit_render("call_helpers.uce", context); + String render_output = ob_get_close(); + + site_tests_page_start("Units", "Unit discovery, metadata, exported-function invocation, and render-path coverage."); + + check("units_list()", unit_paths.size() > 0, "count=" + std::to_string(unit_paths.size())); + check("unit_info()", info["path"].to_string() != "", json_encode(info)); + check("unit_call()", call_output.find("UNIT_CALL_EXPORT_OK") != String::npos, call_output); + check("unit_render()", render_output.find("data-unit-render=\"ok\"") != String::npos, render_output); + + site_tests_summary(passed, failed, skipped, "The local fixture call_helpers.uce keeps these checks deterministic and self-contained within site/tests."); + site_tests_page_end(); +} \ No newline at end of file diff --git a/site/tests/websockets.ws.uce b/site/tests/websockets.ws.uce new file mode 100644 index 0000000..ce39a76 --- /dev/null +++ b/site/tests/websockets.ws.uce @@ -0,0 +1,126 @@ +#include "testlib.h" + +DTree websocket_suite_event(Request& context, String type, String nonce) +{ + DTree event; + event["type"] = type; + event["nonce"] = nonce; + event["connection_id"] = ws_connection_id(); + event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope()); + event["online"] = (f64)ws_connection_count(); + event["opcode"] = (f64)ws_opcode(); + event["binary"] = ws_is_binary() ? "true" : "false"; + return(event); +} + +RENDER(Request& context) +{ + String ws_url = context.params["DOCUMENT_URI"]; + site_tests_page_start("WebSockets", "Browser-driven handshake and broadcast checks for ws_* helpers on the current .ws.uce endpoint."); + ?>
This page self-tests in the browser: it opens a socket to itself, waits for a targeted hello acknowledgment, then verifies a broadcast acknowledgment from the same endpoint.
+
+
+
hello acknowledgmentWAIT
connecting...
+
broadcast acknowledgmentWAIT
waiting...
+
connection stateWAIT
opening websocket...
+
+
+ auto compiler_with_registry_lock(Request* context, TCallback callback) -> decltype(callback()) { auto lock_file_name = compiler_registry_lock_file_name(context); - int fdlock = compiler_open_lock_file(lock_file_name); - if(fdlock != -1) - flock(fdlock, LOCK_EX); + int fdlock = compiler_open_lock_file(lock_file_name, "compiler-registry"); auto result = callback(); compiler_close_lock_file(fdlock); return(result); @@ -794,9 +793,16 @@ SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name setup_unit_paths(context, su, file_name); su->opt_so_optional = opt_so_optional; - int fdlock = compiler_open_lock_file(su->so_name + ".lock"); - if(fdlock != -1) - flock(fdlock, LOCK_EX); + int fdlock = compiler_open_lock_file(su->so_name + ".lock", "shared-unit:" + file_name); + if(fdlock == -1) + { + su->compiler_messages = "could not open compile lock"; + su->compile_status = "lock_error"; + su->compile_error_status = su->compiler_messages; + su->last_error = time(); + context->server->units[file_name] = su; + return(su); + } cached = compiler_reusable_cached_unit(context, file_name, opt_so_optional, force_recompile); if(cached) diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 00df1d7..2dde2bd 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -10,6 +9,12 @@ #include #include "sys.h" +namespace { + +constexpr f64 FILE_LOCK_WAIT_TIMEOUT_SECONDS = 3.0; + +} + String capture_backtrace_string(u32 max_frames, u32 skip_frames) { if(max_frames == 0) @@ -188,12 +193,15 @@ bool file_exists(String path) return(std::filesystem::exists(fp)); } -int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode) +int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) { + (void)wait_timeout_seconds; + (void)purpose; int fd = open(file_name.c_str(), open_flags, create_mode); if(fd == -1) return(-1); - if(flock(fd, lock_type) == -1) + fcntl(fd, F_SETFD, FD_CLOEXEC); + if(flock(fd, lock_type) != 0) { close(fd); return(-1); @@ -209,6 +217,11 @@ void file_close_locked(int fd) close(fd); } +void file_release_process_locks(String reason) +{ + (void)reason; +} + String file_get_contents_locked_fd(int fd) { if(fd == -1) @@ -254,7 +267,7 @@ bool file_put_contents_locked_fd(int fd, String content) String file_get_contents(String file_name) { - s32 fd = file_open_locked(file_name, O_RDONLY, LOCK_SH); + s32 fd = file_open_locked(file_name, O_RDONLY, LOCK_SH, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_get_contents:" + file_name); if(fd == -1) { printf("(!) Could not read %s\n", file_name.c_str()); @@ -267,7 +280,7 @@ String file_get_contents(String file_name) bool file_put_contents(String file_name, String content) { - s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); + s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_put_contents:" + file_name); if(fd == -1) { printf("(!) Could not write %s\n", file_name.c_str()); @@ -285,7 +298,7 @@ bool file_put_contents(String file_name, String content) bool file_append_contents(String file_name, String content) { - s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); + s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_append:" + file_name); if(fd == -1) { printf("(!) Could not append %s\n", file_name.c_str()); @@ -583,6 +596,7 @@ pid_t spawn_subprocess(std::function exec_after_spawn) p = fork(); if(p == 0) { + file_release_process_locks("fork child startup"); my_pid = getpid(); //printf("(C) child procress started, PID:%i\n", my_pid); prctl(PR_SET_PDEATHSIG, SIGHUP); @@ -629,7 +643,9 @@ 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); + int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task:" + key); + if(lock_fd == -1) + return(0); String status_file = file_get_contents(status_file_name); pid_t p; if(status_file != "") @@ -647,6 +663,7 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) p = fork(); if(p == 0) { + file_release_process_locks("task child startup"); file_close_locked(lock_fd); my_pid = getpid(); @@ -655,7 +672,7 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) //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); + int exit_lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-exit:" + key); 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); diff --git a/src/lib/sys.h b/src/lib/sys.h index 0d16460..81aa52b 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -11,8 +11,9 @@ 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); +int file_open_locked(String file_name, int open_flags, int lock_type = LOCK_SH, int create_mode = 0644, f64 wait_timeout_seconds = 3.0, String purpose = ""); void file_close_locked(int fd); +void file_release_process_locks(String reason = ""); 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); diff --git a/src/lib/types.cpp b/src/lib/types.cpp index 03ede5c..33fb047 100644 --- a/src/lib/types.cpp +++ b/src/lib/types.cpp @@ -98,11 +98,6 @@ 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 049c352..4d3aa14 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -155,9 +155,7 @@ struct Request { StringMap post; StringMap cookies; StringMap session; - String session_file_name = ""; - String session_serialized = ""; - int session_lock_fd = -1; + String session_loaded_hash = ""; DTree var; DTree cfg; @@ -211,6 +209,9 @@ struct Request { String websocket_connection_id = ""; String websocket_scope = ""; DTree* websocket_connection_state = 0; + StringList websocket_scope_connection_ids; + DTree websocket_dispatch_commands; + bool websocket_dispatch_capture = false; u8 websocket_opcode = 0; bool websocket_is_binary = false; bool websocket_is_text = false; diff --git a/src/lib/uri.cpp b/src/lib/uri.cpp index f049dda..5dfd4ca 100644 --- a/src/lib/uri.cpp +++ b/src/lib/uri.cpp @@ -544,15 +544,21 @@ String session_file_path(String session_id) namespace { -void session_release_lock(Request* request) +String session_serialize(const StringMap& data) { - 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 = ""; + return(encode_query(data)); +} + +String session_hash_serialized(String serialized) +{ + return(gen_sha1(serialized)); +} + +String session_load_serialized(String session_path) +{ + if(session_path == "" || !file_exists(session_path)) + return(""); + return(file_get_contents(session_path)); } } @@ -562,47 +568,36 @@ 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))); + return(parse_query(session_load_serialized(session_path))); } void save_session_data(String session_id, StringMap data) { String session_path = session_file_path(session_id); - String encoded = encode_query(data); + String encoded = session_serialize(data); + String encoded_hash = session_hash_serialized(encoded); if(session_path == "") { printf("(!) Refusing to save invalid session id\n"); return; } - if(context && context->session_lock_fd != -1 && context->session_file_name == session_path) + if(context && encoded_hash == context->session_loaded_hash) + return; + if(!file_put_contents(session_path, encoded)) { - if(encoded == context->session_serialized) - return; - if(file_put_contents_locked_fd(context->session_lock_fd, encoded)) - context->session_serialized = encoded; + printf("(!) Refusing to save session file %s\n", session_path.c_str()); 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); + if(context) + context->session_loaded_hash = encoded_hash; } String session_start(String session_name) { - if(context->session_lock_fd != -1 && context->session_name == session_name && context->session_id != "") + if(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_loaded_hash = ""; context->session_id = ""; context->session_name = ""; @@ -617,15 +612,10 @@ 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); + auto session_path = session_file_path(context->session_id); + auto serialized = session_load_serialized(session_path); + context->session_loaded_hash = session_hash_serialized(serialized); + context->session = parse_query(serialized); return(context->session_id); } @@ -636,7 +626,6 @@ 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 e0f6c3e..c4fea0d 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -1,5 +1,9 @@ #include "lib/uce_lib.cpp" #include +#include +#include +#include +#include ServerState server_state; @@ -8,12 +12,20 @@ ServerState server_state; FastCGIServer server; pid_t http_worker_pid = 0; pid_t proactive_compiler_pid = 0; +pid_t websocket_exec_pid = 0; bool worker_accepts_http = false; static sigjmp_buf request_fault_jmp; static volatile sig_atomic_t request_fault_active = 0; static volatile sig_atomic_t request_fault_signal = 0; static Request* request_fault_request = 0; static String request_fault_trace = ""; +static int websocket_exec_fd = -1; +static String websocket_exec_read_buffer = ""; +static std::deque websocket_exec_pending_jobs; +static DTree websocket_exec_inflight_job; +static String websocket_exec_write_buffer = ""; + +void close_inherited_server_sockets(); Request* set_active_request(Request& request) { @@ -119,6 +131,545 @@ void restore_request_fault_handlers() signal(SIGFPE, on_segfault); } +namespace { + +const char* websocket_ipc_base64_alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +String websocket_ipc_base64_encode(String raw) +{ + String result; + size_t i = 0; + while(i < raw.length()) + { + unsigned char input[3] = {0, 0, 0}; + size_t chunk = 0; + for(; chunk < 3 && i < raw.length(); ++chunk, ++i) + input[chunk] = (unsigned char)raw[i]; + + result += websocket_ipc_base64_alphabet[input[0] >> 2]; + result += websocket_ipc_base64_alphabet[((input[0] & 0x03) << 4) | (input[1] >> 4)]; + result += (chunk > 1 ? websocket_ipc_base64_alphabet[((input[1] & 0x0F) << 2) | (input[2] >> 6)] : '='); + result += (chunk > 2 ? websocket_ipc_base64_alphabet[input[2] & 0x3F] : '='); + } + return(result); +} + +int websocket_ipc_base64_value(char c) +{ + if(c >= 'A' && c <= 'Z') + return(c - 'A'); + if(c >= 'a' && c <= 'z') + return(c - 'a' + 26); + if(c >= '0' && c <= '9') + return(c - '0' + 52); + if(c == '+') + return(62); + if(c == '/') + return(63); + return(-1); +} + +String websocket_ipc_base64_decode(String raw, bool& ok) +{ + ok = false; + String filtered = ""; + for(auto c : raw) + { + if(!isspace((unsigned char)c)) + filtered.append(1, c); + } + if(filtered == "") + { + ok = true; + return(""); + } + if(filtered.length() % 4 != 0) + return(""); + + String result; + for(size_t i = 0; i < filtered.length(); i += 4) + { + int values[4] = {0, 0, 0, 0}; + int padding = 0; + for(int j = 0; j < 4; ++j) + { + char c = filtered[i + j]; + if(c == '=') + { + values[j] = 0; + padding += 1; + continue; + } + values[j] = websocket_ipc_base64_value(c); + if(values[j] < 0) + return(""); + } + + result.append(1, (char)((values[0] << 2) | (values[1] >> 4))); + if(padding < 2) + result.append(1, (char)(((values[1] & 0x0F) << 4) | (values[2] >> 2))); + if(padding < 1) + result.append(1, (char)(((values[2] & 0x03) << 6) | values[3])); + } + + ok = true; + return(result); +} + +bool websocket_ipc_set_nonblocking(int fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if(flags == -1) + return(false); + return(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0); +} + +bool websocket_exec_enabled_for_process() +{ + return(worker_accepts_http); +} + +u64 websocket_exec_queue_limit_bytes() +{ + u64 configured = int_val(server_state.config["WEBSOCKET_EXEC_QUEUE_BYTES"]); + if(configured < 64 * 1024) + configured = 1024 * 1024; + return(configured); +} + +bool websocket_exec_has_inflight_job() +{ + return(websocket_exec_inflight_job.to_bool()); +} + +FastCGIServer::Connection* websocket_find_connection(String connection_id) +{ + for(auto& item : server.client_sockets) + { + FastCGIServer::Connection* connection = item.second; + if(connection->is_websocket && connection->websocket_connection_id == connection_id) + return(connection); + } + return(0); +} + +void websocket_exec_clear_ipc_state() +{ + if(websocket_exec_fd != -1) + close(websocket_exec_fd); + websocket_exec_fd = -1; + websocket_exec_read_buffer = ""; + websocket_exec_write_buffer = ""; + websocket_exec_inflight_job.clear(); +} + +void websocket_exec_close_connection(String connection_id, u16 status_code = 1011, String reason = "websocket handler unavailable") +{ + if(connection_id == "") + return; + server.websocket_close(connection_id, status_code, reason); +} + +void websocket_exec_fail_inflight_job(String reason = "websocket handler unavailable") +{ + if(!websocket_exec_has_inflight_job()) + return; + websocket_exec_close_connection(websocket_exec_inflight_job["connection_id"].to_string(), 1011, reason); + websocket_exec_inflight_job.clear(); + websocket_exec_write_buffer = ""; +} + +void websocket_exec_queue_job(DTree job) +{ + if(job["connection_id"].to_string() == "") + return; + + u64 queued_bytes = websocket_exec_write_buffer.length(); + if(websocket_exec_has_inflight_job()) + queued_bytes += websocket_exec_inflight_job["serialized"].to_string().length(); + for(auto& pending : websocket_exec_pending_jobs) + queued_bytes += pending["serialized"].to_string().length(); + queued_bytes += json_encode(job).length(); + + if(queued_bytes > websocket_exec_queue_limit_bytes()) + { + printf("(!) websocket dispatch queue overflow for %s\n", job["connection_id"].to_string().c_str()); + websocket_exec_close_connection(job["connection_id"].to_string(), 1013, "websocket server busy"); + return; + } + + job["serialized"] = json_encode(job) + "\n"; + websocket_exec_pending_jobs.push_back(job); +} + +StringList websocket_exec_snapshot_connections(String scope) +{ + return(server.websocket_connection_ids(scope)); +} + +void websocket_exec_append_command(DTree command) +{ + if(!context) + return; + context->resources.websocket_dispatch_commands.push(command); +} + +void websocket_exec_apply_command(DTree command) +{ + String action = command["action"].to_string(); + if(action == "broadcast") + { + bool ok = false; + String payload = websocket_ipc_base64_decode(command["message_b64"].to_string(), ok); + if(!ok) + return; + server.websocket_broadcast(command["scope"].to_string(), payload, command["binary"].to_bool()); + return; + } + if(action == "send_to") + { + bool ok = false; + String payload = websocket_ipc_base64_decode(command["message_b64"].to_string(), ok); + if(!ok) + return; + server.websocket_send_to(command["connection_id"].to_string(), payload, command["binary"].to_bool()); + return; + } + if(action == "close") + { + server.websocket_close( + command["connection_id"].to_string(), + (u16)command["status_code"].to_u64(), + command["reason"].to_string() + ); + } +} + +void websocket_exec_apply_result(DTree result) +{ + if(result["type"].to_string() != "result") + return; + + String inflight_connection_id = websocket_exec_inflight_job["connection_id"].to_string(); + String result_connection_id = result["connection_id"].to_string(); + if(inflight_connection_id != "" && result_connection_id != "" && inflight_connection_id != result_connection_id) + { + printf("(!) websocket dispatch result mismatch: expected %s got %s\n", + inflight_connection_id.c_str(), + result_connection_id.c_str()); + } + + FastCGIServer::Connection* connection = websocket_find_connection(result_connection_id); + if(connection) + connection->websocket_state = result["connection_state"]; + + result["commands"].each([] (DTree command, String) { + websocket_exec_apply_command(command); + }); + + websocket_exec_inflight_job.clear(); +} + +void websocket_exec_handle_ipc_line(String line) +{ + line = trim(line); + if(line == "") + return; + DTree result = json_decode(line); + websocket_exec_apply_result(result); +} + +void websocket_exec_read_results() +{ + if(websocket_exec_fd == -1) + return; + + char buffer[4096]; + for(;;) + { + ssize_t read_result = read(websocket_exec_fd, buffer, sizeof(buffer)); + if(read_result == 0) + { + printf("(!) websocket executor disconnected\n"); + websocket_exec_fail_inflight_job(); + websocket_exec_clear_ipc_state(); + return; + } + if(read_result < 0) + { + if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + break; + perror("websocket executor read"); + websocket_exec_fail_inflight_job(); + websocket_exec_clear_ipc_state(); + return; + } + + websocket_exec_read_buffer.append(buffer, read_result); + for(;;) + { + size_t line_end = websocket_exec_read_buffer.find('\n'); + if(line_end == String::npos) + break; + String line = websocket_exec_read_buffer.substr(0, line_end); + websocket_exec_read_buffer.erase(0, line_end + 1); + websocket_exec_handle_ipc_line(line); + } + } +} + +void websocket_exec_flush_queue() +{ + if(websocket_exec_fd == -1) + return; + + if(websocket_exec_write_buffer == "" && !websocket_exec_has_inflight_job() && websocket_exec_pending_jobs.size() > 0) + { + websocket_exec_inflight_job = websocket_exec_pending_jobs.front(); + websocket_exec_pending_jobs.pop_front(); + websocket_exec_write_buffer = websocket_exec_inflight_job["serialized"].to_string(); + } + + while(websocket_exec_write_buffer != "") + { + ssize_t write_result = write(websocket_exec_fd, websocket_exec_write_buffer.data(), websocket_exec_write_buffer.length()); + if(write_result < 0) + { + if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return; + perror("websocket executor write"); + websocket_exec_fail_inflight_job(); + websocket_exec_clear_ipc_state(); + return; + } + if(write_result == 0) + return; + websocket_exec_write_buffer.erase(0, write_result); + } +} + +Request websocket_exec_build_event_request(DTree job, String message) +{ + Request event_request; + event_request.server = &server_state; + event_request.params = job["params"].to_stringmap(); + event_request.params["REQUEST_METHOD"] = "WEBSOCKET"; + event_request.get = parse_query(event_request.params["QUERY_STRING"]); + event_request.resources.is_websocket = true; + event_request.resources.websocket_connection_id = job["connection_id"].to_string(); + event_request.resources.websocket_scope = job["scope"].to_string(); + event_request.resources.websocket_opcode = (u8)job["opcode"].to_u64(); + event_request.resources.websocket_is_binary = job["is_binary"].to_bool(); + event_request.resources.websocket_is_text = job["is_text"].to_bool(); + event_request.resources.websocket_dispatch_capture = true; + job["scope_connections"].each([&] (DTree item, String) { + event_request.resources.websocket_scope_connection_ids.push_back(item.to_string()); + }); + event_request.connection = job["connection_state"]; + event_request.stats.time_init = time_precise(); + event_request.stats.time_start = event_request.stats.time_init; + event_request.random_index = 0; + event_request.random_seed = gen_noise64(*reinterpret_cast(&event_request.stats.time_start)); + event_request.response_code = "WEBSOCKET"; + event_request.header["Content-Type"] = server_state.config["CONTENT_TYPE"]; + event_request.in = message; + + if(event_request.params["HTTP_COOKIE"].length() > 0) + event_request.cookies = parse_cookies(event_request.params["HTTP_COOKIE"]); + + event_request.var["ws"]["message"] = message; + event_request.var["ws"]["connection_id"] = event_request.resources.websocket_connection_id; + event_request.var["ws"]["scope"] = event_request.resources.websocket_scope; + event_request.var["ws"]["connection_count"] = (f64)event_request.resources.websocket_scope_connection_ids.size(); + event_request.var["ws"]["opcode"] = (f64)event_request.resources.websocket_opcode; + event_request.var["ws"]["is_binary"].set_bool(event_request.resources.websocket_is_binary); + event_request.var["ws"]["is_text"].set_bool(event_request.resources.websocket_is_text); + event_request.var["ws"]["document_uri"] = first( + event_request.params["DOCUMENT_URI"], + event_request.params["REQUEST_URI"] + ); + + event_request.call["message"] = message; + event_request.call["connection_id"] = event_request.resources.websocket_connection_id; + event_request.call["scope"] = event_request.resources.websocket_scope; + event_request.call["opcode"] = (f64)event_request.resources.websocket_opcode; + event_request.call["document_uri"] = event_request.var["ws"]["document_uri"].to_string(); + return(event_request); +} + +bool websocket_exec_send_response(int fd, DTree response) +{ + String encoded = json_encode(response) + "\n"; + size_t offset = 0; + while(offset < encoded.length()) + { + ssize_t write_result = write(fd, encoded.data() + offset, encoded.length() - offset); + if(write_result < 0) + { + if(errno == EINTR) + continue; + return(false); + } + offset += (size_t)write_result; + } + return(true); +} + +void websocket_exec_process_job_line(int fd, String line) +{ + line = trim(line); + if(line == "") + return; + + DTree job = json_decode(line); + if(job["type"].to_string() != "dispatch") + return; + + bool decoded = false; + String message = websocket_ipc_base64_decode(job["message_b64"].to_string(), decoded); + if(!decoded) + { + printf("(!) invalid websocket IPC payload for %s\n", job["connection_id"].to_string().c_str()); + return; + } + + Request event_request = websocket_exec_build_event_request(job, message); + Request* previous_context = set_active_request(event_request); + server_state.request_count += 1; + + compiler_invoke_websocket(&event_request, event_request.params["SCRIPT_FILENAME"]); + + if(event_request.session_id.length() > 0) + save_session_data(event_request.session_id, event_request.session); + cleanup_mysql_connections(); + + DTree response; + response["type"] = "result"; + response["connection_id"] = event_request.resources.websocket_connection_id; + response["connection_state"] = event_request.connection; + response["commands"] = event_request.resources.websocket_dispatch_commands; + + restore_active_request(previous_context); + if(!websocket_exec_send_response(fd, response)) + exit(1); +} + +void websocket_exec_child_loop(int fd) +{ + Request background_context; + my_pid = getpid(); + context = &background_context; + close_inherited_server_sockets(); + signal(SIGSEGV, on_segfault); + signal(SIGABRT, on_segfault); + signal(SIGBUS, on_segfault); + signal(SIGILL, on_segfault); + signal(SIGFPE, on_segfault); + signal(SIGPIPE, SIG_IGN); + setpriority(PRIO_PROCESS, 0, 5); + + String read_buffer; + char buffer[4096]; + for(;;) + { + ssize_t read_result = read(fd, buffer, sizeof(buffer)); + if(read_result == 0) + exit(0); + if(read_result < 0) + { + if(errno == EINTR) + continue; + exit(1); + } + + read_buffer.append(buffer, read_result); + for(;;) + { + size_t line_end = read_buffer.find('\n'); + if(line_end == String::npos) + break; + String line = read_buffer.substr(0, line_end); + read_buffer.erase(0, line_end + 1); + websocket_exec_process_job_line(fd, line); + } + } +} + +bool websocket_exec_alive() +{ + return(websocket_exec_pid > 0 && task_kill(websocket_exec_pid, 0) == 0 && websocket_exec_fd != -1); +} + +void ensure_websocket_executor() +{ + if(!websocket_exec_enabled_for_process()) + return; + if(websocket_exec_alive()) + return; + + if(websocket_exec_pid > 0 || websocket_exec_fd != -1) + { + websocket_exec_fail_inflight_job(); + websocket_exec_clear_ipc_state(); + websocket_exec_pid = 0; + } + + int sockets[2] = {-1, -1}; + if(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0) + { + perror("socketpair"); + return; + } + + pid_t p = fork(); + if(p < 0) + { + perror("fork"); + close(sockets[0]); + close(sockets[1]); + return; + } + if(p == 0) + { + parent_pid = getppid(); + file_release_process_locks("websocket executor fork"); + prctl(PR_SET_PDEATHSIG, SIGHUP); + close(sockets[0]); + websocket_exec_child_loop(sockets[1]); + exit(0); + } + + close(sockets[1]); + if(!websocket_ipc_set_nonblocking(sockets[0])) + { + printf("(!) failed to set websocket executor socket nonblocking\n"); + close(sockets[0]); + return; + } + + websocket_exec_fd = sockets[0]; + websocket_exec_pid = p; + printf("(P) websocket executor spawned: PID %i\n", p); +} + +void websocket_exec_tick() +{ + if(!websocket_exec_enabled_for_process()) + return; + if(!websocket_exec_alive()) + { + websocket_exec_fail_inflight_job(); + websocket_exec_clear_ipc_state(); + websocket_exec_pid = 0; + ensure_websocket_executor(); + } + websocket_exec_read_results(); + websocket_exec_flush_queue(); +} + +} + String current_ws_scope() { if(!context) @@ -185,6 +736,13 @@ bool ws_is_binary() StringList ws_connections(String scope) { + if(context && context->resources.websocket_dispatch_capture) + { + String normalized_scope = normalize_ws_scope(scope); + if(normalized_scope == context->resources.websocket_scope) + return(context->resources.websocket_scope_connection_ids); + return(StringList()); + } return(server.websocket_connection_ids(normalize_ws_scope(scope))); } @@ -195,11 +753,31 @@ u64 ws_connection_count(String scope) bool ws_send(String message, bool binary, String scope) { + if(context && context->resources.websocket_dispatch_capture) + { + DTree command; + command["action"] = "broadcast"; + command["scope"] = normalize_ws_scope(scope); + command["binary"].set_bool(binary); + command["message_b64"] = websocket_ipc_base64_encode(message); + websocket_exec_append_command(command); + return(true); + } return(server.websocket_broadcast(normalize_ws_scope(scope), message, binary) > 0); } bool ws_send_to(String connection_id, String message, bool binary) { + if(context && context->resources.websocket_dispatch_capture) + { + DTree command; + command["action"] = "send_to"; + command["connection_id"] = connection_id; + command["binary"].set_bool(binary); + command["message_b64"] = websocket_ipc_base64_encode(message); + websocket_exec_append_command(command); + return(true); + } return(server.websocket_send_to(connection_id, message, binary)); } @@ -209,6 +787,16 @@ bool ws_close(String connection_id) connection_id = ws_connection_id(); if(connection_id == "") return(false); + if(context && context->resources.websocket_dispatch_capture) + { + DTree command; + command["action"] = "close"; + command["connection_id"] = connection_id; + command["status_code"] = (f64)1000; + command["reason"] = ""; + websocket_exec_append_command(command); + return(true); + } return(server.websocket_close(connection_id)); } @@ -334,55 +922,36 @@ int handle_complete(FastCGIRequest& request) { int handle_websocket_message(FastCGIRequest& request, const String& message, u8 opcode) { - Request event_request; - ByteStream ws_output; + ensure_websocket_executor(); + if(!websocket_exec_alive()) + { + printf("(!) websocket executor unavailable for %s\n", request.resources.websocket_connection_id.c_str()); + server.websocket_close(request.resources.websocket_connection_id, 1011, "websocket handler unavailable"); + return(0); + } - Request* previous_context = set_active_request(event_request); - server_state.request_count += 1; - event_request.server = &server_state; - event_request.params = request.params; - event_request.params["REQUEST_METHOD"] = "WEBSOCKET"; - event_request.get = parse_query(event_request.params["QUERY_STRING"]); - event_request.resources = request.resources; - if(event_request.resources.websocket_connection_state) - event_request.connection.set_reference(event_request.resources.websocket_connection_state); - event_request.stats.time_init = time_precise(); - event_request.stats.time_start = event_request.stats.time_init; - event_request.random_index = 0; - event_request.random_seed = gen_noise64(*reinterpret_cast(&event_request.stats.time_start)); - event_request.response_code = "WEBSOCKET"; - event_request.header["Content-Type"] = context->server->config["CONTENT_TYPE"]; - event_request.in = message; - event_request.ob = &ws_output; + DTree job; + job["type"] = "dispatch"; + job["connection_id"] = request.resources.websocket_connection_id; + job["scope"] = request.resources.websocket_scope; + job["opcode"] = (f64)opcode; + job["is_binary"].set_bool(request.resources.websocket_is_binary); + job["is_text"].set_bool(request.resources.websocket_is_text); + job["message_b64"] = websocket_ipc_base64_encode(message); + if(request.resources.websocket_connection_state) + job["connection_state"] = *request.resources.websocket_connection_state; - if(event_request.params["HTTP_COOKIE"].length() > 0) - event_request.cookies = parse_cookies(event_request.params["HTTP_COOKIE"]); + for(auto& item : request.params) + job["params"][item.first] = item.second; - event_request.var["ws"]["message"] = message; - event_request.var["ws"]["connection_id"] = request.resources.websocket_connection_id; - event_request.var["ws"]["scope"] = request.resources.websocket_scope; - event_request.var["ws"]["connection_count"] = (f64)server.websocket_connection_ids(request.resources.websocket_scope).size(); - event_request.var["ws"]["opcode"] = (f64)opcode; - event_request.var["ws"]["is_binary"].set_bool(request.resources.websocket_is_binary); - event_request.var["ws"]["is_text"].set_bool(request.resources.websocket_is_text); - event_request.var["ws"]["document_uri"] = first( - request.params["DOCUMENT_URI"], - request.params["REQUEST_URI"] - ); + for(auto& connection_id : websocket_exec_snapshot_connections(request.resources.websocket_scope)) + { + DTree snapshot_item; + snapshot_item = connection_id; + job["scope_connections"].push(snapshot_item); + } - event_request.call["message"] = message; - event_request.call["connection_id"] = request.resources.websocket_connection_id; - event_request.call["scope"] = request.resources.websocket_scope; - event_request.call["opcode"] = (f64)opcode; - event_request.call["document_uri"] = event_request.var["ws"]["document_uri"].to_string(); - - compiler_invoke_websocket(&event_request, request.params["SCRIPT_FILENAME"]); - - if(event_request.session_id.length() > 0) - save_session_data(event_request.session_id, event_request.session); - - cleanup_mysql_connections(); - restore_active_request(previous_context); + websocket_exec_queue_job(job); return 0; } @@ -520,7 +1089,7 @@ void run_proactive_compiler() retry_after.erase(file_name); } background_context.session.clear(); - background_context.session_serialized = ""; + background_context.session_loaded_hash = ""; clear_shared_unit_cache(server_state); usleep(250000); continue; @@ -548,6 +1117,7 @@ void ensure_proactive_compiler() pid_t p = fork(); if(p == 0) { + file_release_process_locks("proactive compiler fork"); prctl(PR_SET_PDEATHSIG, SIGHUP); run_proactive_compiler(); exit(0); @@ -579,9 +1149,14 @@ void listen_for_connections() server.on_data = &handle_data; server.on_complete = &handle_complete; server.on_websocket_message = &handle_websocket_message; + if(worker_accepts_http) + ensure_websocket_executor(); for(;;) { - server.process(); + file_release_process_locks("worker loop cleanup"); + server.process(worker_accepts_http ? 50 : -1); + if(worker_accepts_http) + websocket_exec_tick(); } } diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..6b1ffc4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,81 @@ +# Network Tests + +This directory contains a dependency-less vanilla Python 3 network test runner and plugin-style test cases. + +## Runner + +Run the default local UCE battery: + +```bash +./tests/run_network_tests.py +/bin/python3 tests/run_network_tests.py +``` + +The default battery is aimed at public or cross-host LAN checks and currently focuses on stable published doc endpoints, the published `site/tests` public coverage pages, plus a basic TCP reachability check. +Internal-only cases are tagged `internal` and are not part of the default smoke path unless you ask for them explicitly. + +For the local runtime, the target is localhost rather than `http://k-uce`. + +Run against another machine on the local network: + +```bash +/bin/python3 tests/run_network_tests.py --base-url http://k-uce --host-header uce.openfu.com +``` + +Explicit localhost example: + +```bash +/bin/python3 tests/run_network_tests.py --base-url http://localhost --host-header uce.openfu.com +``` + +List discovered cases without running them: + +```bash +/bin/python3 tests/run_network_tests.py --list +``` + +Run only one plugin or a subset of tagged tests: + +```bash +/bin/python3 tests/run_network_tests.py --plugin uce_http_smoke +/bin/python3 tests/run_network_tests.py --tag tcp +/bin/python3 tests/run_network_tests.py --match session +/bin/python3 tests/run_network_tests.py --tag internal --include-internal +``` + +Override the network target: + +```bash +/bin/python3 tests/run_network_tests.py --host 10.8.0.12 --port 80 --host-header uce.openfu.com +/bin/python3 tests/run_network_tests.py --base-url http://10.8.0.12:8080 +``` + +Write a JSON report: + +```bash +/bin/python3 tests/run_network_tests.py --json-report tests/last-report.json +``` + +## Plugin Contract + +Each plugin is a plain Python file under `tests/plugins/` that exports: + +```python +def register(registry): + ... +``` + +Use `registry.case(name, callback, tags=[...])` to register cases. + +Each callback receives a `TestContext` and may use helpers such as: + +- `context.request(...)` +- `context.expect_status(...)` +- `context.expect_body_contains(...)` +- `context.tcp_connect(...)` + +## Included Plugins + +- `uce_http_smoke.py`: HTTP 200/body-marker checks for stable published doc pages +- `uce_site_suite.py`: HTTP 200/body-marker checks for the published `site/tests` coverage pages; local-only pages are tagged `internal` +- `uce_tcp_smoke.py`: basic TCP listener checks, with public smoke checks on port 80 and an internal-only HTTP/WebSocket listener probe on port `8080` tagged `internal` \ No newline at end of file diff --git a/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc b/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc new file mode 100644 index 0000000..c20f552 Binary files /dev/null and b/tests/plugins/__pycache__/uce_http_smoke.cpython-313.pyc differ diff --git a/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc b/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc new file mode 100644 index 0000000..7d8c04f Binary files /dev/null and b/tests/plugins/__pycache__/uce_site_suite.cpython-313.pyc differ diff --git a/tests/plugins/__pycache__/uce_tcp_smoke.cpython-313.pyc b/tests/plugins/__pycache__/uce_tcp_smoke.cpython-313.pyc new file mode 100644 index 0000000..38aa7f6 Binary files /dev/null and b/tests/plugins/__pycache__/uce_tcp_smoke.cpython-313.pyc differ diff --git a/tests/plugins/uce_http_smoke.py b/tests/plugins/uce_http_smoke.py new file mode 100644 index 0000000..6a5b48c --- /dev/null +++ b/tests/plugins/uce_http_smoke.py @@ -0,0 +1,18 @@ +def register(registry): + pages = [ + ("doc index", "/doc/index.uce", ""), + ("doc singlepage", "/doc/singlepage.uce", ""), + ("doc component page", "/doc/index.uce?p=component", "component()"), + ("doc relative time page", "/doc/index.uce?p=time_format_relative", "time_format_relative"), + ] + + for name, path, needle in pages: + def make_case(page_path=path, expected_text=needle): + def run(context): + response = context.expect_status(page_path, 200) + context.expect_body_contains(response, expected_text) + return "HTTP 200 with expected body marker for %s" % page_path + + return run + + registry.case(name, make_case(), tags=["http", "smoke", "uce", "public"]) \ No newline at end of file diff --git a/tests/plugins/uce_site_suite.py b/tests/plugins/uce_site_suite.py new file mode 100644 index 0000000..0b313c4 --- /dev/null +++ b/tests/plugins/uce_site_suite.py @@ -0,0 +1,38 @@ +ERROR_MARKERS = [ + "compile error", + "runtime error", + "timed out acquiring compile lock", + "near line", +] + + +def register(registry): + pages = [ + ("site tests index", "/tests/index.uce", "Coverage Index", ["http", "suite", "uce", "public"]), + ("site tests core", "/tests/core.uce", "Core APIs", ["http", "suite", "uce", "public"]), + ("site tests http", "/tests/http.uce", "HTTP And Session", ["http", "suite", "uce", "public"]), + ("site tests components", "/tests/components.uce", "Components", ["http", "suite", "uce", "public"]), + ("site tests markdown", "/tests/markdown.uce", "Markdown", ["http", "suite", "uce", "public"]), + ("site tests units", "/tests/units.uce", "Units", ["http", "suite", "uce", "public"]), + ("site tests websockets page", "/tests/websockets.ws.uce", "WebSockets", ["http", "suite", "uce", "public", "websocket"]), + ("site tests filesystem", "/tests/io.uce", "Filesystem", ["http", "suite", "uce", "internal"]), + ("site tests services", "/tests/services.uce", "Sockets And Services", ["http", "suite", "uce", "internal"]), + ("site tests tasks", "/tests/tasks.uce", "Tasks", ["http", "suite", "uce", "internal"]), + ] + + for name, path, title, tags in pages: + def make_case(page_path=path, expected_title=title): + def run(context): + response = context.expect_status(page_path, 200) + context.expect_body_contains(response, expected_title) + + body_lower = response.text.lower() + for marker in ERROR_MARKERS: + if marker in body_lower: + raise Exception("response body contained error marker %r for %s" % (marker, page_path)) + + return "HTTP 200 with suite page marker for %s" % page_path + + return run + + registry.case(name, make_case(), tags=tags) \ No newline at end of file diff --git a/tests/plugins/uce_tcp_smoke.py b/tests/plugins/uce_tcp_smoke.py new file mode 100644 index 0000000..e4ba18f --- /dev/null +++ b/tests/plugins/uce_tcp_smoke.py @@ -0,0 +1,12 @@ +def register(registry): + def port_80(context): + context.tcp_connect(port=80) + return "TCP connect succeeded on port 80" + + registry.case("frontend port 80", port_80, tags=["tcp", "smoke", "uce", "public"]) + + def port_8080(context): + context.tcp_connect(port=8080) + return "TCP connect succeeded on port 8080" + + registry.case("http websocket port 8080", port_8080, tags=["tcp", "uce", "internal"]) \ No newline at end of file diff --git a/tests/run_network_tests.py b/tests/run_network_tests.py new file mode 100755 index 0000000..672d8a0 --- /dev/null +++ b/tests/run_network_tests.py @@ -0,0 +1,349 @@ +#!/bin/python3 + +import argparse +import http.client +import importlib.util +import json +import re +import socket +import sys +import time +from urllib.parse import urlparse + +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Callable, Dict, Iterable, List, Optional + + +@dataclass +class Target: + scheme: str = "http" + host: str = "localhost" + port: int = 80 + host_header: Optional[str] = None + timeout: float = 5.0 + + @property + def label(self) -> str: + return "%s://%s:%s" % (self.scheme, self.host, self.port) + + +@dataclass +class HttpResponse: + status: int + reason: str + headers: Dict[str, str] + body: bytes + duration_ms: float + + @property + def text(self) -> str: + return self.body.decode("utf-8", errors="replace") + + +@dataclass +class TestResult: + name: str + ok: bool + summary: str + plugin: str + duration_ms: float = 0.0 + details: str = "" + tags: List[str] = field(default_factory=list) + + +class TestFailure(Exception): + pass + + +class TestContext: + def __init__(self, target: Target, args: argparse.Namespace): + self.target = target + self.args = args + + def request( + self, + path: str, + method: str = "GET", + headers: Optional[Dict[str, str]] = None, + body: Optional[bytes] = None, + timeout: Optional[float] = None, + ) -> HttpResponse: + request_headers = dict(headers or {}) + if self.target.host_header and "Host" not in request_headers: + request_headers["Host"] = self.target.host_header + + connection_class = http.client.HTTPConnection + if self.target.scheme == "https": + connection_class = http.client.HTTPSConnection + + started_at = time.perf_counter() + connection = connection_class(self.target.host, self.target.port, timeout=timeout or self.target.timeout) + try: + connection.request(method, path, body=body, headers=request_headers) + response = connection.getresponse() + content = response.read() + return HttpResponse( + status=response.status, + reason=response.reason, + headers=dict(response.getheaders()), + body=content, + duration_ms=(time.perf_counter() - started_at) * 1000.0, + ) + finally: + connection.close() + + def expect_status(self, path: str, expected_status: int = 200, method: str = "GET") -> HttpResponse: + response = self.request(path, method=method) + if response.status != expected_status: + raise TestFailure( + "expected HTTP %s for %s, got %s %s" % ( + expected_status, + path, + response.status, + response.reason, + ) + ) + return response + + def expect_body_contains(self, response: HttpResponse, needle: str) -> None: + if needle not in response.text: + raise TestFailure("response body did not contain expected text: %r" % needle) + + def tcp_connect(self, host: Optional[str] = None, port: Optional[int] = None, timeout: Optional[float] = None) -> None: + sock = socket.create_connection( + (host or self.target.host, port or self.target.port), + timeout or self.target.timeout, + ) + sock.close() + + +@dataclass +class NetworkTestCase: + name: str + callback: Callable[[TestContext], Optional[str]] + plugin: str + tags: List[str] = field(default_factory=list) + + def run(self, context: TestContext) -> TestResult: + started_at = time.perf_counter() + try: + summary = self.callback(context) or "ok" + return TestResult( + name=self.name, + ok=True, + summary=summary, + plugin=self.plugin, + duration_ms=(time.perf_counter() - started_at) * 1000.0, + tags=list(self.tags), + ) + except TestFailure as exc: + return TestResult( + name=self.name, + ok=False, + summary=str(exc), + plugin=self.plugin, + duration_ms=(time.perf_counter() - started_at) * 1000.0, + tags=list(self.tags), + ) + except Exception as exc: + return TestResult( + name=self.name, + ok=False, + summary="unexpected error: %s" % exc, + plugin=self.plugin, + duration_ms=(time.perf_counter() - started_at) * 1000.0, + details=repr(exc), + tags=list(self.tags), + ) + + +class TestRegistry: + def __init__(self): + self._cases = [] + self._active_plugin = "" + + def set_plugin(self, plugin_name: str) -> None: + self._active_plugin = plugin_name + + def case(self, name: str, callback: Callable[[TestContext], Optional[str]], tags: Optional[Iterable[str]] = None) -> None: + if not self._active_plugin: + raise RuntimeError("plugin name was not set before registering tests") + self._cases.append( + NetworkTestCase( + name=name, + callback=callback, + plugin=self._active_plugin, + tags=list(tags or []), + ) + ) + + @property + def cases(self) -> List[NetworkTestCase]: + return list(self._cases) + + +def discover_plugin_files(plugin_dir: Path) -> List[Path]: + if not plugin_dir.exists(): + return [] + return sorted( + path for path in plugin_dir.iterdir() + if path.is_file() and path.suffix == ".py" and path.name != "__init__.py" + ) + + +def load_plugin_module(module_path: Path): + module_name = "network_test_plugin_%s" % module_path.stem + spec = importlib.util.spec_from_file_location(module_name, str(module_path)) + if spec is None or spec.loader is None: + raise RuntimeError("could not load plugin %s" % module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def register_plugin_cases(registry: TestRegistry, module_path: Path) -> None: + module = load_plugin_module(module_path) + if not hasattr(module, "register"): + raise RuntimeError("plugin %s does not define register(registry)" % module_path.name) + registry.set_plugin(module_path.stem) + module.register(registry) + + +def filter_cases( + cases: List[NetworkTestCase], + plugins: List[str], + name_pattern: Optional[str], + tags: List[str], + include_internal: bool, +) -> List[NetworkTestCase]: + filtered = list(cases) + if plugins: + filtered = [case for case in filtered if case.plugin in plugins] + if not include_internal: + filtered = [case for case in filtered if "internal" not in case.tags] + if name_pattern: + matcher = re.compile(name_pattern) + filtered = [case for case in filtered if matcher.search(case.name)] + if tags: + filtered = [case for case in filtered if any(tag in case.tags for tag in tags)] + return filtered + + +def print_case_list(cases: List[NetworkTestCase]) -> None: + for case in cases: + tag_suffix = "" + if case.tags: + tag_suffix = " tags=%s" % ",".join(case.tags) + print("%s:%s%s" % (case.plugin, case.name, tag_suffix)) + + +def print_result(result: TestResult) -> None: + status = "PASS" if result.ok else "FAIL" + print("[%s] %s:%s (%.1f ms) - %s" % ( + status, + result.plugin, + result.name, + result.duration_ms, + result.summary, + )) + if result.details: + print(" %s" % result.details) + + +def write_json_report(results: List[TestResult], report_path: Path) -> None: + report_path.write_text(json.dumps([asdict(result) for result in results], indent=2) + "\n", encoding="utf-8") + + +def build_parser() -> argparse.ArgumentParser: + base_dir = Path(__file__).resolve().parent + parser = argparse.ArgumentParser(description="Dependency-less plugin-style network test runner") + parser.add_argument("--base-url", help="full base URL, for example http://k-uce or http://127.0.0.1:8080") + parser.add_argument("--host", default="localhost", help="target host or IP") + parser.add_argument("--port", type=int, default=80, help="target port") + parser.add_argument("--scheme", default="http", choices=["http", "https"], help="target scheme") + parser.add_argument("--host-header", default="uce.openfu.com", help="optional Host header override") + parser.add_argument("--timeout", type=float, default=5.0, help="per-request timeout in seconds") + parser.add_argument("--plugin-dir", default=str(base_dir / "plugins"), help="directory containing plugin files") + parser.add_argument("--plugin", action="append", default=[], help="limit to one or more plugin basenames") + parser.add_argument("--match", help="regex filter for case names") + parser.add_argument("--tag", action="append", default=[], help="only run cases with at least one matching tag") + parser.add_argument("--include-internal", action="store_true", help="include tests tagged internal in the run") + parser.add_argument("--list", action="store_true", help="list discovered tests without running them") + parser.add_argument("--fail-fast", action="store_true", help="stop on first failing test") + parser.add_argument("--json-report", help="write full results to a JSON file") + return parser + + +def build_target(args: argparse.Namespace) -> Target: + if args.base_url: + parsed = urlparse(args.base_url) + if parsed.scheme not in ("http", "https") or not parsed.hostname: + raise SystemExit("--base-url must include an http or https scheme and hostname") + default_port = 443 if parsed.scheme == "https" else 80 + return Target( + scheme=parsed.scheme, + host=parsed.hostname, + port=parsed.port or default_port, + host_header=args.host_header or parsed.hostname, + timeout=args.timeout, + ) + + return Target( + scheme=args.scheme, + host=args.host, + port=args.port, + host_header=args.host_header or None, + timeout=args.timeout, + ) + + +def main(argv: Optional[List[str]] = None) -> int: + args = build_parser().parse_args(argv) + plugin_dir = Path(args.plugin_dir).resolve() + plugin_files = discover_plugin_files(plugin_dir) + if not plugin_files: + print("No plugin files found in %s" % plugin_dir, file=sys.stderr) + return 2 + + registry = TestRegistry() + for plugin_path in plugin_files: + register_plugin_cases(registry, plugin_path) + + cases = filter_cases(registry.cases, args.plugin, args.match, args.tag, args.include_internal) + if not cases: + print("No tests matched the selected filters", file=sys.stderr) + return 2 + + if args.list: + print_case_list(cases) + return 0 + + context = TestContext( + target=build_target(args), + args=args, + ) + + print("Running %s test(s) against %s" % (len(cases), context.target.label)) + if context.target.host_header: + print("Host header: %s" % context.target.host_header) + + results = [] + for case in cases: + result = case.run(context) + results.append(result) + print_result(result) + if args.fail_fast and not result.ok: + break + + if args.json_report: + write_json_report(results, Path(args.json_report).resolve()) + + failures = [result for result in results if not result.ok] + print("") + print("Summary: %s passed, %s failed" % (len(results) - len(failures), len(failures))) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file