@gemboss/website-ui 0.5.0 → 0.7.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.
@@ -36,7 +36,7 @@ Don't remove the skip link, and don't leave out `id="main"`.
36
36
 
37
37
  `js/website.js` (a `<script defer>`) or `js/website.mjs` (an `import`) wires every part that moves: the mega menu,
38
38
  the mobile menu, the theme toggle, reveal, the header's scroll state, the current page, the segmented control, the
39
- busy button, an article's contents list and the scroll box of its tables (0.5.0). Markup that arrives after the page loaded (a client-side route change, a builder block, a CMS
39
+ busy button, an article's contents list and the scroll box of its tables (0.5.0), and the footer's folds (0.7.0). Markup that arrives after the page loaded (a client-side route change, a builder block, a CMS
40
40
  preview) is wired by calling `init` on it, as often as needed: an element is wired once.
41
41
 
42
42
  ```js
@@ -116,8 +116,9 @@ Don't: more than three night bands on a page (the closing CTA does not count), o
116
116
 
117
117
  40px tall, the action radius (8, 12 in `soft`), 14px/600, sentence case, the action colour as fill: ink, or the
118
118
  brand's accent with `--gw-on-accent` words under `data-gw-action="accent"` (one colour on the whole site). Variants: `--secondary` (hairline, transparent), `--ghost`
119
- (no frame), `--on-night` (light fill for a dark surface that is not a `.gw-band--night`), `--sm` (36px, with a 44px
120
- hit area), `--lg` (48px, 16px type), `--block` (full width). On touch screens every button is at least 44px tall and
119
+ (no frame), `--on-night` (light fill for a dark surface that is not a `.gw-band--night`), `--danger` (0.6.0: the
120
+ danger fill with `--gw-on-danger` words, for the action of a page that destroys something: delete a workspace, erase
121
+ my data), `--sm` (36px, with a 44px hit area), `--lg` (48px, 16px type), `--block` (full width). On touch screens every button is at least 44px tall and
121
122
  its label may wrap. Inside a night band the default button turns light by itself.
122
123
 
123
124
  ```html
@@ -146,6 +147,9 @@ button.setAttribute('aria-busy', 'true'); // the request leaves
146
147
  button.removeAttribute('aria-busy'); // the answer came
147
148
  ```
148
149
 
150
+ `--danger` is never a second primary beside `.gw-btn` and never a way to make an action louder: it marks what cannot
151
+ be undone, and the words around it say what will be lost.
152
+
149
153
  Don't: a fill of your own (the accent comes only from the action profile, set on `<html>`), a gradient or a wash; a
150
154
  raised or inset shadow; pill-shaped buttons; uppercase; two filled buttons side by side (one
151
155
  `.gw-btn` per view, the second is `--secondary` or a link); a `<div>` with a click handler instead of `<a>`/`<button>`;
@@ -196,8 +200,9 @@ groups with the same name on one page; the old `role="group"` with `aria-pressed
196
200
 
197
201
  ### Badges `.gw-badge`
198
202
 
199
- Small status, a pill, 12px. Variants: `--accent` (a test, the recommended plan), `--success` and `--warning` (state:
200
- a coloured dot, ink words, because the state hues fail 4.5:1 as small text on a tint).
203
+ Small status, a pill, 12px. Variants: `--accent` (a test, the recommended plan), `--success`, `--warning` and
204
+ `--danger` (0.6.0, an error: "Payment failed") (state: a coloured dot, ink words, because the state hues fail 4.5:1 as
205
+ small text on a tint).
201
206
 
202
207
  ```html
203
208
  <span class="gw-badge gw-badge--success">Live</span>
@@ -349,15 +354,62 @@ hero pulled up under the header.
349
354
 
350
355
  ## Footer
351
356
 
357
+ The footer of gemboss.ai (0.7.0): one per site, the same on every page, from `templates/footer.html`. A brand fills
358
+ its rows (its columns, its family, its words) and deletes the optional parts it does not use; it never adds a footer
359
+ part of its own. See it in `demo/footer.html`, with a switch to the same footer for a GemPages-style brand.
360
+
352
361
  | class | for |
353
362
  |---|---|
354
363
  | `.gw-footer` | the footer: one hairline on top, white like the page |
355
- | `.gw-footer__grid` | brand block and link columns: 2 columns on phones, 3 from 768px, one row from 1080px |
364
+ | `.gw-footer__grid` | brand block and link columns: 2 columns on phones, 3 from 768px, one row from 1080px. With folding columns (0.7.0): one track of rows below 768px, and from 1080px the brand's track as on gemboss.ai (193.5px beside 1120px), the columns sharing the rest: three to six |
356
365
  | `.gw-footer__brand` | the brand and one sentence, full width on phones |
357
366
  | `.gw-footer__col`, `__heading` | a `<nav>` column: a sentence-case heading and a `<ul>` of links |
367
+ | `.gw-footer__fold` | 0.7.0: the column's `<details>`, its `<summary>` holding the `__heading`. Below 768px a 52px row with a chevron, folded until tapped (44px links); from 768px the open column, the summary only its heading (no box, no Tab stop). Works without the script |
368
+ | `.gw-footer__ask`, `__ask-label` | 0.7.0, optional, in the brand block: "Ask AI about [Brand]" and links that open ChatGPT, Claude, Perplexity and Google AI Mode with one neutral question typed in |
369
+ | `.gw-footer__family` | 0.7.0, optional, after the grid: a `<nav>` row of products, a fold whose heading is its label, a list of `<a><b>Name</b> <i>One line</i></a>`; two a line on phones, four from 768px, from 1080px the label beside the items. Repeat it for a second family |
370
+ | `.gw-footer__ext` | 0.7.0: the drawn ↗ after a link that leaves the site, `aria-hidden="true"` |
358
371
  | `.gw-footer__bottom` | copyright, legal links, social links; alone (no grid above), it drops its top rule |
372
+ | `.gw-footer__toggle` | 0.7.0, optional: with `.gw-theme-toggle` and `data-gw-theme-toggle`, the theme toggle in the bottom row, a 32px square with one hairline (44px on touch), before the social links |
373
+ | `.gw-footer__wordmark` | 0.7.0, optional, last: the brand's mark (inline SVG in `currentColor`) and name, ink at 10%, `--gw-display-wordmark`; `aria-hidden="true"`; not on phones |
374
+ | `.gw-footer__newsletter` | 0.6.0, optional: a `<form>` in the brand block, 24px under its sentence: a `.gw-field` with its label, a `.gw-form-inline` row (the email field and a `--secondary` button) and the note |
375
+ | `.gw-visually-hidden` | 0.7.0, anywhere: words for a screen reader only, " (opens in a new tab)" in a link with `target="_blank"` |
376
+
377
+ ```html
378
+ <nav class="gw-footer__col" aria-label="Product">
379
+ <details class="gw-footer__fold">
380
+ <summary><h2 class="gw-footer__heading">Product</h2></summary>
381
+ <ul><li><a href="/funnel">Funnel builder</a></li> …</ul>
382
+ </details>
383
+ </nav>
384
+ …
385
+ <nav class="gw-footer__family" aria-label="More from GemBoss">
386
+ <details class="gw-footer__fold">
387
+ <summary><h2 class="gw-footer__heading">More from GemBoss</h2></summary>
388
+ <ul>
389
+ <li><a href="/custom-theme"><b>GemThemes</b> <i>Custom theme</i></a></li>
390
+ <li><a href="https://community.gemboss.ai" rel="noopener"><b>Community<span class="gw-footer__ext" aria-hidden="true"></span></b> <i>Brands building with GemBoss</i></a></li>
391
+ </ul>
392
+ </details>
393
+ </nav>
394
+ ```
395
+
396
+ Ask AI: each link carries the same question, URL-encoded after `?q=`: `https://chatgpt.com/?q=…`,
397
+ `https://claude.ai/new?q=…`, `https://www.perplexity.ai/search/new?q=…`, `https://www.google.com/search?udm=50&q=…`.
398
+ gemboss.ai asks "What is GemBoss (gemboss.ai), and how does it help Shopify brands turn ad clicks into sales?": what the
399
+ brand is and what it does for the reader, never a leading question, so the answer comes from what the web says.
400
+ Words, not the assistants' logos (another company's mark on a page is a permission question).
359
401
 
360
- Full markup: `templates/footer.html`. Don't: uppercase column headings; a newsletter form that stores nothing.
402
+ Links that leave the site carry the ↗ (`.gw-footer__ext`); a link with `target="_blank"` also says so to a screen
403
+ reader: `<span class="gw-visually-hidden"> (opens in a new tab)</span>` inside the `<a>`. A link without
404
+ `target="_blank"` does not.
405
+
406
+ A GemCommerce brand lists its siblings in its family row: a GemPages site might show GemBoss, GemX and GemUpsells.
407
+
408
+ The newsletter note says how often you write and how to stop, and links to the privacy policy. Don't: uppercase
409
+ column headings; a footer part that is not in the template (a second logo row, a badge strip, a map); a column
410
+ without its heading in the `<summary>`; a wordmark a screen reader reads (`check`: `footer-markup`); the assistants'
411
+ logos in the Ask AI row; a newsletter form that stores nothing; a consent box ticked for the reader; a filled primary
412
+ button in the footer (the closing CTA above it is the page's action).
361
413
 
362
414
  ## Sections
363
415
 
@@ -594,6 +646,127 @@ visible on the page, with the same words:
594
646
  </script>
595
647
  ```
596
648
 
649
+ ## Forms (0.6.0)
650
+
651
+ A public site asks as little as it can. One field beats a form (the home page's "What do you sell?", the audit's store
652
+ URL), and a form keeps two or three fields; a question with a good guess is a choice the reader confirms, with the
653
+ guess chosen. The look is the dashboard's field family (`@gemboss/ui` `gemboss-field`, `ui-select`, `ui-textarea`,
654
+ `ui-check`, `ui-help`, `ui-error`) at the website's sizes. See every state in `demo/forms.html`; a short form is
655
+ `templates/sections/form.html`, the footer newsletter `templates/footer.html`.
656
+
657
+ | class | for |
658
+ |---|---|
659
+ | `.gw-field` | one field: the label, the control, the error, the hint, 8px apart |
660
+ | `.gw-field__label` | the `<label for>` above the control (14px/600, ink); also the `<legend>` of a group |
661
+ | `.gw-field__error` | under the control: what to do, in words, after a 16px icon, in `--gw-danger` (14px/500); empty, it takes no room |
662
+ | `.gw-field__hint` | under the error: a short help line (14px, muted) |
663
+ | `.gw-input` | a line of text (`type` email, url, tel, text, search, password…): 40px tall, 44px on touch, 16px words, the action radius (8, 12 in `soft`), a 1px `--gw-line-control` border (3:1) on the surface |
664
+ | `.gw-select` | a choice from a list (`<select>`), the same box with a drawn caret |
665
+ | `.gw-textarea` | a few lines, the same box from 96px tall, resizes down the page |
666
+ | `.gw-choices` | a group of checkboxes or radios: a `<fieldset>`, its question in `<legend class="gw-field__label">`, one choice per row |
667
+ | `.gw-check`, `.gw-radio` | a `<label>` around one `<input type="checkbox">` or `type="radio"` and its words: the row is the target (32px, 44px on touch); chosen, the box takes the accent |
668
+ | `.gw-check__count` | a filter's count at the end of its row, a small neutral pill |
669
+ | `.gw-form-inline` | one field and its button on one line (a newsletter, "email me the guide"); below 480px, or in a narrow box, each takes a full line |
670
+ | `.gw-form` | a short form: fields 24px apart, the actions 32px below, 560px at most; on a `.gw-card` or inside one |
671
+
672
+ States: hover darkens the border; focus is the kit's accent ring (2px) with an ink border; read-only
673
+ (`readonly`) sits on the soft ground; disabled (`disabled`) is faded, always with its reason in the hint; invalid
674
+ (`aria-invalid="true"`) takes the danger border and tint, and its `.gw-field__error` says why. No success state: say
675
+ what happened after sending, in a `role="status"` line.
676
+
677
+ ```html
678
+ <div class="gw-field">
679
+ <label class="gw-field__label" for="email">Work email</label>
680
+ <input class="gw-input" id="email" name="email" type="email" autocomplete="email" required
681
+ aria-invalid="true" aria-describedby="email-error email-hint">
682
+ <p class="gw-field__error" id="email-error">Enter your work email, like name@example.com.</p>
683
+ <p class="gw-field__hint" id="email-hint">We reply to this address.</p>
684
+ </div>
685
+
686
+ <fieldset class="gw-choices">
687
+ <legend class="gw-field__label">Industry</legend>
688
+ <label class="gw-check"><input type="checkbox" name="industry" value="beauty" checked><span>Beauty</span><span class="gw-check__count">96</span></label>
689
+ <label class="gw-check"><input type="checkbox" name="industry" value="home"><span>Home</span><span class="gw-check__count">41</span></label>
690
+ </fieldset>
691
+
692
+ <div class="gw-field">
693
+ <label class="gw-field__label" for="news-email">Product news by email</label>
694
+ <div class="gw-form-inline">
695
+ <input class="gw-input" id="news-email" name="email" type="email" autocomplete="email" required aria-describedby="news-email-error news-email-note">
696
+ <button class="gw-btn gw-btn--secondary" type="submit">Subscribe</button>
697
+ </div>
698
+ <p class="gw-field__error" id="news-email-error"></p>
699
+ <p class="gw-field__hint" id="news-email-note">Once a month. Unsubscribe in one click. <a href="/privacy">Privacy policy</a></p>
700
+ </div>
701
+ ```
702
+
703
+ **Validation is the page's.** The kit ships no validator and no form script: the page (or its server) decides what is
704
+ wrong and writes the words. On send, set `aria-invalid="true"` on each field in error and its message in the
705
+ `.gw-field__error`, then move the focus to the first one: a screen reader reads its label and its error (through
706
+ `aria-describedby`, the error named first). Keep the empty error element in the markup, so the description is already
707
+ wired. Clear both when the field is right again, on the next send or as the reader fixes it. While the form is sent,
708
+ `aria-busy="true"` on its button; when the answer comes, a `role="status"` line (`.gw-note`) says what happened
709
+ ("Sent. We'll reply to name@example.com."). Use `novalidate` on the `<form>` when the page writes its own messages, so
710
+ the browser's bubbles do not speak over them.
711
+
712
+ Words: the label names the answer ("Work email", not "Enter your email"); a hint only when the answer is not obvious;
713
+ an error says what to do, not what went wrong ("Enter the whole address, like name@example.com", not "Invalid
714
+ input"); optional fields say "(optional)" in their label, required ones need no mark.
715
+
716
+ Don't: a placeholder as the only label; a field across a wide screen (keep a form in a `.gw-form` or a narrow
717
+ column); a form split into steps to start something free; a consent box ticked in advance or required to get
718
+ something else; an error told by its colour alone; `disabled` on a field without its reason; a filled primary button
719
+ in a footer form.
720
+
721
+ ## Ratings and testimonials (0.6.0)
722
+
723
+ Proof from people who are not the brand: the store's score and its reviews. **Every rating and every testimonial
724
+ links to the page it comes from** (the store's review page, the review itself) and copies only what that page shows:
725
+ the score and the count on the day you copy them, the reviewer's own words (cut a long one with an ellipsis, never
726
+ reword it), their name and store as they wrote them. No made-up review, no score you averaged yourself, no source you
727
+ cannot link. `check` warns about a badge or a card without a link (`rating-source`). See them in `demo/forms.html`;
728
+ the section is `templates/sections/testimonials.html`.
729
+
730
+ | class | for |
731
+ |---|---|
732
+ | `.gw-stars` | a value from 0 to 5 drawn as five 16px stars in ink, the last one cut where the value ends (4.5 shows half a star); `role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"` |
733
+ | `.gw-testimonials` | the wall: a `<ul role="list">` of cards, one column on phones, two from 768px, three from 1080px; the cards of a row share its height (no masonry) |
734
+ | `.gw-testimonial` (with `.gw-card`, on a `<figure>`) | one review: stars, `__quote` (a `<blockquote>`), `__by` (a `<figcaption>`: the optional `__avatar`, the name in `<b>`, the store and the link to the review) |
735
+ | `.gw-rating-badge` (on an `<a href>`) | the store's score: `__icon` (the store's mark, or the generic bag), `__body` with `__score` (the number and the stars) and `__count` (the reviews and where) |
736
+
737
+ ```html
738
+ <a class="gw-rating-badge" href="https://apps.shopify.com/your-app/reviews">
739
+ <span class="gw-rating-badge__icon" aria-hidden="true"><svg …></svg></span>
740
+ <span class="gw-rating-badge__body">
741
+ <span class="gw-rating-badge__score"><span aria-hidden="true">4.9</span><span class="gw-stars" role="img" aria-label="4.9 out of 5" style="--gw-stars: 4.9"></span></span>
742
+ <span class="gw-rating-badge__count">1,234 reviews on the Shopify App Store</span>
743
+ </span>
744
+ </a>
745
+
746
+ <ul class="gw-testimonials" role="list">
747
+ <li>
748
+ <figure class="gw-card gw-testimonial">
749
+ <span class="gw-stars" role="img" aria-label="5 out of 5" style="--gw-stars: 5"></span>
750
+ <blockquote class="gw-testimonial__quote"><p>The reviewer's exact words.</p></blockquote>
751
+ <figcaption class="gw-testimonial__by">
752
+ <img class="gw-testimonial__avatar" src="/img/reviewer.webp" alt="" width="40" height="40" loading="lazy">
753
+ <span><b>Name</b>Store · <a href="https://apps.shopify.com/your-app/reviews?…">Shopify App Store</a></span>
754
+ </figcaption>
755
+ </figure>
756
+ </li>
757
+ </ul>
758
+ ```
759
+
760
+ The stars' value is written twice on purpose, once for the drawing (`--gw-stars`) and once in words (`aria-label`),
761
+ and `check` reports them when they disagree, when the value is not a number from 0 to 5, or when the name is missing
762
+ (`stars-markup`). A number shown beside the stars is `aria-hidden="true"`, so it is not read twice. In another language
763
+ the label is written for its reader ("4,9 trên 5"); the number stays the same. The badge's icon is the generic bag of
764
+ the template, or the store's own mark only where that store's brand rules allow it; the stars stay ink on every brand.
765
+
766
+ Don't: a rating or a quote without its link; a quote longer than three sentences (link to the rest); a star row with
767
+ no value in words; stars in a colour of your own; a "4.9" with no count and no source; a photo of the reviewer they did
768
+ not make public with the review; masonry or a carousel that moves by itself.
769
+
597
770
  ## Motion
598
771
 
599
772
  | class | for |
@@ -603,6 +776,23 @@ visible on the page, with the same words:
603
776
  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
604
777
  the first screen, and don't animate anything but `transform` and `opacity`.
605
778
 
779
+ ## Additions to the markup contract in 0.7.0
780
+
781
+ The footer of gemboss.ai: `.gw-footer__fold` (a `<details>` in a `.gw-footer__col` or a `.gw-footer__family`, its
782
+ `<summary>` holding the `.gw-footer__heading`) with the script's `fold` part (`data-gw-ready="fold"`), `.gw-footer__ask`
783
+ (`__ask-label`), `.gw-footer__family`, `.gw-footer__ext`, `.gw-footer__toggle` (on a `.gw-theme-toggle`),
784
+ `.gw-footer__wordmark`, and `.gw-visually-hidden`. One token: `--gw-display-wordmark`. No event. No markup or class of
785
+ 0.6.0 changes: the 0.6.0 footer markup still works and looks the same.
786
+
787
+ ## Additions to the markup contract in 0.6.0
788
+
789
+ Forms: `.gw-field` (`__label`, `__error`, `__hint`), `.gw-input`, `.gw-select`, `.gw-textarea`, `.gw-choices`, `.gw-check`
790
+ and `.gw-radio` (`.gw-check__count`), `.gw-form-inline`, `.gw-form`, the states `readonly`, `disabled` and
791
+ `aria-invalid="true"`, `.gw-footer__newsletter`, `.gw-btn--danger` and `.gw-badge--danger`. Ratings: `.gw-stars` with its
792
+ `--gw-stars` input, `.gw-testimonials`, `.gw-testimonial` (`__quote`, `__by`, `__avatar`), `.gw-rating-badge` (`__icon`,
793
+ `__body`, `__score`, `__count`). Tokens: `--gw-danger`, `--gw-danger-soft`, `--gw-on-danger`, `--gw-line-control`. No
794
+ script, no hook and no event: the page owns validation. No markup or class of 0.5.0 changes.
795
+
606
796
  ## Additions to the markup contract in 0.5.0
607
797
 
608
798
  The blog parts: `.gw-breadcrumbs`, `.gw-article-meta` (`__avatar`), `.gw-article` (`__head`, `__hero`), `.gw-toc` with
@@ -28,7 +28,7 @@ BEM-ish: `.gw-block`, `.gw-block__part`, `.gw-block--variant`.
28
28
 
29
29
  One script wires every part below: the mega menu, the mobile menu, the theme toggle, reveal, the header's scroll
30
30
  state, the current page, the segmented control, the busy button, an article's contents list and the scroll box of its
31
- prose tables (0.5.0). It comes in two entries built from one source
31
+ prose tables (0.5.0), and the footer's folds (0.7.0). It comes in two entries built from one source
32
32
  (`js/website.mjs`; `scripts/build.mjs` writes `js/website.js` from it and `--check` fails when they differ):
33
33
 
34
34
  | entry | file | how it starts |
@@ -43,9 +43,9 @@ prose tables (0.5.0). It comes in two entries built from one source
43
43
  to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
44
44
  - **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
45
45
  mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
46
- - **`version`**: `'0.5.0'`. The classic script exposes the three as `window.gwWebsite`.
46
+ - **`version`**: `'0.7.0'`. The classic script exposes the three as `window.gwWebsite`.
47
47
  - **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `theme`, `reveal`, `header`,
48
- `segmented`, `toc`, `table`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
48
+ `segmented`, `toc`, `table`, `fold`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
49
49
  is still wired.
50
50
  - **One copy per page.** The first copy that runs owns the page (`window.gwWebsite`); another one (the classic
51
51
  script next to a bundle, two bundles) hands its calls to it, so nothing is wired twice.
@@ -160,6 +160,60 @@ Script behaviour:
160
160
  menu's links whose `href` pathname equals `location.pathname` (when the markup did not already). Each `init` takes
161
161
  its own marks back first, so after a client-side navigation they follow the new path; a mark in the markup stays.
162
162
 
163
+ ## Footer (0.7.0)
164
+
165
+ ```html
166
+ <footer class="gw-footer">
167
+ <div class="gw-wrap">
168
+ <div class="gw-footer__grid">
169
+ <div class="gw-footer__brand">
170
+ <a class="gw-brand" href="/">…</a><p>One sentence.</p>
171
+ <div class="gw-footer__ask"> <!-- optional -->
172
+ <p class="gw-footer__ask-label" id="footer-ask">Ask AI about Brand</p>
173
+ <ul aria-labelledby="footer-ask"><li><a href="https://chatgpt.com/?q=…" target="_blank" rel="noopener">ChatGPT<span class="gw-visually-hidden"> (opens in a new tab)</span></a></li> …</ul>
174
+ </div>
175
+ <form class="gw-footer__newsletter" …>…</form> <!-- optional (0.6.0) -->
176
+ </div>
177
+ <nav class="gw-footer__col" aria-label="Product"> <!-- three to six -->
178
+ <details class="gw-footer__fold"> <!-- no open: folded on phones -->
179
+ <summary><h2 class="gw-footer__heading">Product</h2></summary>
180
+ <ul><li><a href="…">…</a></li> …</ul>
181
+ </details>
182
+ </nav>
183
+ </div>
184
+ <nav class="gw-footer__family" aria-label="More from Brand"> <!-- optional, repeatable -->
185
+ <details class="gw-footer__fold">
186
+ <summary><h2 class="gw-footer__heading">More from Brand</h2></summary>
187
+ <ul><li><a href="…"><b>Name<span class="gw-footer__ext" aria-hidden="true"></span>?</b> <i>One line</i></a></li> …</ul>
188
+ </details>
189
+ </nav>
190
+ <div class="gw-footer__bottom">
191
+ <span>© …</span><ul aria-label="Legal">…</ul>
192
+ <button class="gw-theme-toggle gw-footer__toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme">svg</button> <!-- optional -->
193
+ <ul aria-label="Social">…</ul>
194
+ </div>
195
+ <div class="gw-footer__wordmark" aria-hidden="true"><svg viewBox="…">…currentColor…</svg><span>Brand</span></div> <!-- optional -->
196
+ </div>
197
+ </footer>
198
+ ```
199
+
200
+ - **Folds** (`.gw-footer__fold`, a `<details>` whose `<summary>` holds the `.gw-footer__heading`): the CSS folds them
201
+ below 768px (52px rows, the browser opens and closes them) and draws the open column from 768px, where the summary
202
+ is `display: contents` and the list shows even while the `<details>` is closed (`::details-content`). The script,
203
+ for each fold with a summary:
204
+ - from 768px (`matchMedia('(min-width: 768px)')`) sets `open`, puts `tabindex="-1"` on the summary and cancels a
205
+ click on it (`preventDefault`), so a heading never folds its column away, in a browser without
206
+ `::details-content` too;
207
+ - below 768px gives back the markup's state: `open` as the markup had it (folded unless it said `open`), the
208
+ summary's own `tabindex` (none, usually); again on every change of width;
209
+ - `destroy`: the markup's state, its listener and the `fold` mark go. No event.
210
+ - **Theme toggle**: a `[data-gw-theme-toggle]` in the bottom row is wired like the header's (above); with both on the
211
+ page, a press on either relabels both.
212
+ - **Not the script's**: the Ask AI row, the family rows, the ↗ and the wordmark are markup and CSS only. The question
213
+ in the Ask AI links is written in the markup, URL-encoded.
214
+ - `check` reports a `.gw-footer__fold` that is not a `<details>`, has no `<summary>`, or whose summary does not hold
215
+ the `.gw-footer__heading`, and a `.gw-footer__wordmark` without `aria-hidden="true"` (`footer-markup`).
216
+
163
217
  ## Article: contents and prose tables (0.5.0)
164
218
 
165
219
  ```html
@@ -204,6 +258,63 @@ Script behaviour:
204
258
  - A framework that renders the prose's table itself (JSX, not HTML from the CMS) should call `destroy(prose)`
205
259
  before it re-renders that part, as for every element the script changed.
206
260
 
261
+ ## Forms (0.6.0)
262
+
263
+ CSS only: the script does not touch a form. The page (or its server) owns validation and the words.
264
+
265
+ ```html
266
+ <form class="gw-form" action="…" method="post" novalidate> <!-- or .gw-card.gw-form; novalidate when the page writes its own messages -->
267
+ <div class="gw-field">
268
+ <label class="gw-field__label" for="ID">Label</label>
269
+ <input class="gw-input" id="ID" … aria-describedby="ID-error ID-hint" aria-invalid?="true"> <!-- or select.gw-select, textarea.gw-textarea -->
270
+ <p class="gw-field__error" id="ID-error">What to do</p> <!-- empty until the page fills it; empty = no room -->
271
+ <p class="gw-field__hint" id="ID-hint">Help</p> <!-- optional -->
272
+ </div>
273
+ <fieldset class="gw-choices">
274
+ <legend class="gw-field__label">Question</legend>
275
+ <label class="gw-check"><input type="checkbox" …><span>Words</span><span class="gw-check__count">12</span></label> <!-- or .gw-radio with type="radio"; __count optional -->
276
+ </fieldset>
277
+ <div class="gw-field"> <!-- one field and its button -->
278
+ <label class="gw-field__label" for="ID2">Label</label>
279
+ <div class="gw-form-inline"><input class="gw-input" id="ID2" …><button class="gw-btn" type="submit">Action</button></div>
280
+ </div>
281
+ <div class="gw-actions"><button class="gw-btn" type="submit">Send</button><p class="gw-note" role="status"></p></div>
282
+ </form>
283
+ ```
284
+
285
+ - The error and the hint are children of `.gw-field` (or `.gw-choices`); `aria-describedby` names the error first.
286
+ - States are attributes, never classes: `aria-invalid="true"` (danger border and tint; on a checkbox or radio, a
287
+ danger box while unchecked), `readonly`, `disabled`. Hover and focus are the browser's.
288
+ - `.gw-check` / `.gw-radio` wrap exactly one `<input>` and its words, so the whole row is the target.
289
+ - `.gw-footer__newsletter` is a `<form>` in `.gw-footer__brand`, after the brand's sentence.
290
+
291
+ ## Ratings (0.6.0)
292
+
293
+ ```html
294
+ <span class="gw-stars" role="img" aria-label="N out of 5" style="--gw-stars: N"></span> <!-- N from 0 to 5, the same in both -->
295
+
296
+ <a class="gw-rating-badge" href="SOURCE">
297
+ <span class="gw-rating-badge__icon" aria-hidden="true"><svg|img></span>
298
+ <span class="gw-rating-badge__body">
299
+ <span class="gw-rating-badge__score"><span aria-hidden="true">N</span><span class="gw-stars" …></span></span>
300
+ <span class="gw-rating-badge__count">COUNT reviews on STORE</span>
301
+ </span>
302
+ </a>
303
+
304
+ <ul class="gw-testimonials" role="list">
305
+ <li><figure class="gw-card gw-testimonial">
306
+ <span class="gw-stars" …></span>
307
+ <blockquote class="gw-testimonial__quote"><p>Words</p></blockquote>
308
+ <figcaption class="gw-testimonial__by"><img class="gw-testimonial__avatar" alt="" …>?<span><b>Name</b>Store · <a href="SOURCE">Where</a></span></figcaption>
309
+ </figure></li>
310
+ </ul>
311
+ ```
312
+
313
+ - `--gw-stars` is the one `--gw-` variable a page writes, on the `.gw-stars` element itself: an input, not a token.
314
+ `check` accepts it there and reports a value outside 0 to 5, a missing `role="img"` or name, and a name that does not
315
+ carry the same number (`stars-markup`), and a `.gw-rating-badge` or `.gw-testimonial` without an `<a href>`
316
+ (`rating-source`, a warning).
317
+
207
318
  ## Brand profiles (0.3.0)
208
319
 
209
320
  - `data-gw-action` and `data-gw-radius` sit on `<html>` only, with a name from `system.json` `profiles` (`ink` or
@@ -247,13 +358,17 @@ Script behaviour:
247
358
 
248
359
  | class | what |
249
360
  |---|---|
250
- | `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--sm`, `--lg`, `--block`) | actions; a rectangle with the action radius, the action colour as fill (ink, or the accent under `data-gw-action="accent"`); `:active` pressed, `[aria-busy="true"]` loading (script above) |
361
+ | `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--danger` (0.6.0), `--sm`, `--lg`, `--block`) | actions; a rectangle with the action radius, the action colour as fill (ink, or the accent under `data-gw-action="accent"`); `:active` pressed, `[aria-busy="true"]` loading (script above) |
251
362
  | `.gw-link` (+ `--arrow`) | inline text link in the accent; `--arrow` adds → that moves 2px on hover |
252
363
  | `.gw-label` | the label above a heading: 13px muted, 8px accent square before it |
253
364
  | `.gw-dim` | the muted first sentence inside a headline |
254
365
  | `.gw-chip` (+ `.is-active`) | selectable token, pill |
255
366
  | `.gw-segmented` (+ `--block`) | one choice out of two to four (script above) |
256
- | `.gw-badge` (+ `--accent`, `--success`, `--warning`) | small status, pill |
367
+ | `.gw-badge` (+ `--accent`, `--success`, `--warning`, `--danger` (0.6.0)) | small status, pill |
368
+ | `.gw-field` (`__label`, `__error`, `__hint`), `.gw-input`, `.gw-select`, `.gw-textarea` | a field and its controls; states by attribute (forms above, 0.6.0) |
369
+ | `.gw-choices`, `.gw-check`, `.gw-radio` (`.gw-check__count`) | a group of checkboxes or radios, a row per choice (0.6.0) |
370
+ | `.gw-form-inline`, `.gw-form` | one field and its button on a line; a short form's stack (0.6.0) |
371
+ | `.gw-stars`, `.gw-rating-badge` (`__icon`, `__body`, `__score`, `__count`) | a 0 to 5 value; the store's score, a link to its reviews (ratings above, 0.6.0) |
257
372
  | `.gw-card` (+ `--soft`, `--link`) | white box, 1px hairline, the card radius; `--link` makes the whole card an `<a>` |
258
373
  | `.gw-card__media` (+ `--wide`, `--square`) | the card's picture, first child, edge to edge on top; 16:9, 1.91:1 or 1:1 (0.5.0) |
259
374
  | `.gw-callout` (+ `--note`, `--tip`, `--warning`, `__label`) | a note, a tip or a warning in an article, `role="note"`, its kind in words (0.5.0) |
@@ -268,6 +383,7 @@ Script behaviour:
268
383
  | `.gw-table` (+ `--compare`) | data / comparison table; scrolls inside its box on phones |
269
384
  | `.gw-faq` (`<details class="gw-faq__item"><summary>`) | FAQ accordion with no script |
270
385
  | `.gw-prose` (+ `--sm`) | long-form reading: an article's own HTML, no classes inside; its bare tables get a `.gw-prose__table` box from the script (0.5.0) |
386
+ | `.gw-visually-hidden` | words for a screen reader only, " (opens in a new tab)" in a link with `target="_blank"` (0.7.0) |
271
387
  | `.gw-tabs` (`role="tablist"`) | NOT in the kit yet (a reported gap would add it) |
272
388
 
273
389
  ## Sections
@@ -285,6 +401,7 @@ Script behaviour:
285
401
  | `.gw-showcase` | night band with a large screen and two or three points |
286
402
  | `.gw-steps` (`<ol>`) | numbered steps, how it works |
287
403
  | `.gw-quote` | one real testimonial with name, role, company |
404
+ | `.gw-testimonials` (`.gw-card.gw-testimonial`: `__quote`, `__by`, `__avatar`) | a wall of short reviews, each linked to its source (0.6.0) |
288
405
  | `.gw-pricing` (`.gw-plan`, `--featured`) | plan cards; the featured one has a hairline in the accent, not a fill |
289
406
  | `.gw-cta` | closing call to action band |
290
- | `.gw-footer` (`__grid`, `__col`, `__heading`, `__bottom`) | footer columns + bottom row |
407
+ | `.gw-footer` (`__grid`, `__col`, `__heading`, `__bottom`, `__newsletter`, and 0.7.0: `__fold`, `__ask`, `__ask-label`, `__family`, `__ext`, `__toggle`, `__wordmark`) | footer columns + bottom row; an optional newsletter form in the brand block (0.6.0); the footer of gemboss.ai: columns that fold on phones, Ask AI, family rows, the toggle, the wordmark (0.7.0, above) |
package/docs/theming.md CHANGED
@@ -14,8 +14,9 @@ every component follows. Everything else stays the kit's: the neutrals, the type
14
14
  widths. That is what makes the sites look like one family, and why a site built by someone with no design training
15
15
  still looks careful. `npx gemboss-website check` reports anything outside this set.
16
16
 
17
- Working examples: `demo/theme-example.html` (the accent only) and `demo/brand-profiles.html` (a GemPages-style brand:
18
- the accent, all three profiles and the floating header).
17
+ Working examples: `demo/theme-example.html` (the accent only), `demo/brand-profiles.html` (a GemPages-style brand:
18
+ the accent, all three profiles and the floating header) and `demo/footer.html` (the footer, with a switch to the same
19
+ footer for a GemPages-style brand).
19
20
 
20
21
  ## 1 to 3. Accent, wash, logo
21
22
 
@@ -43,6 +44,11 @@ The logo and the favicon are files, not variables: the `<img class="gw-brand__ma
43
44
  `alt=""` because the name is written next to it) and the `<span class="gw-brand__name">` in the header and footer
44
45
  partials, and `<link rel="icon">` in the page head.
45
46
 
47
+ The footer (0.7.0, `templates/footer.html`) is the same for every brand; a brand fills it. Its family row lists the
48
+ brand's siblings (a GemPages site: GemBoss, GemX, GemUpsells), its Ask AI row asks about the brand, and its wordmark
49
+ is the brand's own mark as an inline SVG drawn in `currentColor` (so it takes the faint colour) and its name. The
50
+ radius profile reaches the family items and the toggle (8px, 12px in `soft`); nothing else of the footer changes.
51
+
46
52
  ## 4 to 6. The three profiles
47
53
 
48
54
  Chosen once, in the page skeleton (the layout or partial every page shares). The first of each is the default and
@@ -57,7 +63,7 @@ needs no markup, so a GemBoss page, and every page written for 0.2.0, stays exac
57
63
  | knob | profiles | what changes |
58
64
  |---|---|---|
59
65
  | action colour, `data-gw-action` on `<html>` | `ink` (GemBoss, default), `accent` | `accent`: the primary `.gw-btn` is filled with `--gw-accent`, its words are `--gw-on-accent`, hover and pressed are `--gw-accent-strong`. In dark mode and night bands it takes the `-dark` family. Secondary, ghost and on-night buttons do not change. |
60
- | radius, `data-gw-radius` on `<html>` | `tight` (GemBoss, default): 8 action, 12 card, 16 panel, 20 mega menu; `soft` (GemPages): 12, 20, 24, 24 | every button, control, card, window, table, FAQ, plan, quote and the mega menu. Chips and badges stay pills. |
66
+ | radius, `data-gw-radius` on `<html>` | `tight` (GemBoss, default): 8 action, 12 card, 16 panel, 20 mega menu; `soft` (GemPages): 12, 20, 24, 24 | every button, control (fields, lists, the rating badge's icon), card, window, table, FAQ, plan, quote and the mega menu. Chips, badges and a filter's count stay pills; a checkbox keeps its 4px. |
61
67
  | header, a class on the `<header>` | flat (default), `.gw-header--float` | from 1080px the bar becomes an inset card: the panel radius of the profile (16 tight, 24 soft), the surface at 85% over a 10px blur, one hairline, the raised shadow, 16px from the top. Same bar, same mega menu, same script; below 1080px it is the flat bar, so the phone menu is unchanged. Solid for a visitor who asks for less transparency, and in a browser that cannot blur. |
62
68
 
63
69
  Why the action colour is a mode and not a colour of its own: the button stays one colour on the whole site, and that
@@ -71,10 +77,11 @@ the panels; in `tight` that is the 16px the header was designed with.
71
77
  ## What you may not change
72
78
 
73
79
  - **Neutrals**: `--gw-bg`, `--gw-bg-soft`, `--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`,
74
- `--gw-line-strong`, and the night colours. No cream or grey page, no brand-coloured ink.
80
+ `--gw-line-strong`, `--gw-line-control` (a form control's border, 3:1), and the night colours. No cream or grey page, no brand-coloured ink.
75
81
  - **The action tokens**: `--gw-action`, `--gw-action-hover`, `--gw-on-action`. Choose the `accent` profile instead;
76
82
  a colour set here is not checked and does not follow dark mode or night bands.
77
- - **State**: `--gw-success`, `--gw-warning` and their tints. Green and amber mean state on every GemCommerce site.
83
+ - **State**: `--gw-success`, `--gw-warning`, `--gw-danger` and their tints (`--gw-on-danger` too). Green, amber and red
84
+ mean state on every GemCommerce site; the stars of a rating stay ink.
78
85
  - **Scales and shapes**: type sizes and weights, spacing, the radius tokens (`--gw-radius-*`: choose a profile), shadows,
79
86
  widths, the header height, the font family. No pill buttons, no bigger shadows, no other font.
80
87
  - **Kit classes**: never restyle a `.gw-*` class. If the kit cannot do what your brand needs, that is a gap
package/js/website.js CHANGED
@@ -1,11 +1,11 @@
1
- /*! @gemboss/website-ui 0.5.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
1
+ /*! @gemboss/website-ui 0.7.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
2
2
  (function () {
3
3
  'use strict';
4
4
  if (window.gwWebsite) return;
5
5
 
6
- var version = '0.5.0';
6
+ var version = '0.7.0';
7
7
  var api = { version: version, init: init, destroy: destroy };
8
- var doc, root, mine, fine, sys, wide, live = [], megaOpen = null, megaTimer = 0, queued = false;
8
+ var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
9
9
  var READY = 'data-gw-ready';
10
10
 
11
11
  function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
@@ -250,15 +250,32 @@ function tables(ctx) {
250
250
  });
251
251
  }
252
252
 
253
+ function folds(ctx) {
254
+ each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
255
+ var s = d.querySelector('summary'), own = d.open, tab = s && s.getAttribute('tabindex');
256
+ if (!s) return;
257
+ function tabs(v) { if (v === null) s.removeAttribute('tabindex'); else s.setAttribute('tabindex', v); }
258
+ var r = {
259
+ fit: function () { d.open = mid.matches || own; tabs(mid.matches ? '-1' : tab); },
260
+ stop: function () { d.open = own; tabs(tab); }
261
+ };
262
+ on(s, 'click', function (e) { if (mid.matches) e.preventDefault(); });
263
+ r.fit();
264
+ return r;
265
+ });
266
+ }
267
+
253
268
  function install() {
254
269
  doc = document; root = doc.documentElement; mine = new WeakSet();
255
270
  fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
256
271
  var stored = null;
257
272
  wide = mq('(min-width: 1080px)');
273
+ mid = mq('(min-width: 768px)');
258
274
  on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
259
275
  on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
260
276
  on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
261
277
  watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
278
+ watch(mid, function () { records('fold', function (r) { r.fit(); }); });
262
279
  on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
263
280
  try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
264
281
  if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
@@ -283,7 +300,7 @@ function init(ctx) {
283
300
  window.gwWebsite = api;
284
301
  if (!doc) run([install]);
285
302
  live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
286
- run([mega, mobile, theme, reveal, header, current, segmented, toc, tables], ctx && ctx.querySelectorAll ? ctx : doc);
303
+ run([mega, mobile, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
287
304
  }
288
305
 
289
306
  /** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
package/js/website.mjs CHANGED
@@ -6,10 +6,10 @@
6
6
  // the export line and the // comments, plus auto-init): edit this file, then run the build. Keep comments on lines of
7
7
  // their own, so the build can leave them out.
8
8
 
9
- var version = '0.5.0';
9
+ var version = '0.7.0';
10
10
  var api = { version: version, init: init, destroy: destroy };
11
11
  // Page state. Set up by the first init(), never at import: a server can import this file.
12
- var doc, root, mine, fine, sys, wide, live = [], megaOpen = null, megaTimer = 0, queued = false;
12
+ var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
13
13
  var READY = 'data-gw-ready';
14
14
 
15
15
  function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
@@ -286,6 +286,27 @@ function tables(ctx) {
286
286
  });
287
287
  }
288
288
 
289
+ // Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row). Below 768px it keeps
290
+ // the markup's own state (folded unless the markup says open) and the browser opens and closes it. From 768px it is a
291
+ // plain column: the script sets `open`, takes the <summary> out of the Tab order (tabindex="-1") and ignores a click on
292
+ // it, so the heading cannot fold the list away. Again on every change of width; destroy gives back the markup's state.
293
+ // The CSS shows the list from 768px without the script too (::details-content), so this is for state and for browsers
294
+ // without that.
295
+ function folds(ctx) {
296
+ each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
297
+ var s = d.querySelector('summary'), own = d.open, tab = s && s.getAttribute('tabindex');
298
+ if (!s) return;
299
+ function tabs(v) { if (v === null) s.removeAttribute('tabindex'); else s.setAttribute('tabindex', v); }
300
+ var r = {
301
+ fit: function () { d.open = mid.matches || own; tabs(mid.matches ? '-1' : tab); },
302
+ stop: function () { d.open = own; tabs(tab); }
303
+ };
304
+ on(s, 'click', function (e) { if (mid.matches) e.preventDefault(); });
305
+ r.fit();
306
+ return r;
307
+ });
308
+ }
309
+
289
310
  // Once per page: what belongs to the document, the window or a media query rather than to one element.
290
311
  function install() {
291
312
  doc = document; root = doc.documentElement; mine = new WeakSet();
@@ -293,10 +314,13 @@ function install() {
293
314
  var stored = null;
294
315
  // 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
295
316
  wide = mq('(min-width: 1080px)');
317
+ // 768px: the footer's columns stand open from here, and fold below it
318
+ mid = mq('(min-width: 768px)');
296
319
  on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
297
320
  on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
298
321
  on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
299
322
  watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
323
+ watch(mid, function () { records('fold', function (r) { r.fit(); }); });
300
324
  on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
301
325
  try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
302
326
  if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
@@ -327,7 +351,7 @@ function init(ctx) {
327
351
  if (!doc) run([install]);
328
352
  // an element removed from the page since the last init is let go
329
353
  live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
330
- run([mega, mobile, theme, reveal, header, current, segmented, toc, tables], ctx && ctx.querySelectorAll ? ctx : doc);
354
+ run([mega, mobile, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
331
355
  }
332
356
 
333
357
  /** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/website-ui",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",