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, " &bull; " 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}