@gemboss/website-ui 0.6.0 → 0.8.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 +25 -2
- package/CHANGELOG.md +130 -0
- package/README.md +8 -8
- package/bin/lib/check.mjs +70 -1
- package/css/base.css +12 -0
- package/css/footer.css +178 -0
- package/css/header.css +176 -0
- package/css/tokens.css +1 -0
- package/demo/footer.html +339 -0
- package/demo/header.html +356 -0
- package/demo/index.html +2 -0
- package/docs/components.md +125 -8
- package/docs/markup-contract.md +158 -6
- package/docs/theming.md +9 -2
- package/js/website.js +136 -5
- package/js/website.mjs +170 -5
- package/package.json +1 -1
- package/skill/SKILL.md +7 -0
- package/system.json +2 -0
- package/templates/footer.html +108 -20
- package/templates/header.html +34 -6
- package/templates/page.html +16 -11
- package/website.css +368 -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), the footer's folds (0.7.0), and the mobile menu's drill-down and the language switcher (0.8.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
|
|
@@ -51,7 +51,7 @@ import { init } from '@gemboss/website-ui/website.mjs';
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
`destroy(root)` takes it back when a framework unmounts. The Next.js recipe, the `data-gw-ready` mark and the
|
|
54
|
-
events (`gw:mega`, `gw:theme`, `gw:segmented`, `gw:toc`) are in `docs/markup-contract.md`, "Script".
|
|
54
|
+
events (`gw:mega`, `gw:theme`, `gw:segmented`, `gw:toc`, `gw:menu`) are in `docs/markup-contract.md`, "Script".
|
|
55
55
|
|
|
56
56
|
Don't: wire a kit part with your own script (a click handler on a trigger, a class toggled by hand), or load two
|
|
57
57
|
copies of the script and expect both to work (the first owns the page).
|
|
@@ -343,10 +343,67 @@ Don't: hide content a reader needs to decide (prices, terms) only inside a close
|
|
|
343
343
|
| `.gw-mobile-menu__group` | a `<details>` group with a `<summary>` row |
|
|
344
344
|
| `.gw-mobile-menu__list`, `__link` | the links inside a group, 44px rows |
|
|
345
345
|
| `.gw-mobile-menu__top` | a top-level row without a group (Pricing) |
|
|
346
|
-
| `.gw-mobile-menu__actions` | the buttons and Sign in at the end of the sheet |
|
|
346
|
+
| `.gw-mobile-menu__actions` | the buttons and Sign in at the end of the sheet; in the drill-down menu, a bar that stays at the bottom of the sheet |
|
|
347
|
+
| `.gw-mobile-menu[data-gw-menu="drill"]` | 0.8.0, optional: the drill-down menu (below) |
|
|
348
|
+
| `.gw-mobile-menu__head`, `__back`, `__title` | 0.8.0: a level's head, Back and the level's title (`tabindex="-1"`, the focus lands there); drawn only for the open level |
|
|
349
|
+
| `.gw-mobile-menu__heading`, `__icon`, `__all` | 0.8.0, optional in a level: a muted heading over a run of links; a 32px icon tile before a link's `<b>` name and `<i>` line; the last link, to the section's own page, in the accent with its arrow |
|
|
350
|
+
| `.gw-lang` (`data-gw-lang`, `hidden`), `__btn`, `__short`, `__menu`, `__link` | 0.8.0, optional: the language switcher (below): a ghost button with the globe and the current language's code, its list of the other languages |
|
|
347
351
|
|
|
348
352
|
Header buttons: the secondary one shows from 768px, the primary one from 480px; below that they live in the mobile
|
|
349
|
-
menu. Full markup: `templates/header.html`.
|
|
353
|
+
menu. Full markup: `templates/header.html`. See both 0.8.0 options in `demo/header.html`.
|
|
354
|
+
|
|
355
|
+
### Drill-down mobile menu (0.8.0)
|
|
356
|
+
|
|
357
|
+
gemboss.ai's phone menu: the top level lists the sections in big rows; a tap on one shows its level alone, with Back
|
|
358
|
+
and its title; the actions stay in a bar at the bottom of the sheet while the list scrolls. Put `data-gw-menu="drill"`
|
|
359
|
+
on the `.gw-mobile-menu` and a head in each group; everything else is the 0.7.0 menu.
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<details class="gw-mobile-menu__group">
|
|
363
|
+
<summary>Product</summary>
|
|
364
|
+
<div class="gw-mobile-menu__head"><button class="gw-mobile-menu__back" type="button">Back</button><h2 class="gw-mobile-menu__title" tabindex="-1">Product</h2></div>
|
|
365
|
+
<p class="gw-mobile-menu__heading">Build</p>
|
|
366
|
+
<ul class="gw-mobile-menu__list">
|
|
367
|
+
<li><a class="gw-mobile-menu__link" href="/funnel"><span class="gw-mobile-menu__icon" aria-hidden="true">svg</span><span><b>Funnel builder</b><i>Angles, advertorials, offers</i></span></a></li>
|
|
368
|
+
</ul>
|
|
369
|
+
<a class="gw-mobile-menu__all" href="/product">All of Product</a>
|
|
370
|
+
</details>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- The actions bar (`.gw-mobile-menu__actions`): the language switcher (optional) and Sign in on the first line (two
|
|
374
|
+
things at most: the first on the left, the last on the right), the two buttons as `gw-btn--lg gw-btn--block` under it.
|
|
375
|
+
Words in the page's language, Back included.
|
|
376
|
+
- The script moves the focus to the title as a level opens and back to the section on Back, closes the sheet on
|
|
377
|
+
Escape from anywhere, keeps Tab inside the open sheet and its toggle, and fires `gw:menu` (`{ open, level }`). A level
|
|
378
|
+
slides in (none under reduced motion). Without the script the groups open in place, as in 0.7.0.
|
|
379
|
+
- When to use it: a site whose sections hold more than a handful of links each (gemboss.ai: four sections, six to
|
|
380
|
+
twelve links). A site with a few links per section can keep the 0.7.0 menu.
|
|
381
|
+
|
|
382
|
+
Don't: a level without its head (`check`: `menu-markup`); Back in English on a translated page; a level that only
|
|
383
|
+
repeats the section's own page; your own script on the summaries.
|
|
384
|
+
|
|
385
|
+
### Language switcher (0.8.0)
|
|
386
|
+
|
|
387
|
+
The header's language button, as gemboss.ai's: the globe and the current language's code; its list links to the same
|
|
388
|
+
page in the other languages, each named in its own language. It reads the page's `<link rel="alternate" hreflang>`, so
|
|
389
|
+
it shows only where the page has another language and stays hidden everywhere else.
|
|
390
|
+
|
|
391
|
+
```html
|
|
392
|
+
<div class="gw-lang" data-gw-lang hidden>
|
|
393
|
+
<button class="gw-lang__btn" type="button" aria-expanded="false" aria-controls="gw-lang-menu" data-gw-label-language="Language"><svg aria-hidden="true">globe</svg><span class="gw-lang__short"></span></button>
|
|
394
|
+
<ul class="gw-lang__menu" id="gw-lang-menu" hidden></ul>
|
|
395
|
+
</div>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- Two copies: one in `.gw-header__actions` (drawn from 1080px), one in the mobile menu's actions (its list opens
|
|
399
|
+
upward), each with its own list `id`. Keep `hidden` on both and the list empty: the script fills them.
|
|
400
|
+
- The head is the source: one `<link rel="alternate" hreflang="…" href="…">` per language the page has (its own
|
|
401
|
+
included), and `x-default` if you like (it is not listed). A `title` on a link names that language in the list.
|
|
402
|
+
- `data-gw-label-language` is the word for "Language" in the page's language ("Ngôn ngữ", "语言").
|
|
403
|
+
|
|
404
|
+
Don't: links typed into the list, a switcher shown by the markup, a translation's `<link rel="alternate">` without
|
|
405
|
+
`hreflang` (`check`: `lang-markup`); a flag for a language; a switcher that saves a choice the page's address does not
|
|
406
|
+
carry.
|
|
350
407
|
|
|
351
408
|
Don't: a floating, rounded or blurred header of your own (the one floating header is `.gw-header--float`, chosen
|
|
352
409
|
for the whole site); more than five top-level items; a second filled button in the bar; a
|
|
@@ -354,18 +411,62 @@ hero pulled up under the header.
|
|
|
354
411
|
|
|
355
412
|
## Footer
|
|
356
413
|
|
|
414
|
+
The footer of gemboss.ai (0.7.0): one per site, the same on every page, from `templates/footer.html`. A brand fills
|
|
415
|
+
its rows (its columns, its family, its words) and deletes the optional parts it does not use; it never adds a footer
|
|
416
|
+
part of its own. See it in `demo/footer.html`, with a switch to the same footer for a GemPages-style brand.
|
|
417
|
+
|
|
357
418
|
| class | for |
|
|
358
419
|
|---|---|
|
|
359
420
|
| `.gw-footer` | the footer: one hairline on top, white like the page |
|
|
360
|
-
| `.gw-footer__grid` | brand block and link columns: 2 columns on phones, 3 from 768px, one row from 1080px |
|
|
421
|
+
| `.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 |
|
|
361
422
|
| `.gw-footer__brand` | the brand and one sentence, full width on phones |
|
|
362
423
|
| `.gw-footer__col`, `__heading` | a `<nav>` column: a sentence-case heading and a `<ul>` of links |
|
|
424
|
+
| `.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 |
|
|
425
|
+
| `.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 |
|
|
426
|
+
| `.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 |
|
|
427
|
+
| `.gw-footer__ext` | 0.7.0: the drawn ↗ after a link that leaves the site, `aria-hidden="true"` |
|
|
363
428
|
| `.gw-footer__bottom` | copyright, legal links, social links; alone (no grid above), it drops its top rule |
|
|
429
|
+
| `.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 |
|
|
430
|
+
| `.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 |
|
|
364
431
|
| `.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 |
|
|
432
|
+
| `.gw-visually-hidden` | 0.7.0, anywhere: words for a screen reader only, " (opens in a new tab)" in a link with `target="_blank"` |
|
|
365
433
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
434
|
+
```html
|
|
435
|
+
<nav class="gw-footer__col" aria-label="Product">
|
|
436
|
+
<details class="gw-footer__fold">
|
|
437
|
+
<summary><h2 class="gw-footer__heading">Product</h2></summary>
|
|
438
|
+
<ul><li><a href="/funnel">Funnel builder</a></li> …</ul>
|
|
439
|
+
</details>
|
|
440
|
+
</nav>
|
|
441
|
+
…
|
|
442
|
+
<nav class="gw-footer__family" aria-label="More from GemBoss">
|
|
443
|
+
<details class="gw-footer__fold">
|
|
444
|
+
<summary><h2 class="gw-footer__heading">More from GemBoss</h2></summary>
|
|
445
|
+
<ul>
|
|
446
|
+
<li><a href="/custom-theme"><b>GemThemes</b> <i>Custom theme</i></a></li>
|
|
447
|
+
<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>
|
|
448
|
+
</ul>
|
|
449
|
+
</details>
|
|
450
|
+
</nav>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Ask AI: each link carries the same question, URL-encoded after `?q=`: `https://chatgpt.com/?q=…`,
|
|
454
|
+
`https://claude.ai/new?q=…`, `https://www.perplexity.ai/search/new?q=…`, `https://www.google.com/search?udm=50&q=…`.
|
|
455
|
+
gemboss.ai asks "What is GemBoss (gemboss.ai), and how does it help Shopify brands turn ad clicks into sales?": what the
|
|
456
|
+
brand is and what it does for the reader, never a leading question, so the answer comes from what the web says.
|
|
457
|
+
Words, not the assistants' logos (another company's mark on a page is a permission question).
|
|
458
|
+
|
|
459
|
+
Links that leave the site carry the ↗ (`.gw-footer__ext`); a link with `target="_blank"` also says so to a screen
|
|
460
|
+
reader: `<span class="gw-visually-hidden"> (opens in a new tab)</span>` inside the `<a>`. A link without
|
|
461
|
+
`target="_blank"` does not.
|
|
462
|
+
|
|
463
|
+
A GemCommerce brand lists its siblings in its family row: a GemPages site might show GemBoss, GemX and GemUpsells.
|
|
464
|
+
|
|
465
|
+
The newsletter note says how often you write and how to stop, and links to the privacy policy. Don't: uppercase
|
|
466
|
+
column headings; a footer part that is not in the template (a second logo row, a badge strip, a map); a column
|
|
467
|
+
without its heading in the `<summary>`; a wordmark a screen reader reads (`check`: `footer-markup`); the assistants'
|
|
468
|
+
logos in the Ask AI row; a newsletter form that stores nothing; a consent box ticked for the reader; a filled primary
|
|
469
|
+
button in the footer (the closing CTA above it is the page's action).
|
|
369
470
|
|
|
370
471
|
## Sections
|
|
371
472
|
|
|
@@ -732,6 +833,22 @@ not make public with the review; masonry or a carousel that moves by itself.
|
|
|
732
833
|
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
|
|
733
834
|
the first screen, and don't animate anything but `transform` and `opacity`.
|
|
734
835
|
|
|
836
|
+
## Additions to the markup contract in 0.8.0
|
|
837
|
+
|
|
838
|
+
The drill-down mobile menu: `data-gw-menu="drill"` on `.gw-mobile-menu`, `.gw-mobile-menu__head` (`__back`, `__title`),
|
|
839
|
+
`.gw-mobile-menu__heading`, `__icon`, `__all`, the script's `.is-drill`, `.is-drilled` and `.is-back` on the sheet and
|
|
840
|
+
its `--gw-menu-top`. The language switcher: `.gw-lang` (`data-gw-lang`, `__btn`, `__short`, `__menu`, `__link`) and the
|
|
841
|
+
script's `lang` part. One event, `gw:menu` (`{ open, level }`), on every mobile menu. No markup or class of 0.7.0
|
|
842
|
+
changes: the 0.7.0 header still works and looks the same.
|
|
843
|
+
|
|
844
|
+
## Additions to the markup contract in 0.7.0
|
|
845
|
+
|
|
846
|
+
The footer of gemboss.ai: `.gw-footer__fold` (a `<details>` in a `.gw-footer__col` or a `.gw-footer__family`, its
|
|
847
|
+
`<summary>` holding the `.gw-footer__heading`) with the script's `fold` part (`data-gw-ready="fold"`), `.gw-footer__ask`
|
|
848
|
+
(`__ask-label`), `.gw-footer__family`, `.gw-footer__ext`, `.gw-footer__toggle` (on a `.gw-theme-toggle`),
|
|
849
|
+
`.gw-footer__wordmark`, and `.gw-visually-hidden`. One token: `--gw-display-wordmark`. No event. No markup or class of
|
|
850
|
+
0.6.0 changes: the 0.6.0 footer markup still works and looks the same.
|
|
851
|
+
|
|
735
852
|
## Additions to the markup contract in 0.6.0
|
|
736
853
|
|
|
737
854
|
Forms: `.gw-field` (`__label`, `__error`, `__hint`), `.gw-input`, `.gw-select`, `.gw-textarea`, `.gw-choices`, `.gw-check`
|
package/docs/markup-contract.md
CHANGED
|
@@ -28,7 +28,8 @@ 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).
|
|
31
|
+
prose tables (0.5.0), the footer's folds (0.7.0), and the mobile menu's drill-down and the language switcher (0.8.0).
|
|
32
|
+
It comes in two entries built from one source
|
|
32
33
|
(`js/website.mjs`; `scripts/build.mjs` writes `js/website.js` from it and `--check` fails when they differ):
|
|
33
34
|
|
|
34
35
|
| entry | file | how it starts |
|
|
@@ -43,9 +44,9 @@ prose tables (0.5.0). It comes in two entries built from one source
|
|
|
43
44
|
to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
|
|
44
45
|
- **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
|
|
45
46
|
mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
|
|
46
|
-
- **`version`**: `'0.
|
|
47
|
-
- **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `
|
|
48
|
-
`segmented`, `toc`, `table`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
|
|
47
|
+
- **`version`**: `'0.8.0'`. The classic script exposes the three as `window.gwWebsite`.
|
|
48
|
+
- **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `lang`, `theme`, `reveal`,
|
|
49
|
+
`header`, `segmented`, `toc`, `table`, `fold`). A drill-down menu is wired with its toggle (`menu`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
|
|
49
50
|
is still wired.
|
|
50
51
|
- **One copy per page.** The first copy that runs owns the page (`window.gwWebsite`); another one (the classic
|
|
51
52
|
script next to a bundle, two bundles) hands its calls to it, so nothing is wired twice.
|
|
@@ -94,11 +95,13 @@ The script tells the page's own code what changed. Every event is a `CustomEvent
|
|
|
94
95
|
| `gw:mega` | the trigger (`[data-gw-mega]`) | `{ id, open }`: the panel's `id`, `true` when it opened | a panel opens or closes: click, hover, Escape, a click outside, focus leaving, a link inside, `destroy` of an open one. Moving to another menu fires a close, then an open |
|
|
95
96
|
| `gw:theme` | `<html>` | `{ theme: 'light' \| 'dark' }`: the theme the page now shows | a press on a theme toggle; also a change of the system's scheme while the page follows it (no `data-theme`) |
|
|
96
97
|
| `gw:segmented` | the group (`[data-gw-segmented]`) | `{ name, value }`: the group's name, the chosen option's `value` | the reader chooses an option (click, arrows, Home, End) |
|
|
98
|
+
| `gw:menu` | the mobile menu (`.gw-mobile-menu`) | `{ open, level }`: `true` while it is open; `level` is the title of the drill-down level in view, `''` for the top level (and for a 0.7.0 menu) | the menu opens or closes (the toggle, Escape, a link inside, resizing past 1080px, `destroy` of an open one), and a drill-down menu moves to a level or back to the top. Closing from a level fires only the close (0.8.0) |
|
|
97
99
|
| `gw:toc` | the contents (`[data-gw-toc]`) | `{ id }`: the id of the heading whose section is now marked, `''` above the first one | the section in view changes: scrolling, a contents link, a jump. Also once after load when the page opens inside a section (a link to `#id`): the observer reports after `init`, never during it |
|
|
98
100
|
|
|
99
101
|
```js
|
|
100
102
|
document.addEventListener('gw:theme', function (e) { chart.setTheme(e.detail.theme); });
|
|
101
103
|
document.addEventListener('gw:toc', function (e) { analytics.track('section', e.detail.id); });
|
|
104
|
+
document.addEventListener('gw:menu', function (e) { if (e.detail.level) analytics.track('menu', e.detail.level); });
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
## Header
|
|
@@ -125,13 +128,14 @@ document.addEventListener('gw:toc', function (e) { analytics.track('section', e.
|
|
|
125
128
|
</nav>
|
|
126
129
|
<div class="gw-header__actions">
|
|
127
130
|
<button class="gw-theme-toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme">svg</button>
|
|
131
|
+
<div class="gw-lang" data-gw-lang hidden> … </div> <!-- optional: the language switcher (0.8.0, below) -->
|
|
128
132
|
<a class="gw-nav__link gw-header__signin" href="…">Sign in</a>
|
|
129
133
|
<a class="gw-btn gw-btn--secondary gw-btn--sm" href="…">Book a demo</a>
|
|
130
134
|
<a class="gw-btn gw-btn--sm" href="…">Start free</a>
|
|
131
135
|
<button class="gw-menu-toggle" type="button" aria-expanded="false" aria-controls="gw-mobile-menu" data-gw-menu-toggle aria-label="Open menu">svg</button>
|
|
132
136
|
</div>
|
|
133
137
|
</div>
|
|
134
|
-
<div class="gw-mobile-menu" id="gw-mobile-menu" hidden> …the same links as lists, and the actions… </div>
|
|
138
|
+
<div class="gw-mobile-menu" id="gw-mobile-menu" data-gw-menu?="drill" hidden> …the same links as lists, and the actions… </div>
|
|
135
139
|
</header>
|
|
136
140
|
```
|
|
137
141
|
|
|
@@ -159,6 +163,153 @@ Script behaviour:
|
|
|
159
163
|
- **Current page**: the script marks `aria-current="page"` on `.gw-nav__link` / `.gw-mega__link` and the mobile
|
|
160
164
|
menu's links whose `href` pathname equals `location.pathname` (when the markup did not already). Each `init` takes
|
|
161
165
|
its own marks back first, so after a client-side navigation they follow the new path; a mark in the markup stays.
|
|
166
|
+
- **`gw:menu`** (0.8.0): opening and closing a mobile menu fire `gw:menu` on it (`{ open, level: '' }`; Events above).
|
|
167
|
+
|
|
168
|
+
### Drill-down mobile menu (0.8.0)
|
|
169
|
+
|
|
170
|
+
Opt-in on the sheet, `data-gw-menu="drill"`; the markup is the 0.7.0 menu plus a head in each group. The groups, the
|
|
171
|
+
top-level links and the actions are children of the sheet's `<nav>`.
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<div class="gw-mobile-menu" id="gw-mobile-menu" data-gw-menu="drill" hidden>
|
|
175
|
+
<nav class="gw-wrap" aria-label="Main, mobile">
|
|
176
|
+
<details class="gw-mobile-menu__group">
|
|
177
|
+
<summary>Product</summary>
|
|
178
|
+
<div class="gw-mobile-menu__head"><button class="gw-mobile-menu__back" type="button">Back</button><h2 class="gw-mobile-menu__title" tabindex="-1">Product</h2></div>
|
|
179
|
+
<p class="gw-mobile-menu__heading">Build</p> <!-- optional -->
|
|
180
|
+
<ul class="gw-mobile-menu__list">
|
|
181
|
+
<li><a class="gw-mobile-menu__link" href="/funnel"><span class="gw-mobile-menu__icon" aria-hidden="true">svg</span><span><b>Funnel builder</b><i>Angles, advertorials, offers</i></span></a></li>
|
|
182
|
+
<li><a class="gw-mobile-menu__link" href="/data">Analytics & data</a></li> <!-- the icon and the line are optional -->
|
|
183
|
+
</ul>
|
|
184
|
+
<a class="gw-mobile-menu__all" href="/product">All of Product</a> <!-- optional -->
|
|
185
|
+
</details>
|
|
186
|
+
<a class="gw-mobile-menu__top" href="/pricing">Pricing</a>
|
|
187
|
+
<div class="gw-mobile-menu__actions"> <!-- the bar at the bottom -->
|
|
188
|
+
<div class="gw-lang" data-gw-lang hidden> … </div> <!-- optional: first line, left -->
|
|
189
|
+
<a class="gw-btn gw-btn--lg gw-btn--block" href="…">Start free</a>
|
|
190
|
+
<a class="gw-btn gw-btn--secondary gw-btn--lg gw-btn--block" href="…">Book a demo</a>
|
|
191
|
+
<a class="gw-mobile-menu__link" href="…">Sign in</a> <!-- first line, right -->
|
|
192
|
+
</div>
|
|
193
|
+
</nav>
|
|
194
|
+
</div>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- **CSS, from the attribute** (works before and without the script): the sheet is a column with no bottom padding,
|
|
198
|
+
clipped sideways; top-level rows of 64px with 24px words; a level's links 56px with a hairline, their `<b>` and
|
|
199
|
+
`<i>`, the 32px `__icon` tile, the muted `__heading`; the actions are a bar that stays at the bottom of the sheet
|
|
200
|
+
(`position: sticky`): what is not a `.gw-btn` on the first line (two things at most), the last of those on the right,
|
|
201
|
+
the buttons side by side on the second line. Without the script the groups are `<details>` that open in place (the 0.7.0 menu), their
|
|
202
|
+
chevron pointing down, and the heads are not drawn.
|
|
203
|
+
- **Script** (`.is-drill` on the sheet once wired, with the toggle's `menu` record). Each group that has a `<summary>`
|
|
204
|
+
and a `.gw-mobile-menu__title` is a level; the script closes them all (and gives the markup's `open` back on
|
|
205
|
+
`destroy`). A press on a summary (click, tap, Enter, Space; its default is cancelled) opens that group alone, puts
|
|
206
|
+
`.is-drilled` on the sheet (the CSS then draws only that level and its head, and hides its summary), scrolls the
|
|
207
|
+
sheet to its top, moves the focus to the title and fires `gw:menu` (`{ open: true, level: <the title's words> }`).
|
|
208
|
+
A press on `.gw-mobile-menu__back` shows the top level (`.is-back` on the sheet, for the slide back), puts the focus
|
|
209
|
+
on the summary it came from and fires `gw:menu` (`level: ''`). Closing the sheet (the toggle, Escape from anywhere in
|
|
210
|
+
it, a link, resizing past 1080px, `destroy`) shows the top level again without an event of its own.
|
|
211
|
+
- **While it is open**: Tab on the sheet's last shown stop goes to the toggle, Shift+Tab on the toggle to that stop, and
|
|
212
|
+
a focus found outside comes back in (the trap; a 0.7.0 menu has none). The page is locked as in 0.7.0
|
|
213
|
+
(`html.is-menu-open`). As it opens, the script sets `--gw-menu-top` on the sheet to where the header ends, and the
|
|
214
|
+
header stays `sticky` (not `fixed`), so the sheet starts under the header and an announcement bar above it stays.
|
|
215
|
+
- **Motion**: a level slides in from the right, the sections back from the left (`transform` and `opacity`, 0.24s, the
|
|
216
|
+
kit's ease), only under `prefers-reduced-motion: no-preference`.
|
|
217
|
+
- `check` reports a group of a drill menu that is not a `<details>`, has no `<summary>`, no back button, no title or a
|
|
218
|
+
title without `tabindex="-1"`, and `data-gw-menu` with another value (`menu-markup`).
|
|
219
|
+
|
|
220
|
+
### Language switcher (0.8.0)
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<head>
|
|
224
|
+
<link rel="alternate" hreflang="en" href="https://example.com/pricing">
|
|
225
|
+
<link rel="alternate" hreflang="vi" href="https://example.com/vi/pricing">
|
|
226
|
+
<link rel="alternate" hreflang="x-default" href="https://example.com/pricing">
|
|
227
|
+
</head>
|
|
228
|
+
…
|
|
229
|
+
<div class="gw-lang" data-gw-lang hidden>
|
|
230
|
+
<button class="gw-lang__btn" type="button" aria-expanded="false" aria-controls="gw-lang-menu" data-gw-label-language="Language"><svg aria-hidden="true">globe</svg><span class="gw-lang__short"></span></button>
|
|
231
|
+
<ul class="gw-lang__menu" id="gw-lang-menu" hidden></ul>
|
|
232
|
+
</div>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- **Where**: in `.gw-header__actions`, before Sign in (drawn from 1080px), and a second one, with its own list `id`, in
|
|
236
|
+
the mobile menu's `.gw-mobile-menu__actions` (its list opens upward there). Both are filled the same way.
|
|
237
|
+
- **The languages are the head's** `<link rel="alternate" hreflang href>`, not `x-default`, the first link of each
|
|
238
|
+
`hreflang`: the links search engines read. The current language is the one whose `hreflang` is `<html lang>`, else the
|
|
239
|
+
first with the same base language (`<html lang="zh">` is `zh-Hans`). The script writes each other language into the
|
|
240
|
+
list as `<li><a class="gw-lang__link" href hreflang lang>Name</a></li>`, the name in its own language: the `<link>`'s
|
|
241
|
+
`title`, else the kit's (`en` English, `vi` Tiếng Việt, `zh` and `zh-Hans` 中文), else the browser's
|
|
242
|
+
(`Intl.DisplayNames`, first letter capitalised), else the code. A link whose host is the host of
|
|
243
|
+
`<link rel="canonical">` (else this page's host) keeps only its path, search and hash, so a preview deployment stays
|
|
244
|
+
on itself. The button's `.gw-lang__short` gets the current language's short code (`EN`, `VI`, `中`, else its base code
|
|
245
|
+
in capitals) and its `lang`.
|
|
246
|
+
- **Hidden** while the list is empty: a page with no other language (no alternates, or only its own) keeps `hidden`;
|
|
247
|
+
the script takes it away when there is one. Every `init` reads the links again, so a route change that brings other
|
|
248
|
+
alternates updates both switchers.
|
|
249
|
+
- **Words**: `data-gw-label-language` on the button (else its `aria-label`, else "Language"): the button is named
|
|
250
|
+
"Language: English" (the word, then the current language's name), the list "Language". `destroy` gives back the
|
|
251
|
+
button's own label, empties the list and hides the switcher.
|
|
252
|
+
- **Keys and pointer**: the button opens and closes the list (`aria-expanded`); ArrowDown and ArrowUp on it open the
|
|
253
|
+
list on its first or last link; in the list ArrowDown and ArrowUp move (wrapping), Home and End go to the ends;
|
|
254
|
+
Escape closes it, gives the focus back to the button and stops there (an open mobile menu around it stays open); the
|
|
255
|
+
focus leaving it, or a click outside, closes it. Closing the mobile menu closes the list inside it. No event.
|
|
256
|
+
- `check` reports a `.gw-lang` that is not `hidden`, has no `data-gw-lang`, no `.gw-lang__btn`, a button whose
|
|
257
|
+
`aria-controls` names no list inside it or without `aria-expanded`, links typed into the list, and, on a page with a
|
|
258
|
+
switcher, a `<link rel="alternate">` without `type` that lacks `hreflang` or `href` (`lang-markup`).
|
|
259
|
+
|
|
260
|
+
## Footer (0.7.0)
|
|
261
|
+
|
|
262
|
+
```html
|
|
263
|
+
<footer class="gw-footer">
|
|
264
|
+
<div class="gw-wrap">
|
|
265
|
+
<div class="gw-footer__grid">
|
|
266
|
+
<div class="gw-footer__brand">
|
|
267
|
+
<a class="gw-brand" href="/">…</a><p>One sentence.</p>
|
|
268
|
+
<div class="gw-footer__ask"> <!-- optional -->
|
|
269
|
+
<p class="gw-footer__ask-label" id="footer-ask">Ask AI about Brand</p>
|
|
270
|
+
<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>
|
|
271
|
+
</div>
|
|
272
|
+
<form class="gw-footer__newsletter" …>…</form> <!-- optional (0.6.0) -->
|
|
273
|
+
</div>
|
|
274
|
+
<nav class="gw-footer__col" aria-label="Product"> <!-- three to six -->
|
|
275
|
+
<details class="gw-footer__fold"> <!-- no open: folded on phones -->
|
|
276
|
+
<summary><h2 class="gw-footer__heading">Product</h2></summary>
|
|
277
|
+
<ul><li><a href="…">…</a></li> …</ul>
|
|
278
|
+
</details>
|
|
279
|
+
</nav>
|
|
280
|
+
</div>
|
|
281
|
+
<nav class="gw-footer__family" aria-label="More from Brand"> <!-- optional, repeatable -->
|
|
282
|
+
<details class="gw-footer__fold">
|
|
283
|
+
<summary><h2 class="gw-footer__heading">More from Brand</h2></summary>
|
|
284
|
+
<ul><li><a href="…"><b>Name<span class="gw-footer__ext" aria-hidden="true"></span>?</b> <i>One line</i></a></li> …</ul>
|
|
285
|
+
</details>
|
|
286
|
+
</nav>
|
|
287
|
+
<div class="gw-footer__bottom">
|
|
288
|
+
<span>© …</span><ul aria-label="Legal">…</ul>
|
|
289
|
+
<button class="gw-theme-toggle gw-footer__toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme">svg</button> <!-- optional -->
|
|
290
|
+
<ul aria-label="Social">…</ul>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="gw-footer__wordmark" aria-hidden="true"><svg viewBox="…">…currentColor…</svg><span>Brand</span></div> <!-- optional -->
|
|
293
|
+
</div>
|
|
294
|
+
</footer>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- **Folds** (`.gw-footer__fold`, a `<details>` whose `<summary>` holds the `.gw-footer__heading`): the CSS folds them
|
|
298
|
+
below 768px (52px rows, the browser opens and closes them) and draws the open column from 768px, where the summary
|
|
299
|
+
is `display: contents` and the list shows even while the `<details>` is closed (`::details-content`). The script,
|
|
300
|
+
for each fold with a summary:
|
|
301
|
+
- from 768px (`matchMedia('(min-width: 768px)')`) sets `open`, puts `tabindex="-1"` on the summary and cancels a
|
|
302
|
+
click on it (`preventDefault`), so a heading never folds its column away, in a browser without
|
|
303
|
+
`::details-content` too;
|
|
304
|
+
- below 768px gives back the markup's state: `open` as the markup had it (folded unless it said `open`), the
|
|
305
|
+
summary's own `tabindex` (none, usually); again on every change of width;
|
|
306
|
+
- `destroy`: the markup's state, its listener and the `fold` mark go. No event.
|
|
307
|
+
- **Theme toggle**: a `[data-gw-theme-toggle]` in the bottom row is wired like the header's (above); with both on the
|
|
308
|
+
page, a press on either relabels both.
|
|
309
|
+
- **Not the script's**: the Ask AI row, the family rows, the ↗ and the wordmark are markup and CSS only. The question
|
|
310
|
+
in the Ask AI links is written in the markup, URL-encoded.
|
|
311
|
+
- `check` reports a `.gw-footer__fold` that is not a `<details>`, has no `<summary>`, or whose summary does not hold
|
|
312
|
+
the `.gw-footer__heading`, and a `.gw-footer__wordmark` without `aria-hidden="true"` (`footer-markup`).
|
|
162
313
|
|
|
163
314
|
## Article: contents and prose tables (0.5.0)
|
|
164
315
|
|
|
@@ -329,6 +480,7 @@ CSS only: the script does not touch a form. The page (or its server) owns valida
|
|
|
329
480
|
| `.gw-table` (+ `--compare`) | data / comparison table; scrolls inside its box on phones |
|
|
330
481
|
| `.gw-faq` (`<details class="gw-faq__item"><summary>`) | FAQ accordion with no script |
|
|
331
482
|
| `.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) |
|
|
483
|
+
| `.gw-visually-hidden` | words for a screen reader only, " (opens in a new tab)" in a link with `target="_blank"` (0.7.0) |
|
|
332
484
|
| `.gw-tabs` (`role="tablist"`) | NOT in the kit yet (a reported gap would add it) |
|
|
333
485
|
|
|
334
486
|
## Sections
|
|
@@ -349,4 +501,4 @@ CSS only: the script does not touch a form. The page (or its server) owns valida
|
|
|
349
501
|
| `.gw-testimonials` (`.gw-card.gw-testimonial`: `__quote`, `__by`, `__avatar`) | a wall of short reviews, each linked to its source (0.6.0) |
|
|
350
502
|
| `.gw-pricing` (`.gw-plan`, `--featured`) | plan cards; the featured one has a hairline in the accent, not a fill |
|
|
351
503
|
| `.gw-cta` | closing call to action band |
|
|
352
|
-
| `.gw-footer` (`__grid`, `__col`, `__heading`, `__bottom`, `__newsletter`) | footer columns + bottom row; an optional newsletter form in the brand block (0.6.0) |
|
|
504
|
+
| `.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,10 @@ 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), `demo/footer.html` (the footer, with a switch to the same
|
|
19
|
+
footer for a GemPages-style brand) and `demo/header.html` (the header with the drill-down phone menu and the language
|
|
20
|
+
switcher, with the same switch: the bar's buttons and the language list follow the radius and action profiles).
|
|
19
21
|
|
|
20
22
|
## 1 to 3. Accent, wash, logo
|
|
21
23
|
|
|
@@ -43,6 +45,11 @@ The logo and the favicon are files, not variables: the `<img class="gw-brand__ma
|
|
|
43
45
|
`alt=""` because the name is written next to it) and the `<span class="gw-brand__name">` in the header and footer
|
|
44
46
|
partials, and `<link rel="icon">` in the page head.
|
|
45
47
|
|
|
48
|
+
The footer (0.7.0, `templates/footer.html`) is the same for every brand; a brand fills it. Its family row lists the
|
|
49
|
+
brand's siblings (a GemPages site: GemBoss, GemX, GemUpsells), its Ask AI row asks about the brand, and its wordmark
|
|
50
|
+
is the brand's own mark as an inline SVG drawn in `currentColor` (so it takes the faint colour) and its name. The
|
|
51
|
+
radius profile reaches the family items and the toggle (8px, 12px in `soft`); nothing else of the footer changes.
|
|
52
|
+
|
|
46
53
|
## 4 to 6. The three profiles
|
|
47
54
|
|
|
48
55
|
Chosen once, in the page skeleton (the layout or partial every page shares). The first of each is the default and
|
package/js/website.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.8.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.8.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 }; }
|
|
@@ -93,24 +93,136 @@ function mobile(ctx) {
|
|
|
93
93
|
var menu = doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
|
|
94
94
|
if (!menu) return;
|
|
95
95
|
var label = [attr(btn, 'data-gw-label-open', attr(btn, 'aria-label', 'Open menu')), attr(btn, 'data-gw-label-close', 'Close menu')];
|
|
96
|
+
var groups = attr(menu, 'data-gw-menu', '') !== 'drill' ? [] : all('.gw-mobile-menu__group', menu).filter(function (g) { return g.querySelector('summary') && head(g); });
|
|
97
|
+
var own = groups.map(function (g) { return g.open; }), cur = null;
|
|
98
|
+
function head(g) { return g.querySelector('.gw-mobile-menu__title'); }
|
|
96
99
|
var r = {
|
|
97
100
|
open: function () { return !menu.hidden; },
|
|
98
101
|
set: function (o, focus) {
|
|
102
|
+
if (o && groups.length) menu.style.setProperty('--gw-menu-top', Math.round((btn.closest('[data-gw-header]') || menu.parentNode).getBoundingClientRect().bottom) + 'px');
|
|
99
103
|
menu.hidden = !o;
|
|
100
104
|
btn.setAttribute('aria-expanded', String(o));
|
|
101
105
|
btn.setAttribute('aria-label', label[+o]);
|
|
102
106
|
root.classList.toggle('is-menu-open', o);
|
|
107
|
+
if (!o) {
|
|
108
|
+
r.go(null);
|
|
109
|
+
records('lang', function (l) { if (menu.contains(l.el)) l.set(false); });
|
|
110
|
+
}
|
|
103
111
|
if (focus) btn.focus();
|
|
112
|
+
emit(menu, 'gw:menu', { open: o, level: '' });
|
|
104
113
|
},
|
|
105
|
-
|
|
114
|
+
go: function (g, focus) {
|
|
115
|
+
var from = cur;
|
|
116
|
+
if (g === cur) return;
|
|
117
|
+
cur = g;
|
|
118
|
+
groups.forEach(function (x) { x.open = x === g; });
|
|
119
|
+
menu.classList.toggle('is-drilled', !!g);
|
|
120
|
+
menu.classList.toggle('is-back', !g && r.open());
|
|
121
|
+
menu.scrollTop = 0;
|
|
122
|
+
if (focus) (g ? head(g) : from.querySelector('summary')).focus();
|
|
123
|
+
if (r.open()) emit(menu, 'gw:menu', { open: true, level: g ? words(head(g)) : '' });
|
|
124
|
+
},
|
|
125
|
+
trap: function (e) {
|
|
126
|
+
var stops = [btn].concat(all('a[href], button, summary, input, select, textarea, [tabindex="0"]', menu).filter(function (el) { return !el.disabled && el.getClientRects().length; }));
|
|
127
|
+
var a = doc.activeElement, last = stops[stops.length - 1];
|
|
128
|
+
if ((a !== btn && !menu.contains(a)) || a === (e.shiftKey ? btn : last)) { e.preventDefault(); (e.shiftKey ? last : btn).focus(); }
|
|
129
|
+
},
|
|
130
|
+
stop: function () {
|
|
131
|
+
if (r.open()) r.set(false);
|
|
132
|
+
if (!groups.length) return;
|
|
133
|
+
menu.classList.remove('is-drill');
|
|
134
|
+
menu.style.removeProperty('--gw-menu-top');
|
|
135
|
+
groups.forEach(function (g, i) { g.open = own[i]; });
|
|
136
|
+
}
|
|
106
137
|
};
|
|
107
138
|
on(btn, 'click', function () { r.set(!r.open()); });
|
|
108
139
|
on(menu, 'click', function (e) { if (link(e)) r.set(false); });
|
|
140
|
+
groups.forEach(function (g) {
|
|
141
|
+
var back = g.querySelector('.gw-mobile-menu__back');
|
|
142
|
+
on(g.querySelector('summary'), 'click', function (e) { e.preventDefault(); r.go(g, true); });
|
|
143
|
+
if (back) on(back, 'click', function () { r.go(null, true); });
|
|
144
|
+
g.open = false;
|
|
145
|
+
});
|
|
146
|
+
if (groups.length) menu.classList.add('is-drill'); else r.trap = null;
|
|
109
147
|
return r;
|
|
110
148
|
});
|
|
111
149
|
}
|
|
112
150
|
function closeMenus(focus) { records('menu', function (r) { if (r.open()) r.set(false, focus); }); }
|
|
113
151
|
|
|
152
|
+
var LANGS = { en: ['English', 'EN'], vi: ['Ti\u1ebfng Vi\u1ec7t', 'VI'], zh: ['\u4e2d\u6587', '\u4e2d'], 'zh-hans': ['\u4e2d\u6587', '\u4e2d'] };
|
|
153
|
+
function base(code) { return code.toLowerCase().split(/[-_]/)[0]; }
|
|
154
|
+
function speak(x) {
|
|
155
|
+
var k = LANGS[x.code.toLowerCase()], n = x.title || (k && k[0]);
|
|
156
|
+
try { n = n || new Intl.DisplayNames([x.code], { type: 'language' }).of(x.code); } catch (e) { /* an old browser, a bad code */ }
|
|
157
|
+
n = n || x.code;
|
|
158
|
+
return n.charAt(0).toUpperCase() + n.slice(1);
|
|
159
|
+
}
|
|
160
|
+
function alternates() {
|
|
161
|
+
var here = attr(root, 'lang', '').toLowerCase(), c = doc.querySelector('link[rel="canonical"][href]'), host = location.host, seen = {}, list = [];
|
|
162
|
+
try { if (c) host = new URL(attr(c, 'href', ''), location.href).host; } catch (e) { /* this host */ }
|
|
163
|
+
all('link[rel="alternate"][hreflang][href]').forEach(function (l) {
|
|
164
|
+
var code = attr(l, 'hreflang', ''), k = code.toLowerCase(), u;
|
|
165
|
+
try { u = new URL(attr(l, 'href', ''), location.href); } catch (e) { return; }
|
|
166
|
+
if (k === 'x-default' || seen[k]) return;
|
|
167
|
+
seen[k] = 1;
|
|
168
|
+
list.push({ code: code, title: attr(l, 'title', ''), here: k === here, href: u.host === host ? u.pathname + u.search + u.hash : u.href });
|
|
169
|
+
});
|
|
170
|
+
if (!list.some(function (x) { return x.here; })) list.some(function (x) { return (x.here = base(x.code) === base(here)); });
|
|
171
|
+
return list;
|
|
172
|
+
}
|
|
173
|
+
function lang(ctx) {
|
|
174
|
+
each(ctx, '[data-gw-lang]', 'lang', function (box, on) {
|
|
175
|
+
var btn = box.querySelector('.gw-lang__btn'), list = btn && doc.getElementById(attr(btn, 'aria-controls', ''));
|
|
176
|
+
if (!list) return;
|
|
177
|
+
var short = box.querySelector('.gw-lang__short'), name = attr(btn, 'aria-label', null), word = attr(btn, 'data-gw-label-language', name || 'Language');
|
|
178
|
+
function links() { return all('a[href]', list); }
|
|
179
|
+
var r = {
|
|
180
|
+
open: function () { return !list.hidden; },
|
|
181
|
+
set: function (o, focus) { list.hidden = !o; btn.setAttribute('aria-expanded', String(o)); if (focus) btn.focus(); },
|
|
182
|
+
fill: function () {
|
|
183
|
+
var alts = alternates(), cur = alts.filter(function (x) { return x.here; })[0] || { code: attr(root, 'lang', '') }, k = LANGS[cur.code.toLowerCase()];
|
|
184
|
+
list.textContent = '';
|
|
185
|
+
alts.forEach(function (x) {
|
|
186
|
+
if (x === cur) return;
|
|
187
|
+
var li = doc.createElement('li'), a = doc.createElement('a');
|
|
188
|
+
a.className = 'gw-lang__link';
|
|
189
|
+
a.setAttribute('href', x.href);
|
|
190
|
+
a.setAttribute('hreflang', x.code);
|
|
191
|
+
a.setAttribute('lang', x.code);
|
|
192
|
+
a.textContent = speak(x);
|
|
193
|
+
li.appendChild(a);
|
|
194
|
+
list.appendChild(li);
|
|
195
|
+
});
|
|
196
|
+
box.hidden = !links().length;
|
|
197
|
+
if (box.hidden) r.set(false);
|
|
198
|
+
if (short) { short.textContent = k ? k[1] : base(cur.code).toUpperCase(); short.setAttribute('lang', cur.code); }
|
|
199
|
+
btn.setAttribute('aria-label', word + ': ' + speak(cur));
|
|
200
|
+
list.setAttribute('aria-label', word);
|
|
201
|
+
},
|
|
202
|
+
stop: function () {
|
|
203
|
+
r.set(false);
|
|
204
|
+
list.textContent = '';
|
|
205
|
+
box.hidden = true;
|
|
206
|
+
if (short) { short.textContent = ''; short.removeAttribute('lang'); }
|
|
207
|
+
if (name === null) btn.removeAttribute('aria-label'); else btn.setAttribute('aria-label', name);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
on(btn, 'click', function () { r.set(!r.open()); });
|
|
211
|
+
on(box, 'keydown', function (e) {
|
|
212
|
+
if (esc(e)) { if (r.open()) { e.stopPropagation(); r.set(false, true); } return; }
|
|
213
|
+
var l = links(), n = l.length, i = l.indexOf(doc.activeElement);
|
|
214
|
+
var k = { ArrowDown: i + 1, ArrowUp: (i < 0 ? 0 : i) - 1 + n, Home: 0, End: n - 1 }[e.key];
|
|
215
|
+
if (k === undefined || !n || (i < 0 && (e.target !== btn || /^(Home|End)$/.test(e.key))) || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
r.set(true);
|
|
218
|
+
l[k % n].focus();
|
|
219
|
+
});
|
|
220
|
+
on(box, 'focusout', function (e) { if (e.relatedTarget && !box.contains(e.relatedTarget)) r.set(false); });
|
|
221
|
+
return r;
|
|
222
|
+
});
|
|
223
|
+
records('lang', function (r) { r.fill(); });
|
|
224
|
+
}
|
|
225
|
+
|
|
114
226
|
function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
|
|
115
227
|
function relabel() { records('theme', function (r) { r.el.setAttribute('aria-label', r.labels[+dark()]); }); }
|
|
116
228
|
function theme(ctx) {
|
|
@@ -250,15 +362,34 @@ function tables(ctx) {
|
|
|
250
362
|
});
|
|
251
363
|
}
|
|
252
364
|
|
|
365
|
+
function folds(ctx) {
|
|
366
|
+
each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
|
|
367
|
+
var s = d.querySelector('summary'), own = d.open, tab = s && s.getAttribute('tabindex');
|
|
368
|
+
if (!s) return;
|
|
369
|
+
function tabs(v) { if (v === null) s.removeAttribute('tabindex'); else s.setAttribute('tabindex', v); }
|
|
370
|
+
var r = {
|
|
371
|
+
fit: function () { d.open = mid.matches || own; tabs(mid.matches ? '-1' : tab); },
|
|
372
|
+
stop: function () { d.open = own; tabs(tab); }
|
|
373
|
+
};
|
|
374
|
+
on(s, 'click', function (e) { if (mid.matches) e.preventDefault(); });
|
|
375
|
+
r.fit();
|
|
376
|
+
return r;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
253
380
|
function install() {
|
|
254
381
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
255
382
|
fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
|
|
256
383
|
var stored = null;
|
|
257
384
|
wide = mq('(min-width: 1080px)');
|
|
385
|
+
mid = mq('(min-width: 768px)');
|
|
258
386
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
259
387
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
260
388
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
389
|
+
on(doc, 'keydown', function (e) { if (e.key === 'Tab') records('menu', function (r) { if (r.trap && r.open()) r.trap(e); }); });
|
|
390
|
+
on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
|
|
261
391
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
392
|
+
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
262
393
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
263
394
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
264
395
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -283,7 +414,7 @@ function init(ctx) {
|
|
|
283
414
|
window.gwWebsite = api;
|
|
284
415
|
if (!doc) run([install]);
|
|
285
416
|
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);
|
|
417
|
+
run([mega, mobile, lang, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
287
418
|
}
|
|
288
419
|
|
|
289
420
|
/** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
|