trying to port web app starter from PHP

This commit is contained in:
udo
2026-04-19 09:38:23 +00:00
parent 46d98a092f
commit be514d63d6
546 changed files with 76910 additions and 2807 deletions
@@ -0,0 +1,27 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Login";
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
{
result = starter_user_sign_in(context.post["email"], context.post["password"], context);
if(result["result"].to_string() != "")
{
starter_redirect("account/profile", context);
return;
}
}
<>
<h1>Login</h1>
<? if(result["message"].to_string() != "") { ?><div class="banner error"><?= result["message"].to_string() ?></div><? } ?>
<form method="post">
<label>Email: <input type="email" name="email" required></label><br>
<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>
</>
}
@@ -0,0 +1,8 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
starter_user_logout(context);
starter_redirect("account/login", context);
}
@@ -0,0 +1,26 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
if(!starter_is_signed_in(context))
{
starter_redirect("account/login", context);
return;
}
context.var["starter"]["page_title"] = "Profile";
DTree user = starter_current_user(context);
String roles = "";
user["roles"].each([&](DTree role, String key) {
if(roles != "")
roles += ", ";
roles += role.to_string();
});
<>
<h1>Profile</h1>
<p>Email: <?= user["email"].to_string() ?></p>
<p>Roles: <?= roles ?></p>
<p>Created: <?= date("%Y-%m-%dT%H:%M:%S", int_val(user["created"].to_string())) ?></p>
<p><a href="<?= starter_link("account/logout", context) ?>">Logout</a></p>
</>
}
@@ -0,0 +1,21 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Register";
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
result = starter_user_create(context.post["email"], context.post["password"], 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["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>
<label>Password: <input type="password" name="password" required></label><br>
<button type="submit">Register</button>
</form>
</>
}
@@ -0,0 +1,50 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "OAuth Callback";
String code = context.get["code"];
String state = context.get["state"];
String error = context.get["error"];
String error_description = context.get["error_description"];
<>
<div class="oauth-callback-page">
<div class="callback-container">
<? if(error != "") { ?>
<div class="callback-result error">
<div class="result-icon">✗</div>
<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>
</div>
</div>
<? } else if(code != "" && state != "") { ?>
<div class="callback-result success">
<div class="result-icon">✓</div>
<h2>Authentication Successful</h2>
<p>OAuth callback received successfully! Demo mode is active until real provider credentials are configured.</p>
<div class="callback-details">
<p><strong>Service:</strong> <?= first(context.session["oauth_service"], "unknown") ?></p>
<p><strong>Code:</strong> <?= code.substr(0, std::min((u64)20, (u64)code.length())) ?>...</p>
<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>
</div>
</div>
<? } else { ?>
<div class="callback-result error">
<div class="result-icon">✗</div>
<h2>Invalid Callback</h2>
<p>Missing required OAuth parameters.</p>
</div>
<? } ?>
</div>
</div>
</>
}
@@ -0,0 +1,40 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Auth";
starter_register_css("views/marketing.css", context);
DTree props;
props["title"] = "Sign In to Your Account";
props["subtitle"] = "Choose your preferred authentication method to continue";
props["google_client_id"] = "YOUR_GOOGLE_CLIENT_ID";
props["github_client_id"] = "YOUR_GITHUB_CLIENT_ID";
props["discord_client_id"] = "YOUR_DISCORD_CLIENT_ID";
props["twitch_client_id"] = "YOUR_TWITCH_CLIENT_ID";
props["callback_url"] = starter_link("auth/callback", context);
<>
<h1>Authentication Demo</h1>
<div class="card">
<h2>OAuth Authentication</h2>
<p>This component provides secure OAuth authentication with popular identity providers.</p>
<? print(component("../../components/auth/oauth-client", props, context)); ?>
</div>
<div class="card">
<h2>Current Session Status</h2>
<? if(context.session["starter_user_id"] != "") { ?>
<div class="success-card">
<h3>Logged In</h3>
<p>User ID: <?= context.session["starter_user_id"] ?></p>
</div>
<? } else { ?>
<div class="component-card">
<h3>Not Logged In</h3>
<p>Use the OAuth component above to sign in with Google, GitHub, Discord, or Twitch.</p>
</div>
<? } ?>
</div>
</>
}
@@ -0,0 +1,21 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_type"] = "json";
DTree body = json_decode(context.in);
if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "")
{
context.session["oauth_service"] = body["oauth_service"].to_string();
context.session["oauth_state"] = body["oauth_state"].to_string();
context.var["starter"]["json"]["status"] = "success";
}
else
{
starter_set_status(context, 400, "Bad Request");
context.var["starter"]["json"]["status"] = "error";
context.var["starter"]["json"]["message"] = "Invalid input";
}
}
@@ -0,0 +1,205 @@
.dashboard-intro p,
.dashboard-panel-header p,
.dashboard-note {
color: var(--text-secondary);
max-width: 72ch;
}
.dashboard-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
margin-bottom: 2rem;
overflow: hidden;
padding: 1.5rem;
}
.dashboard-panel-header {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 1.25rem;
}
.dashboard-panel-header h2 {
font-size: 1.4rem;
margin-bottom: 0;
}
.dashboard-stat-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.dashboard-stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
color: inherit;
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 1rem;
text-decoration: none;
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
position: relative;
overflow: hidden;
}
.dashboard-stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
text-decoration: none;
}
.dashboard-stat-card::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: var(--primary);
}
.dashboard-stat-card.tone-success::before {
background: var(--success);
}
.dashboard-stat-card.tone-warning::before {
background: var(--warning);
}
.dashboard-stat-card.tone-danger::before {
background: var(--error);
}
.dashboard-stat-card.tone-info::before {
background: var(--info);
}
.dashboard-stat-label {
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.dashboard-stat-value {
font-size: clamp(1.6rem, 2vw, 2.1rem);
font-variant-numeric: tabular-nums;
font-weight: 700;
line-height: 1.1;
}
.dashboard-stat-meta {
color: var(--text-secondary);
font-size: 0.92rem;
}
.dashboard-chart-canvas {
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.05), transparent 45%),
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(15, 23, 42, 0.08));
border: 1px solid var(--border);
border-radius: var(--radius);
display: block;
width: 100%;
min-height: 180px;
}
.dashboard-table-wrap {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: var(--radius);
}
.u-sortable-table {
border-collapse: collapse;
font-size: 0.95rem;
min-width: 720px;
width: 100%;
}
.u-sortable-table thead th {
background: var(--surface-elevated);
border-bottom: 1px solid var(--border);
color: var(--text-primary);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.05em;
padding: 0.85rem 1rem;
position: relative;
text-transform: uppercase;
white-space: nowrap;
}
.u-sortable-table thead th.sortable {
cursor: pointer;
padding-right: 2rem;
user-select: none;
}
.u-sortable-table thead th.sortable::after {
content: ':';
color: var(--text-muted);
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.8rem;
}
.u-sortable-table thead th.sorted-asc::after {
content: '^';
color: var(--primary);
}
.u-sortable-table thead th.sorted-desc::after {
content: 'v';
color: var(--primary);
}
.u-sortable-table tbody tr:nth-child(odd) {
background: rgba(148, 163, 184, 0.05);
}
.u-sortable-table tbody tr:hover {
background: rgba(96, 165, 250, 0.08);
}
.u-sortable-table td {
border-bottom: 1px solid var(--border);
padding: 0.85rem 1rem;
vertical-align: top;
font-variant-numeric: tabular-nums;
}
.u-sortable-table tbody tr:last-child td {
border-bottom: none;
}
.u-sortable-table .align-right {
text-align: right;
}
.u-sortable-table .align-center {
text-align: center;
}
.u-sortable-table .muted {
color: var(--text-secondary);
text-align: center;
}
@media (max-width: 768px) {
.dashboard-panel {
padding: 1rem;
}
.u-sortable-table {
min-width: 600px;
}
}
@@ -0,0 +1,70 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Dashboard";
starter_register_css("views/dashboard.css", context);
DTree props;
DTree item;
item["label"] = "24h Requests"; item["value"] = "18,420"; item["meta"] = "+12.8% vs yesterday"; item["tone"] = "info"; props["items"].push(item); item.clear();
item["label"] = "Median Latency"; item["value"] = "182 ms"; item["meta"] = "stable over last 7 samples"; item["tone"] = "success"; props["items"].push(item); item.clear();
item["label"] = "Resident Memory"; item["value"] = "2.4 GB"; item["meta"] = "combined across workers"; item["tone"] = "warning"; props["items"].push(item); item.clear();
item["label"] = "Healthy Services"; item["value"] = "3 / 4"; item["meta"] = "one degraded background worker"; item["tone"] = "danger"; props["items"].push(item);
props["title"] = "Starter-Friendly Overview Cards";
props["subtitle"] = "Small summary tiles work well across admin pages, internal tools, and SSR dashboards.";
<>
<h1>Dashboard Primitives</h1>
<div class="card dashboard-intro"><p>This page is the first backport slice from the LocalAI dashboard frontend. It keeps the generic parts that fit the starter itself.</p></div>
</>
print(component("../components/data/widgets:SUMMARY_METRICS", props, context));
props.clear();
DTree series;
DTree values;
DTree entry;
entry["key"] = "requests"; entry["label"] = "Requests"; entry["color"] = "#60a5fa"; entry["axis"] = "left"; entry["format"] = "count";
values = "240"; entry["values"].push(values); values = "268"; entry["values"].push(values); values = "294"; entry["values"].push(values); values = "322"; entry["values"].push(values); values = "301"; entry["values"].push(values); values = "356"; entry["values"].push(values); values = "388"; entry["values"].push(values);
props["series"].push(entry); entry.clear();
entry["key"] = "latency"; entry["label"] = "Latency"; entry["color"] = "#f59e0b"; entry["axis"] = "right"; entry["format"] = "duration-ms";
values = "182"; entry["values"].push(values); values = "176"; entry["values"].push(values); values = "191"; entry["values"].push(values); values = "204"; entry["values"].push(values); values = "188"; entry["values"].push(values); values = "166"; entry["values"].push(values); values = "159"; entry["values"].push(values);
props["series"].push(entry);
values = "08:00"; props["x_labels"].push(values); values = "09:00"; props["x_labels"].push(values); values = "10:00"; props["x_labels"].push(values); values = "11:00"; props["x_labels"].push(values); values = "12:00"; props["x_labels"].push(values); values = "13:00"; props["x_labels"].push(values); values = "14:00"; props["x_labels"].push(values);
props["id"] = "dashboard-demo-traffic";
props["title"] = "Requests vs Latency";
props["subtitle"] = "Same generic chart primitive can track throughput, job backlog, token volume, or queue time.";
props["height"] = "320";
props["x_axis_label"] = "Last 7 Hours";
props["y_axis_left_label"] = "Requests";
props["y_axis_right_label"] = "Latency";
props["y_axis_left_format"] = "count";
props["y_axis_right_format"] = "duration-ms";
print(component("../components/data/widgets:TIMESERIES_CHART", props, context));
props.clear();
props["id"] = "dashboard-service-table";
props["title"] = "Service Snapshot";
props["subtitle"] = "Vanilla HTML table enhancement for cases where ag-Grid is overkill.";
props["storage_key"] = "starter.dashboard.services";
props["sort"]["column"] = "2";
props["sort"]["direction"] = "desc";
DTree col;
col["key"] = "service"; col["label"] = "Service"; props["columns"].push(col); col.clear();
col["key"] = "uptime"; col["label"] = "Uptime"; props["columns"].push(col); col.clear();
col["key"] = "requests"; col["label"] = "Requests"; col["align"] = "right"; col["format"] = "number"; props["columns"].push(col); col.clear();
col["key"] = "memory"; col["label"] = "Memory"; col["align"] = "right"; col["format"] = "bytes"; props["columns"].push(col); col.clear();
col["key"] = "p95_latency"; col["label"] = "P95 Latency"; col["align"] = "right"; col["format"] = "duration-ms"; props["columns"].push(col); col.clear();
col["key"] = "healthy"; col["label"] = "Healthy"; col["align"] = "center"; col["format"] = "bool"; props["columns"].push(col);
DTree row;
row["service"] = "router"; row["uptime"] = "12 days"; row["requests"] = "142890"; row["memory"] = "402653184"; row["p95_latency"] = "148"; row["healthy"] = "true"; props["rows"].push(row); row.clear();
row["service"] = "queue-worker"; row["uptime"] = "8 days"; row["requests"] = "98214"; row["memory"] = "654311424"; row["p95_latency"] = "231"; row["healthy"] = "true"; props["rows"].push(row); row.clear();
row["service"] = "vector-index"; row["uptime"] = "5 days"; row["requests"] = "44892"; row["memory"] = "1241513984"; row["p95_latency"] = "312"; row["healthy"] = "true"; props["rows"].push(row); row.clear();
row["service"] = "sandbox"; row["uptime"] = "19 hours"; row["requests"] = "12810"; row["memory"] = "295698432"; row["p95_latency"] = "418"; row["healthy"] = "false"; props["rows"].push(row);
print(component("../components/data/widgets:SORTABLE_TABLE", props, context));
<>
<div class="card"><h2>What Was Backported</h2><p class="dashboard-note">The charting logic and data-formatting ideas come directly from the more complex dashboard frontend, but the starter version is stripped down to generic building blocks.</p></div>
</>
}
@@ -0,0 +1,13 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Features";
starter_register_css("views/marketing.css", context);
<>
<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>
</>
print(component("../components/example/marketing_blocks:FEATURES_GRID", context));
}
@@ -0,0 +1,90 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Gauges";
starter_register_css("themes/common/css/gauges.css", context);
DTree props;
DTree item;
<>
<h1>Gauge Components Demo</h1>
<div class="demo-section gauge-demo-row">
</>
props["id"] = "horizontal_demo";
props["title"] = "Horizontal Progress Bar";
props["subtitle"] = "The original bar gauges, restyled to share the same card and token language as the new arc gauges.";
props["layout"] = "horizontal";
props["style"] = "flex:1 1 24rem";
props["listen"].set_bool(true);
item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item; item.clear();
item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item; item.clear();
item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item;
print(component("../components/gauges/gauges:PROGRESSBAR", props, context));
props.clear();
props["id"] = "vertical_demo";
props["title"] = "Vertical Progress Bar";
props["subtitle"] = "Same abstraction, but stacked as compact KPI cards.";
props["layout"] = "vertical";
props["style"] = "flex:1 1 24rem";
props["height"] = "350";
props["listen"].set_bool(true);
item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; props["items"]["cpu"] = item;
item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "RAM"; props["items"]["memory"] = item;
item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "I/O"; props["items"]["disk"] = item;
print(component("../components/gauges/gauges:PROGRESSBAR", props, context));
<>
<div class="gauge-control-panel">
<div class="control-group">
<h4>Event Binding</h4>
<div class="gauge-slider-group"><label for="cpu-slider">CPU</label><input type="range" id="cpu-slider" min="0" max="200" value="45" onchange="$.events.emit('value-broadcast', { name: 'cpu', value: this.value });"></div>
<div class="gauge-slider-group"><label for="memory-slider">Memory</label><input type="range" id="memory-slider" min="-50" max="100" value="92" onchange="$.events.emit('value-broadcast', { name: 'memory', value: this.value });"></div>
<div class="gauge-slider-group"><label for="disk-slider">Disk I/O</label><input type="range" id="disk-slider" min="0" max="100" value="28" onchange="$.events.emit('value-broadcast', { name: 'disk', value: this.value });"></div>
</div>
</div>
</div>
<div class="demo-section gauge-demo-row">
</>
props.clear();
props["id"] = "needle_demo";
props["title"] = "Needle Gauge";
props["subtitle"] = "The original analog gauge now uses the same elevated panels and theme-token palette as the arc gauges.";
props["style"] = "flex:1 1 24rem";
props["listen"].set_bool(true);
item.clear(); item["value"] = "45"; item["min"] = "0"; item["max"] = "200"; item["label"] = "CPU"; item["tooltip"] = "CPU Usage"; props["items"]["cpu"] = item;
item.clear(); item["value"] = "92"; item["min"] = "-50"; item["max"] = "100"; item["label"] = "Memory"; item["tooltip"] = "Memory Usage"; props["items"]["memory"] = item;
item.clear(); item["value"] = "28"; item["min"] = "0"; item["max"] = "100"; item["label"] = "Disk I/O"; item["tooltip"] = "Disk I/O"; props["items"]["disk"] = item;
print(component("../components/gauges/gauges:NEEDLEGAUGE", props, context));
<>
</div>
<div class="demo-section gauge-demo-row">
</>
props.clear();
props["id"] = "arc_demo";
props["title"] = "SVG Arc Gauges";
props["subtitle"] = "Backported from the llm2 overview as reusable KPI-style gauges.";
props["style"] = "flex: 2 1 36rem";
props["listen"].set_bool(true);
item.clear(); item["label"] = "System Load"; item["value"] = "1.8"; item["max"] = "8"; item["precision"] = "1"; item["caption"] = "LOAD 1M"; item["meta"] = "4 cores available"; props["items"]["load"] = item;
item.clear(); item["label"] = "Memory"; item["value"] = "62"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = "%"; item["caption"] = "MEMORY"; item["meta"] = "9.9 / 16 GB"; props["items"]["memory_arc"] = item;
item.clear(); item["label"] = "Network"; item["value"] = "18"; item["max"] = "100"; item["precision"] = "0"; item["unit"] = " MB/s"; item["caption"] = "THROUGHPUT"; item["meta"] = "Inbound + outbound"; props["items"]["network"] = item;
print(component("../components/gauges/gauges:ARCGAUGE", props, context));
<>
<div class="gauge-control-panel">
<h4>Arc Gauge Controls</h4>
<div class="gauge-slider-group"><label for="load-slider">Load</label><input type="range" id="load-slider" min="0" max="8" value="1.8" step="0.1" oninput="$.events.emit('value-broadcast', { name: 'load', value: this.value, meta: this.value + ' / 8.0 load' });"></div>
<div class="gauge-slider-group"><label for="memory-arc-slider">Memory</label><input type="range" id="memory-arc-slider" min="0" max="100" value="62" step="1" oninput="$.events.emit('value-broadcast', { name: 'memory_arc', value: this.value, meta: this.value + '% used' });"></div>
<div class="gauge-slider-group"><label for="network-slider">Network Throughput</label><input type="range" id="network-slider" min="0" max="100" value="18" step="1" oninput="$.events.emit('value-broadcast', { name: 'network', value: this.value, meta: this.value + ' MB/s aggregate' });"></div>
</div>
</div>
</>
}
+128
View File
@@ -0,0 +1,128 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Home";
starter_register_css("views/marketing.css", context);
DTree props;
props["title"] = "Stunning Apps";
props["subtitle"] = "Experience the power of super bloated PHP development with our gigantic and truly unwieldy component-based framework. Seriously though, this page only serves as an example repository of different styles and blocks.";
props["cta_text"] = "Get Started Free(mium)";
props["cta_link"] = "#features";
print(component("../components/example/marketing_blocks:HERO_SECTION", props, context));
print(component("../components/example/marketing_blocks:FEATURES_GRID", context));
props.clear();
DTree stat;
stat["number"] = "10K+"; stat["label"] = "Happy Vibe Coders"; props["stats"].push(stat); stat.clear();
stat["number"] = "<1s"; stat["label"] = "Page Load Time"; props["stats"].push(stat); stat.clear();
stat["number"] = "99.9%"; stat["label"] = "Uptime"; props["stats"].push(stat); stat.clear();
stat["number"] = "24/7"; stat["label"] = "Support"; props["stats"].push(stat);
print(component("../components/example/marketing_blocks:STATS_SECTION", props, context));
print(component("../components/example/marketing_blocks:BRANDS_SHOWCASE", context));
print(component("../components/example/marketing_blocks:TESTIMONIALS", context));
print(component("../components/example/marketing_blocks:PRICING_TABLE", context));
<>
<div class="demo-section">
<div class="demo-container">
<h2>Demo</h2>
<p>Try our unbelievable components in action</p>
<div class="demo-grid">
<div class="demo-card">
<h3>Modern Forms</h3>
<form class="demo-form">
<div>
<label for="demo-full-name">Full Name</label>
<input id="demo-full-name" name="full_name" type="text" placeholder="Enter your name" autocomplete="name" />
</div>
<div>
<label for="demo-email-address">Email Address</label>
<input id="demo-email-address" name="email" type="email" placeholder="you@example.com" autocomplete="email" />
</div>
<div>
<label for="demo-message">Message</label>
<textarea id="demo-message" name="message" placeholder="Tell us what you think..." rows="4"></textarea>
</div>
<button type="submit">Send Message</button>
</form>
</div>
<div class="demo-card">
<h3>Button Variations</h3>
<div class="button-showcase">
<button class="btn">Primary Button</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-outline">Outline</button>
<button class="btn btn-large">Large Button</button>
</div>
<div class="notification-demo">
<div class="banner success">Success message example</div>
<div class="banner warning">Warning message example</div>
<div class="banner error">Error message example</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<h2>Theme Families</h2>
<p>Compare the starters built-in theme families and jump to the live gallery.</p>
<div class="theme-grid">
<? starter_cfg("theme/options", context).each([&](DTree theme, String key) {
if(key != "portal-light" && key != "portal-dark" && key != "localfirst")
return;
StringMap theme_params;
theme_params["theme"] = key;
?><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>
</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>
</div>
</div>
</div>
</>
props.clear();
props["title"] = "Ready to Transform Your Development?";
props["subtitle"] = "Join thousands of developers who have already modernized their workflow with our framework.";
props["cta_text"] = "Start Your Project";
props["secondary_text"] = "View GitHub";
print(component("../components/example/marketing_blocks:CTA_SECTION", props, context));
<>
<div class="card">
<h2>Component Development Guidelines</h2>
<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>
<div class="guideline-item" style="border-left-color: var(--accent);"><span class="guideline-icon">📱</span><span>Add responsive design with mobile-first approach</span></div>
<div class="guideline-item" style="border-left-color: var(--success);"><span class="guideline-icon">♿</span><span>Include accessibility attributes</span></div>
<div class="guideline-item" style="border-left-color: var(--warning);"><span class="guideline-icon">⚡</span><span>Use progressive enhancement for JavaScript features</span></div>
</div>
</div>
</>
props.clear();
DTree row;
row["name"] = "John Doe"; row["age"] = "25"; row["gender"] = "male"; row["department"] = "Engineering"; row["salary"] = "75000"; row["active"] = "true"; props["items"].push(row); row.clear();
row["name"] = "Jane Smith"; row["age"] = "30"; row["gender"] = "female"; row["department"] = "Design"; row["salary"] = "82000"; row["active"] = "true"; props["items"].push(row); row.clear();
row["name"] = "Bob Johnson"; row["age"] = "35"; row["gender"] = "male"; row["department"] = "Marketing"; row["salary"] = "68000"; row["active"] = "false"; props["items"].push(row); row.clear();
row["name"] = "Alice Brown"; row["age"] = "40"; row["gender"] = "female"; row["department"] = "Engineering"; row["salary"] = "95000"; row["active"] = "true"; props["items"].push(row);
props["height"] = "350px";
<>
<div class="card">
<h2>Simple Data Table (ag-Grid)</h2>
<p>Basic data table with auto-generated columns, sorting, and filtering:</p>
<?: component("../components/data/widgets:DATA_TABLE", props, context) ?>
</div>
</>
}
+233
View File
@@ -0,0 +1,233 @@
/* Shared base styles */
.mono-text,
.code-block {
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
}
/* Code block container with terminal styling */
.code-block-container {
background: linear-gradient(135deg, var(--surface) 0%, var(--surface-elevated) 100%);
padding: 2rem;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
margin: 1rem 0;
position: relative;
overflow: hidden;
}
/* Terminal header bar */
.terminal-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
.terminal-header.primary-gradient {
background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%);
}
.terminal-header.success-gradient {
background: linear-gradient(90deg, var(--success) 0%, var(--primary) 50%, var(--secondary) 100%);
}
.terminal-header.warning-gradient {
background: linear-gradient(90deg, var(--warning) 0%, var(--accent) 50%, var(--primary) 100%);
}
/* Terminal window controls */
.terminal-controls {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.window-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.window-dot.red { background: #ff5f56; }
.window-dot.yellow { background: #ffbd2e; }
.window-dot.green {
background: #27ca3f;
margin-right: 1rem;
}
/* Monospace text styling */
.mono-text {
font-size: 0.875rem;
color: var(--text-muted);
}
/* Code block styling */
.code-block {
margin: 0;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-primary);
background: none;
}
/* Card components - using shared base styles */
.component-card,
.success-card,
.enhancement-card {
padding: 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
}
.component-card {
background: var(--surface-elevated);
}
.success-card {
background: var(--success-bg);
border-color: var(--success-border);
}
.enhancement-card {
color: var(--bg-color);
text-align: center;
border: none;
}
/* Grid layouts - shared base properties */
.components-grid,
.theme-grid,
.enhancements-grid,
.guidelines-grid {
display: grid;
gap: 1rem;
}
.components-grid {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
margin: 1rem 0;
}
.theme-grid {
grid-template-columns: 1fr 1fr;
margin: 1rem 0;
}
.enhancements-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.guidelines-grid {
gap: 0.5rem;
}
/* Guideline items */
.guideline-item {
display: flex;
align-items: center;
background: var(--surface-elevated);
padding: 1rem;
border-radius: var(--radius);
border-left: 4px solid;
}
.guideline-icon {
font-size: 1.5rem;
margin-right: 1rem;
}
/* Demo section styling */
.demo-section {
padding: 4rem 0;
background: var(--bg-color);
}
.demo-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
text-align: center;
}
.demo-container h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.demo-container > p {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 3rem;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 2rem;
margin-top: 3rem;
}
.demo-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: 2.5rem;
box-shadow: var(--shadow-md);
text-align: left;
}
.demo-card h3 {
margin-bottom: 2rem;
color: var(--text-primary);
text-align: center;
}
.demo-form {
max-width: none;
}
/* Flex layouts */
.button-showcase,
.notification-demo {
display: flex;
gap: 1rem;
}
.button-showcase {
flex-wrap: wrap;
margin-bottom: 2rem;
justify-content: center;
}
.notification-demo {
flex-direction: column;
}
/* Responsive design lol */
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.demo-card {
padding: 2rem 1.5rem;
}
.button-showcase {
flex-direction: column;
align-items: center;
}
.button-showcase .btn {
width: 100%;
max-width: 250px;
}
}
+59
View File
@@ -0,0 +1,59 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Components";
starter_register_css("views/marketing.css", context);
<>
<h1>Component System</h1>
<div class="card">
<h2>Component Declaration</h2>
<div class="code-block-container">
<div class="terminal-header primary-gradient"></div>
<div class="terminal-controls">
<div class="window-dot red"></div>
<div class="window-dot yellow"></div>
<div class="window-dot green"></div>
<span class="mono-text">marketing_blocks.uce</span>
</div>
<pre class="code-block"><code>RENDER:HERO_SECTION(Request&amp; context)
{
String title = first(context.call["title"].to_string(), "Welcome");
&lt;&gt;
&lt;div class="hero-section"&gt;...&lt;/div&gt;
&lt;/&gt;
}</code></pre>
</div>
</div>
<div class="card">
<h2>Example Components</h2>
<div class="components-grid">
<div class="component-card"><h4>marketing_blocks:HERO_SECTION</h4><p>Landing page hero with CTA buttons</p></div>
<div class="component-card"><h4>marketing_blocks:FEATURES_GRID</h4><p>Feature showcase grid</p></div>
<div class="component-card"><h4>widgets:DATA_TABLE</h4><p>ag-Grid table wrapper</p></div>
<div class="component-card"><h4>widgets:SORTABLE_TABLE</h4><p>Lightweight sortable HTML table</p></div>
<div class="component-card"><h4>primitives:APP_FRAME</h4><p>Workspace shell wrapper</p></div>
<div class="component-card"><h4>gauges:ARCGAUGE</h4><p>SVG KPI gauge component</p></div>
</div>
</div>
<div class="card">
<h2>Usage Examples</h2>
<div class="code-block-container">
<div class="terminal-header success-gradient"></div>
<div class="terminal-controls">
<div class="window-dot red"></div>
<div class="window-dot yellow"></div>
<div class="window-dot green"></div>
<span class="mono-text">views/index.uce</span>
</div>
<pre class="code-block"><code>DTree props;
props["title"] = "Stunning Apps";
&lt;&gt;&lt;?: component("example/marketing_blocks:HERO_SECTION", props, context) ?&gt;&lt;/&gt;</code></pre>
</div>
</div>
</>
}
@@ -0,0 +1,11 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_type"] = "blank";
<>
<?= std::to_string((u64)time()) ?> - Page 2 Section 1 loaded
<pre>UCE starter AJAX fragment response</pre>
</>
}
+16
View File
@@ -0,0 +1,16 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["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 onclick="$('#page2-section1').load('<?= starter_link("page2-section1", context) ?>')">Load new text</button>
</div>
</>
}
@@ -0,0 +1,69 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Theme Preview";
starter_register_css("views/themes.css", context);
String current_theme_key = starter_theme_value("key", context);
String theme_label = first(starter_theme_value("label", context), current_theme_key);
String theme_mode = starter_theme_value("mode", context);
StringMap theme_params;
theme_params["theme"] = current_theme_key;
<>
<div class="theme-preview-shell">
<section class="theme-preview-hero">
<span class="theme-preview-kicker">Starter Theme Preview</span>
<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>
</div>
</section>
<section class="theme-preview-grid">
<div class="theme-preview-card">
<h2>Tokens at a Glance</h2>
<div class="theme-preview-stat-grid">
<div class="theme-preview-stat"><span>Current Theme</span><strong><?= theme_label ?></strong></div>
<div class="theme-preview-stat"><span>Mode</span><strong><?= theme_mode ?></strong></div>
<div class="theme-preview-stat"><span>Preview Route</span><strong>theme-preview</strong></div>
</div>
<div class="theme-preview-swatches">
<span style="background: var(--primary);">Primary</span>
<span style="background: var(--secondary);">Secondary</span>
<span style="background: var(--accent);">Accent</span>
<span style="background: var(--surface-elevated, var(--surface)); color: var(--text-primary); border: 1px solid var(--border);">Surface</span>
</div>
</div>
<div class="theme-preview-card">
<h2>System Message States</h2>
<div class="banner success">Success state keeps contrast and border semantics intact.</div>
<div class="banner warning">Warning state checks how the theme handles warm accents and muted text.</div>
<div class="banner error">Error state shows whether danger colors stay legible over each surface.</div>
</div>
<div class="theme-preview-card">
<h2>Form Controls</h2>
<form class="theme-preview-form">
<div><label for="preview-project">Project Name</label><input id="preview-project" name="project" type="text" placeholder="Starter backport playground" /></div>
<div><label for="preview-owner">Owner</label><input id="preview-owner" name="owner" type="email" placeholder="team@example.com" /></div>
<div><label for="preview-goal">Notes</label><textarea id="preview-goal" name="goal" rows="4" placeholder="Describe the kind of product this theme should support."></textarea></div>
<div class="theme-preview-actions"><button type="button" class="btn">Primary Action</button><button type="button" class="btn btn-outline">Secondary Action</button></div>
</form>
</div>
<div class="theme-preview-card">
<h2>Dense Content Block</h2>
<table>
<thead><tr><th>Area</th><th>Expectation</th><th>Status</th></tr></thead>
<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>
</section>
</div>
</>
}
+180
View File
@@ -0,0 +1,180 @@
.theme-gallery-shell,
.theme-preview-shell {
display: grid;
gap: 1.5rem;
}
.theme-gallery-kicker,
.theme-preview-kicker {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: 0.75rem;
}
.theme-gallery-hero h1,
.theme-preview-hero h1 {
margin-bottom: 0.75rem;
}
.theme-gallery-hero p,
.theme-preview-hero p {
max-width: 72ch;
color: var(--text-secondary);
}
.theme-gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.25rem;
}
.theme-gallery-card,
.theme-preview-card,
.theme-preview-hero {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg, var(--radius));
padding: 1.25rem;
box-shadow: var(--shadow-sm);
}
.theme-gallery-card.is-active {
border-color: var(--primary);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 35%, transparent 65%), var(--shadow-md);
}
.theme-gallery-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.theme-gallery-head p {
margin: 0.4rem 0 0;
color: var(--text-secondary);
}
.theme-gallery-badge {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.65rem;
border-radius: 999px;
background: color-mix(in srgb, var(--primary) 15%, transparent 85%);
color: var(--primary);
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
}
.theme-gallery-actions,
.theme-preview-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1rem;
}
.theme-gallery-frame-wrap {
border: 1px solid var(--border);
border-radius: calc(var(--radius-lg, var(--radius)) - 4px);
overflow: hidden;
background: var(--bg-secondary);
}
.theme-gallery-frame-wrap iframe {
display: block;
width: 100%;
height: 430px;
border: 0;
background: #fff;
}
.theme-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.theme-preview-stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.theme-preview-stat {
display: grid;
gap: 0.2rem;
padding: 0.85rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-elevated, var(--bg-secondary));
}
.theme-preview-stat span {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text-muted);
}
.theme-preview-swatches {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.75rem;
}
.theme-preview-swatches span {
display: flex;
align-items: end;
min-height: 92px;
padding: 0.75rem;
border-radius: var(--radius);
color: #fff;
font-weight: 700;
box-shadow: inset 0 -20px 30px rgba(0, 0, 0, 0.12);
}
.theme-preview-form {
max-width: none;
display: grid;
gap: 0.85rem;
}
.embed-mode nav,
.embed-mode footer,
.embed-mode #theme-switcher,
.embed-mode .admin-toolbar {
display: none !important;
}
.embed-mode #content,
.embed-mode .admin-content {
margin-top: 0 !important;
min-height: 100vh !important;
padding-top: 1rem !important;
}
@media (max-width: 720px) {
.theme-gallery-grid,
.theme-preview-grid {
grid-template-columns: 1fr;
}
.theme-gallery-head {
flex-direction: column;
}
.theme-gallery-frame-wrap iframe {
height: 360px;
}
}
@@ -0,0 +1,46 @@
#load "../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Themes";
starter_register_css("views/themes.css", context);
String current_theme = starter_theme_value("key", context);
<>
<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>
</section>
<div class="theme-gallery-grid">
<? starter_cfg("theme/options", context).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);
StringMap home_params;
home_params["theme"] = theme_key;
?><section class="theme-gallery-card<?= theme_key == current_theme ? " is-active" : "" ?>">
<div class="theme-gallery-head">
<div>
<h2><?= theme_info["label"].to_string() ?></h2>
<p><?= first(theme_info["description"].to_string(), "Reusable starter theme family.") ?></p>
</div>
<? if(theme_key == current_theme) { ?><span class="theme-gallery-badge">Active</span><? } ?>
</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>
</div>
</section><?
}); ?>
</div>
</div>
</>
}
@@ -0,0 +1,134 @@
#load "../../lib/app.uce"
RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Workspace";
String section = first(context.var["starter"]["route"]["param"].to_string(), "overview");
if(section != "overview" && section != "projects" && section != "activity")
section = "overview";
String title = section == "projects" ? "Projects queue" : (section == "activity" ? "Recent activity" : "Workspace overview");
String subtitle = section == "projects" ? "Nested path fallback lets one controller serve multiple panes cleanly." : (section == "activity" ? "Sidebar-first tools often need a live or recent-events pane." : "A generic app-shell pattern for tools, admin consoles, and internal products.");
String status_label = section == "projects" ? "3 active" : (section == "activity" ? "Monitoring" : "Ready");
String status_variant = section == "projects" ? "info" : (section == "activity" ? "warn" : "success");
String description = section == "projects" ? "This page is served from views/workspace/index.uce while the route remains workspace/projects." : (section == "activity" ? "The workspace shell is a better fit than the marketing-style home page when the app is navigation-heavy and stateful." : "This slice comes from the portal app: a reusable workspace shell with sidebar navigation, compact mobile controls, and semantic panel primitives.");
String sidebar_body = "";
ob_start();
<>
<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>
</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>
</>
sidebar_body = ob_get_close();
DTree sidebar_note;
sidebar_note["icon_class"] = "fas fa-circle-info";
sidebar_note["text"] = "Use workspace/overview, workspace/projects, or workspace/activity";
sidebar_body += component("../../components/workspace/primitives:LIST_STATE", sidebar_note, context);
DTree toolbar_props;
toolbar_props["action_html"] = "<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["search_input_id"] = "workspace-demo-search";
toolbar_props["search_input_name"] = "workspace_demo_search";
toolbar_props["search_placeholder"] = "Search workspace sections";
String sidebar_top = component("../../components/workspace/primitives:SIDEBAR_TOOLBAR", toolbar_props, context);
DTree sidebar_props;
sidebar_props["id"] = "workspace-demo-sidebar";
sidebar_props["top_html"] = sidebar_top;
sidebar_props["body_html"] = sidebar_body;
String sidebar = component("../../components/workspace/primitives:SIDEBAR_SHELL", sidebar_props, context);
DTree mobile_props;
mobile_props["button_id"] = "workspace-demo-toggle";
mobile_props["title"] = "Starter Workspace";
String mobile_bar = component("../../components/workspace/primitives:MOBILE_BAR", mobile_props, context);
DTree pill_props;
pill_props["label"] = status_label;
pill_props["variant"] = status_variant;
String status_html = component("../../components/workspace/primitives:STATUS_PILL", pill_props, context);
DTree header_props;
header_props["title"] = title;
header_props["subtitle"] = subtitle;
header_props["actions_html"] = status_html;
String header = component("../../components/workspace/primitives:PANEL_HEADER", header_props, context);
String stats_html = "";
ob_start();
<>
<div class="ws-stat-grid">
<div class="ws-stat-card"><div class="ws-stat-card-label">Sidebar pattern</div><div class="ws-stat-card-value">Responsive</div><div class="ws-stat-card-meta">Overlay on mobile</div></div>
<div class="ws-stat-card"><div class="ws-stat-card-label">Routing mode</div><div class="ws-stat-card-value">Nested</div><div class="ws-stat-card-meta">Parent-path fallback</div></div>
<div class="ws-stat-card"><div class="ws-stat-card-label">Source app</div><div class="ws-stat-card-value">uh-ai</div><div class="ws-stat-card-meta">Portal shell</div></div>
</div>
</>
stats_html = ob_get_close();
String detail_html = "";
ob_start();
<>
<div class="ws-detail-grid">
<div class="ws-detail-card"><h4>Shell layout</h4><p>Sidebar plus main panel, responsive overlay behavior, and a compact mobile header.</p></div>
<div class="ws-detail-card"><h4>Semantic primitives</h4><p>Panels, section heads, status pills, list states, and empty-state placeholders.</p></div>
<div class="ws-detail-card"><h4>Starter-friendly</h4><p>Backported against the starter theme variables instead of portal-specific branding tokens.</p></div>
</div>
</>
detail_html = ob_get_close();
DTree section_head;
section_head["title"] = "Why this belongs in the starter";
String why_head = component("../../components/workspace/primitives:SECTION_HEAD", section_head, context);
DTree section_props;
section_props["header_html"] = why_head;
section_props["body_html"] = "<p class=\"ws-section-copy\">" + html_escape(description) + "</p>" + stats_html;
String main_body = component("../../components/workspace/primitives:SECTION", section_props, context);
section_head.clear();
section_head["title"] = "Starter demo content";
String demo_head = component("../../components/workspace/primitives:SECTION_HEAD", section_head, context);
section_props.clear();
section_props["header_html"] = demo_head;
section_props["body_html"] = detail_html + "<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>";
main_body += component("../../components/workspace/primitives:SECTION", section_props, context);
if(section == "activity")
{
DTree empty_props;
empty_props["icon_class"] = "fas fa-clock-rotate-left";
empty_props["title"] = "No live stream wired yet";
empty_props["text"] = "The shell is generic. Add your own websocket, polling, or event-driven runtime behind it when a real product needs one.";
empty_props["action_html"] = "<a class=\"ws-primary-btn\" href=\"" + starter_link("dashboard", context) + "\">Open dashboard demo</a>";
main_body += component("../../components/workspace/primitives:EMPTY_STATE", empty_props, context);
}
DTree panel_props;
panel_props["header_html"] = header;
panel_props["body_html"] = main_body;
String main = mobile_bar + component("../../components/workspace/primitives:PANEL", panel_props, context);
DTree app_props;
app_props["id"] = "workspace-demo-shell";
app_props["overlay_id"] = "workspace-demo-overlay";
app_props["sidebar_html"] = sidebar;
app_props["main_html"] = main;
print(component("../../components/workspace/primitives:APP_FRAME", app_props, context));
<>
<script>
(function () {
if (typeof UWorkspaceShell === 'undefined') return;
UWorkspaceShell.init({
sidebarId: 'workspace-demo-sidebar',
overlayId: 'workspace-demo-overlay',
toggleButtonId: 'workspace-demo-toggle'
});
}());
</script>
</>
}