code wiki / _hdl_build / nx_web_builder.nx
nx_web_builder.nx source
↩ module page · 141 lines · 11592 B
1// nx_web_builder.nx -- a TEAM CAPABILITY: build UX-compliant responsive websites from a SPEC (operator:
2// "build the team's capability to build", not a one-off page). The cited UX/CX research is BAKED INTO
3// the generator, so every site it produces is compliant by construction and it REFUSES a spec that
4// violates the rules:
5// - <= 3 resolution steps (decision-fatigue; the "3-click rule" is debunked so it's about step COUNT
6// + clarity, knowledge/research/2026-06-04-website-ux-andelinwest.md)
7// - a clear, obvious primary CTA (Doherty < 400ms feel; value clear within 10s)
8// - responsive by construction (mobile-first viewport + fluid grid baked into wb_doc_open)
9// - onsite search present (wired to the team's BM25/PPMI)
10// - trust/credibility signals (Fogg: ~75% judge credibility from design)
11// The team feeds it andelinwest's spec to build that site, and a different spec to build the next
12// lawyer/doctor/ecommerce site (the shadow-clone reuse). Content strings are the caller's (the legal
13// PROSE is the flagged LLM-gap); the STRUCTURE + UX + responsiveness are the team's, bits-up.
14// license_tier: ORIGINAL Emit is raw sys_write -> sovereign, no template engine, no third-party.
15
16import "nx_syscalls.nx"
17import "nx_brand_tokens.nx" // R1b: the design-token SSOT -> :root{--nx-*}; brand becomes DATA, not hardcoded CSS
18import "nx_brand_polish.nx" // R7: token-driven motion + micro-interactions (reduced-motion-safe, JS-free)
19import "nx_brand_assets.nx" // brand asset slots (favicon/logo/og) -> <head>, attribute-escaped (additive)
20
21const WB_MAX_STEPS: i64 = 3 // operator's <=3-step rule (cited decision-fatigue research)
22
23// ===== UX-compliance gate (the research, enforced) ===============================================
24func wb_steps_ok(n_steps: i64) -> i64 { if n_steps < 1 { return 0 } if n_steps > WB_MAX_STEPS { return 0 } return 1 }
25func wb_ux_compliant(n_steps: i64, has_cta: i64, has_search: i64, n_trust: i64) -> i64 {
26 if wb_steps_ok(n_steps) == 0 { return 0 }
27 if has_cta != 1 { return 0 }
28 if has_search != 1 { return 0 }
29 if n_trust < 1 { return 0 }
30 return 1
31}
32// a 0-100 UX score (weighted): steps(25) + cta(25) + search(15) + trust(15) + responsive(20, always on).
33func wb_ux_grade(n_steps: i64, has_cta: i64, has_search: i64, n_trust: i64, responsive: i64) -> i64 {
34 var g: i64 = 0
35 if wb_steps_ok(n_steps) == 1 { g = g + 25 }
36 if has_cta == 1 { g = g + 25 }
37 if has_search == 1 { g = g + 15 }
38 if n_trust >= 1 { g = g + 15 }
39 if responsive == 1 { g = g + 20 }
40 return g
41}
42
43// ===== emit primitives (raw, sovereign) ==========================================================
44func wb_w(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return n }
45
46// open the doc with mobile-first RESPONSIVE CSS baked in (so every generated site is responsive).
47// R1b: the doc opener is now BRAND-DRIVEN. wb_doc_open_branded emits the brand's design tokens as a
48// :root{--nx-*} block (via nx_brand_tokens) then references them with var(--nx-*) -- so re-branding is a DATA
49// edit (swap the .brand), not a CSS edit. wb_doc_open keeps its (fd,title) signature (backward-compatible: every
50// existing caller is unchanged) and delegates with the default Nishi brand.
51func wb_doc_open_branded(fd: i64, title: *u8, brand: *u8, blen: i64) -> i64 {
52 wb_w(fd, "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n" as *u8)
53 wb_w(fd, "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" as *u8)
54 wb_w(fd, "<title>" as *u8); wb_w(fd, title); wb_w(fd, "</title>\n" as *u8)
55 bas_emit_head(brand, blen, fd) // brand asset slots (favicon/logo/og) into <head>; nothing if no asset tokens
56 wb_w(fd, "<style>\n" as *u8)
57 bt_emit_root_fd(brand, blen, fd) // <-- the brand's design tokens as :root{--nx-*} custom properties
58 bt_emit_dark_fd(brand, blen, fd) // R8: dark-mode override block (emits nothing if the brand has no dark tokens)
59 wb_w(fd, "*{box-sizing:border-box}body{margin:0;font-family:var(--nx-font-sans);font-size:var(--nx-font-size-base);line-height:var(--nx-font-leading);color:var(--nx-color-ink);background:var(--nx-color-bg)}\n" as *u8)
60 wb_w(fd, ".wrap{max-width:980px;margin:0 auto;padding:0 20px}\n" as *u8)
61 wb_w(fd, "header{background:var(--nx-color-primary);color:#fff;padding:14px 0;position:sticky;top:0;z-index:9}\n" as *u8)
62 wb_w(fd, "header .wrap{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}\n" as *u8)
63 wb_w(fd, ".brand{font-weight:700;font-size:1.15rem}\n" as *u8)
64 wb_w(fd, ".cta{background:var(--nx-color-accent);color:var(--nx-color-accent-ink);font-weight:700;padding:12px 22px;border-radius:var(--nx-radius-sm);text-decoration:none;display:inline-block;min-height:44px}\n" as *u8)
65 wb_w(fd, ".hero{background:var(--nx-color-hero);color:#fff;padding:54px 0;text-align:center}.hero h1{font-size:2rem;margin:0 0 8px}.hero p{font-size:1.15rem;opacity:.92;margin:0 0 22px}\n" as *u8)
66 wb_w(fd, ".search{margin:22px 0;display:flex;gap:8px;flex-wrap:wrap}.search input{flex:1;min-width:200px;padding:13px;font-size:1rem;border:1px solid var(--nx-color-line);border-radius:var(--nx-radius-sm)}\n" as *u8)
67 wb_w(fd, ".cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:28px 0}.card{border:1px solid var(--nx-color-line);border-radius:var(--nx-radius-md);padding:20px}.card h3{margin:0 0 6px}\n" as *u8)
68 wb_w(fd, ".steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin:18px 0}.step{background:var(--nx-color-muted);border-radius:var(--nx-radius-md);padding:18px}.step b{color:var(--nx-color-hero)}\n" as *u8)
69 wb_w(fd, ".trust{background:var(--nx-color-muted);padding:18px;border-radius:var(--nx-radius-md);margin:24px 0;text-align:center;font-weight:600}\n" as *u8)
70 wb_w(fd, "section{padding:8px 0}h2{color:var(--nx-color-primary)}footer{background:var(--nx-color-primary);color:var(--nx-color-line);padding:20px 0;margin-top:30px;text-align:center}\n" as *u8)
71 wb_w(fd, ".faq details{border:1px solid var(--nx-color-line);border-radius:var(--nx-radius-md);padding:12px 16px;margin:8px 0}.faq summary{font-weight:600;cursor:pointer}.faq p{margin:10px 0 2px}\n" as *u8)
72 wb_w(fd, ".gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin:24px 0}.gallery img{width:100%;border-radius:var(--nx-radius-md);display:block;aspect-ratio:4/3;object-fit:cover}\n" as *u8)
73 wb_w(fd, ".ctaband{background:var(--nx-color-hero);color:#fff;text-align:center;padding:40px 0;margin:24px 0;border-radius:10px}.ctaband h2{color:#fff;margin:0 0 8px}.ctaband p{opacity:.9;margin:0 0 18px}\n" as *u8)
74 wb_w(fd, "@media(max-width:600px){.hero h1{font-size:1.5rem}.hero{padding:36px 0}}\n" as *u8)
75 pol_emit(fd) // R7: token-driven motion + micro-interactions (reduced-motion-safe, JS-free)
76 wb_w(fd, "</style></head><body>\n" as *u8)
77 return 0
78}
79func wb_doc_open(fd: i64, title: *u8) -> i64 {
80 let d: *u8 = bt_default_brand()
81 return wb_doc_open_branded(fd, title, d, bt_len(d))
82}
83func wb_doc_close(fd: i64) -> i64 { wb_w(fd, "</body></html>\n" as *u8); return 0 }
84
85func wb_header(fd: i64, brand: *u8, cta_label: *u8, cta_href: *u8) -> i64 {
86 wb_w(fd, "<header><div class=\"wrap\"><span class=\"brand\">" as *u8); wb_w(fd, brand)
87 wb_w(fd, "</span><a class=\"cta\" href=\"" as *u8); wb_w(fd, cta_href); wb_w(fd, "\">" as *u8); wb_w(fd, cta_label)
88 wb_w(fd, "</a></div></header>\n" as *u8); return 0
89}
90func wb_hero(fd: i64, headline: *u8, sub: *u8, cta_label: *u8, cta_href: *u8) -> i64 {
91 wb_w(fd, "<div class=\"hero\"><div class=\"wrap\"><h1>" as *u8); wb_w(fd, headline)
92 wb_w(fd, "</h1><p>" as *u8); wb_w(fd, sub); wb_w(fd, "</p><a class=\"cta\" href=\"" as *u8); wb_w(fd, cta_href)
93 wb_w(fd, "\">" as *u8); wb_w(fd, cta_label); wb_w(fd, "</a></div></div>\n" as *u8); return 0
94}
95func wb_search(fd: i64, action: *u8, placeholder: *u8) -> i64 {
96 wb_w(fd, "<div class=\"wrap\"><form class=\"search\" role=\"search\" action=\"" as *u8); wb_w(fd, action)
97 wb_w(fd, "\" method=\"get\"><input type=\"search\" name=\"q\" placeholder=\"" as *u8); wb_w(fd, placeholder)
98 wb_w(fd, "\" aria-label=\"Search this site\"><button class=\"cta\" type=\"submit\">Search</button></form></div>\n" as *u8); return 0
99}
100func wb_cards_open(fd: i64, heading: *u8) -> i64 { wb_w(fd, "<section class=\"wrap\"><h2>" as *u8); wb_w(fd, heading); wb_w(fd, "</h2><div class=\"cards\">\n" as *u8); return 0 }
101func wb_card(fd: i64, title: *u8, body: *u8) -> i64 {
102 wb_w(fd, "<div class=\"card\"><h3>" as *u8); wb_w(fd, title); wb_w(fd, "</h3><p>" as *u8); wb_w(fd, body); wb_w(fd, "</p></div>\n" as *u8); return 0
103}
104func wb_cards_close(fd: i64) -> i64 { wb_w(fd, "</div></section>\n" as *u8); return 0 }
105
106// the resolution steps -- ENFORCES the <=3 rule: refuses (returns -1, emits nothing) if too many.
107func wb_steps(fd: i64, heading: *u8, labels: *i64, n: i64) -> i64 {
108 if wb_steps_ok(n) == 0 { return 0 - 1 }
109 wb_w(fd, "<section class=\"wrap\"><h2>" as *u8); wb_w(fd, heading); wb_w(fd, "</h2><div class=\"steps\">\n" as *u8)
110 var i: i64 = 0
111 while i < n {
112 wb_w(fd, "<div class=\"step\"><b>Step " as *u8); let d: *u8 = sys_mmap(4); d[0] = (49 + i) as u8; sys_write(fd, d, 1)
113 wb_w(fd, "</b><br>" as *u8); wb_w(fd, labels[i] as *u8); wb_w(fd, "</div>\n" as *u8)
114 i = i + 1
115 }
116 wb_w(fd, "</div></section>\n" as *u8)
117 return n
118}
119func wb_trust(fd: i64, signals: *i64, n: i64) -> i64 {
120 wb_w(fd, "<div class=\"wrap\"><div class=\"trust\">" as *u8)
121 var i: i64 = 0
122 while i < n { if i > 0 { wb_w(fd, " • " as *u8) } wb_w(fd, signals[i] as *u8); i = i + 1 }
123 wb_w(fd, "</div></div>\n" as *u8); return 0
124}
125func wb_footer(fd: i64, text: *u8) -> i64 { wb_w(fd, "<footer><div class=\"wrap\">" as *u8); wb_w(fd, text); wb_w(fd, "</div></footer>\n" as *u8); return 0 }
126
127// ===== enriched content blocks (additive; optional -- do not affect UX compliance) ================
128// FAQ: accessible native <details>/<summary> accordion, no JS.
129func wb_faq_open(fd: i64, heading: *u8) -> i64 { wb_w(fd, "<section class=\"wrap faq\"><h2>" as *u8); wb_w(fd, heading); wb_w(fd, "</h2>\n" as *u8); return 0 }
130func wb_faq_item(fd: i64, q: *u8, a: *u8) -> i64 { wb_w(fd, "<details><summary>" as *u8); wb_w(fd, q); wb_w(fd, "</summary><p>" as *u8); wb_w(fd, a); wb_w(fd, "</p></details>\n" as *u8); return 0 }
131func wb_faq_close(fd: i64) -> i64 { wb_w(fd, "</section>\n" as *u8); return 0 }
132// GALLERY: responsive image grid (lazy-loaded).
133func wb_gallery_open(fd: i64, heading: *u8) -> i64 { wb_w(fd, "<section class=\"wrap\"><h2>" as *u8); wb_w(fd, heading); wb_w(fd, "</h2><div class=\"gallery\">\n" as *u8); return 0 }
134func wb_gallery_item(fd: i64, src: *u8, alt: *u8) -> i64 { wb_w(fd, "<img src=\"" as *u8); wb_w(fd, src); wb_w(fd, "\" alt=\"" as *u8); wb_w(fd, alt); wb_w(fd, "\" loading=\"lazy\">\n" as *u8); return 0 }
135func wb_gallery_close(fd: i64) -> i64 { wb_w(fd, "</div></section>\n" as *u8); return 0 }
136// CTA banner: a mid-page call-to-action strip.
137func wb_cta(fd: i64, heading: *u8, sub: *u8, label: *u8, href: *u8) -> i64 {
138 wb_w(fd, "<div class=\"wrap\"><div class=\"ctaband\"><h2>" as *u8); wb_w(fd, heading)
139 wb_w(fd, "</h2><p>" as *u8); wb_w(fd, sub); wb_w(fd, "</p><a class=\"cta\" href=\"" as *u8); wb_w(fd, href)
140 wb_w(fd, "\">" as *u8); wb_w(fd, label); wb_w(fd, "</a></div></div>\n" as *u8); return 0
141}