trying to port web app starter from PHP
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -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 starter’s 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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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& context)
|
||||
{
|
||||
String title = first(context.call["title"].to_string(), "Welcome");
|
||||
<>
|
||||
<div class="hero-section">...</div>
|
||||
</>
|
||||
}</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";
|
||||
<><?: component("example/marketing_blocks:HERO_SECTION", props, context) ?></></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>
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
Reference in New Issue
Block a user