feat(examples): add saas tutorial skeleton

Add a compile-tested v1 tutorial skeleton that proves the production-shaped app boundary without provider-heavy scope: auth/session context, CSRF-checked mutation, local persistence adapter, generated form/slot/keyed row/page/push effects, plain CSS, SSE shape, and one explicit metrics island.

req: examples/001

req: auth/001

req: auth/002

req: auth/004

req: form/001

req: failure/004

req: page_swap/002

req: push/003
This commit is contained in:
slhx agent
2026-06-05 09:15:57 +02:00
parent 3dfb3b684f
commit 38cfb263aa
15 changed files with 716 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
:root { color-scheme: light; font-family: Inter, system-ui, sans-serif; }
body { margin: 0; background: #f7f4ee; color: #201b16; }
.dashboard { max-width: 960px; margin: 0 auto; padding: 2rem; }
.hero, .panel, .status-row { background: white; border: 1px solid #e6ded2; border-radius: 18px; padding: 1.25rem; box-shadow: 0 12px 40px rgba(34, 24, 8, 0.08); }
.eyebrow { color: #8a5a00; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.lede { max-width: 56rem; color: #5d5147; }
.tabs, .project-form, .status-row { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
.tabs { margin: 1rem 0; }
button, input { font: inherit; }
button { border: 0; border-radius: 999px; background: #1f5eff; color: white; padding: .65rem 1rem; }
input { border: 1px solid #cfc4b8; border-radius: 10px; padding: .55rem .7rem; }
.field-error, .flash { color: #a02b12; font-weight: 700; }
.summary { color: #516034; }
.project-list { display: grid; gap: .7rem; padding: 0; list-style: none; }
.project-row { display: flex; justify-content: space-between; border: 1px solid #eee0cb; border-radius: 12px; padding: .75rem; }
.metrics-island { min-width: 18rem; border-left: 4px solid #1f5eff; padding-left: 1rem; }
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{+ self.title +}</title>
<link rel="stylesheet" href="/app.css">
<script src="/hemx.js" defer></script>
<script src="/metrics.js" defer></script>
</head>
<body>
{+= self.body =+}
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
<section class="dashboard" data-hemx-root="dashboard" data-hemx-sse="/events">
<header class="hero">
<p class="eyebrow">Production-shaped SaaS path</p>
<h1>Projects</h1>
<p class="lede">Auth-gated mutations, CSRF checks, local persistence, typed forms, generated swaps, page swaps, live status, plain CSS, and one explicit island.</p>
</header>
<nav class="tabs" data-hemx-slot="nav">
<a href="/" data-hemx-nav="">Projects</a>
<button type="button" data-hemx-handle="open_settings">Settings</button>
</nav>
<section class="panel" data-hemx-slot="page_panel">
<form class="project-form" data-hemx-handle="create_project" data-hemx-form="new_project" data-hemx-disable-while-pending>
<input type="hidden" name="csrf" +value="self.csrf">
<label>Project name
<input name="name" required="required" maxlength="64" value="Launch checklist">
</label>
<button type="submit">Create project</button>
<p class="field-error" data-hemx-error-for="name"></p>
</form>
<p class="flash" data-hemx-slot="flash">{+ self.flash +}</p>
<p class="summary" data-hemx-slot="summary">{+ self.summary +}</p>
<ul class="project-list" data-hemx-slot="project_row">
<template h-for="row in &self.rows" h-key="row.id">
{+ row +}
</template>
</ul>
</section>
<section class="status-row">
<p data-hemx-slot="live_status">Waiting for status…</p>
<article class="metrics-island" data-hemx-island="metrics" +data-project-count="self.project_count">
<h2>Metrics island</h2>
<canvas width="320" height="140" aria-label="Project metrics chart"></canvas>
<p data-island-readout="">Waiting for island script…</p>
</article>
</section>
</section>
+14
View File
@@ -0,0 +1,14 @@
(() => {
function render(island) {
const count = island.getAttribute("data-project-count") || "0";
const readout = island.querySelector("[data-island-readout]");
if (readout) readout.textContent = `${count} persisted project${count === "1" ? "" : "s"}`;
}
function boot() {
for (const island of document.querySelectorAll('[data-hemx-island="metrics"]')) render(island);
}
document.addEventListener("DOMContentLoaded", boot);
document.addEventListener("hemx:after-settle", boot);
})();
@@ -0,0 +1,4 @@
<li class="project-row" +data-key="self.id">
<strong>{+ self.name +}</strong>
<span>{+ self.owner +}</span>
</li>
@@ -0,0 +1,4 @@
<section class="settings-page">
<h2>Settings</h2>
<p>{+ self.message +}</p>
</section>