@gemboss/website-ui 0.7.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 +13 -3
- package/CHANGELOG.md +69 -0
- package/README.md +8 -8
- package/bin/lib/check.mjs +53 -1
- package/css/header.css +176 -0
- package/demo/header.html +356 -0
- package/demo/index.html +1 -0
- package/docs/components.md +69 -4
- package/docs/markup-contract.md +102 -5
- package/docs/theming.md +3 -2
- package/js/website.js +118 -4
- package/js/website.mjs +145 -4
- package/package.json +1 -1
- package/skill/SKILL.md +4 -0
- package/templates/header.html +34 -6
- package/website.css +177 -1
- package/website.min.css +1 -1
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), and the footer's folds (0.7.0). It comes in two entries bu
|
|
|
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`, `fold`). 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,99 @@ 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`).
|
|
162
259
|
|
|
163
260
|
## Footer (0.7.0)
|
|
164
261
|
|
package/docs/theming.md
CHANGED
|
@@ -15,8 +15,9 @@ widths. That is what makes the sites look like one family, and why a site built
|
|
|
15
15
|
still looks careful. `npx gemboss-website check` reports anything outside this set.
|
|
16
16
|
|
|
17
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)
|
|
19
|
-
footer for 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).
|
|
20
21
|
|
|
21
22
|
## 1 to 3. Accent, wash, logo
|
|
22
23
|
|
package/js/website.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
8
|
var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
9
9
|
var READY = 'data-gw-ready';
|
|
@@ -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) {
|
|
@@ -274,6 +386,8 @@ function install() {
|
|
|
274
386
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
275
387
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
276
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); }); });
|
|
277
391
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
278
392
|
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
279
393
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
@@ -300,7 +414,7 @@ function init(ctx) {
|
|
|
300
414
|
window.gwWebsite = api;
|
|
301
415
|
if (!doc) run([install]);
|
|
302
416
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
303
|
-
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
417
|
+
run([mega, mobile, lang, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
304
418
|
}
|
|
305
419
|
|
|
306
420
|
/** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
|
package/js/website.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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.8.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
12
|
var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
@@ -101,30 +101,168 @@ function mega(ctx) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Mobile menu. Labels: data-gw-label-open/-close, else the markup's aria-label, else English. A link inside closes it
|
|
104
|
-
// (same-page anchors do not reload). Taken back while open, it closes first: it must not keep the page locked.
|
|
104
|
+
// (same-page anchors do not reload). Taken back while open, it closes first: it must not keep the page locked. Opening
|
|
105
|
+
// and closing fire gw:menu on the menu ({ open, level: '' }); closing also closes a language list inside it.
|
|
106
|
+
//
|
|
107
|
+
// Drill-down (0.8.0, gap #41): a menu with data-gw-menu="drill". Each group in it (a <details class="gw-mobile-menu__group">
|
|
108
|
+
// with its <summary> and a .gw-mobile-menu__title) is a level. A press on the summary shows that level alone
|
|
109
|
+
// (.is-drilled on the menu, the group open, the menu scrolled to its top) and puts the focus on the level's title; its
|
|
110
|
+
// .gw-mobile-menu__back shows the top level again (.is-back, for the CSS's slide) and puts the focus back on the summary.
|
|
111
|
+
// Each move fires gw:menu ({ open: true, level }: the title's words, '' for the top level); closing the menu goes back
|
|
112
|
+
// to the top level without a word of its own. While it is open, Tab and Shift+Tab go round the toggle and the menu (the
|
|
113
|
+
// focus trap), and the menu starts where the header ends (--gw-menu-top, measured as it opens), so an announcement bar
|
|
114
|
+
// above the header stays where it is. .is-drill on the menu says it is wired: without the script (or before init) the
|
|
115
|
+
// groups are the 0.7.0 <details> that open in place. destroy gives back the groups' own open state.
|
|
105
116
|
function mobile(ctx) {
|
|
106
117
|
each(ctx, '[data-gw-menu-toggle]', 'menu', function (btn, on) {
|
|
107
118
|
var menu = doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
|
|
108
119
|
if (!menu) return;
|
|
109
120
|
var label = [attr(btn, 'data-gw-label-open', attr(btn, 'aria-label', 'Open menu')), attr(btn, 'data-gw-label-close', 'Close menu')];
|
|
121
|
+
var groups = attr(menu, 'data-gw-menu', '') !== 'drill' ? [] : all('.gw-mobile-menu__group', menu).filter(function (g) { return g.querySelector('summary') && head(g); });
|
|
122
|
+
var own = groups.map(function (g) { return g.open; }), cur = null;
|
|
123
|
+
function head(g) { return g.querySelector('.gw-mobile-menu__title'); }
|
|
110
124
|
var r = {
|
|
111
125
|
open: function () { return !menu.hidden; },
|
|
112
126
|
set: function (o, focus) {
|
|
127
|
+
if (o && groups.length) menu.style.setProperty('--gw-menu-top', Math.round((btn.closest('[data-gw-header]') || menu.parentNode).getBoundingClientRect().bottom) + 'px');
|
|
113
128
|
menu.hidden = !o;
|
|
114
129
|
btn.setAttribute('aria-expanded', String(o));
|
|
115
130
|
btn.setAttribute('aria-label', label[+o]);
|
|
116
131
|
root.classList.toggle('is-menu-open', o);
|
|
132
|
+
if (!o) {
|
|
133
|
+
r.go(null);
|
|
134
|
+
records('lang', function (l) { if (menu.contains(l.el)) l.set(false); });
|
|
135
|
+
}
|
|
117
136
|
if (focus) btn.focus();
|
|
137
|
+
emit(menu, 'gw:menu', { open: o, level: '' });
|
|
138
|
+
},
|
|
139
|
+
// the level in view: a group, or null for the top level
|
|
140
|
+
go: function (g, focus) {
|
|
141
|
+
var from = cur;
|
|
142
|
+
if (g === cur) return;
|
|
143
|
+
cur = g;
|
|
144
|
+
groups.forEach(function (x) { x.open = x === g; });
|
|
145
|
+
menu.classList.toggle('is-drilled', !!g);
|
|
146
|
+
menu.classList.toggle('is-back', !g && r.open());
|
|
147
|
+
menu.scrollTop = 0;
|
|
148
|
+
if (focus) (g ? head(g) : from.querySelector('summary')).focus();
|
|
149
|
+
if (r.open()) emit(menu, 'gw:menu', { open: true, level: g ? words(head(g)) : '' });
|
|
118
150
|
},
|
|
119
|
-
stop
|
|
151
|
+
// Tab on the last stop goes to the toggle, Shift+Tab on the toggle to the last stop; focus found outside comes in
|
|
152
|
+
trap: function (e) {
|
|
153
|
+
var stops = [btn].concat(all('a[href], button, summary, input, select, textarea, [tabindex="0"]', menu).filter(function (el) { return !el.disabled && el.getClientRects().length; }));
|
|
154
|
+
var a = doc.activeElement, last = stops[stops.length - 1];
|
|
155
|
+
if ((a !== btn && !menu.contains(a)) || a === (e.shiftKey ? btn : last)) { e.preventDefault(); (e.shiftKey ? last : btn).focus(); }
|
|
156
|
+
},
|
|
157
|
+
stop: function () {
|
|
158
|
+
if (r.open()) r.set(false);
|
|
159
|
+
if (!groups.length) return;
|
|
160
|
+
menu.classList.remove('is-drill');
|
|
161
|
+
menu.style.removeProperty('--gw-menu-top');
|
|
162
|
+
groups.forEach(function (g, i) { g.open = own[i]; });
|
|
163
|
+
}
|
|
120
164
|
};
|
|
121
165
|
on(btn, 'click', function () { r.set(!r.open()); });
|
|
122
166
|
on(menu, 'click', function (e) { if (link(e)) r.set(false); });
|
|
167
|
+
groups.forEach(function (g) {
|
|
168
|
+
var back = g.querySelector('.gw-mobile-menu__back');
|
|
169
|
+
on(g.querySelector('summary'), 'click', function (e) { e.preventDefault(); r.go(g, true); });
|
|
170
|
+
if (back) on(back, 'click', function () { r.go(null, true); });
|
|
171
|
+
g.open = false;
|
|
172
|
+
});
|
|
173
|
+
if (groups.length) menu.classList.add('is-drill'); else r.trap = null;
|
|
123
174
|
return r;
|
|
124
175
|
});
|
|
125
176
|
}
|
|
126
177
|
function closeMenus(focus) { records('menu', function (r) { if (r.open()) r.set(false, focus); }); }
|
|
127
178
|
|
|
179
|
+
// Language switcher (0.8.0, gap #29): a .gw-lang with data-gw-lang, its button .gw-lang__btn and the list its
|
|
180
|
+
// aria-controls names. The languages are the page's own <link rel="alternate" hreflang href> (not x-default): the links
|
|
181
|
+
// search engines read, so the switcher offers exactly the translations they are told about. The one whose hreflang is
|
|
182
|
+
// <html lang> (else the first with the same base language: zh for zh-Hans) is the current one: the button shows its
|
|
183
|
+
// short code. Each other one is a link in the list (href, hreflang, lang), named in its own language: the <link>'s
|
|
184
|
+
// title, else the kit's name (en, vi, zh), else the browser's (Intl.DisplayNames), else its code. A link to the host of
|
|
185
|
+
// <link rel="canonical"> (else to this host) keeps only its path, so a preview deployment stays on itself. With no other
|
|
186
|
+
// language the switcher keeps `hidden`. Every init reads the links again: a route change brings the new page's.
|
|
187
|
+
// The button opens and closes the list. ArrowDown / ArrowUp on it open the list on its first / last link; in the list
|
|
188
|
+
// they move, wrapping, and Home / End go to the ends. Escape closes it and gives the focus back to the button (and goes
|
|
189
|
+
// no further: an open mobile menu around it stays open); the focus leaving it, or a click outside, closes it.
|
|
190
|
+
// Labels: data-gw-label-language on the button, else its aria-label, else "Language": the button is named
|
|
191
|
+
// "<label>: <the current language>", the list "<label>". destroy empties the list and hides the switcher again.
|
|
192
|
+
var LANGS = { en: ['English', 'EN'], vi: ['Ti\u1ebfng Vi\u1ec7t', 'VI'], zh: ['\u4e2d\u6587', '\u4e2d'], 'zh-hans': ['\u4e2d\u6587', '\u4e2d'] };
|
|
193
|
+
function base(code) { return code.toLowerCase().split(/[-_]/)[0]; }
|
|
194
|
+
function speak(x) {
|
|
195
|
+
var k = LANGS[x.code.toLowerCase()], n = x.title || (k && k[0]);
|
|
196
|
+
try { n = n || new Intl.DisplayNames([x.code], { type: 'language' }).of(x.code); } catch (e) { /* an old browser, a bad code */ }
|
|
197
|
+
n = n || x.code;
|
|
198
|
+
return n.charAt(0).toUpperCase() + n.slice(1);
|
|
199
|
+
}
|
|
200
|
+
function alternates() {
|
|
201
|
+
var here = attr(root, 'lang', '').toLowerCase(), c = doc.querySelector('link[rel="canonical"][href]'), host = location.host, seen = {}, list = [];
|
|
202
|
+
try { if (c) host = new URL(attr(c, 'href', ''), location.href).host; } catch (e) { /* this host */ }
|
|
203
|
+
all('link[rel="alternate"][hreflang][href]').forEach(function (l) {
|
|
204
|
+
var code = attr(l, 'hreflang', ''), k = code.toLowerCase(), u;
|
|
205
|
+
try { u = new URL(attr(l, 'href', ''), location.href); } catch (e) { return; }
|
|
206
|
+
if (k === 'x-default' || seen[k]) return;
|
|
207
|
+
seen[k] = 1;
|
|
208
|
+
list.push({ code: code, title: attr(l, 'title', ''), here: k === here, href: u.host === host ? u.pathname + u.search + u.hash : u.href });
|
|
209
|
+
});
|
|
210
|
+
if (!list.some(function (x) { return x.here; })) list.some(function (x) { return (x.here = base(x.code) === base(here)); });
|
|
211
|
+
return list;
|
|
212
|
+
}
|
|
213
|
+
function lang(ctx) {
|
|
214
|
+
each(ctx, '[data-gw-lang]', 'lang', function (box, on) {
|
|
215
|
+
var btn = box.querySelector('.gw-lang__btn'), list = btn && doc.getElementById(attr(btn, 'aria-controls', ''));
|
|
216
|
+
if (!list) return;
|
|
217
|
+
var short = box.querySelector('.gw-lang__short'), name = attr(btn, 'aria-label', null), word = attr(btn, 'data-gw-label-language', name || 'Language');
|
|
218
|
+
function links() { return all('a[href]', list); }
|
|
219
|
+
var r = {
|
|
220
|
+
open: function () { return !list.hidden; },
|
|
221
|
+
set: function (o, focus) { list.hidden = !o; btn.setAttribute('aria-expanded', String(o)); if (focus) btn.focus(); },
|
|
222
|
+
fill: function () {
|
|
223
|
+
var alts = alternates(), cur = alts.filter(function (x) { return x.here; })[0] || { code: attr(root, 'lang', '') }, k = LANGS[cur.code.toLowerCase()];
|
|
224
|
+
list.textContent = '';
|
|
225
|
+
alts.forEach(function (x) {
|
|
226
|
+
if (x === cur) return;
|
|
227
|
+
var li = doc.createElement('li'), a = doc.createElement('a');
|
|
228
|
+
a.className = 'gw-lang__link';
|
|
229
|
+
a.setAttribute('href', x.href);
|
|
230
|
+
a.setAttribute('hreflang', x.code);
|
|
231
|
+
a.setAttribute('lang', x.code);
|
|
232
|
+
a.textContent = speak(x);
|
|
233
|
+
li.appendChild(a);
|
|
234
|
+
list.appendChild(li);
|
|
235
|
+
});
|
|
236
|
+
box.hidden = !links().length;
|
|
237
|
+
if (box.hidden) r.set(false);
|
|
238
|
+
if (short) { short.textContent = k ? k[1] : base(cur.code).toUpperCase(); short.setAttribute('lang', cur.code); }
|
|
239
|
+
btn.setAttribute('aria-label', word + ': ' + speak(cur));
|
|
240
|
+
list.setAttribute('aria-label', word);
|
|
241
|
+
},
|
|
242
|
+
stop: function () {
|
|
243
|
+
r.set(false);
|
|
244
|
+
list.textContent = '';
|
|
245
|
+
box.hidden = true;
|
|
246
|
+
if (short) { short.textContent = ''; short.removeAttribute('lang'); }
|
|
247
|
+
if (name === null) btn.removeAttribute('aria-label'); else btn.setAttribute('aria-label', name);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
on(btn, 'click', function () { r.set(!r.open()); });
|
|
251
|
+
on(box, 'keydown', function (e) {
|
|
252
|
+
if (esc(e)) { if (r.open()) { e.stopPropagation(); r.set(false, true); } return; }
|
|
253
|
+
var l = links(), n = l.length, i = l.indexOf(doc.activeElement);
|
|
254
|
+
var k = { ArrowDown: i + 1, ArrowUp: (i < 0 ? 0 : i) - 1 + n, Home: 0, End: n - 1 }[e.key];
|
|
255
|
+
if (k === undefined || !n || (i < 0 && (e.target !== btn || /^(Home|End)$/.test(e.key))) || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
r.set(true);
|
|
258
|
+
l[k % n].focus();
|
|
259
|
+
});
|
|
260
|
+
on(box, 'focusout', function (e) { if (e.relatedTarget && !box.contains(e.relatedTarget)) r.set(false); });
|
|
261
|
+
return r;
|
|
262
|
+
});
|
|
263
|
+
records('lang', function (r) { r.fill(); });
|
|
264
|
+
}
|
|
265
|
+
|
|
128
266
|
// Theme toggle. Labels: data-gw-label-dark/-light, else the markup's aria-label, else English. A press fires gw:theme
|
|
129
267
|
// on <html>; so does a change of the system's scheme while the page follows it.
|
|
130
268
|
function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
|
|
@@ -319,6 +457,9 @@ function install() {
|
|
|
319
457
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
320
458
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
321
459
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
460
|
+
// a drill-down menu that is open keeps the focus (its trap); a click outside a language list closes it
|
|
461
|
+
on(doc, 'keydown', function (e) { if (e.key === 'Tab') records('menu', function (r) { if (r.trap && r.open()) r.trap(e); }); });
|
|
462
|
+
on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
|
|
322
463
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
323
464
|
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
324
465
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
@@ -351,7 +492,7 @@ function init(ctx) {
|
|
|
351
492
|
if (!doc) run([install]);
|
|
352
493
|
// an element removed from the page since the last init is let go
|
|
353
494
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
354
|
-
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
495
|
+
run([mega, mobile, lang, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
355
496
|
}
|
|
356
497
|
|
|
357
498
|
/** 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.8.0",
|
|
4
4
|
"description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/skill/SKILL.md
CHANGED
|
@@ -28,6 +28,10 @@ the kit and its rules are how the page ends up looking designed. Compose from th
|
|
|
28
28
|
- The footer is `KIT/templates/footer.html` (the footer of gemboss.ai), filled once in the site's shared partial:
|
|
29
29
|
fill its rows (columns, family rows, words), delete the optional parts you do not use (Ask AI, newsletter, family
|
|
30
30
|
rows, theme toggle, wordmark), never invent a footer part. Columns fold on phones by themselves (`<details>`).
|
|
31
|
+
- The header is `KIT/templates/header.html`, filled once in the site's shared partial. A site with deep sections
|
|
32
|
+
uses its drill-down mobile menu (`data-gw-menu="drill"`, a head with Back and the title in each group); a site in
|
|
33
|
+
more than one language keeps its language switcher (`.gw-lang`, `hidden`, empty) and gives every page its
|
|
34
|
+
`<link rel="alternate" hreflang>`: the switcher lists those, and stays hidden on a page in one language.
|
|
31
35
|
- Changed page: keep its kit classes; change words, order and choice of sections.
|
|
32
36
|
- Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
|
|
33
37
|
scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
|