@gemboss/website-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/demo/index.html CHANGED
@@ -22,7 +22,6 @@
22
22
  @media (min-width: 480px) { .demo-metrics b { font-size: 18px; } }
23
23
  .demo-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
24
24
  .demo-ask { margin-left: auto; max-width: 320px; padding: 8px 12px; border-radius: var(--gw-radius-card); background: var(--gw-bg-soft); color: var(--gw-ink); }
25
- .demo-illus { margin-top: 12px; color: var(--gw-muted); font-size: 12px; }
26
25
  .demo-mt { margin-top: 16px; }
27
26
  .demo-specimen + .demo-specimen { margin-top: 48px; }
28
27
  .demo-specimen > h3 { margin-bottom: 16px; font-size: 20px; }
@@ -170,8 +169,8 @@
170
169
  <div class="demo-row"><span><small>Product page</small><b>The Radiance Serum</b></span><span class="gw-badge gw-badge--success">Live</span></div>
171
170
  <div class="demo-row"><span><small>Offer</small><b>Build a box, save 25%</b></span><span class="gw-badge">Draft</span></div>
172
171
  </div>
173
- <p class="demo-illus">GemBoss-built demo pages for Vital Glow. Illustrative.</p>
174
172
  </div>
173
+ <p class="gw-window__caption">Illustrative data. Vital Glow is a GemBoss demo brand.</p>
175
174
  </div>
176
175
  </div>
177
176
  </section>
@@ -287,8 +286,8 @@
287
286
  <p><b>0.004</b>CLS</p>
288
287
  <p><b>0.05 s</b>TTFB</p>
289
288
  </div>
290
- <p class="demo-illus">Measured on a live GemBoss store: mobile, simulated 4G, median of 8 Lighthouse runs, 10 Oct 2026.</p>
291
289
  </div>
290
+ <p class="gw-window__caption">Measured on a live GemBoss store: mobile, simulated 4G, median of 8 Lighthouse runs, 10 Oct 2026.</p>
292
291
  </div>
293
292
  </div>
294
293
  </section>
@@ -315,8 +314,8 @@
315
314
  <div class="demo-row"><span><b>What dermatologists check first</b><small>Solution aware · ingredient readers</small></span><span class="gw-badge">Drafted</span></div>
316
315
  <div class="demo-row"><span><b>A gift that looks after her</b><small>Product aware · gift buyers</small></span><span class="gw-badge">Drafted</span></div>
317
316
  </div>
318
- <p class="demo-illus">GemBoss-built demo pages for Vital Glow. Illustrative.</p>
319
317
  </div>
318
+ <p class="gw-window__caption">Illustrative data. Vital Glow is a GemBoss demo brand.</p>
320
319
  </div>
321
320
  </div>
322
321
  </section>
@@ -339,8 +338,8 @@
339
338
  <li>Checked the render on desktop and phone</li>
340
339
  </ul>
341
340
  <div class="demo-head demo-mt"><span>The change waits on the canvas until you publish it.</span><span class="gw-badge gw-badge--accent">Draft</span></div>
342
- <p class="demo-illus">Vital Glow is a GemBoss demo store. Illustrative.</p>
343
341
  </div>
342
+ <p class="gw-window__caption">Illustrative data. Vital Glow is a GemBoss demo store.</p>
344
343
  </div>
345
344
  <div class="gw-features gw-grid--3">
346
345
  <div class="gw-feature"><h3>Ask in plain words</h3><p>"Add a trust row under the buy box" is enough.</p></div>
@@ -383,12 +382,17 @@
383
382
  <p class="gw-label">Pricing</p>
384
383
  <h2 id="pricing-h"><span class="gw-dim">Every angle gets its own funnel.</span> Pay for the traffic you send it.</h2>
385
384
  <p class="gw-lead">Build as many funnels as you like on every plan, and try it free for 14 days.</p>
385
+ <div class="gw-segmented" role="radiogroup" aria-label="Billing period" data-gw-segmented="billing">
386
+ <button type="button" role="radio" aria-checked="true" value="monthly">Monthly</button>
387
+ <button type="button" role="radio" aria-checked="false" value="yearly">Yearly –20%</button>
388
+ </div>
386
389
  </div>
387
390
  <div class="gw-pricing">
388
391
  <div class="gw-plan">
389
392
  <h3>Starter</h3>
390
393
  <p>Find your first winning angle.</p>
391
- <p class="gw-plan__price">$49 <small>/ month</small></p>
394
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">$49</span><span data-gw-show="billing:yearly" hidden>$39</span> <small>/ month</small></p>
395
+ <p><span data-gw-show="billing:monthly">Billed monthly through Shopify</span><span data-gw-show="billing:yearly" hidden>Billed $470.40 a year through Shopify</span></p>
392
396
  <a class="gw-btn gw-btn--secondary gw-btn--block" href="https://admin.gemboss.ai/signup">Start 14-day free trial</a>
393
397
  <ul class="gw-list--check">
394
398
  <li>10,000 funnel sessions a month</li>
@@ -401,7 +405,8 @@
401
405
  <span class="gw-badge gw-badge--accent">Recommended for $10k–50k</span>
402
406
  <h3>Growth</h3>
403
407
  <p>Test new angles every week.</p>
404
- <p class="gw-plan__price">$149 <small>/ month</small></p>
408
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">$149</span><span data-gw-show="billing:yearly" hidden>$119</span> <small>/ month</small></p>
409
+ <p><span data-gw-show="billing:monthly">Billed monthly through Shopify</span><span data-gw-show="billing:yearly" hidden>Billed $1,430.40 a year through Shopify</span></p>
405
410
  <a class="gw-btn gw-btn--block" href="https://admin.gemboss.ai/signup">Start 14-day free trial</a>
406
411
  <ul class="gw-list--check">
407
412
  <li>50,000 funnel sessions a month</li>
@@ -413,7 +418,8 @@
413
418
  <div class="gw-plan">
414
419
  <h3>Scale</h3>
415
420
  <p>Run many angles at once.</p>
416
- <p class="gw-plan__price">$399 <small>/ month</small></p>
421
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">$399</span><span data-gw-show="billing:yearly" hidden>$319</span> <small>/ month</small></p>
422
+ <p><span data-gw-show="billing:monthly">Billed monthly through Shopify</span><span data-gw-show="billing:yearly" hidden>Billed $3,830.40 a year through Shopify</span></p>
417
423
  <a class="gw-btn gw-btn--secondary gw-btn--block" href="https://admin.gemboss.ai/signup">Start 14-day free trial</a>
418
424
  <ul class="gw-list--check">
419
425
  <li>200,000 funnel sessions a month</li>
@@ -450,7 +456,7 @@
450
456
  <tr><th scope="col">Plan</th><th scope="col">Starter</th><th scope="col">Growth</th><th scope="col">Scale</th><th scope="col">Enterprise</th></tr>
451
457
  </thead>
452
458
  <tbody>
453
- <tr><th scope="row">Price a month</th><td>$49</td><td>$149</td><td>$399</td><td>from $999</td></tr>
459
+ <tr><th scope="row">Price a month</th><td><span data-gw-show="billing:monthly">$49</span><span data-gw-show="billing:yearly" hidden>$39</span></td><td><span data-gw-show="billing:monthly">$149</span><span data-gw-show="billing:yearly" hidden>$119</span></td><td><span data-gw-show="billing:monthly">$399</span><span data-gw-show="billing:yearly" hidden>$319</span></td><td>from $999</td></tr>
454
460
  <tr><th scope="row">Funnel sessions a month</th><td>10,000</td><td>50,000</td><td>200,000</td><td>Custom</td></tr>
455
461
  <tr><th scope="row">Funnels live at once</th><td>3</td><td>15</td><td>Unlimited</td><td>Unlimited</td></tr>
456
462
  <tr><th scope="row">A/B tests at once</th><td>2</td><td>5</td><td>20</td><td>Unlimited</td></tr>
@@ -480,6 +486,20 @@
480
486
  <a class="gw-btn gw-btn--sm" href="https://admin.gemboss.ai/signup">Start free</a>
481
487
  <a class="gw-btn gw-btn--lg" href="https://admin.gemboss.ai/signup">Start 14-day free trial</a>
482
488
  </div>
489
+ <div class="gw-actions demo-mt">
490
+ <button class="gw-btn" type="button" data-demo-busy>Send the request</button>
491
+ <button class="gw-btn gw-btn--secondary" type="button" data-demo-busy>Run the audit</button>
492
+ </div>
493
+ <p class="gw-note demo-mt">Press one: it waits two seconds, keeps its width and ignores a second press.</p>
494
+ </div>
495
+ <div class="demo-specimen">
496
+ <h3>Segmented control</h3>
497
+ <div class="gw-segmented" role="radiogroup" aria-label="How you want to work" data-gw-segmented="work">
498
+ <button type="button" role="radio" aria-checked="true" value="self">Run it yourself</button>
499
+ <button type="button" role="radio" aria-checked="false" value="managed">We run it for you</button>
500
+ </div>
501
+ <p class="demo-mt" data-gw-show="work:self">Self-serve plans from $49 a month. You build, test and promote the funnels.</p>
502
+ <p class="demo-mt" data-gw-show="work:managed" hidden>Managed from $690 a month. A dedicated operator builds and tests the funnels you approve.</p>
483
503
  </div>
484
504
  <div class="demo-specimen">
485
505
  <h3>Links</h3>
@@ -521,6 +541,51 @@
521
541
  </div>
522
542
  </section>
523
543
 
544
+ <!-- prose -->
545
+ <section class="gw-section" aria-labelledby="prose-h">
546
+ <div class="gw-wrap gw-wrap--narrow">
547
+ <div class="gw-section__head">
548
+ <p class="gw-label">Prose</p>
549
+ <h2 id="prose-h"><span class="gw-dim">Blog, docs and legal.</span> Plain HTML, easy to read.</h2>
550
+ <p class="gw-lead">A bare article inside .gw-prose: no class on its headings, lists, quote, code or table.</p>
551
+ </div>
552
+ <article class="gw-prose" aria-label="Why the kit looks the way it does">
553
+ <h2>One language for the site and the product</h2>
554
+ <p>A visitor crosses from the website into the product in one click. If the two disagree, a cream site and a white app, a purple button outside and a black one inside, the seam is exactly where trust is decided. So the website uses the product's language: <a href="https://gemboss.ai/">white canvas, ink actions, one accent</a>, hairlines, almost no shadow.</p>
555
+ <h3>Scales instead of eyeballing</h3>
556
+ <p>On gemboss.ai, before the scales, the four most common paddings were 9, 5, 12 and 15 pixels, and 1,291 of 2,076 spacing values belonged to no scale. The scales make "a bit more space" a choice between two real values:</p>
557
+ <ul>
558
+ <li>Spacing: 0 2 4 6 8 12 16 20 24 32 40 48 64 80 96 120 (px)</li>
559
+ <li>Type: 11 12 13 14 16 18 20 22 24 30, plus the two display sizes</li>
560
+ <li>Radii: 8 for actions, 12 for cards, 16 for panels, fully round for chips</li>
561
+ </ul>
562
+ <blockquote><p>A made-up number or a fake testimonial is found out, and then every other claim on the page is doubted.</p></blockquote>
563
+ <h3>Check before you hand it over</h3>
564
+ <p>Run <code>npx gemboss-website check</code> in the site; <code>--fix-list</code> prints the same findings as a to-do list. Then look at the page at 320px and 1366px.</p>
565
+ <figure>
566
+ <pre><code>npx gemboss-website check
567
+ npx gemboss-website check --fix-list</code></pre>
568
+ <figcaption>The check exits 1 on an error, so it can run in the site's CI too.</figcaption>
569
+ </figure>
570
+ <h3>What a brand changes</h3>
571
+ <table>
572
+ <thead><tr><th scope="col">Variable</th><th scope="col">What it is</th><th scope="col">GemBoss</th></tr></thead>
573
+ <tbody>
574
+ <tr><th scope="row"><code>--gw-accent-light</code></th><td>Links, focus ring, active item, label square</td><td><code>#5661f5</code></td></tr>
575
+ <tr><th scope="row"><code>--gw-accent-strong-light</code></th><td>Accent as text, and on its own tint</td><td><code>#3a44de</code></td></tr>
576
+ <tr><th scope="row"><code>--gw-accent-soft-light</code></th><td>The tint: active chip, chosen option</td><td><code>#eef0fe</code></td></tr>
577
+ <tr><th scope="row"><code>--gw-on-accent-light</code></th><td>Text on a solid accent</td><td><code>#ffffff</code></td></tr>
578
+ </tbody>
579
+ </table>
580
+ <ol>
581
+ <li>Set the eight accent variables once, in a <code>:root</code> block after the kit.</li>
582
+ <li>Check the contrast of the accent as text on white and on the soft ground.</li>
583
+ <li>Look at the site in light and dark, at 320px and 1366px.</li>
584
+ </ol>
585
+ </article>
586
+ </div>
587
+ </section>
588
+
524
589
  <!-- faq -->
525
590
  <section class="gw-section" aria-labelledby="faq-h">
526
591
  <div class="gw-wrap gw-wrap--narrow">
@@ -625,5 +690,14 @@
625
690
  </footer>
626
691
 
627
692
  <script src="../js/website.js" defer></script>
693
+ <script>
694
+ // demo only: what a site's own code does while a request is on its way
695
+ document.addEventListener('click', function (e) {
696
+ var b = e.target.closest('[data-demo-busy]');
697
+ if (!b || b.getAttribute('aria-busy') === 'true') return;
698
+ b.setAttribute('aria-busy', 'true');
699
+ setTimeout(function () { b.removeAttribute('aria-busy'); }, 2000);
700
+ });
701
+ </script>
628
702
  </body>
629
703
  </html>
@@ -98,8 +98,28 @@ its label may wrap. Inside a night band the default button turns light by itself
98
98
  </div>
99
99
  ```
100
100
 
101
+ Pressed (`:active`): the ink one step darker (the hover ink), and nothing moves; `--secondary` takes the soft tint and
102
+ `--ghost` the hairline tint.
103
+
104
+ **Loading.** While a `<button>` waits on the network (a form sent, an audit started), set `aria-busy="true"` on it and
105
+ remove it when the answer comes. The label stays in the markup, so the button keeps its width and its accessible name,
106
+ but it is not painted: a 16px ring in the button's own ink turns in its place, and under `prefers-reduced-motion` it is
107
+ a still ellipsis. `website.js` ignores presses on a busy button, by mouse or keyboard, so nothing is sent twice. When
108
+ the wait ends, say what happened in words (a `.gw-note` or a `role="status"` line next to it).
109
+
110
+ ```html
111
+ <button class="gw-btn" type="submit" aria-busy="true">Send the request</button>
112
+ ```
113
+
114
+ ```js
115
+ button.setAttribute('aria-busy', 'true'); // the request leaves
116
+ button.removeAttribute('aria-busy'); // the answer came
117
+ ```
118
+
101
119
  Don't: an accent, gradient or wash fill; pill-shaped buttons; uppercase; two filled buttons side by side (one
102
- `.gw-btn` per view, the second is `--secondary` or a link); a `<div>` with a click handler instead of `<a>`/`<button>`.
120
+ `.gw-btn` per view, the second is `--secondary` or a link); a `<div>` with a click handler instead of `<a>`/`<button>`;
121
+ a loading class, your own spinner or `disabled` on a waiting button (`check` reports `busy-button`: `disabled` throws
122
+ the keyboard focus to the top of the page); a "Loading…" label in place of the real one.
103
123
 
104
124
  ### Chips `.gw-chip`
105
125
 
@@ -112,6 +132,37 @@ hairline on accent-soft). As a `<button>`, `<a>` or `<label>` it is 44px tall on
112
132
 
113
133
  Don't: use a chip as the page's action; square it; more than about six in a row.
114
134
 
135
+ ### Segmented control `.gw-segmented`
136
+
137
+ One choice out of two to four, shown as one group: Monthly / Yearly on a pricing page, Run it yourself / We run it for
138
+ you. It is a radio group: one Tab stop, the arrow keys (and Home, End) move and choose, a click or Space chooses. The
139
+ look is the dashboard's (`@gemboss/ui` `.ui-seg`): one hairline group, radius 8, a hairline between options, the chosen
140
+ option on the accent tint, so it follows a brand's accent. 40px tall; on touch screens every option is at least 44px
141
+ and its label may wrap. `--block` stretches it to the row and shares the width equally.
142
+
143
+ What an option shows: any element with `data-gw-show="group:value"` (the group's `data-gw-segmented` name, the option's
144
+ `value`) is hidden unless that option is chosen. Put every version in the markup and `hidden` on all but the chosen
145
+ one's: without the script the page reads with the default choice, with it `website.js` swaps them. An element may
146
+ belong to several options (`data-gw-show="billing:monthly billing:quarterly"`). A choice also fires `gw:segmented` on
147
+ the group (it bubbles; `detail` is `{ name, value }`) for the page's own script, for example to move a "Recommended"
148
+ badge.
149
+
150
+ ```html
151
+ <div class="gw-segmented" role="radiogroup" aria-label="Billing period" data-gw-segmented="billing">
152
+ <button type="button" role="radio" aria-checked="true" value="monthly">Monthly</button>
153
+ <button type="button" role="radio" aria-checked="false" value="yearly">Yearly –20%</button>
154
+ </div>
155
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">$49</span><span data-gw-show="billing:yearly" hidden>$39</span> <small>/ month</small></p>
156
+ ```
157
+
158
+ Labels: one to three words, as short in every language as in English (gemboss.ai: "Tháng / Năm –20%", "按月 / 按年
159
+ –20%"). The chosen label does not change weight, so nothing moves. A discount in a label is the one billing gives.
160
+
161
+ Don't: links dressed as a segmented control (to go to another page, use links); panels of one page that the reader
162
+ should be able to see side by side; more than four options (use chips); a choice that changes nothing visible; two
163
+ groups with the same name on one page; the old `role="group"` with `aria-pressed` buttons (`check` reports
164
+ `segmented-markup`).
165
+
115
166
  ### Badges `.gw-badge`
116
167
 
117
168
  Small status, a pill, 12px. Variants: `--accent` (a test, the recommended plan), `--success` and `--warning` (state:
@@ -142,17 +193,27 @@ other than 12 (16 is for large panels: plans, quote).
142
193
 
143
194
  ### Window `.gw-window`
144
195
 
145
- A framed screen: `__bar` (three dots and where the screen lives) and `__body` (16px padding, none around a single
146
- `<img>`). Use it around real product screenshots, or around a drawn mock-up built in the site's CSS with the tokens.
196
+ A framed screen: `__bar` (three dots and where the screen lives), `__body` (16px padding, none around a single
197
+ `<img>`) and `__caption`. Use it around real product screenshots, or around a drawn mock-up built in the site's CSS with
198
+ the tokens.
199
+
200
+ `__caption` says what the numbers in the window are: 12px, muted, at the end of the window, the last child after
201
+ `__body` (a `<figcaption>` when the window is a `<figure>`). Under a drawn mock-up whose numbers are made up it says
202
+ **"Illustrative data."**, in exactly these words: vi "Dữ liệu minh hoạ.", zh "示意数据。". Add a second sentence only when
203
+ the mock-up shows a demo brand ("Vital Glow is a GemBoss demo brand."). Under real numbers it names their source
204
+ ("Measured on a live GemBoss store, 10 Oct 2026."). `check` warns about a drawn window with numbers and no caption
205
+ (`illustrative-caption`).
147
206
 
148
207
  ```html
149
208
  <div class="gw-window">
150
209
  <div class="gw-window__bar">Funnels / Radiance Serum</div>
151
210
  <div class="gw-window__body"><img src="/img/funnels.webp" alt="The funnel list with four steps" width="1200" height="750"></div>
211
+ <p class="gw-window__caption">Illustrative data. Vital Glow is a GemBoss demo brand.</p>
152
212
  </div>
153
213
  ```
154
214
 
155
- Don't: put interactive controls inside a mock-up; show made-up numbers without "Illustrative" under them.
215
+ Don't: put interactive controls inside a mock-up; show made-up numbers without the caption; write the caption in
216
+ other words ("Sample", "Demo numbers") or leave it in English on a translated page.
156
217
 
157
218
  ### Icon `.gw-icon`
158
219
 
@@ -204,7 +265,7 @@ Native `<details class="gw-faq__item">` with a `<summary>`, no script. The + tur
204
265
 
205
266
  Don't: hide content a reader needs to decide (prices, terms) only inside a closed answer; more than eight questions.
206
267
 
207
- ### Not in 0.1.0
268
+ ### Not in the kit yet
208
269
 
209
270
  `.gw-tabs` does not exist yet. If a page needs tabs, build a stopgap (`kitgap-tabs`) and report the gap.
210
271
 
@@ -282,6 +343,48 @@ One snippet per section, with its limits in a comment, is in `templates/sections
282
343
  Don't: a number without a source; logos or quotes you do not have permission for; a filled button on every plan
283
344
  (only the featured one); a hero without a real first line of content above the fold on a 390px phone.
284
345
 
346
+ ## Prose
347
+
348
+ | class | for |
349
+ |---|---|
350
+ | `.gw-prose` | a page that is read rather than scanned (a blog post, a docs page, terms, privacy): the article's own HTML inside it, 18px body, 68ch measure |
351
+ | `.gw-prose--sm` | the same at a 16px body, for docs and legal pages |
352
+
353
+ The HTML a CMS or a markdown renderer writes needs no classes. Inside `.gw-prose`:
354
+
355
+ | element | look |
356
+ |---|---|
357
+ | `h1` | the article title, at the section-headline size (`--gw-display-h2`) |
358
+ | `h2`, `h3`, `h4` | 30, 24, 20px (24, 20, 18px in `--sm`), weight 650; 48, 40, 32px of air above, 12px below |
359
+ | `p`, `ul`, `ol` | 20px between blocks (16px in `--sm`); bullets and numbers with muted markers, 8px between items |
360
+ | `blockquote` | a 3px rule on its left, muted words (the dashboard's reader quote) |
361
+ | `code` | a tint chip in `--gw-font-mono` (16px, 14px in `--sm`) that wraps on a phone; in a table it stays whole |
362
+ | `pre` | a soft box, 14px mono, that scrolls sideways inside itself |
363
+ | `table` | as a direct child: the `.gw-table` look, and it scrolls inside its own box when it is wider than the column |
364
+ | `img`, `figure`, `figcaption` | images radius 12; a caption 14px muted, 12px under its figure |
365
+ | `hr`, `a` | a hairline with 32px of air; links in the accent, underlined |
366
+
367
+ Kit components inside the prose keep their own look (`.gw-table`, `.gw-window`, `.gw-list--check`, `.gw-quote`,
368
+ `.gw-btn`). Prefer the `.gw-table` wrapper (`role="region"`, a name, `tabindex="0"`) when you write the markup: a
369
+ keyboard can scroll it.
370
+
371
+ ```html
372
+ <section class="gw-section">
373
+ <div class="gw-wrap gw-wrap--narrow">
374
+ <article class="gw-prose">
375
+ <h1>Why the kit looks the way it does</h1>
376
+ <p>Each rule came from measuring real pages, not from taste alone.</p>
377
+ <h2>One language for the site and the product</h2>
378
+ <p>A visitor crosses from the website into the product in one click. …</p>
379
+ <ul><li>…</li></ul>
380
+ </article>
381
+ </div>
382
+ </section>
383
+ ```
384
+
385
+ Don't: marketing sections inside the prose (end the article, then the section); headings chosen for their size (keep
386
+ them in order); a page without `.gw-prose` that styles its articles in site CSS (that was the stopgap of gap #46).
387
+
285
388
  ## Motion
286
389
 
287
390
  | class | for |
@@ -291,6 +394,11 @@ Don't: a number without a source; logos or quotes you do not have permission for
291
394
  Put it on a block (a grid, a list, a window), not on each card in a grid. Don't use it on the hero or anything in
292
395
  the first screen, and don't animate anything but `transform` and `opacity`.
293
396
 
397
+ ## Additions to the markup contract in 0.2.0
398
+
399
+ The button states (`:active`, `[aria-busy="true"]`), `.gw-segmented` (+ `--block`) with its `data-gw-segmented` /
400
+ `data-gw-show` hooks, `.gw-window__caption` and `.gw-prose` (+ `--sm`). Two tokens: `--gw-font-mono` and `--gw-spin-ms`.
401
+
294
402
  ## Additions to the markup contract in 0.1.0
295
403
 
296
404
  The contract names the classes the script relies on; these CSS-only helpers were added with it: `.gw-page`
@@ -77,23 +77,56 @@ Script behaviour:
77
77
  - **Current page**: the script marks `aria-current="page"` on `.gw-nav__link` / `.gw-mega__link` whose `href`
78
78
  pathname equals `location.pathname` (when the markup did not already).
79
79
 
80
+ ## Segmented control (0.2.0)
81
+
82
+ ```html
83
+ <div class="gw-segmented" role="radiogroup" aria-label="Billing period" data-gw-segmented="billing">
84
+ <button type="button" role="radio" aria-checked="true" value="monthly">Monthly</button>
85
+ <button type="button" role="radio" aria-checked="false" value="yearly">Yearly –20%</button>
86
+ </div>
87
+ …
88
+ <span data-gw-show="billing:monthly">$49</span><span data-gw-show="billing:yearly" hidden>$39</span>
89
+ ```
90
+
91
+ - The group: `.gw-segmented` (+ `--block`), `role="radiogroup"`, a name (`aria-label` or `aria-labelledby`) and
92
+ `data-gw-segmented="<name>"`, unique on the page. Two to four `<button type="button" role="radio">` children, each
93
+ with a `value` and `aria-checked`, exactly one `"true"`: the choice the page shows without the script.
94
+ - Content: any element with `data-gw-show="<name>:<value>"` (several tokens, space-separated, for several options) is
95
+ shown only while that option is chosen. The markup sets `hidden` on every version but the default's.
96
+ - Script behaviour: on load the checked option (or the first) is chosen again, which corrects `hidden` on the content
97
+ and sets the roving `tabindex` (`0` on the chosen option, `-1` on the others: one Tab stop). A click chooses;
98
+ ArrowRight / ArrowDown and ArrowLeft / ArrowUp move to the next and previous option, wrapping, Home and End to the
99
+ first and last, and the moved-to option is chosen and focused (the radio-group pattern). Each choice made by the
100
+ reader fires `gw:segmented` on the group (`bubbles: true`, `detail: { name, value }`); loading does not.
101
+
102
+ ## Busy button (0.2.0)
103
+
104
+ - Markup: `aria-busy="true"` on a `<button class="gw-btn">` while it waits; the page's own script sets and removes it.
105
+ No `disabled` at the same time, no loading class, no spinner element: the CSS draws the ring (`::before`; `--sm`
106
+ keeps `::after` for its hit area).
107
+ - Script behaviour: one click listener on the document, in the capture phase, cancels a click on a
108
+ `.gw-btn[aria-busy="true"]` (or inside one) before the page's handlers see it: `preventDefault` and
109
+ `stopPropagation`. A form is not submitted twice, by mouse, Enter or Space, and the button keeps the focus.
110
+
80
111
  ## Components (CSS only)
81
112
 
82
113
  | class | what |
83
114
  |---|---|
84
- | `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--sm`, `--lg`, `--block`) | actions; rectangle 8px, ink fill by default |
115
+ | `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--sm`, `--lg`, `--block`) | actions; rectangle 8px, ink fill by default; `:active` pressed, `[aria-busy="true"]` loading (script above) |
85
116
  | `.gw-link` (+ `--arrow`) | inline text link in the accent; `--arrow` adds → that moves 2px on hover |
86
117
  | `.gw-label` | the label above a heading: 13px muted, 8px accent square before it |
87
118
  | `.gw-dim` | the muted first sentence inside a headline |
88
119
  | `.gw-chip` (+ `.is-active`) | selectable token, pill |
120
+ | `.gw-segmented` (+ `--block`) | one choice out of two to four (script above) |
89
121
  | `.gw-badge` (+ `--accent`, `--success`, `--warning`) | small status, pill |
90
122
  | `.gw-card` (+ `--soft`, `--link`) | white box, 1px hairline, radius 12; `--link` makes the whole card an `<a>` |
91
- | `.gw-window` (+ `__bar`, `__body`) | a framed screenshot/mock-up, like a browser or app window |
123
+ | `.gw-window` (+ `__bar`, `__body`, `__caption`) | a framed screenshot/mock-up, like a browser or app window; `__caption` (last child) says what its numbers are: "Illustrative data." or their source |
92
124
  | `.gw-icon` | 20px inline stroke SVG wrapper |
93
125
  | `.gw-list--check` | list with check marks |
94
126
  | `.gw-table` (+ `--compare`) | data / comparison table; scrolls inside its box on phones |
95
127
  | `.gw-faq` (`<details class="gw-faq__item"><summary>`) | FAQ accordion with no script |
96
- | `.gw-tabs` (`role="tablist"`) | NOT in 0.1.0 (a reported gap would add it) |
128
+ | `.gw-prose` (+ `--sm`) | long-form reading: an article's own HTML, no classes inside |
129
+ | `.gw-tabs` (`role="tablist"`) | NOT in the kit yet (a reported gap would add it) |
97
130
 
98
131
  ## Sections
99
132
 
package/docs/theming.md CHANGED
@@ -16,7 +16,7 @@ else.
16
16
  |---|---|---|
17
17
  | `--gw-accent-light` | the accent on light pages: links, focus ring, active item, label square, chart line | `#5661f5` |
18
18
  | `--gw-accent-strong-light` | the accent on hover and as text on `--gw-accent-soft` | `#3a44de` |
19
- | `--gw-accent-soft-light` | the accent's tint: active chip, accent badge, icon hover | `#eef0fe` |
19
+ | `--gw-accent-soft-light` | the accent's tint: active chip, chosen segmented option, accent badge, icon hover | `#eef0fe` |
20
20
  | `--gw-on-accent-light` | text on a solid accent (rare; the kit never fills a button with it) | `#ffffff` |
21
21
  | `--gw-accent-dark` | the accent in dark mode **and inside night bands** | `#7c84ff` |
22
22
  | `--gw-accent-strong-dark` | its hover / strong form | `#a3a9ff` |
@@ -82,7 +82,8 @@ So:
82
82
 
83
83
  1. **Accent text on white at least 4.5:1**: `--gw-accent-light` and `--gw-accent-strong-light` on `#ffffff`.
84
84
  Check `--gw-accent-strong-light` on `--gw-bg-soft` (`#f2f3f5`) too, where links sit in soft sections.
85
- 2. `--gw-accent-strong-light` on `--gw-accent-soft-light`: at least 4.5:1 (the accent badge and the active chip).
85
+ 2. `--gw-accent-strong-light` on `--gw-accent-soft-light`: at least 4.5:1 (the accent badge, the active chip and the
86
+ chosen option of a segmented control).
86
87
  3. `--gw-accent-dark` and `--gw-accent-strong-dark` on `#0d0d12` (dark page) and on `#0c0e14` (night band): at
87
88
  least 4.5:1.
88
89
  4. `--gw-accent-strong-dark` on `--gw-accent-soft-dark`: at least 4.5:1.
package/js/website.js CHANGED
@@ -2,7 +2,7 @@
2
2
  (function () {
3
3
  'use strict';
4
4
  if (window.gwWebsite) return;
5
- window.gwWebsite = { version: '0.1.0' };
5
+ window.gwWebsite = { version: '0.2.0' };
6
6
  var doc = document, root = doc.documentElement;
7
7
  function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
8
8
  function watch(m, fn) { if (m.addEventListener) m.addEventListener('change', fn); }
@@ -140,8 +140,45 @@
140
140
  });
141
141
  }
142
142
 
143
+ // Segmented control: a radio group, one Tab stop; arrows (Home, End) move and choose. What one option shows carries
144
+ // data-gw-show="group:value" and is hidden unless that option is chosen; without the script the markup's own choice shows.
145
+ function segmented() {
146
+ all('[data-gw-segmented]').forEach(function (g) {
147
+ var name = attr(g, 'data-gw-segmented', ''), opts = all('[role="radio"]', g);
148
+ if (!opts.length) return;
149
+ function pick(o, focus, said) {
150
+ var v = attr(o, 'value', '');
151
+ opts.forEach(function (x) { x.setAttribute('aria-checked', String(x === o)); x.setAttribute('tabindex', x === o ? '0' : '-1'); });
152
+ if (focus) o.focus();
153
+ all('[data-gw-show]').forEach(function (el) {
154
+ var mine = attr(el, 'data-gw-show', '').split(/\s+/).filter(function (t) { return t.split(':')[0] === name; });
155
+ if (mine.length) el.hidden = mine.indexOf(name + ':' + v) < 0;
156
+ });
157
+ if (said && window.CustomEvent) g.dispatchEvent(new CustomEvent('gw:segmented', { bubbles: true, detail: { name: name, value: v } }));
158
+ }
159
+ opts.forEach(function (o, i) {
160
+ on(o, 'click', function () { pick(o, false, true); });
161
+ on(o, 'keydown', function (e) {
162
+ var n = opts.length, k = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
163
+ if (k === undefined || e.altKey || e.ctrlKey || e.metaKey) return;
164
+ e.preventDefault();
165
+ pick(opts[k % n], true, true);
166
+ });
167
+ });
168
+ pick(opts.filter(function (o) { return attr(o, 'aria-checked', '') === 'true'; })[0] || opts[0]);
169
+ });
170
+ }
171
+
172
+ // A busy button (aria-busy="true") ignores presses, by mouse or keyboard: no double submit. It keeps its focus and name.
173
+ function busy() {
174
+ on(doc, 'click', function (e) {
175
+ var b = e.target.closest && e.target.closest('.gw-btn[aria-busy="true"]');
176
+ if (b) { e.preventDefault(); e.stopPropagation(); }
177
+ }, true);
178
+ }
179
+
143
180
  function init() {
144
- [mega, mobile, theme, reveal, header, current].forEach(function (f) {
181
+ [mega, mobile, theme, reveal, header, current, segmented, busy].forEach(function (f) {
145
182
  try { f(); } catch (e) { console.error('[gw-website]', e); }
146
183
  });
147
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/website-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -22,7 +22,8 @@ the kit and its rules are how the page ends up looking designed. Compose from th
22
22
  scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
23
23
  - Another brand: change only the accent family, wash hues, logo and favicon (`KIT/docs/theming.md`).
24
24
  4. **Words**: real facts only. A missing price, number, name or date becomes a visible `[PLACEHOLDER]` and a
25
- question to the person. No em dash in visible text, no "Learn more" as a whole link.
25
+ question to the person. Made-up numbers in a drawn mock-up get `<p class="gw-window__caption">Illustrative
26
+ data.</p>` in its window. No em dash in visible text, no "Learn more" as a whole link.
26
27
  5. **Check**: `npx gemboss-website check` and fix every error (`--fix-list` gives a to-do list). Then look at the
27
28
  page at 320px and 1366px. Only then say it is done.
28
29
 
package/system.json CHANGED
@@ -79,6 +79,7 @@
79
79
  },
80
80
  "type": {
81
81
  "family": "Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
82
+ "mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
82
83
  "weights": [400, 500, 600, 650, 700],
83
84
  "scale": [11, 12, 13, 14, 16, 18, 20, 22, 24, 30],
84
85
  "display_above": 30,
@@ -115,6 +116,7 @@
115
116
  "properties": ["transform", "opacity"],
116
117
  "ease": "cubic-bezier(.22,1,.36,1)",
117
118
  "reveal_ms": 700,
118
- "$comment": "Animate transform and opacity only. Everything is still under prefers-reduced-motion."
119
+ "spin_ms": 700,
120
+ "$comment": "Animate transform and opacity only. Everything is still under prefers-reduced-motion (a busy button's ring turns once every spin_ms, as the dashboard's spinner, and stands still as an ellipsis)."
119
121
  }
120
122
  }
@@ -4,7 +4,8 @@
4
4
  Add .gw-band--night for a dark hero (it counts as one of the page's three night bands).
5
5
  Limits: a short label, a two-part headline (muted first sentence, ink second), at most two lines under it,
6
6
  one .gw-btn and one secondary action. The optional window holds a real product screen (img with alt, width, height)
7
- or a mock-up that says "Illustrative" when its numbers are not real. Never tuck the hero under the header.
7
+ or a drawn mock-up. Numbers in it that are not real get the caption "Illustrative data." (vi: "Dữ liệu minh hoạ.",
8
+ zh: "示意数据。"); real ones get their source there. Never tuck the hero under the header.
8
9
  -->
9
10
  <section class="gw-hero">
10
11
  <div class="gw-hero__wash" aria-hidden="true"></div>
@@ -20,6 +21,7 @@
20
21
  <div class="gw-window">
21
22
  <div class="gw-window__bar">[Where this screen lives, e.g. the app section]</div>
22
23
  <div class="gw-window__body"><img src="[/img/screen.webp]" alt="[What the screen shows]" width="[1200]" height="[750]"></div>
24
+ <p class="gw-window__caption">[Illustrative data. Or the source of the numbers. Delete when the screen shows no numbers.]</p>
23
25
  </div>
24
26
  </div>
25
27
  </section>
@@ -2,6 +2,11 @@
2
2
  Pricing: plan cards with the brand's real plan names and prices, exactly as billed. A missing price is [PRICE]
3
3
  and a question to the person, never a guess. One plan may be .gw-plan--featured (an accent hairline, never a fill)
4
4
  and only that one gets the filled .gw-btn; the others get .gw-btn--secondary.
5
+ Monthly / yearly: the .gw-segmented radio group, only when the brand really bills yearly for less, with the real
6
+ discount in its label. Every price is in the markup twice, data-gw-show="billing:monthly" and
7
+ data-gw-show="billing:yearly" (hidden): the page reads the monthly prices without the script, and website.js
8
+ swaps them on a choice. Under the price, say what is billed ("Billed $470.40 a year"). No yearly billing: delete
9
+ the control and every data-gw-show="billing:yearly" element.
5
10
  Limits: two to four plans, four to six lines per plan, the same lines in the same order on every plan.
6
11
  -->
7
12
  <section class="gw-section">
@@ -10,12 +15,17 @@
10
15
  <p class="gw-label">Pricing</p>
11
16
  <h2>[Headline about how pricing works]</h2>
12
17
  <p class="gw-lead">[One line: trial, billing, what every plan includes.]</p>
18
+ <div class="gw-segmented" role="radiogroup" aria-label="[Billing period]" data-gw-segmented="billing">
19
+ <button type="button" role="radio" aria-checked="true" value="monthly">[Monthly]</button>
20
+ <button type="button" role="radio" aria-checked="false" value="yearly">[Yearly –DISCOUNT%]</button>
21
+ </div>
13
22
  </div>
14
23
  <div class="gw-pricing">
15
24
  <div class="gw-plan">
16
25
  <h3>[Plan]</h3>
17
26
  <p>[Who it is for, in one line]</p>
18
- <p class="gw-plan__price">[PRICE] <small>/ month</small></p>
27
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">[PRICE]</span><span data-gw-show="billing:yearly" hidden>[PRICE A MONTH, BILLED YEARLY]</span> <small>/ month</small></p>
28
+ <p><span data-gw-show="billing:monthly">[Billed monthly]</span><span data-gw-show="billing:yearly" hidden>[Billed $X a year]</span></p>
19
29
  <a class="gw-btn gw-btn--secondary gw-btn--block" href="[link]">[Action]</a>
20
30
  <ul class="gw-list--check"><li>[What is included]</li><li>[What is included]</li></ul>
21
31
  </div>
@@ -23,14 +33,16 @@
23
33
  <span class="gw-badge gw-badge--accent">[Why this one, e.g. Recommended for …]</span>
24
34
  <h3>[Plan]</h3>
25
35
  <p>[Who it is for]</p>
26
- <p class="gw-plan__price">[PRICE] <small>/ month</small></p>
36
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">[PRICE]</span><span data-gw-show="billing:yearly" hidden>[PRICE A MONTH, BILLED YEARLY]</span> <small>/ month</small></p>
37
+ <p><span data-gw-show="billing:monthly">[Billed monthly]</span><span data-gw-show="billing:yearly" hidden>[Billed $X a year]</span></p>
27
38
  <a class="gw-btn gw-btn--block" href="[link]">[Action]</a>
28
39
  <ul class="gw-list--check"><li>[What is included]</li><li>[What is included]</li></ul>
29
40
  </div>
30
41
  <div class="gw-plan">
31
42
  <h3>[Plan]</h3>
32
43
  <p>[Who it is for]</p>
33
- <p class="gw-plan__price">[PRICE] <small>/ month</small></p>
44
+ <p class="gw-plan__price"><span data-gw-show="billing:monthly">[PRICE]</span><span data-gw-show="billing:yearly" hidden>[PRICE A MONTH, BILLED YEARLY]</span> <small>/ month</small></p>
45
+ <p><span data-gw-show="billing:monthly">[Billed monthly]</span><span data-gw-show="billing:yearly" hidden>[Billed $X a year]</span></p>
34
46
  <a class="gw-btn gw-btn--secondary gw-btn--block" href="[link]">[Action]</a>
35
47
  <ul class="gw-list--check"><li>[What is included]</li><li>[What is included]</li></ul>
36
48
  </div>