@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.
- package/AGENTS.md +44 -4
- package/CHANGELOG.md +116 -0
- package/README.md +8 -8
- package/bin/lib/check.mjs +47 -0
- package/css/base.css +12 -0
- package/css/components.css +8 -0
- package/css/footer.css +181 -0
- package/css/forms.css +154 -0
- package/css/ratings.css +78 -0
- package/css/tokens.css +21 -0
- package/demo/assets/avatar-sample.svg +1 -0
- package/demo/footer.html +339 -0
- package/demo/forms.html +505 -0
- package/demo/index.html +2 -0
- package/docs/components.md +197 -7
- package/docs/markup-contract.md +123 -6
- package/docs/theming.md +12 -5
- package/js/website.js +21 -4
- package/js/website.mjs +27 -3
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/system.json +12 -1
- package/templates/footer.html +119 -16
- package/templates/page.html +16 -11
- package/templates/sections/form.html +42 -0
- package/templates/sections/testimonials.html +58 -0
- package/website.css +457 -1
- package/website.min.css +1 -1
package/docs/components.md
CHANGED
|
@@ -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`), `--
|
|
120
|
-
|
|
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
|
|
200
|
-
a coloured dot, ink words, because the state hues fail 4.5:1 as
|
|
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
|
-
|
|
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
|
package/docs/markup-contract.md
CHANGED
|
@@ -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.
|
|
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)
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|