code wiki / _hdl_build / nx_connect_ui.nx
nx_connect_ui.nx source
↩ module page · 417 lines · 27907 B
1// nx_connect_ui.nx -- the CONNECT APP-TIER UI emitter. PURE EMIT: no main, no syscalls, no imports, so a
2// LIVE REQUEST-SERVING DAEMON can compose it per request (the reason it is not nx_ui_kit: that organ is a
3// file->file CLI with a main(), a hardcoded ops nav, and a disk read per page -- structurally unusable
4// inside an accept loop, and unimportable because of the main()).
5//
6// TOKEN SSOT: this kit carries the canonical sovereign design system knowledge/registry/nishi_ds.css
7// token vocabulary VERBATIM (--bg/--surface/--surface-2/--border/--text/--muted/--faint/--accent/--good/
8// --warn/--bad/--shadow, fluid --fs-0..4, --sp-1..7, --radius, --dur) so CONNECT inherits the same design
9// language as every other Nishi surface instead of drifting into a private palette. What it ADDS is the
10// APP tier that a dashboard design system has no vocabulary for: tab navigation with aria-current, gradient
11// avatars, roster rows, chat bubbles carrying a translation + correction line, a real composer, action
12// banners, labelled meters, and empty states.
13//
14// ACCESSIBILITY BY CONSTRUCTION: every page gets lang, a skip-link, semantic header/nav/main/footer, real
15// label[for]/control[id] pairing, aria-current on the active tab, role=status on action banners, and
16// :focus-visible rings. Dual light/dark via prefers-color-scheme; motion honours prefers-reduced-motion.
17// ZERO JavaScript and ZERO external assets (no CDN font, no remote image) -- avatars are computed gradients
18// and icons are inline SVG, so the app renders identically offline, on a low-bandwidth link, and inside our
19// own Nishi browser engine.
20//
21// Consumers: _hdl_build/nx_connect_serve (the live app core) + _hdl_build/nx_connect_ui_gate (the gate).
22// license_tier: ORIGINAL
23
24// ---------------------------------------------------------------------------- primitives
25
26func cu_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
27
28// append NUL-terminated s at off; return the new off.
29func cu_puts(buf: *u8, off: i64, s: *u8) -> i64 {
30 var o: i64 = off
31 var i: i64 = 0
32 while s[i] != (0 as u8) { buf[o] = s[i]; o = o + 1; i = i + 1 }
33 return o
34}
35
36// append a base-10 integer (negatives render as 0 -- no UI surface wants a negative count).
37// Deliberately allocation-free: it derives the leading power of ten and emits digits most-significant-first,
38// so this kit needs NO scratch buffer and therefore NO syscall -- that is what keeps it importable by a gate
39// that does not pull in the syscall layer.
40func cu_putn(buf: *u8, off: i64, v: i64) -> i64 {
41 var o: i64 = off
42 if v <= 0 { buf[o] = 48 as u8; return o + 1 }
43 var d: i64 = 1
44 var m: i64 = v
45 while m >= 10 { d = d * 10; m = m / 10 }
46 var r: i64 = v
47 while d > 0 {
48 let q: i64 = r / d
49 buf[o] = (48 + q) as u8
50 o = o + 1
51 r = r - (q * d)
52 d = d / 10
53 }
54 return o
55}
56
57// HTML-escape & < > " so ONE escaper is safe in both text and attribute position.
58func cu_esc(buf: *u8, off: i64, s: *u8) -> i64 {
59 var o: i64 = off
60 var i: i64 = 0
61 while s[i] != (0 as u8) {
62 let c: i64 = s[i] as i64
63 if c == 38 { o = cu_puts(buf, o, "&" as *u8) } else {
64 if c == 60 { o = cu_puts(buf, o, "<" as *u8) } else {
65 if c == 62 { o = cu_puts(buf, o, ">" as *u8) } else {
66 if c == 34 { o = cu_puts(buf, o, """ as *u8) } else { buf[o] = s[i]; o = o + 1 } } } }
67 i = i + 1
68 }
69 return o
70}
71
72// ---------------------------------------------------------------------------- stylesheet
73// Emitted in chunks: nx_cc has a string-literal ceiling, so each literal stays well under it.
74
75func cu_css_tokens(buf: *u8, off: i64) -> i64 {
76 var o: i64 = off
77 o = cu_puts(buf, o, ":root{color-scheme:light dark;--font:ui-sans-serif,system-ui,-apple-system,Roboto,Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;" as *u8)
78 o = cu_puts(buf, o, "--fs-0:clamp(.78rem,.18vw + .73rem,.85rem);--fs-1:clamp(.94rem,.28vw + .86rem,1.02rem);--fs-2:clamp(1.05rem,.5vw + .9rem,1.25rem);--fs-3:clamp(1.4rem,1.5vw + 1rem,2rem);--fs-4:clamp(2rem,3.4vw + 1.1rem,3.1rem);" as *u8)
79 o = cu_puts(buf, o, "--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:16px;--sp-5:24px;--sp-6:38px;--sp-7:60px;--radius:14px;--radius-sm:10px;--maxw:1060px;--dur:.18s;" as *u8)
80 o = cu_puts(buf, o, "--bg:#0b0e15;--grad:radial-gradient(1100px 560px at 80% -10%,rgba(107,155,255,.12),transparent 62%);--surface:#141a29;--surface-2:#1b2233;--border:#252d40;--text:#e8ecf5;--muted:#98a2b8;--faint:#6a7488;--accent:#7aa2ff;--good:#4bdca6;--warn:#ffc879;--bad:#ff8080;--shadow:0 1px 2px rgba(0,0,0,.45),0 14px 30px -16px rgba(0,0,0,.6)}" as *u8)
81 o = cu_puts(buf, o, "@media(prefers-color-scheme:light){:root{--bg:#f5f7fb;--grad:radial-gradient(1100px 560px at 80% -10%,rgba(47,95,208,.09),transparent 62%);--surface:#ffffff;--surface-2:#eef2f8;--border:#e3e8f0;--text:#182031;--muted:#586274;--faint:#8b94a6;--accent:#2f5fd0;--good:#0f7a50;--warn:#8a5800;--bad:#c23b2c;--shadow:0 1px 2px rgba(16,24,40,.06),0 14px 30px -16px rgba(16,24,40,.16)}}" as *u8)
82 return o
83}
84
85func cu_css_base(buf: *u8, off: i64) -> i64 {
86 var o: i64 = off
87 o = cu_puts(buf, o, "*{box-sizing:border-box}body{margin:0;background:var(--bg);background-image:var(--grad);background-attachment:fixed;color:var(--text);font-family:var(--font);font-size:var(--fs-1);line-height:1.62;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}" as *u8)
88 o = cu_puts(buf, o, "a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}.wrap{max-width:var(--maxw);margin:0 auto;padding:0 var(--sp-5)}" as *u8)
89 o = cu_puts(buf, o, ".skip{position:absolute;left:-9999px}.skip:focus{left:var(--sp-4);top:var(--sp-4);position:fixed;z-index:60;background:var(--accent);color:var(--bg);padding:10px 16px;border-radius:var(--radius-sm)}" as *u8)
90 o = cu_puts(buf, o, "header.nx{position:sticky;top:0;z-index:20;background:var(--surface);border-bottom:1px solid var(--border)}header.nx .bar{display:flex;align-items:center;gap:var(--sp-3);max-width:var(--maxw);margin:0 auto;padding:var(--sp-3) var(--sp-5);flex-wrap:wrap}" as *u8)
91 o = cu_puts(buf, o, ".brand{font-weight:800;letter-spacing:-.02em;font-size:var(--fs-2);color:var(--text);display:inline-flex;align-items:center;gap:9px}.brand:hover{text-decoration:none}.brand .dot{width:22px;height:22px;border-radius:7px;background:linear-gradient(135deg,var(--accent),var(--good));flex:none}" as *u8)
92 o = cu_puts(buf, o, "nav.nx{display:flex;gap:2px;flex-wrap:wrap;margin-left:auto}nav.nx a{padding:7px 13px;border-radius:99px;color:var(--muted);font-size:var(--fs-0);font-weight:600;transition:color var(--dur),background var(--dur)}nav.nx a:hover{color:var(--text);background:var(--surface-2);text-decoration:none}" as *u8)
93 o = cu_puts(buf, o, "nav.nx a[aria-current=page]{color:var(--bg);background:var(--accent)}main{padding:var(--sp-6) 0 var(--sp-7);animation:rise .5s ease both}@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}" as *u8)
94 o = cu_puts(buf, o, "h1{font-size:var(--fs-4);line-height:1.06;letter-spacing:-.025em;margin:0 0 var(--sp-3);background:linear-gradient(94deg,var(--text) 30%,var(--accent));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}" as *u8)
95 o = cu_puts(buf, o, "h2{font-size:var(--fs-3);letter-spacing:-.01em;margin:var(--sp-6) 0 var(--sp-3)}h3{font-size:var(--fs-2);margin:0 0 var(--sp-2);letter-spacing:-.01em}p.lede{color:var(--muted);font-size:var(--fs-2);max-width:62ch;margin:0 0 var(--sp-5)}" as *u8)
96 o = cu_puts(buf, o, "footer.nx{border-top:1px solid var(--border);color:var(--faint);font-size:var(--fs-0);padding:var(--sp-5) 0 var(--sp-6);margin-top:var(--sp-6)}.muted{color:var(--muted)}.mono{font-family:var(--mono)}:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:5px}" as *u8)
97 return o
98}
99
100func cu_css_app(buf: *u8, off: i64) -> i64 {
101 var o: i64 = off
102 o = cu_puts(buf, o, ".grid{display:grid;gap:var(--sp-4);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:var(--sp-5) 0}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:var(--sp-5);box-shadow:var(--shadow)}" as *u8)
103 o = cu_puts(buf, o, "a.card{display:block;color:inherit;transition:transform var(--dur),border-color var(--dur)}a.card:hover{text-decoration:none;transform:translateY(-3px);border-color:var(--accent)}.card .k{font-size:var(--fs-0);text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:700}.card .v{font-size:var(--fs-3);font-weight:800;letter-spacing:-.02em;margin-top:6px}" as *u8)
104 o = cu_puts(buf, o, ".badge{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:99px;font-size:var(--fs-0);font-weight:700;white-space:nowrap}.b-good{color:var(--good);background:color-mix(in srgb,var(--good) 16%,transparent)}.b-warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}.b-bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 16%,transparent)}.b-mute{color:var(--muted);background:var(--surface-2)}" as *u8)
105 o = cu_puts(buf, o, ".note{color:var(--muted);font-size:var(--fs-0);margin:var(--sp-3) 0 0;max-width:70ch}.ban{display:flex;gap:var(--sp-3);align-items:flex-start;border:1px solid var(--border);border-left-width:4px;border-radius:var(--radius-sm);padding:var(--sp-3) var(--sp-4);margin:0 0 var(--sp-4);background:var(--surface)}" as *u8)
106 o = cu_puts(buf, o, ".ban b{display:block}.ban span{color:var(--muted);font-size:var(--fs-0)}.ban-good{border-left-color:var(--good)}.ban-bad{border-left-color:var(--bad)}.ban-info{border-left-color:var(--accent)}.ban .ic{flex:none;width:20px;height:20px}" as *u8)
107 o = cu_puts(buf, o, ".av{flex:none;width:42px;height:42px;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:800;font-size:var(--fs-1);letter-spacing:-.02em;text-shadow:0 1px 2px rgba(0,0,0,.35)}.av-sm{width:30px;height:30px;font-size:var(--fs-0)}" as *u8)
108 o = cu_puts(buf, o, ".rows{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--sp-2)}.row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);transition:border-color var(--dur)}.row:hover{border-color:var(--accent)}" as *u8)
109 o = cu_puts(buf, o, ".row .who{flex:1;min-width:0}.row .who b{display:block;letter-spacing:-.01em}.row .who span{display:block;color:var(--muted);font-size:var(--fs-0)}.empty{border:1px dashed var(--border);border-radius:var(--radius);padding:var(--sp-5);text-align:center;color:var(--muted);font-size:var(--fs-0)}" as *u8)
110 return o
111}
112
113func cu_css_chat(buf: *u8, off: i64) -> i64 {
114 var o: i64 = off
115 o = cu_puts(buf, o, ".thread{list-style:none;margin:0 0 var(--sp-4);padding:0;display:flex;flex-direction:column;gap:var(--sp-3)}.msg{display:flex;gap:var(--sp-3);align-items:flex-end;max-width:100%}.msg.own{flex-direction:row-reverse}" as *u8)
116 o = cu_puts(buf, o, ".bub{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);border-bottom-left-radius:4px;padding:var(--sp-3) var(--sp-4);max-width:min(78%,52ch);box-shadow:var(--shadow)}.msg.own .bub{border-bottom-left-radius:var(--radius);border-bottom-right-radius:4px;background:var(--surface-2)}" as *u8)
117 o = cu_puts(buf, o, ".bub .nm{font-size:var(--fs-0);font-weight:700;color:var(--muted);margin-bottom:2px}.bub .tx{overflow-wrap:anywhere}.bub .tr{display:block;margin-top:5px;padding-top:5px;border-top:1px dashed var(--border);color:var(--accent);font-size:var(--fs-0)}" as *u8)
118 o = cu_puts(buf, o, ".bub .co{display:block;margin-top:5px;color:var(--good);font-size:var(--fs-0)}.meter{height:8px;border-radius:99px;background:var(--surface-2);overflow:hidden;min-width:100px;flex:1}.meter i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--good))}" as *u8)
119 o = cu_puts(buf, o, ".mrow{display:flex;align-items:center;gap:var(--sp-3);margin:0 0 var(--sp-3)}.mrow .lb{min-width:11ch;font-size:var(--fs-0)}.mrow .ct{font-variant-numeric:tabular-nums;color:var(--muted);font-size:var(--fs-0);min-width:3ch;text-align:right}" as *u8)
120 return o
121}
122
123func cu_css_form(buf: *u8, off: i64) -> i64 {
124 var o: i64 = off
125 o = cu_puts(buf, o, ".frm{display:flex;flex-wrap:wrap;gap:var(--sp-3);align-items:flex-end;margin:var(--sp-4) 0 0}.fld{display:flex;flex-direction:column;gap:5px;flex:1;min-width:150px}.fld label{font-size:var(--fs-0);font-weight:700;color:var(--muted)}" as *u8)
126 o = cu_puts(buf, o, ".fld input,.fld select{font:inherit;font-size:var(--fs-1);color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;width:100%;transition:border-color var(--dur),box-shadow var(--dur)}" as *u8)
127 o = cu_puts(buf, o, ".fld input:focus-visible,.fld select:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 26%,transparent)}.fld input::placeholder{color:var(--faint)}" as *u8)
128 o = cu_puts(buf, o, "button.go{font:inherit;font-size:var(--fs-1);font-weight:700;color:var(--bg);background:var(--accent);border:0;border-radius:var(--radius-sm);padding:11px 20px;cursor:pointer;transition:transform var(--dur),filter var(--dur);white-space:nowrap}button.go:hover{transform:translateY(-1px);filter:brightness(1.08)}" as *u8)
129 o = cu_puts(buf, o, "@media(prefers-reduced-motion:reduce){main{animation:none}*{transition:none!important}a.card:hover,button.go:hover{transform:none}}@media(max-width:640px){.wrap,header.nx .bar{padding-left:var(--sp-4);padding-right:var(--sp-4)}nav.nx{margin-left:0;width:100%}h1{font-size:var(--fs-3)}.bub{max-width:88%}}" as *u8)
130 return o
131}
132
133// ---------------------------------------------------------------------------- document shell
134
135// <!doctype> .. <body> + skip-link. title is emitted RAW (caller supplies safe markup/entities).
136func cu_head(buf: *u8, off: i64, title: *u8) -> i64 {
137 var o: i64 = off
138 o = cu_puts(buf, o, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>" as *u8)
139 o = cu_puts(buf, o, title)
140 o = cu_puts(buf, o, "</title><style>" as *u8)
141 o = cu_css_tokens(buf, o)
142 o = cu_css_base(buf, o)
143 o = cu_css_app(buf, o)
144 o = cu_css_chat(buf, o)
145 o = cu_css_form(buf, o)
146 o = cu_puts(buf, o, "</style></head><body><a class=\"skip\" href=\"#main\">Skip to content</a>" as *u8)
147 return o
148}
149
150// sticky app bar + brand; leaves <nav> OPEN for cu_tab calls.
151func cu_bar_open(buf: *u8, off: i64, brand: *u8, href: *u8) -> i64 {
152 var o: i64 = off
153 o = cu_puts(buf, o, "<header class=\"nx\"><div class=\"bar\"><a class=\"brand\" href=\"" as *u8)
154 o = cu_puts(buf, o, href)
155 o = cu_puts(buf, o, "\"><span class=\"dot\"></span>" as *u8)
156 o = cu_puts(buf, o, brand)
157 o = cu_puts(buf, o, "</a><nav class=\"nx\" aria-label=\"Sections\">" as *u8)
158 return o
159}
160
161// one tab. current=1 marks it aria-current=page (screen-reader + styled state, no JS).
162func cu_tab(buf: *u8, off: i64, label: *u8, href: *u8, current: i64) -> i64 {
163 var o: i64 = off
164 o = cu_puts(buf, o, "<a href=\"" as *u8)
165 o = cu_puts(buf, o, href)
166 if current == 1 { o = cu_puts(buf, o, "\" aria-current=\"page\">" as *u8) } else { o = cu_puts(buf, o, "\">" as *u8) }
167 o = cu_puts(buf, o, label)
168 o = cu_puts(buf, o, "</a>" as *u8)
169 return o
170}
171
172// close nav + header, open <main id=main> and the width container.
173func cu_bar_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</nav></div></header><main id=\"main\"><div class=\"wrap\">" as *u8) }
174
175// close container/main + footer + document.
176func cu_foot(buf: *u8, off: i64, note: *u8) -> i64 {
177 var o: i64 = off
178 o = cu_puts(buf, o, "</div></main><footer class=\"nx\"><div class=\"wrap\">" as *u8)
179 o = cu_puts(buf, o, note)
180 o = cu_puts(buf, o, "</div></footer></body></html>" as *u8)
181 return o
182}
183
184// ---------------------------------------------------------------------------- page furniture
185
186func cu_h1(buf: *u8, off: i64, title: *u8, lede: *u8) -> i64 {
187 var o: i64 = cu_puts(buf, off, "<h1>" as *u8)
188 o = cu_puts(buf, o, title)
189 o = cu_puts(buf, o, "</h1><p class=\"lede\">" as *u8)
190 o = cu_puts(buf, o, lede)
191 return cu_puts(buf, o, "</p>" as *u8)
192}
193func cu_h2(buf: *u8, off: i64, text: *u8) -> i64 {
194 var o: i64 = cu_puts(buf, off, "<h2>" as *u8)
195 o = cu_puts(buf, o, text)
196 return cu_puts(buf, o, "</h2>" as *u8)
197}
198func cu_note(buf: *u8, off: i64, html: *u8) -> i64 {
199 var o: i64 = cu_puts(buf, off, "<p class=\"note\">" as *u8)
200 o = cu_puts(buf, o, html)
201 return cu_puts(buf, o, "</p>" as *u8)
202}
203func cu_empty(buf: *u8, off: i64, text: *u8) -> i64 {
204 var o: i64 = cu_puts(buf, off, "<p class=\"empty\">" as *u8)
205 o = cu_puts(buf, o, text)
206 return cu_puts(buf, o, "</p>" as *u8)
207}
208
209// action feedback. kind: 0=info 1=good 2=bad. role=status so assistive tech announces the result of a POST.
210func cu_banner(buf: *u8, off: i64, kind: i64, title: *u8, body: *u8) -> i64 {
211 var o: i64 = cu_puts(buf, off, "<div class=\"ban ban-" as *u8)
212 if kind == 1 { o = cu_puts(buf, o, "good" as *u8) } else {
213 if kind == 2 { o = cu_puts(buf, o, "bad" as *u8) } else { o = cu_puts(buf, o, "info" as *u8) } }
214 o = cu_puts(buf, o, "\" role=\"status\"><svg class=\"ic\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">" as *u8)
215 if kind == 1 { o = cu_puts(buf, o, "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 12l3 3 5-6\"/>" as *u8) } else {
216 if kind == 2 { o = cu_puts(buf, o, "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v6M12 16h.01\"/>" as *u8) } else { o = cu_puts(buf, o, "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 11v6M12 8h.01\"/>" as *u8) } }
217 o = cu_puts(buf, o, "</svg><div><b>" as *u8)
218 o = cu_puts(buf, o, title)
219 o = cu_puts(buf, o, "</b><span>" as *u8)
220 o = cu_puts(buf, o, body)
221 return cu_puts(buf, o, "</span></div></div>" as *u8)
222}
223
224// state pill. kind: 0=mute 1=good 2=warn 3=bad.
225func cu_badge(buf: *u8, off: i64, kind: i64, label: *u8) -> i64 {
226 var o: i64 = cu_puts(buf, off, "<span class=\"badge b-" as *u8)
227 if kind == 1 { o = cu_puts(buf, o, "good" as *u8) } else {
228 if kind == 2 { o = cu_puts(buf, o, "warn" as *u8) } else {
229 if kind == 3 { o = cu_puts(buf, o, "bad" as *u8) } else { o = cu_puts(buf, o, "mute" as *u8) } } }
230 o = cu_puts(buf, o, "\">" as *u8)
231 o = cu_puts(buf, o, label)
232 return cu_puts(buf, o, "</span>" as *u8)
233}
234
235// ---------------------------------------------------------------------------- containers
236
237func cu_grid_open(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "<div class=\"grid\">" as *u8) }
238func cu_grid_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</div>" as *u8) }
239func cu_card_open(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "<section class=\"card\">" as *u8) }
240func cu_card_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</section>" as *u8) }
241
242// a whole-card link (cross-page navigation affordance).
243func cu_card_link(buf: *u8, off: i64, title: *u8, body: *u8, href: *u8) -> i64 {
244 var o: i64 = cu_puts(buf, off, "<a class=\"card\" href=\"" as *u8)
245 o = cu_puts(buf, o, href)
246 o = cu_puts(buf, o, "\"><h3>" as *u8)
247 o = cu_puts(buf, o, title)
248 o = cu_puts(buf, o, "</h3><p class=\"muted\">" as *u8)
249 o = cu_puts(buf, o, body)
250 return cu_puts(buf, o, "</p></a>" as *u8)
251}
252
253// a stat tile (value + uppercase label).
254func cu_stat(buf: *u8, off: i64, value: i64, label: *u8) -> i64 {
255 var o: i64 = cu_puts(buf, off, "<div class=\"card\"><div class=\"k\">" as *u8)
256 o = cu_puts(buf, o, label)
257 o = cu_puts(buf, o, "</div><div class=\"v\">" as *u8)
258 o = cu_putn(buf, o, value)
259 return cu_puts(buf, o, "</div></div>" as *u8)
260}
261
262func cu_rows_open(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "<ul class=\"rows\">" as *u8) }
263func cu_rows_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</ul>" as *u8) }
264
265// ---------------------------------------------------------------------------- social components
266
267// deterministic gradient avatar from the name's first byte -- no external image, no tracking pixel, and the
268// same person always gets the same colour. small=1 renders the compact variant used inside chat bubbles.
269func cu_avatar(buf: *u8, off: i64, name: *u8, small: i64) -> i64 {
270 var seed: i64 = 0
271 if name[0] != (0 as u8) { seed = name[0] as i64 }
272 if name[1] != (0 as u8) { seed = seed + ((name[1] as i64) * 7) }
273 let h1: i64 = (seed * 37) % 360
274 var h2: i64 = (h1 + 48) % 360
275 var o: i64 = cu_puts(buf, off, "<span class=\"av" as *u8)
276 if small == 1 { o = cu_puts(buf, o, " av-sm" as *u8) }
277 o = cu_puts(buf, o, "\" aria-hidden=\"true\" style=\"background:linear-gradient(135deg,hsl(" as *u8)
278 o = cu_putn(buf, o, h1)
279 o = cu_puts(buf, o, ",62%,52%),hsl(" as *u8)
280 o = cu_putn(buf, o, h2)
281 o = cu_puts(buf, o, ",62%,40%))\">" as *u8)
282 // initial: uppercase an ASCII lowercase letter; anything else (incl. UTF-8 lead bytes) renders as-is.
283 var c: i64 = 0
284 if name[0] != (0 as u8) { c = name[0] as i64 }
285 if c >= 97 { if c <= 122 { c = c - 32 } }
286 if c >= 32 { if c < 127 { buf[o] = c as u8; o = o + 1 } }
287 return cu_puts(buf, o, "</span>" as *u8)
288}
289
290// roster row: avatar + name + meta line + a state badge. name/meta are escaped by the CALLER when they carry
291// user text; the fixed-copy call sites pass safe markup.
292func cu_person(buf: *u8, off: i64, name: *u8, meta: *u8, bkind: i64, blabel: *u8) -> i64 {
293 var o: i64 = cu_puts(buf, off, "<li class=\"row\">" as *u8)
294 o = cu_avatar(buf, o, name, 0)
295 o = cu_puts(buf, o, "<div class=\"who\"><b>" as *u8)
296 o = cu_puts(buf, o, name)
297 o = cu_puts(buf, o, "</b><span>" as *u8)
298 o = cu_puts(buf, o, meta)
299 o = cu_puts(buf, o, "</span></div>" as *u8)
300 o = cu_badge(buf, o, bkind, blabel)
301 return cu_puts(buf, o, "</li>" as *u8)
302}
303
304func cu_thread_open(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "<ul class=\"thread\">" as *u8) }
305func cu_thread_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</ul>" as *u8) }
306
307// chat bubble OPEN: avatar + name + the message text slot. own=1 right-aligns (the speaker is you).
308// text is emitted ESCAPED here -- this is the one component that always carries user input.
309func cu_bubble_open(buf: *u8, off: i64, name: *u8, own: i64, text: *u8) -> i64 {
310 var o: i64 = cu_puts(buf, off, "<li class=\"msg" as *u8)
311 if own == 1 { o = cu_puts(buf, o, " own" as *u8) }
312 o = cu_puts(buf, o, "\">" as *u8)
313 o = cu_avatar(buf, o, name, 1)
314 o = cu_puts(buf, o, "<div class=\"bub\"><div class=\"nm\">" as *u8)
315 o = cu_esc(buf, o, name)
316 o = cu_puts(buf, o, "</div><div class=\"tx\">" as *u8)
317 o = cu_esc(buf, o, text)
318 return cu_puts(buf, o, "</div>" as *u8)
319}
320// the inline translation line (kept BESIDE the original, never replacing it -- the learning loop).
321func cu_bubble_tr(buf: *u8, off: i64, tr: *u8) -> i64 {
322 var o: i64 = cu_puts(buf, off, "<span class=\"tr\">→ " as *u8)
323 o = cu_esc(buf, o, tr)
324 return cu_puts(buf, o, "</span>" as *u8)
325}
326// an attached correction (the original is preserved above it).
327func cu_bubble_corr(buf: *u8, off: i64, corr: *u8) -> i64 {
328 var o: i64 = cu_puts(buf, off, "<span class=\"co\">corrected: " as *u8)
329 o = cu_esc(buf, o, corr)
330 return cu_puts(buf, o, "</span>" as *u8)
331}
332func cu_bubble_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</div></li>" as *u8) }
333
334// labelled proportional bar (poll tallies, roster fill). Integer math only -- no floats anywhere.
335func cu_meter(buf: *u8, off: i64, label: *u8, value: i64, max: i64) -> i64 {
336 var pct: i64 = 0
337 if max > 0 { if value > 0 { pct = (value * 100) / max } }
338 if pct > 100 { pct = 100 }
339 var o: i64 = cu_puts(buf, off, "<div class=\"mrow\"><span class=\"lb\">" as *u8)
340 o = cu_puts(buf, o, label)
341 o = cu_puts(buf, o, "</span><span class=\"meter\"><i style=\"width:" as *u8)
342 o = cu_putn(buf, o, pct)
343 o = cu_puts(buf, o, "%\"></i></span><span class=\"ct\">" as *u8)
344 o = cu_putn(buf, o, value)
345 return cu_puts(buf, o, "</span></div>" as *u8)
346}
347
348// ---------------------------------------------------------------------------- forms
349
350func cu_form_open(buf: *u8, off: i64, action: *u8) -> i64 {
351 var o: i64 = cu_puts(buf, off, "<form class=\"frm\" method=\"post\" action=\"" as *u8)
352 o = cu_puts(buf, o, action)
353 return cu_puts(buf, o, "\">" as *u8)
354}
355// label[for] / input[id] are paired BY CONSTRUCTION: id and name are the same string, so a field can never
356// ship unlabelled.
357func cu_input(buf: *u8, off: i64, id: *u8, label: *u8, ph: *u8, maxlen: i64) -> i64 {
358 var o: i64 = cu_puts(buf, off, "<div class=\"fld\"><label for=\"" as *u8)
359 o = cu_puts(buf, o, id)
360 o = cu_puts(buf, o, "\">" as *u8)
361 o = cu_puts(buf, o, label)
362 o = cu_puts(buf, o, "</label><input id=\"" as *u8)
363 o = cu_puts(buf, o, id)
364 o = cu_puts(buf, o, "\" name=\"" as *u8)
365 o = cu_puts(buf, o, id)
366 o = cu_puts(buf, o, "\" type=\"text\" autocomplete=\"off\" maxlength=\"" as *u8)
367 o = cu_putn(buf, o, maxlen)
368 o = cu_puts(buf, o, "\" placeholder=\"" as *u8)
369 o = cu_puts(buf, o, ph)
370 return cu_puts(buf, o, "\"></div>" as *u8)
371}
372// a password (or otherwise typed) field: same labelled-by-construction shape as cu_input, but the caller
373// chooses the input type and the autocomplete hint (so a password manager can offer current/new-password).
374func cu_input_typed(buf: *u8, off: i64, id: *u8, label: *u8, ph: *u8, maxlen: i64, itype: *u8, ac: *u8) -> i64 {
375 var o: i64 = cu_puts(buf, off, "<div class=\"fld\"><label for=\"" as *u8)
376 o = cu_puts(buf, o, id)
377 o = cu_puts(buf, o, "\">" as *u8)
378 o = cu_puts(buf, o, label)
379 o = cu_puts(buf, o, "</label><input id=\"" as *u8)
380 o = cu_puts(buf, o, id)
381 o = cu_puts(buf, o, "\" name=\"" as *u8)
382 o = cu_puts(buf, o, id)
383 o = cu_puts(buf, o, "\" type=\"" as *u8)
384 o = cu_puts(buf, o, itype)
385 o = cu_puts(buf, o, "\" autocomplete=\"" as *u8)
386 o = cu_puts(buf, o, ac)
387 o = cu_puts(buf, o, "\" maxlength=\"" as *u8)
388 o = cu_putn(buf, o, maxlen)
389 o = cu_puts(buf, o, "\" placeholder=\"" as *u8)
390 o = cu_puts(buf, o, ph)
391 return cu_puts(buf, o, "\"></div>" as *u8)
392}
393func cu_select_open(buf: *u8, off: i64, id: *u8, label: *u8) -> i64 {
394 var o: i64 = cu_puts(buf, off, "<div class=\"fld\"><label for=\"" as *u8)
395 o = cu_puts(buf, o, id)
396 o = cu_puts(buf, o, "\">" as *u8)
397 o = cu_puts(buf, o, label)
398 o = cu_puts(buf, o, "</label><select id=\"" as *u8)
399 o = cu_puts(buf, o, id)
400 o = cu_puts(buf, o, "\" name=\"" as *u8)
401 o = cu_puts(buf, o, id)
402 return cu_puts(buf, o, "\">" as *u8)
403}
404func cu_option(buf: *u8, off: i64, value: *u8, label: *u8) -> i64 {
405 var o: i64 = cu_puts(buf, off, "<option value=\"" as *u8)
406 o = cu_puts(buf, o, value)
407 o = cu_puts(buf, o, "\">" as *u8)
408 o = cu_puts(buf, o, label)
409 return cu_puts(buf, o, "</option>" as *u8)
410}
411func cu_select_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</select></div>" as *u8) }
412func cu_submit(buf: *u8, off: i64, text: *u8) -> i64 {
413 var o: i64 = cu_puts(buf, off, "<button class=\"go\" type=\"submit\">" as *u8)
414 o = cu_puts(buf, o, text)
415 return cu_puts(buf, o, "</button>" as *u8)
416}
417func cu_form_close(buf: *u8, off: i64) -> i64 { return cu_puts(buf, off, "</form>" as *u8) }