fix: harden UCE runtime and starter

This commit is contained in:
udo
2026-06-11 13:44:24 +00:00
parent 71ddcaf7d4
commit 7f757654b6
128 changed files with 276200 additions and 872 deletions
+12 -6
View File
@@ -17,13 +17,19 @@ Theme rendering is split the same way as the PHP starter:
- `themes/common/page.json.uce`
- `themes/<theme>/page.html.uce`
Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes/<theme>/page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation.
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` owns the app-local router, captures the `main` fragment, then hands off to `themes/page.uce`. That component resolves `context.call["app"]["page_type"]` by checking `themes/<current-theme>/page.<page_type>.uce` first and then `themes/common/page.<page_type>.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:
The router is intentionally normal UCE code instead of a runtime feature. It checks `views/<path>.uce`, then `views/<path>/index.uce`, then parent index handlers such as `views/workspace/index.uce` with the last segment stored as `context.call["route"]["param"]`. Matching view files are invoked with `component()`, and view files expose `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are intended to render only through the central router. This gives the starter hierarchical/file-based routing while keeping policy in the app.
- `index.uce`
- `index.uce?page1`
- `index.uce?themes&theme=portal-dark`
- `index.uce?workspace/projects`
Starter-local web affordances live in `components/theme/web_affordances.uce`; currently this provides `COMPONENT:island` for progressive enhancement without adding global UCE runtime APIs. Component-specific CSS/JS is emitted by `ONCE(Request& context)` in the component unit that owns it, or by a small shared asset component when multiple sibling components need the same files. The router's 404 body is also a normal component at `components/basic/notfound.uce`, keeping page fragments out of the front controller.
The example uses query-string routing in the same style as the PHP starter, but the canonical public URL is the directory path because `index.uce` is reached through nginx's default index/try-file behavior. Links generated by the starter therefore target:
- `/examples/uce-starter/`
- `/examples/uce-starter/?page1`
- `/examples/uce-starter/?themes&theme=portal-dark`
- `/examples/uce-starter/?workspace/projects`
Direct requests to `/examples/uce-starter/index.uce` still work, but self-links are canonicalized back to `/examples/uce-starter/`.
The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state.
@@ -4,7 +4,7 @@ COMPONENT(Request& context)
{
String title = first(context.props["title"].to_string(), "Sign in with OAuth");
String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method");
String callback_url = first(context.props["callback_url"].to_string(), starter_link("auth/callback", context));
String callback_url = first(context.props["callback_url"].to_string(), app_link("auth/callback", context));
DTree services = context.props["services"];
if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "")
@@ -46,7 +46,7 @@ COMPONENT(Request& context)
String service_name = service["name"].to_string();
String scope = service["scope"].to_string();
String auth_url = service["auth_url"].to_string();
String store_url = starter_link("auth/store-oauth-session", context);
String store_url = app_link("auth/store-oauth-session", context);
?><div data-service="<?= service_key ?>">
<button type="button" class="btn" onclick='initiateOAuth_<?= handler_name ?>()'>
<i class="<?= service["icon"].to_string() ?>" style="color: <?= service["color"].to_string() ?>"></i>
@@ -2,7 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
<>
<div id="cookie-consent" style="
@@ -0,0 +1,15 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
String message = first(context.props["message"].to_string(), "The requested page does not exist.");
context.set_status(404, "Not Found");
context.call["app"]["page_title"] = "404 Not Found";
<>
<section class="card">
<h1>404 Not Found</h1>
<p><?= message ?></p>
<p>The starter router looked for the route as an exact view, a directory index, and then parent index handlers.</p>
</section>
</>
}
@@ -1,5 +1,15 @@
#load "../../lib/app.uce"
ONCE(Request& context)
{
?><link rel="stylesheet" href="<?= app_asset_url("js/ag-grid/ag-grid.css", context) ?>" />
<link rel="stylesheet" href="<?= app_asset_url("js/ag-grid/ag-theme-alpine.css", context) ?>" />
<script src="<?= app_asset_url("js/u-format.js", context) ?>"></script>
<script src="<?= app_asset_url("js/u-timeseries-chart.js", context) ?>"></script>
<script src="<?= app_asset_url("js/u-sortable-table.js", context) ?>"></script>
<script src="<?= app_asset_url("js/ag-grid/ag-grid-community.min.js", context) ?>"></script><?
}
String data_format_bytes(String raw, bool disk = false)
{
if(trim(raw) == "")
@@ -105,8 +115,6 @@ COMPONENT:SUMMARY_METRICS(Request& context)
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.props["id"].to_string(), "ts-chart-" + std::to_string((u64)time()));
String canvas_id = chart_id + "-canvas";
@@ -148,8 +156,6 @@ COMPONENT:TIMESERIES_CHART(Request& context)
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.props["id"].to_string(), "sortable-table-" + std::to_string((u64)time()));
String title = context.props["title"].to_string();
@@ -214,9 +220,6 @@ COMPONENT:SORTABLE_TABLE(Request& context)
COMPONENT:DATA_TABLE(Request& context)
{
String table_id = first(context.props["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
starter_register_js("js/ag-grid/ag-grid-community.min.js", context);
starter_register_css("js/ag-grid/ag-grid.css", context);
starter_register_css("js/ag-grid/ag-theme-alpine.css", context);
DTree columns = context.props["columns"];
if(columns.get_type_name() != "array")
@@ -2,10 +2,9 @@
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.call["starter"]["route"]["l_path"].to_string();
String route_path = context.call["route"]["l_path"].to_string();
<>
<div id="theme-switcher" style="position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 9999; font-family: inherit;">
@@ -111,7 +110,7 @@ COMPONENT(Request& context)
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String href = starter_link(route_path == "index" ? "" : route_path, params, context);
String href = app_link(route_path == "index" ? "" : route_path, params, context);
?><a class="theme-option<?= theme_key == current_theme ? " is-active" : "" ?>" href="<?= href ?>">
<span><?= theme_info["label"].to_string() ?></span>
<? if(theme_key == current_theme) { ?><small>Active</small><? } ?>
@@ -2,8 +2,10 @@
COMPONENT(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
DTree asset_props;
asset_props["css"]["0"] = "themes/common/css/gauges.css";
asset_props["js"]["0"] = "components/gauges/common.js";
print(component("../theme/assets", asset_props, context));
String gauge_id = first(context.props["id"].to_string(), "arcgauge-" + std::to_string((u64)time()));
String title = context.props["title"].to_string();
@@ -2,8 +2,10 @@
COMPONENT(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
DTree asset_props;
asset_props["css"]["0"] = "themes/common/css/gauges.css";
asset_props["js"]["0"] = "components/gauges/common.js";
print(component("../theme/assets", asset_props, context));
String gauge_id = first(context.props["id"].to_string(), "needlegauge-" + std::to_string((u64)time()));
String style = context.props["style"].to_string();
@@ -2,8 +2,10 @@
COMPONENT(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
DTree asset_props;
asset_props["css"]["0"] = "themes/common/css/gauges.css";
asset_props["js"]["0"] = "components/gauges/common.js";
print(component("../theme/assets", asset_props, context));
String gauge_id = first(context.props["id"].to_string(), "progressbar-" + std::to_string((u64)time()));
String style = context.props["style"].to_string();
@@ -2,7 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser users(context);
DTree user = users.current();
bool signed_in = user["email"].to_string() != "";
@@ -16,14 +15,14 @@ COMPONENT(Request& context)
<? if(signed_in) { ?>
<span class="<?= name_class ?>"><?= first(user["username"].to_string(), user["email"].to_string(), "Account") ?></span>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/profile", context) ?>">Profile</a>
<a href="<?= starter_link("account/logout", context) ?>">Logout</a>
<a href="<?= app_link("account/profile", context) ?>">Profile</a>
<a href="<?= app_link("account/logout", context) ?>">Logout</a>
<? if(links_class != "") { ?></div><? } ?>
<? } else { ?>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/login", context) ?>">Login</a>
<a href="<?= app_link("account/login", context) ?>">Login</a>
<? if(context.cfg.get_by_path("users/enable_signup").to_string() != "") { ?>
<a href="<?= starter_link("account/register", context) ?>">Register</a>
<a href="<?= app_link("account/register", context) ?>">Register</a>
<? } ?>
<? if(links_class != "") { ?></div><? } ?>
<? } ?>
@@ -0,0 +1,39 @@
#load "../../lib/app.uce"
void app_asset_tag_once(Request& context, String kind, String path)
{
if(path == "")
return;
String key = kind + ":" + path;
if(context.call["assets"][key].to_bool())
return;
context.call["assets"][key].set_bool(true);
if(kind == "css")
{
?><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /><?
}
else if(kind == "js")
{
?><script src="<?= app_asset_url(path, context) ?>"></script><?
}
}
void app_asset_tags(Request& context, String kind, DTree assets)
{
String scalar = assets.to_string();
if(scalar != "")
{
app_asset_tag_once(context, kind, scalar);
return;
}
assets.each([&](DTree item, String key) {
app_asset_tag_once(context, kind, item.to_string());
});
}
COMPONENT(Request& context)
@fragment head
{
app_asset_tags(context, "css", context.props["css"]);
app_asset_tags(context, "js", context.props["js"]);
}
@@ -2,11 +2,6 @@
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.props["inner_class"].to_string(), context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "");
@@ -2,10 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
if(starter_page_embed_mode(context))
return;
String cookie_value = context.props["cookie_consent"].to_string();
bool cookie_consent = !(cookie_value == "0" || cookie_value == "false" || cookie_value == "FALSE" || cookie_value == "no" || cookie_value == "NO");
@@ -2,11 +2,10 @@
COMPONENT(Request& context)
{
starter_boot(context);
String title = first(context.call["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string());
String title = first(context.call["app"]["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);
String icon = app_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);
<>
<meta charset="utf-8">
@@ -16,7 +15,11 @@ COMPONENT(Request& context)
<meta name="theme-color" content="<?= theme_color ?>">
<link rel="apple-touch-icon" href="<?= icon ?>" />
<link rel="icon" type="image/png" sizes="32x32" href="<?= icon ?>" />
<? starter_render_registered_css(context); ?>
<? starter_render_registered_js(context); ?>
<link rel="stylesheet" href="<?= app_asset_url(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context) ?>" />
<link rel="stylesheet" href="<?= app_asset_url("themes/common/fontawesome/css/all.min.css", context) ?>" />
<script src="<?= app_asset_url("js/u-query.js", context) ?>"></script>
<script src="<?= app_asset_url("js/morphdom.js", context) ?>"></script>
<script src="<?= app_asset_url("js/site.js", context) ?>"></script>
<?: context.call["fragments"]["head"].to_string() ?>
</>
}
@@ -1,20 +0,0 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
if(context.props["main_html"].to_string() != "")
context.call["starter"]["fragments"]["main"] = context.props["main_html"];
if(context.props["json"].get_type_name() == "array")
context.call["starter"]["json"] = context.props["json"];
if(context.props["page_type"].to_string() != "")
context.call["starter"]["page_type"] = context.props["page_type"];
if(context.props["embed_mode"].to_string() != "")
context.call["starter"]["embed_mode"] = context.props["embed_mode"];
starter_render_page(context);
}
COMPONENT:HEAD(Request& context)
{
print(component("head", context.props, context));
}
@@ -2,7 +2,6 @@
COMPONENT(Request& context)
{
starter_boot(context);
String nav_class = context.props["nav_class"].to_string();
if(nav_class == "" && (context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming"))
nav_class = "nav-shell";
@@ -18,11 +17,12 @@ COMPONENT(Request& context)
<>
<nav<?: nav_class != "" ? " class=\"" + html_escape(nav_class) + "\"" : "" ?>>
<div class="nav-menu">
<a href="<?= starter_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
<a href="<?= app_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
?><a href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
String href = menu_item["external"].to_string() != "" ? "/" + menu_key : app_link(menu_key, context);
?><a href="<?= href ?>"><?= menu_item["title"].to_string() ?></a><?
}); ?>
</div>
<?: component("account_links", account_props, context) ?>
@@ -0,0 +1,18 @@
#load "../../lib/app.uce"
COMPONENT:island(Request& context)
{
String name = context.props["name"].to_string();
String module = context.props["module"].to_string();
String id = first(context.props["id"].to_string(), "island-" + ascii_safe_name(name));
String payload = json_encode(context.props["props"]);
if(name == "")
return;
<>
<div id="<?= id ?>" data-uce-island="<?= name ?>" data-props="<?: html_escape(payload) ?>"></div>
</>
if(module != "")
{
<><script type="module" src="<?= app_asset_url(module, context) ?>"></script></>
}
}
@@ -1,9 +1,13 @@
#load "../../lib/app.uce"
ONCE(Request& context)
{
?><link rel="stylesheet" href="<?= app_asset_url("themes/common/css/workspace.css", context) ?>" />
<script src="<?= app_asset_url("js/u-workspace-shell.js", context) ?>"></script><?
}
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.props["id"].to_string();
String overlay_id = context.props["overlay_id"].to_string();
String class_name = context.props["class"].to_string();
@@ -13,6 +13,7 @@ DTree get_config()
config["theme"]["options"]["light"]["label"] = "Starter Light";
config["theme"]["options"]["light"]["path"] = "themes/light/";
config["theme"]["options"]["light"]["mode"] = "light";
config["theme"]["options"]["light"]["mode-class"] = "";
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";
@@ -21,6 +22,7 @@ DTree get_config()
config["theme"]["options"]["dark"]["label"] = "Starter Dark";
config["theme"]["options"]["dark"]["path"] = "themes/dark/";
config["theme"]["options"]["dark"]["mode"] = "dark";
config["theme"]["options"]["dark"]["mode-class"] = "dark-theme";
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";
@@ -29,6 +31,7 @@ DTree get_config()
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"]["mode-class"] = "";
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";
@@ -37,6 +40,7 @@ DTree get_config()
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"]["mode-class"] = "dark-theme";
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";
@@ -45,6 +49,7 @@ DTree get_config()
config["theme"]["options"]["localfirst"]["label"] = "Local First";
config["theme"]["options"]["localfirst"]["path"] = "themes/localfirst/";
config["theme"]["options"]["localfirst"]["mode"] = "dark";
config["theme"]["options"]["localfirst"]["mode-class"] = "dark-theme";
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";
@@ -53,6 +58,7 @@ DTree get_config()
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"]["mode-class"] = "dark-theme";
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";
+48 -14
View File
@@ -1,29 +1,63 @@
#load "lib/app.uce"
#load "lib/app.uce"
DTree starter_router_result(String file, String param = "")
{
DTree result;
result["file"] = file;
if(param != "")
result["param"] = param;
return(result);
}
DTree starter_router_resolve(Request& context)
{
String lpath = context.call["route"]["l_path"].to_string();
if(lpath == "")
return(DTree());
String exact = "views/" + lpath + ".uce";
if(file_exists(exact))
return(starter_router_result(exact));
String directory_index = "views/" + lpath + "/index.uce";
if(file_exists(directory_index))
return(starter_router_result(directory_index));
auto parts = split(lpath, "/");
while(parts.size() > 1)
{
String param = parts.back();
parts.pop_back();
String parent = join(parts, "/");
String parent_index = "views/" + parent + "/index.uce";
if(file_exists(parent_index))
return(starter_router_result(parent_index, param));
}
return(DTree());
}
RENDER(Request& context)
{
app_init(context);
DTree resolved = app_resolve_view(context);
DTree resolved = starter_router_resolve(context);
ob_start();
if(resolved["file"].to_string() != "")
{
if(resolved["param"].to_string() != "")
context.call["app"]["route"]["param"] = resolved["param"];
unit_render(resolved["file"].to_string(), context);
context.call["route"]["param"] = resolved["param"];
print(component(resolved["file"].to_string(), context));
}
else
{
app_not_found("The requested page does not exist.", context);
<>
<section class="card">
<h1>404 Not Found</h1>
<p><?= context.call["app"]["error"].to_string() ?></p>
</section>
</>
DTree notfound_props;
notfound_props["message"] = "The requested page does not exist.";
print(component("components/basic/notfound", notfound_props, context));
}
String main_html = ob_get_close();
context.call["app"]["fragments"]["main"] = main_html;
app_render_page(context);
context.call["fragments"]["main"] = main_html;
print(component("themes/page", context));
}
+1 -1
View File
@@ -1,4 +1,4 @@
(function (root, factory) { // I really hate this convoluted bullshit
(function (root, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
+17 -368
View File
@@ -1,158 +1,18 @@
#load "../config/settings.uce"
#load "user.class.h"
String app_fs_root(Request& context)
{
String root = context.call["app"]["fs_root"].to_string();
if(root == "")
root = cwd_get();
return(root);
}
String app_script_url(Request& context)
{
String url = context.call["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.call["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.call["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.call["app"]["assets"]["css"][path] = path;
}
void app_register_js(String path, Request& context)
{
context.call["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);
String url = context.params["BASE_URL"] + path;
String fs_path = path_join(dirname(context.params["SCRIPT_FILENAME"]), path);
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.call["app"]["assets"]["css"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><link rel="stylesheet" href="<?= app_asset_url(path, context) ?>" /></>
}
});
}
void app_render_registered_js(Request& context)
{
context.call["app"]["assets"]["js"].each([&](DTree item, String key) {
String path = item.to_string();
if(path != "")
{
<><script src="<?= app_asset_url(path, context) ?>"></script></>
}
});
}
String app_link(String path, Request& context)
{
StringMap params;
@@ -170,249 +30,38 @@ String app_link(String path, StringMap params, Request& context)
query += "&" + extra;
else if(query == "")
query = extra;
String url = app_script_url(context);
String url = context.params["SCRIPT_URL"];
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.call["app"]["error"] = message;
context.call["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.props["main_html"].to_string();
if(main_html == "")
main_html = context.call["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.call["app"]["embed_mode"]));
}
bool app_page_embed_mode(Request& context)
{
String embed_value = context.props["embed_mode"].to_string();
if(embed_value != "")
return(app_bool_value(context.props["embed_mode"]));
return(app_request_embed_mode(context));
}
String app_theme_page_component(Request& context)
{
String page_type = first(context.call["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.call["app"]["fragments"]["main"];
if(context.call["app"]["json"].get_type_name() == "array")
page_props["json"] = context.call["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.call["app"]["booted"].to_string() == "1")
return;
context.call["app"]["booted"] = "1";
context.call["app"]["fs_root"] = cwd_get();
context.call["app"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
String base_url = dirname(context.call["app"]["script_url"].to_string());
if(base_url == "")
base_url = "/";
if(base_url[base_url.length() - 1] != '/')
base_url.append(1, '/');
context.call["app"]["base_url"] = base_url;
context.call["route"]["raw_path"] = context.params["ROUTE_PATH_RAW"];
context.call["route"]["l_path"] = context.params["ROUTE_PATH"];
context.call["route"]["page"] = context.params["ROUTE_PAGE"];
context.call["route"]["valid"].set_bool(context.params["ROUTE_VALID"] != "0");
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")
session_start();
String requested_theme = first(context.get["theme"], context.session["app_theme"], config["theme"]["key"].to_string());
if(config["theme"]["options"][requested_theme].get_type_name() != "array")
requested_theme = config["theme"]["key"].to_string();
if(context.get["theme"] != "")
context.session["app_theme"] = requested_theme;
config["theme"]["options"][requested_theme].each([&](DTree item, String key) {
DTree selected_theme = config["theme"]["options"][requested_theme];
selected_theme.each([&](DTree item, String key) {
config["theme"][key] = item;
});
config["theme"]["key"] = requested_theme;
context.cfg = config;
context.call["cfg"].set_reference(&context.cfg);
context.call["app"]["config"].set_reference(&context.cfg);
context.call["app"]["route"] = app_make_route(context);
context.call["app"]["page_type"] = "html";
context.call["app"]["page_title"] = first(config["menu"][context.call["app"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
context.call["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();
context.call["app"]["page_title"] = first(config["menu"][context.call["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
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);
}
@@ -2,6 +2,5 @@
COMPONENT(Request& context)
{
starter_boot(context);
print(starter_page_main_html(context));
print(context.call["fragments"]["main"].to_string());
}
@@ -2,13 +2,7 @@
COMPONENT(Request& context)
{
starter_boot(context);
context.header["Content-Type"] = "application/json";
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
if(context.props["json"].get_type_name() == "array")
print(json_encode(context.props["json"]));
else if(context.call["starter"]["json"].get_type_name() == "array")
print(json_encode(context.call["starter"]["json"]));
else
print(starter_page_main_html(context));
print(context.call["fragments"]["main"].to_string());
}
@@ -2,26 +2,23 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<body>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
@@ -2,23 +2,20 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<body>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
@@ -2,10 +2,8 @@
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.call["starter"]["route"]["l_path"].to_string();
String main_html = context.call["fragments"]["main"].to_string();
String current_path = context.call["route"]["l_path"].to_string();
DTree global_props;
global_props["cookie_consent"].set_bool(false);
@@ -17,32 +15,31 @@ COMPONENT(Request& context)
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="admin-page localfirst-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
<body class="admin-page localfirst-theme dark-theme">
<?: component("../../components/theme/global_controls", global_props, context) ?>
<div class="admin-shell">
<nav class="admin-nav">
<div class="admin-nav-header">
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
<a class="admin-nav-title" href="<?= app_link("", context) ?>">
<img class="admin-nav-logo-img" src="<?= app_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
</a>
</div>
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
bool active = menu_key != "" && (current_path == menu_key || current_path.rfind(menu_key + "/", 0) == 0);
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
String href = menu_item["external"].to_string() != "" ? "/" + menu_key : app_link(menu_key, context);
?><a class="admin-nav-item<?= active ? " active" : "" ?>" href="<?= href ?>"><?= menu_item["title"].to_string() ?></a><?
}); ?>
</nav>
<div class="admin-main">
<? if(!embed_mode) { ?>
<div class="admin-toolbar">
<?: component("../../components/theme/account_links", account_props, context) ?>
</div>
<? } ?>
<div class="admin-toolbar">
<?: component("../../components/theme/account_links", account_props, context) ?>
</div>
<div id="content" class="admin-content">
<?: main_html ?>
</div>
+22
View File
@@ -0,0 +1,22 @@
#load "../lib/app.uce"
COMPONENT(Request& context)
{
if(context.flags.status >= 300 && context.flags.status < 400)
return;
String page_type = first(context.call["app"]["page_type"].to_string(), "html");
String theme_path = "../" + context.cfg.get_by_path("theme/path").to_string();
String page_component = theme_path + "page." + page_type + ".uce";
if(!file_exists(page_component))
page_component = "../themes/common/page." + page_type + ".uce";
if(file_exists(page_component))
print(component(page_component, context));
else
{
context.set_status(500, "Internal Server Error");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("UCE starter is missing a page template for page type: " + page_type);
}
}
@@ -2,26 +2,23 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="portal-theme portal-dark-theme dark-theme<?= embed_mode ? " embed-mode" : "" ?>">
<body class="portal-theme portal-dark-theme dark-theme">
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
@@ -2,24 +2,21 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
footer_props["inner_class"] = "footer-inner";
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body class="portal-theme portal-light-theme<?= embed_mode ? " embed-mode" : "" ?>">
<body class="portal-theme portal-light-theme">
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
@@ -2,27 +2,24 @@
COMPONENT(Request& context)
{
starter_boot(context);
bool embed_mode = starter_page_embed_mode(context);
String main_html = starter_page_main_html(context);
String main_html = context.call["fragments"]["main"].to_string();
DTree nav_props;
nav_props["account_wrapper_class"] = "nav-account nav-menu";
DTree footer_props;
footer_props["embed_mode"].set_bool(embed_mode);
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<html class="no-js <?= context.cfg.get_by_path("theme/mode-class").to_string() ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<?: component("../../components/theme/head", context) ?>
</head>
<body<?: embed_mode ? " class=\"embed-mode\"" : "" ?>>
<body>
<div class="retro-stars"></div>
<?: component("../../components/theme/global_controls", context) ?>
<?: component("../../components/theme/standard_nav", nav_props, context) ?>
<div id="content"<?: embed_mode ? " class=\"embed-content\"" : "" ?>>
<div id="content">
<?: main_html ?>
</div>
<?: component("../../components/theme/footer", footer_props, context) ?>
@@ -1,9 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Login";
context.call["app"]["page_title"] = "Login";
StarterUser users(context);
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
@@ -11,7 +10,7 @@ RENDER(Request& context)
result = users.sign_in(context.post["email"], context.post["password"]);
if(result["result"].to_string() != "")
{
starter_redirect("account/profile", context);
redirect(app_link("account/profile", context));
return;
}
}
@@ -23,6 +22,6 @@ RENDER(Request& context)
<label>Password: <input type="password" name="password" required></label><br>
<button type="submit">Login</button>
</form>
<p><a href="<?= starter_link("account/register", context) ?>">Register</a></p>
<p><a href="<?= app_link("account/register", context) ?>">Register</a></p>
</>
}
@@ -1,8 +1,7 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser(context).logout();
starter_redirect("account/login", context);
redirect(app_link("account/login", context));
}
@@ -1,15 +1,14 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
StarterUser users(context);
if(!users.is_signed_in())
{
starter_redirect("account/login", context);
redirect(app_link("account/login", context));
return;
}
context.call["starter"]["page_title"] = "Profile";
context.call["app"]["page_title"] = "Profile";
DTree user = users.current();
String roles = "";
user["roles"].each([&](DTree role, String key) {
@@ -22,6 +21,6 @@ RENDER(Request& context)
<p>Email: <?= user["email"].to_string() ?></p>
<p>Roles: <?= roles ?></p>
<p>Created: <?= time_format_local("%Y-%m-%dT%H:%M:%S", int_val(user["created"].to_string())) ?></p>
<p><a href="<?= starter_link("account/logout", context) ?>">Logout</a></p>
<p><a href="<?= app_link("account/logout", context) ?>">Logout</a></p>
</>
}
@@ -1,9 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Register";
context.call["app"]["page_title"] = "Register";
StarterUser users(context);
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
@@ -11,7 +10,7 @@ RENDER(Request& context)
<>
<h1>Register</h1>
<? if(result["result"].to_string() != "") { ?><div class="banner success">Registration successful. <a href="<?= starter_link("account/login", context) ?>">Log in</a></div><? } ?>
<? if(result["result"].to_string() != "") { ?><div class="banner success">Registration successful. <a href="<?= app_link("account/login", context) ?>">Log in</a></div><? } ?>
<? if(result["message"].to_string() != "" && result["result"].to_string() == "") { ?><div class="banner error"><?= result["message"].to_string() ?></div><? } ?>
<form method="post">
<label>Email: <input type="email" name="email" required></label><br>
@@ -1,9 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "OAuth Callback";
context.call["app"]["page_title"] = "OAuth Callback";
String code = context.get["code"];
String state = context.get["state"];
String error = context.get["error"];
@@ -18,8 +17,8 @@ RENDER(Request& context)
<h2>Authentication Failed</h2>
<p><?= first(error_description, error) ?></p>
<div class="callback-actions">
<a href="<?= starter_link("auth/demo", context) ?>" class="btn btn-primary">Try Again</a>
<a href="<?= starter_link("", context) ?>" class="btn btn-outline">Go Home</a>
<a href="<?= app_link("auth/demo", context) ?>" class="btn btn-primary">Try Again</a>
<a href="<?= app_link("", context) ?>" class="btn btn-outline">Go Home</a>
</div>
</div>
<? } else if(code != "" && state != "") { ?>
@@ -33,8 +32,8 @@ RENDER(Request& context)
<p><strong>State:</strong> <?= state ?></p>
</div>
<div class="callback-actions">
<a href="<?= starter_link("auth/demo", context) ?>" class="btn btn-primary">Back to Auth Demo</a>
<a href="<?= starter_link("", context) ?>" class="btn btn-outline">Go Home</a>
<a href="<?= app_link("auth/demo", context) ?>" class="btn btn-primary">Back to Auth Demo</a>
<a href="<?= app_link("", context) ?>" class="btn btn-outline">Go Home</a>
</div>
</div>
<? } else { ?>
@@ -1,10 +1,11 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Auth";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Auth";
DTree props;
props["title"] = "Sign In to Your Account";
@@ -13,7 +14,7 @@ RENDER(Request& context)
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);
props["callback_url"] = app_link("auth/callback", context);
<>
<h1>Authentication Demo</h1>
@@ -1,21 +1,24 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_type"] = "json";
context.call["app"]["page_type"] = "json";
DTree response;
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.call["starter"]["json"]["status"] = "success";
response["status"] = "success";
}
else
{
context.set_status(400, "Bad Request");
context.call["starter"]["json"]["status"] = "error";
context.call["starter"]["json"]["message"] = "Invalid input";
response["status"] = "error";
response["message"] = "Invalid input";
}
print(json_encode(response));
}
@@ -1,10 +1,13 @@
#load "../lib/app.uce"
RENDER(Request& context)
ONCE(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Dashboard";
starter_register_css("views/dashboard.css", context);
?><link rel="stylesheet" href="<?= app_asset_url("views/dashboard.css", context) ?>" /><?
}
COMPONENT(Request& context)
{
context.call["app"]["page_title"] = "Dashboard";
DTree props;
DTree item;
+5 -4
View File
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Features";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Features";
<>
<h1>Features</h1>
<p class="card">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.</p>
+2 -4
View File
@@ -1,10 +1,8 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Gauges";
starter_register_css("themes/common/css/gauges.css", context);
context.call["app"]["page_title"] = "Gauges";
f64 pi = 3.14159265358979323846;
DTree props;
+14 -6
View File
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Home";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Home";
DTree props;
@@ -79,13 +80,13 @@ RENDER(Request& context)
?><div class="component-card">
<h3><?= theme["label"].to_string() ?></h3>
<p><?= theme["description"].to_string() ?></p>
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Preview Theme</a>
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Preview Theme</a>
</div><?
}); ?>
<div class="component-card">
<h3>Starter Themes</h3>
<p>The original light and dark starter skins remain available in the same gallery for side-by-side checks.</p>
<a class="btn btn-secondary" href="<?= starter_link("themes", context) ?>">Open Gallery</a>
<a class="btn btn-secondary" href="<?= app_link("themes", context) ?>">Open Gallery</a>
</div>
</div>
</div>
@@ -98,9 +99,16 @@ RENDER(Request& context)
props["secondary_text"] = "View GitHub";
print(component("../components/example/marketing_blocks:CTA_SECTION", props, context));
DTree island_props;
island_props["name"] = "StarterGuidelines";
island_props["id"] = "starter-guidelines-island";
island_props["props"]["route"] = context.call["route"]["l_path"].to_string();
island_props["props"]["enhancement"] = "progressive";
<>
<div class="card">
<h2>Component Development Guidelines</h2>
<?: component("../components/theme/web_affordances:island", island_props, context) ?>
<div class="guidelines-grid">
<div class="guideline-item" style="border-left-color: var(--primary);"><span class="guideline-icon">🏷️</span><span>Use semantic HTML5 elements</span></div>
<div class="guideline-item" style="border-left-color: var(--secondary);"><span class="guideline-icon">🎨</span><span>Implement CSS custom properties for theming</span></div>
+5 -4
View File
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Components";
starter_register_css("views/marketing.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/marketing.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Components";
<>
<h1>Component System</h1>
@@ -1,9 +1,8 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_type"] = "blank";
context.call["app"]["page_type"] = "blank";
<>
<?= std::to_string((u64)time()) ?> - Page 2 Section 1 loaded
<pre>UCE starter AJAX fragment response</pre>
+3 -4
View File
@@ -1,16 +1,15 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Ajaxy";
context.call["app"]["page_title"] = "Ajaxy";
<>
<h1>Ajax Demo</h1>
<div id="page2-section1">
<p>This is the content of Page 2. You can add more information here.</p>
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
<br/>
<button id="page2-load-button" data-load-url="<?= starter_link("page2-section1", context) ?>" type="button">Load new text</button>
<button id="page2-load-button" data-load-url="<?= app_link("page2-section1", context) ?>" type="button">Load new text</button>
</div>
<script>
(function () {
@@ -1,10 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Theme Preview";
starter_register_css("views/themes.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/themes.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Theme Preview";
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();
@@ -18,8 +19,8 @@ RENDER(Request& context)
<h1><?= theme_label ?></h1>
<p>This route renders the same neutral content inside each theme so downstream projects can compare layout, typography, color tokens, and chrome.</p>
<div class="theme-preview-actions">
<a class="btn" href="<?= starter_link("themes", theme_params, context) ?>">Back to Gallery</a>
<a class="btn btn-secondary" href="<?= starter_link("", theme_params, context) ?>">Open Home in This Theme</a>
<a class="btn" href="<?= app_link("themes", theme_params, context) ?>">Back to Gallery</a>
<a class="btn btn-secondary" href="<?= app_link("", theme_params, context) ?>">Open Home in This Theme</a>
</div>
</section>
<section class="theme-preview-grid">
@@ -59,7 +60,6 @@ RENDER(Request& context)
<tbody>
<tr><td>Navigation</td><td>Should remain readable when menus get long.</td><td>Verified</td></tr>
<tr><td>Cards</td><td>Should keep spacing and hierarchy on both desktop and mobile.</td><td>Verified</td></tr>
<tr><td>Embeds</td><td>Should render cleanly inside the gallery iframe.</td><td>Verified</td></tr>
</tbody>
</table>
</div>
@@ -82,21 +82,6 @@
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));
@@ -150,20 +135,6 @@
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 {
@@ -173,8 +144,4 @@
.theme-gallery-head {
flex-direction: column;
}
.theme-gallery-frame-wrap iframe {
height: 360px;
}
}
+9 -13
View File
@@ -1,26 +1,25 @@
#load "../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Themes";
starter_register_css("views/themes.css", context);
DTree asset_props;
asset_props["css"]["0"] = "views/themes.css";
print(component("../components/theme/assets", asset_props, context));
context.call["app"]["page_title"] = "Themes";
String current_theme = context.cfg.get_by_path("theme/key").to_string();
<>
<div class="theme-gallery-shell">
<section class="theme-gallery-hero card">
<span class="theme-gallery-kicker">Starter Theme Gallery</span>
<h1>Compare Themes Side by Side</h1>
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
<h1>Compare Themes</h1>
<p>Open each theme preview directly to judge shell fit, typography, and token balance.</p>
</section>
<div class="theme-gallery-grid">
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String preview_href = starter_link("theme-preview", params, context);
params["embed"] = "1";
String iframe_href = starter_link("theme-preview", params, context);
String preview_href = app_link("theme-preview", params, context);
StringMap home_params;
home_params["theme"] = theme_key;
?><section class="theme-gallery-card<?= theme_key == current_theme ? " is-active" : "" ?>">
@@ -33,10 +32,7 @@ RENDER(Request& context)
</div>
<div class="theme-gallery-actions">
<a class="btn" href="<?= preview_href ?>">Open Preview</a>
<a class="btn btn-secondary" href="<?= starter_link("", home_params, context) ?>">Open Home</a>
</div>
<div class="theme-gallery-frame-wrap">
<iframe title="<?= theme_info["label"].to_string() ?> preview" src="<?= iframe_href ?>"></iframe>
<a class="btn btn-secondary" href="<?= app_link("", home_params, context) ?>">Open Home</a>
</div>
</section><?
}); ?>
@@ -1,10 +1,9 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
context.call["starter"]["page_title"] = "Workspace";
String section = first(context.call["starter"]["route"]["param"].to_string(), "overview");
context.call["app"]["page_title"] = "Workspace";
String section = first(context.call["route"]["param"].to_string(), "overview");
if(section != "overview" && section != "projects" && section != "activity")
section = "overview";
@@ -19,9 +18,9 @@ RENDER(Request& context)
<>
<div class="ws-nav-group-label">Workspace areas</div>
<div class="ws-nav-list">
<a class="ws-nav-item<?= section == "overview" ? " is-active" : "" ?>" href="<?= starter_link("workspace/overview", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-compass"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Overview</span><span class="ws-nav-meta">Shell</span></span></span></a>
<a class="ws-nav-item<?= section == "projects" ? " is-active" : "" ?>" href="<?= starter_link("workspace/projects", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-folder-tree"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Projects</span><span class="ws-nav-meta">Routes</span></span></span></a>
<a class="ws-nav-item<?= section == "activity" ? " is-active" : "" ?>" href="<?= starter_link("workspace/activity", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-wave-square"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Activity</span><span class="ws-nav-meta">State</span></span></span></a>
<a class="ws-nav-item<?= section == "overview" ? " is-active" : "" ?>" href="<?= app_link("workspace/overview", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-compass"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Overview</span><span class="ws-nav-meta">Shell</span></span></span></a>
<a class="ws-nav-item<?= section == "projects" ? " is-active" : "" ?>" href="<?= app_link("workspace/projects", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-folder-tree"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Projects</span><span class="ws-nav-meta">Routes</span></span></span></a>
<a class="ws-nav-item<?= section == "activity" ? " is-active" : "" ?>" href="<?= app_link("workspace/activity", context) ?>"><span class="ws-nav-item-inner"><span class="ws-nav-icon"><i class="fas fa-wave-square"></i></span><span class="ws-nav-item-text"><span class="ws-nav-title">Activity</span><span class="ws-nav-meta">State</span></span></span></a>
</div>
<div class="ws-demo-sidebar-copy"><p>This sidebar and mobile shell pattern was extracted from the AI portal app and normalized against starter theme tokens.</p></div>
</>
@@ -32,7 +31,7 @@ RENDER(Request& context)
sidebar_body += component("../../components/workspace/primitives:LIST_STATE", sidebar_note, context);
DTree toolbar_props;
toolbar_props["action_html"] = "<a class=\"ws-sidebar-action-btn\" href=\"" + starter_link("workspace/overview", context) + "\"><i class=\"fas fa-grid-2\"></i><span>Open shell</span></a>";
toolbar_props["action_html"] = "<a class=\"ws-sidebar-action-btn\" href=\"" + app_link("workspace/overview", context) + "\"><i class=\"fas fa-grid-2\"></i><span>Open shell</span></a>";
toolbar_props["search_input_id"] = "workspace-demo-search";
toolbar_props["search_input_name"] = "workspace_demo_search";
toolbar_props["search_placeholder"] = "Search workspace sections";
@@ -95,7 +94,7 @@ RENDER(Request& context)
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 + "<p class=\"ws-inline-note\">Try visiting <strong>" + html_escape(starter_link("workspace/projects", context)) + "</strong> or <strong>" + html_escape(starter_link("workspace/activity", context)) + "</strong> to see the nested route fallback in action.</p>";
section_props["body_html"] = detail_html + "<p class=\"ws-inline-note\">Try visiting <strong>" + html_escape(app_link("workspace/projects", context)) + "</strong> or <strong>" + html_escape(app_link("workspace/activity", context)) + "</strong> to see the nested route fallback in action.</p>";
main_body += component("../../components/workspace/primitives:SECTION", section_props, context);
if(section == "activity")
@@ -104,7 +103,7 @@ RENDER(Request& context)
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"] = "<a class=\"ws-primary-btn\" href=\"" + starter_link("dashboard", context) + "\">Open dashboard demo</a>";
empty_props["action_html"] = "<a class=\"ws-primary-btn\" href=\"" + app_link("dashboard", context) + "\">Open dashboard demo</a>";
main_body += component("../../components/workspace/primitives:EMPTY_STATE", empty_props, context);
}