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, "&amp;" as *u8) } else { 64 if c == 60 { o = cu_puts(buf, o, "&lt;" as *u8) } else { 65 if c == 62 { o = cu_puts(buf, o, "&gt;" as *u8) } else { 66 if c == 34 { o = cu_puts(buf, o, "&quot;" 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\">&rarr; " 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) }