code wiki / _hdl_build / nx_kit_market.nx
nx_kit_market.nx source
↩ module page · 193 lines · 15250 B
1// nx_kit_market.nx -- the MARKET design family: a real storefront grammar for commerce briefs (shop,
2// store, boutique, catalog, merch). Fourth first-class family after marketing, Counsel and Journal.
3//
4// What a commerce page actually needs and the marketing grammar never had: a UTILITY BAR (search + cart)
5// above the masthead, CATEGORY CHIPS for browsing, a PRODUCT GRID whose cards carry a visual, a name, a
6// short line, a REAL PRICE and an add-to-cart action, and a SHIPPING/RETURNS/SECURE trust row -- the row
7// that decides whether a stranger buys. What it must NOT have: the SaaS gradient hero, the 3-up feature
8// card grid, the pricing-TIER band (tiers are for SaaS plans, not for beans).
9//
10// ★ Product visuals are GENERATED, never stock: each card gets a deterministic two-stop gradient panel
11// derived from the seed + card index, so a catalog looks like a catalog before a single photograph exists
12// (and the gen-img pipeline can replace the panel later without touching this grammar).
13// Token-driven from the generated palette, both themes, reduced-motion, doc tier included from the start
14// (the round-9 two-languages lesson, applied up front). Pure emit. license_tier: ORIGINAL
15import "nx_uigen_l1.nx"
16import "nx_kit_shell.nx"
17
18func km_styles(buf: *u8, off: i64, hue: i64) -> i64 {
19 var o: i64 = he_puts(buf, off, "<style>:root{" as *u8)
20 o = l1_tok(buf, o, "--km-ink:" as *u8, hue, 18, 13)
21 o = l1_tok(buf, o, "--km-bg:" as *u8, hue, 10, 98)
22 o = l1_tok(buf, o, "--km-card:" as *u8, hue, 8, 100)
23 o = l1_tok(buf, o, "--km-line:" as *u8, hue, 14, 88)
24 o = l1_tok(buf, o, "--km-quiet:" as *u8, hue, 10, 44)
25 o = l1_tok(buf, o, "--km-buy:" as *u8, hue, 64, 34)
26 o = l1_tok(buf, o, "--km-buy2:" as *u8, hue, 60, 44)
27 o = he_puts(buf, o, "}@media(prefers-color-scheme:dark){:root{" as *u8)
28 o = l1_tok(buf, o, "--km-ink:" as *u8, hue, 14, 92)
29 o = l1_tok(buf, o, "--km-bg:" as *u8, hue, 16, 9)
30 o = l1_tok(buf, o, "--km-card:" as *u8, hue, 14, 14)
31 o = l1_tok(buf, o, "--km-line:" as *u8, hue, 14, 24)
32 o = l1_tok(buf, o, "--km-quiet:" as *u8, hue, 10, 66)
33 o = l1_tok(buf, o, "--km-buy:" as *u8, hue, 60, 60)
34 o = l1_tok(buf, o, "--km-buy2:" as *u8, hue, 56, 68)
35 o = he_puts(buf, o, "}}" as *u8)
36 o = he_puts(buf, o, "body{background:var(--km-bg);color:var(--km-ink);font:16px/1.6 system-ui,-apple-system,\"Segoe UI\",sans-serif}" as *u8)
37 // utility bar sits ABOVE the shell header; the shell header becomes the shop masthead
38 o = he_puts(buf, o, ".km-util{background:var(--km-ink);color:var(--km-bg);font-size:.78rem}.km-util .wrap{display:flex;gap:16px;align-items:center;justify-content:space-between;padding:9px 24px;flex-wrap:wrap}.km-util form{display:flex;gap:0;flex:1;max-width:420px}.km-util input{flex:1;min-width:120px;font:inherit;padding:7px 12px;border:0;border-radius:2px 0 0 2px;background:var(--km-bg);color:var(--km-ink)}.km-util button{font:inherit;font-weight:640;border:0;border-radius:0 2px 2px 0;padding:7px 15px;background:var(--km-buy);color:#fff;cursor:pointer}.km-util a{color:var(--km-bg);text-decoration:none;font-weight:640;letter-spacing:.04em}" as *u8)
39 o = kx_shell_reset(buf, o)
40 o = he_puts(buf, o, "header.site{background:var(--km-bg);border-bottom:1px solid var(--km-line)}header.site .bar{padding:16px 24px}header.site .brand{font-weight:800;letter-spacing:-.02em;color:var(--km-ink)}header.site .brand .dot{border-radius:3px;background:var(--km-buy)}header.site nav.top a{font-size:.86rem;font-weight:600;color:var(--km-quiet)}header.site nav.top a:hover{color:var(--km-ink)}" as *u8)
41 o = he_puts(buf, o, "footer.site{border-top:1px solid var(--km-line)}" as *u8)
42 // category chips
43 o = he_puts(buf, o, ".km-chips{padding:18px 0 4px}.km-chips .wrap{display:flex;gap:9px;flex-wrap:wrap}.km-chip{display:inline-block;border:1px solid var(--km-line);background:var(--km-card);border-radius:999px;padding:7px 15px;font-size:.82rem;font-weight:600;text-decoration:none;color:var(--km-ink)}.km-chip:hover{border-color:var(--km-buy);color:var(--km-buy)}" as *u8)
44 // promo strip -- one line of value, not a gradient hero
45 o = he_puts(buf, o, ".km-promo{background:var(--km-card);border-top:1px solid var(--km-line);border-bottom:1px solid var(--km-line);margin-top:14px}.km-promo .wrap{display:flex;gap:14px;align-items:baseline;flex-wrap:wrap;padding:16px 24px}.km-promo b{font-size:1.05rem}.km-promo span{color:var(--km-quiet);font-size:.92rem}" as *u8)
46 // product grid
47 o = he_puts(buf, o, ".km-shop{padding:clamp(24px,4vw,44px) 0}.km-shop h1{font-size:1.22rem;font-weight:750;letter-spacing:-.01em;margin:0 0 4px}.km-shop .km-sub{color:var(--km-quiet);margin:0 0 22px;font-size:.95rem}" as *u8)
48 o = he_puts(buf, o, ".km-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:clamp(14px,2vw,24px)}" as *u8)
49 o = he_puts(buf, o, ".km-item{background:var(--km-card);border:1px solid var(--km-line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}.km-shot{aspect-ratio:4/3;display:block}.km-item h2{font-size:1rem;font-weight:680;margin:14px 15px 3px}.km-item p{color:var(--km-quiet);font-size:.86rem;margin:0 15px 10px;flex:1}.km-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 15px 15px}.km-price{font-weight:750;font-size:1.06rem;font-variant-numeric:tabular-nums}.km-add{font:inherit;font-size:.83rem;font-weight:660;border:0;border-radius:6px;padding:9px 14px;background:var(--km-buy);color:#fff;cursor:pointer;text-decoration:none}.km-add:hover{background:var(--km-buy2)}.km-add:focus-visible{outline:3px solid var(--km-buy2);outline-offset:2px}" as *u8)
50 // shipping / returns / secure -- the row that closes the sale
51 o = he_puts(buf, o, ".km-trust{border-top:1px solid var(--km-line);margin-top:8px;padding:22px 0 30px}.km-trust .wrap{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:18px}.km-trust b{display:block;font-size:.93rem}.km-trust span{color:var(--km-quiet);font-size:.85rem}" as *u8)
52 o = he_puts(buf, o, "@media(prefers-reduced-motion:reduce){.km-item,.km-add{transition:none;animation:none}}</style>" as *u8)
53 return o
54}
55
56// TRUE when a caller-supplied destination is absent. An affordance with nowhere to go is NOT drawn.
57func km_absent(s: *u8) -> i64 {
58 if (s as i64) == 0 { return 1 }
59 if s[0] == (0 as u8) { return 1 }
60 if s[0] == (32 as u8) { if s[1] == (0 as u8) { return 1 } }
61 return 0
62}
63// utility bar: ship note + OPTIONAL search + OPTIONAL cart.
64// A CONTROL THAT GOES NOWHERE IS A FABRICATED FEATURE. This bar used to emit unconditionally a search
65// form whose action was index.html (the q was discarded) and the literal text "Cart (0)" -- a counter
66// that could never be anything but 0, linking to the contact page. Both are now caller-driven and are
67// OMITTED when no destination is given, so a storefront with no commerce backend renders no cart at
68// all rather than a picture of one. (operator 2026-08-10: "cart and a bunch of things havent been
69// built from the first bit up")
70func km_util2(buf: *u8, off: i64, ship: *u8, search_action: *u8, cart_href: *u8, cart_label: *u8) -> i64 {
71 var o: i64 = he_puts(buf, off, "<div class=\"km-util\"><div class=\"wrap\"><span>" as *u8)
72 o = he_puts(buf, o, ship)
73 o = he_puts(buf, o, "</span>" as *u8)
74 if km_absent(search_action) == 0 {
75 o = he_puts(buf, o, "<form role=\"search\" method=\"get\" action=\"" as *u8)
76 o = he_puts(buf, o, search_action)
77 o = he_puts(buf, o, "\"><input type=\"search\" name=\"q\" aria-label=\"Search the shop\" placeholder=\"Search the shop\"><button type=\"submit\">Search</button></form>" as *u8)
78 }
79 if km_absent(cart_href) == 0 {
80 o = he_puts(buf, o, "<a href=\"" as *u8)
81 o = he_puts(buf, o, cart_href)
82 o = he_puts(buf, o, "\">" as *u8)
83 if km_absent(cart_label) == 0 { o = he_puts(buf, o, cart_label) }
84 if km_absent(cart_label) == 1 { o = he_puts(buf, o, "Cart" as *u8) }
85 o = he_puts(buf, o, "</a>" as *u8)
86 }
87 return he_puts(buf, o, "</div></div>" as *u8)
88}
89// legacy arity (rule 19 additive). Resolves to the HONEST rendering, never the fabricated one.
90func km_util(buf: *u8, off: i64, ship: *u8) -> i64 {
91 return km_util2(buf, off, ship, "" as *u8, "" as *u8, "" as *u8)
92}
93func km_chips_open(buf: *u8, off: i64) -> i64 { return he_puts(buf, off, "<nav class=\"km-chips\" aria-label=\"Categories\"><div class=\"wrap\">" as *u8) }
94// ONE category chip. Every chip used to link to index.html, so four different labels all led to the
95// same unfiltered page -- navigation that reports a filter it does not apply. A chip is now emitted
96// ONLY when the caller supplies a real destination; with none it emits NOTHING, because an inert
97// control that looks clickable is the defect, not the absence of one.
98func km_chip2(buf: *u8, off: i64, label: *u8, href: *u8) -> i64 {
99 if km_absent(href) == 1 { return off }
100 var o: i64 = he_puts(buf, off, "<a class=\"km-chip\" href=\"" as *u8)
101 o = he_puts(buf, o, href)
102 o = he_puts(buf, o, "\">" as *u8)
103 o = he_puts(buf, o, label)
104 return he_puts(buf, o, "</a>" as *u8)
105}
106func km_chip(buf: *u8, off: i64, label: *u8) -> i64 {
107 return km_chip2(buf, off, label, "" as *u8)
108}
109func km_chips_close(buf: *u8, off: i64) -> i64 { return he_puts(buf, off, "</div></nav>" as *u8) }
110func km_promo(buf: *u8, off: i64, headline: *u8, sub: *u8) -> i64 {
111 var o: i64 = he_puts(buf, off, "<section class=\"km-promo\"><div class=\"wrap\"><b>" as *u8)
112 o = he_puts(buf, o, headline)
113 o = he_puts(buf, o, "</b><span>" as *u8)
114 o = he_puts(buf, o, sub)
115 return he_puts(buf, o, "</span></div></section>" as *u8)
116}
117func km_shop_open(buf: *u8, off: i64, h2: *u8, sub: *u8) -> i64 {
118 // THE PAGE'S ONE h1. A storefront index/catalog previously had NO h1 at all (nx_page_verify:
119 // "A11Y h1-count=0 (want exactly 1)", a11y-issues=1 on every market-family page ever emitted,
120 // while the doc-tier about/contact scored 0). The shop heading IS the page heading, so it takes
121 // h1 and the item cards drop h3->h2, giving h1 -> h2* : exactly one h1 and no skipped level.
122 var o: i64 = he_puts(buf, off, "<section class=\"km-shop\"><div class=\"wrap\"><h1>" as *u8)
123 o = he_puts(buf, o, h2)
124 o = he_puts(buf, o, "</h1><p class=\"km-sub\">" as *u8)
125 o = he_puts(buf, o, sub)
126 return he_puts(buf, o, "</p><div class=\"km-grid\">" as *u8)
127}
128// one product card. The visual is a GENERATED two-stop gradient derived from hue + index -- a catalog
129// that looks like a catalog before any photograph exists, and never a stock image.
130func km_item2(buf: *u8, off: i64, hue: i64, idx: i64, name: *u8, desc: *u8, price: *u8, buy_href: *u8, buy_label: *u8) -> i64 {
131 // a CATALOG must look like one shop, not a rainbow: drift the hue only slightly per card (±11deg)
132 // and carry the variation in LIGHTNESS instead. The render caught a coffee shop selling a teal bag.
133 var h1: i64 = hue + idx * 11
134 while h1 >= 360 { h1 = h1 - 360 }
135 var h2: i64 = h1 + 14
136 while h2 >= 360 { h2 = h2 - 360 }
137 let l1v: i64 = 76 - (idx % 3) * 7
138 let l2v: i64 = 50 - (idx % 2) * 6
139 var o: i64 = he_puts(buf, off, "<article class=\"km-item\"><svg class=\"km-shot\" viewBox=\"0 0 120 90\" role=\"img\" aria-label=\"\" preserveAspectRatio=\"none\"><defs><linearGradient id=\"g" as *u8)
140 o = l1_put_u8(buf, o, idx)
141 o = he_puts(buf, o, "\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"" as *u8)
142 o = l1_rgb(buf, o, h1, 38, l1v)
143 o = he_puts(buf, o, "\"/><stop offset=\"1\" stop-color=\"" as *u8)
144 o = l1_rgb(buf, o, h2, 48, l2v)
145 o = he_puts(buf, o, "\"/></linearGradient></defs><rect width=\"120\" height=\"90\" fill=\"url(#g" as *u8)
146 o = l1_put_u8(buf, o, idx)
147 o = he_puts(buf, o, ")\"/></svg><h2>" as *u8)
148 o = he_puts(buf, o, name)
149 o = he_puts(buf, o, "</h2><p>" as *u8)
150 o = he_puts(buf, o, desc)
151 o = he_puts(buf, o, "</p><div class=\"km-row\"><span class=\"km-price\">" as *u8)
152 o = he_puts(buf, o, price)
153 // THE BUY CONTROL. It used to be, unconditionally, a link to contact.html labelled "Add to cart"
154 // -- a button that added nothing to anything. Drawn only when the caller names a real destination.
155 if km_absent(buy_href) == 0 {
156 o = he_puts(buf, o, "</span><a class=\"km-add\" href=\"" as *u8)
157 o = he_puts(buf, o, buy_href)
158 o = he_puts(buf, o, "\">" as *u8)
159 if km_absent(buy_label) == 0 { o = he_puts(buf, o, buy_label) }
160 if km_absent(buy_label) == 1 { o = he_puts(buf, o, "View" as *u8) }
161 o = he_puts(buf, o, "</a>" as *u8)
162 }
163 if km_absent(buy_href) == 1 { o = he_puts(buf, o, "</span>" as *u8) }
164 o = he_puts(buf, o, "</div></article>" as *u8)
165 return o
166}
167// legacy arity (rule 19 additive): price, no buy control -- the honest rendering for a catalog whose
168// commerce backend does not exist yet.
169func km_item(buf: *u8, off: i64, hue: i64, idx: i64, name: *u8, desc: *u8, price: *u8) -> i64 {
170 // A STOREFRONT MUST HAVE A BUY CONTROL -- nx_uigen_site_gate T4/T13 count one per priced item and
171 // are RIGHT to. The defect was never that the control existed, it was that it LIED: labelled "Add
172 // to cart" with no cart anywhere in the system. Destination unchanged and still resolves; the LABEL
173 // now states what actually happens. When a real cart exists, callers pass buy_href to km_item2.
174 return km_item2(buf, off, hue, idx, name, desc, price, "contact.html" as *u8, "Enquire" as *u8)
175}
176func km_shop_close(buf: *u8, off: i64) -> i64 { return he_puts(buf, off, "</div></div></section>" as *u8) }
177func km_trust(buf: *u8, off: i64, a1: *u8, b1: *u8, a2: *u8, b2: *u8, a3: *u8, b3: *u8) -> i64 {
178 var o: i64 = he_puts(buf, off, "<section class=\"km-trust\"><div class=\"wrap\"><div><b>" as *u8)
179 o = he_puts(buf, o, a1); o = he_puts(buf, o, "</b><span>" as *u8); o = he_puts(buf, o, b1)
180 o = he_puts(buf, o, "</span></div><div><b>" as *u8)
181 o = he_puts(buf, o, a2); o = he_puts(buf, o, "</b><span>" as *u8); o = he_puts(buf, o, b2)
182 o = he_puts(buf, o, "</span></div><div><b>" as *u8)
183 o = he_puts(buf, o, a3); o = he_puts(buf, o, "</b><span>" as *u8); o = he_puts(buf, o, b3)
184 return he_puts(buf, o, "</span></div></div></section>" as *u8)
185}
186// doc tier so about/contact stay in-family
187func km_doc_styles(buf: *u8, off: i64) -> i64 {
188 var o: i64 = he_puts(buf, off, "<style>.doc h1{font-weight:800;letter-spacing:-.022em}.doc .lede{margin:0 0 1.1em;max-width:60ch;color:var(--km-quiet)}" as *u8)
189 o = he_puts(buf, o, ".doc .callout{background:var(--km-card);border-left:3px solid var(--km-buy);border-radius:0 8px 8px 0}" as *u8)
190 o = he_puts(buf, o, ".field input,.field textarea{border-radius:6px;border:1px solid var(--km-line);background:var(--km-card)}.field input:focus-visible,.field textarea:focus-visible{outline:none;border-color:var(--km-buy);box-shadow:0 0 0 3px var(--km-line)}" as *u8)
191 o = he_puts(buf, o, ".btnp{background:var(--km-buy);border-radius:6px;box-shadow:none}.btnp:hover{background:var(--km-buy2);transform:none}</style>" as *u8)
192 return o
193}