fix: harden UCE runtime and starter
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user