@gemboss/website-ui 0.7.0 → 0.9.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/js/website.mjs CHANGED
@@ -6,10 +6,11 @@
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.7.0';
9
+ var version = '0.9.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;
13
+ var still, lights;
13
14
  var READY = 'data-gw-ready';
14
15
 
15
16
  function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
@@ -101,30 +102,168 @@ function mega(ctx) {
101
102
  }
102
103
 
103
104
  // 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.
105
+ // (same-page anchors do not reload). Taken back while open, it closes first: it must not keep the page locked. Opening
106
+ // and closing fire gw:menu on the menu ({ open, level: '' }); closing also closes a language list inside it.
107
+ //
108
+ // 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">
109
+ // with its <summary> and a .gw-mobile-menu__title) is a level. A press on the summary shows that level alone
110
+ // (.is-drilled on the menu, the group open, the menu scrolled to its top) and puts the focus on the level's title; its
111
+ // .gw-mobile-menu__back shows the top level again (.is-back, for the CSS's slide) and puts the focus back on the summary.
112
+ // Each move fires gw:menu ({ open: true, level }: the title's words, '' for the top level); closing the menu goes back
113
+ // 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
114
+ // focus trap), and the menu starts where the header ends (--gw-menu-top, measured as it opens), so an announcement bar
115
+ // above the header stays where it is. .is-drill on the menu says it is wired: without the script (or before init) the
116
+ // groups are the 0.7.0 <details> that open in place. destroy gives back the groups' own open state.
105
117
  function mobile(ctx) {
106
118
  each(ctx, '[data-gw-menu-toggle]', 'menu', function (btn, on) {
107
119
  var menu = doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
108
120
  if (!menu) return;
109
121
  var label = [attr(btn, 'data-gw-label-open', attr(btn, 'aria-label', 'Open menu')), attr(btn, 'data-gw-label-close', 'Close menu')];
122
+ var groups = attr(menu, 'data-gw-menu', '') !== 'drill' ? [] : all('.gw-mobile-menu__group', menu).filter(function (g) { return g.querySelector('summary') && head(g); });
123
+ var own = groups.map(function (g) { return g.open; }), cur = null;
124
+ function head(g) { return g.querySelector('.gw-mobile-menu__title'); }
110
125
  var r = {
111
126
  open: function () { return !menu.hidden; },
112
127
  set: function (o, focus) {
128
+ if (o && groups.length) menu.style.setProperty('--gw-menu-top', Math.round((btn.closest('[data-gw-header]') || menu.parentNode).getBoundingClientRect().bottom) + 'px');
113
129
  menu.hidden = !o;
114
130
  btn.setAttribute('aria-expanded', String(o));
115
131
  btn.setAttribute('aria-label', label[+o]);
116
132
  root.classList.toggle('is-menu-open', o);
133
+ if (!o) {
134
+ r.go(null);
135
+ records('lang', function (l) { if (menu.contains(l.el)) l.set(false); });
136
+ }
117
137
  if (focus) btn.focus();
138
+ emit(menu, 'gw:menu', { open: o, level: '' });
139
+ },
140
+ // the level in view: a group, or null for the top level
141
+ go: function (g, focus) {
142
+ var from = cur;
143
+ if (g === cur) return;
144
+ cur = g;
145
+ groups.forEach(function (x) { x.open = x === g; });
146
+ menu.classList.toggle('is-drilled', !!g);
147
+ menu.classList.toggle('is-back', !g && r.open());
148
+ menu.scrollTop = 0;
149
+ if (focus) (g ? head(g) : from.querySelector('summary')).focus();
150
+ if (r.open()) emit(menu, 'gw:menu', { open: true, level: g ? words(head(g)) : '' });
118
151
  },
119
- stop: function () { if (r.open()) r.set(false); }
152
+ // Tab on the last stop goes to the toggle, Shift+Tab on the toggle to the last stop; focus found outside comes in
153
+ trap: function (e) {
154
+ var stops = [btn].concat(all('a[href], button, summary, input, select, textarea, [tabindex="0"]', menu).filter(function (el) { return !el.disabled && el.getClientRects().length; }));
155
+ var a = doc.activeElement, last = stops[stops.length - 1];
156
+ if ((a !== btn && !menu.contains(a)) || a === (e.shiftKey ? btn : last)) { e.preventDefault(); (e.shiftKey ? last : btn).focus(); }
157
+ },
158
+ stop: function () {
159
+ if (r.open()) r.set(false);
160
+ if (!groups.length) return;
161
+ menu.classList.remove('is-drill');
162
+ menu.style.removeProperty('--gw-menu-top');
163
+ groups.forEach(function (g, i) { g.open = own[i]; });
164
+ }
120
165
  };
121
166
  on(btn, 'click', function () { r.set(!r.open()); });
122
167
  on(menu, 'click', function (e) { if (link(e)) r.set(false); });
168
+ groups.forEach(function (g) {
169
+ var back = g.querySelector('.gw-mobile-menu__back');
170
+ on(g.querySelector('summary'), 'click', function (e) { e.preventDefault(); r.go(g, true); });
171
+ if (back) on(back, 'click', function () { r.go(null, true); });
172
+ g.open = false;
173
+ });
174
+ if (groups.length) menu.classList.add('is-drill'); else r.trap = null;
123
175
  return r;
124
176
  });
125
177
  }
126
178
  function closeMenus(focus) { records('menu', function (r) { if (r.open()) r.set(false, focus); }); }
127
179
 
180
+ // Language switcher (0.8.0, gap #29): a .gw-lang with data-gw-lang, its button .gw-lang__btn and the list its
181
+ // aria-controls names. The languages are the page's own <link rel="alternate" hreflang href> (not x-default): the links
182
+ // search engines read, so the switcher offers exactly the translations they are told about. The one whose hreflang is
183
+ // <html lang> (else the first with the same base language: zh for zh-Hans) is the current one: the button shows its
184
+ // short code. Each other one is a link in the list (href, hreflang, lang), named in its own language: the <link>'s
185
+ // title, else the kit's name (en, vi, zh), else the browser's (Intl.DisplayNames), else its code. A link to the host of
186
+ // <link rel="canonical"> (else to this host) keeps only its path, so a preview deployment stays on itself. With no other
187
+ // language the switcher keeps `hidden`. Every init reads the links again: a route change brings the new page's.
188
+ // The button opens and closes the list. ArrowDown / ArrowUp on it open the list on its first / last link; in the list
189
+ // they move, wrapping, and Home / End go to the ends. Escape closes it and gives the focus back to the button (and goes
190
+ // no further: an open mobile menu around it stays open); the focus leaving it, or a click outside, closes it.
191
+ // Labels: data-gw-label-language on the button, else its aria-label, else "Language": the button is named
192
+ // "<label>: <the current language>", the list "<label>". destroy empties the list and hides the switcher again.
193
+ var LANGS = { en: ['English', 'EN'], vi: ['Ti\u1ebfng Vi\u1ec7t', 'VI'], zh: ['\u4e2d\u6587', '\u4e2d'], 'zh-hans': ['\u4e2d\u6587', '\u4e2d'] };
194
+ function base(code) { return code.toLowerCase().split(/[-_]/)[0]; }
195
+ function speak(x) {
196
+ var k = LANGS[x.code.toLowerCase()], n = x.title || (k && k[0]);
197
+ try { n = n || new Intl.DisplayNames([x.code], { type: 'language' }).of(x.code); } catch (e) { /* an old browser, a bad code */ }
198
+ n = n || x.code;
199
+ return n.charAt(0).toUpperCase() + n.slice(1);
200
+ }
201
+ function alternates() {
202
+ var here = attr(root, 'lang', '').toLowerCase(), c = doc.querySelector('link[rel="canonical"][href]'), host = location.host, seen = {}, list = [];
203
+ try { if (c) host = new URL(attr(c, 'href', ''), location.href).host; } catch (e) { /* this host */ }
204
+ all('link[rel="alternate"][hreflang][href]').forEach(function (l) {
205
+ var code = attr(l, 'hreflang', ''), k = code.toLowerCase(), u;
206
+ try { u = new URL(attr(l, 'href', ''), location.href); } catch (e) { return; }
207
+ if (k === 'x-default' || seen[k]) return;
208
+ seen[k] = 1;
209
+ list.push({ code: code, title: attr(l, 'title', ''), here: k === here, href: u.host === host ? u.pathname + u.search + u.hash : u.href });
210
+ });
211
+ if (!list.some(function (x) { return x.here; })) list.some(function (x) { return (x.here = base(x.code) === base(here)); });
212
+ return list;
213
+ }
214
+ function lang(ctx) {
215
+ each(ctx, '[data-gw-lang]', 'lang', function (box, on) {
216
+ var btn = box.querySelector('.gw-lang__btn'), list = btn && doc.getElementById(attr(btn, 'aria-controls', ''));
217
+ if (!list) return;
218
+ var short = box.querySelector('.gw-lang__short'), name = attr(btn, 'aria-label', null), word = attr(btn, 'data-gw-label-language', name || 'Language');
219
+ function links() { return all('a[href]', list); }
220
+ var r = {
221
+ open: function () { return !list.hidden; },
222
+ set: function (o, focus) { list.hidden = !o; btn.setAttribute('aria-expanded', String(o)); if (focus) btn.focus(); },
223
+ fill: function () {
224
+ var alts = alternates(), cur = alts.filter(function (x) { return x.here; })[0] || { code: attr(root, 'lang', '') }, k = LANGS[cur.code.toLowerCase()];
225
+ list.textContent = '';
226
+ alts.forEach(function (x) {
227
+ if (x === cur) return;
228
+ var li = doc.createElement('li'), a = doc.createElement('a');
229
+ a.className = 'gw-lang__link';
230
+ a.setAttribute('href', x.href);
231
+ a.setAttribute('hreflang', x.code);
232
+ a.setAttribute('lang', x.code);
233
+ a.textContent = speak(x);
234
+ li.appendChild(a);
235
+ list.appendChild(li);
236
+ });
237
+ box.hidden = !links().length;
238
+ if (box.hidden) r.set(false);
239
+ if (short) { short.textContent = k ? k[1] : base(cur.code).toUpperCase(); short.setAttribute('lang', cur.code); }
240
+ btn.setAttribute('aria-label', word + ': ' + speak(cur));
241
+ list.setAttribute('aria-label', word);
242
+ },
243
+ stop: function () {
244
+ r.set(false);
245
+ list.textContent = '';
246
+ box.hidden = true;
247
+ if (short) { short.textContent = ''; short.removeAttribute('lang'); }
248
+ if (name === null) btn.removeAttribute('aria-label'); else btn.setAttribute('aria-label', name);
249
+ }
250
+ };
251
+ on(btn, 'click', function () { r.set(!r.open()); });
252
+ on(box, 'keydown', function (e) {
253
+ if (esc(e)) { if (r.open()) { e.stopPropagation(); r.set(false, true); } return; }
254
+ var l = links(), n = l.length, i = l.indexOf(doc.activeElement);
255
+ var k = { ArrowDown: i + 1, ArrowUp: (i < 0 ? 0 : i) - 1 + n, Home: 0, End: n - 1 }[e.key];
256
+ if (k === undefined || !n || (i < 0 && (e.target !== btn || /^(Home|End)$/.test(e.key))) || e.altKey || e.ctrlKey || e.metaKey) return;
257
+ e.preventDefault();
258
+ r.set(true);
259
+ l[k % n].focus();
260
+ });
261
+ on(box, 'focusout', function (e) { if (e.relatedTarget && !box.contains(e.relatedTarget)) r.set(false); });
262
+ return r;
263
+ });
264
+ records('lang', function (r) { r.fill(); });
265
+ }
266
+
128
267
  // Theme toggle. Labels: data-gw-label-dark/-light, else the markup's aria-label, else English. A press fires gw:theme
129
268
  // on <html>; so does a change of the system's scheme while the page follows it.
130
269
  function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
@@ -307,6 +446,63 @@ function folds(ctx) {
307
446
  });
308
447
  }
309
448
 
449
+ // Spotlight (0.9.0, issue #74): data-gw-spot on an <a href>, a <button>, or a card that holds exactly one of them. A
450
+ // light in the accent follows the pointer over it (css/effects.css draws it). The element that carries the light, the
451
+ // host or the first one its data-gw-spot="<selector>" names inside it (never a heading), gets .gw-spot and the
452
+ // pointer's place on it, in px from its padding box, as --gw-spot-x and --gw-spot-y. The light trails the pointer: on
453
+ // each frame it goes TRAIL of the way that is left (one requestAnimationFrame loop per element, stopped once it is
454
+ // within SETTLE px), and it starts where the pointer comes in, never sliding in from the middle. A move only notes
455
+ // where the pointer is; the element is measured once a frame, in the loop. The two listeners are there only on a fine
456
+ // pointer and without reduced motion (where the CSS keeps the light still, in the middle), again on every change of
457
+ // either. Nothing is wired without @property and color-mix() (lights), so nothing is drawn. TRAIL and SETTLE are
458
+ // system.json's motion.spot.trail and .settle (the tests hold them equal). No event.
459
+ var TRAIL = 0.18, SETTLE = 0.3, HEADING = /^H[1-6]$/;
460
+ function spot(ctx) {
461
+ if (lights) each(ctx, '[data-gw-spot]', 'spot', function (el) {
462
+ var sel = attr(el, 'data-gw-spot', ''), t = el;
463
+ if (sel) { try { t = el.querySelector(sel); } catch (e) { t = null; } }
464
+ if (!t || HEADING.test(t.tagName) || HEADING.test(el.tagName)) return;
465
+ var had = t.classList.contains('gw-spot'), x = 0, y = 0, px = 0, py = 0, raf = 0, heard = false;
466
+ function put(nx, ny) {
467
+ x = nx; y = ny;
468
+ t.style.setProperty('--gw-spot-x', x.toFixed(1) + 'px');
469
+ t.style.setProperty('--gw-spot-y', y.toFixed(1) + 'px');
470
+ }
471
+ // where the pointer is (a mouse or a pen; never a finger)
472
+ function note(e) { if (e.pointerType === 'touch') return false; px = e.clientX; py = e.clientY; return true; }
473
+ // the pointer's place on the element that carries the light, measured now
474
+ function aim() { var b = t.getBoundingClientRect(); return [px - b.left - (t.clientLeft || 0), py - b.top - (t.clientTop || 0)]; }
475
+ function step() {
476
+ var a = aim(), nx = x + (a[0] - x) * TRAIL, ny = y + (a[1] - y) * TRAIL, done = Math.abs(a[0] - nx) < SETTLE && Math.abs(a[1] - ny) < SETTLE;
477
+ if (done) put(a[0], a[1]); else put(nx, ny);
478
+ raf = done ? 0 : requestAnimationFrame(step);
479
+ }
480
+ function halt() { cancelAnimationFrame(raf); raf = 0; }
481
+ function enter(e) { if (note(e)) { halt(); put.apply(null, aim()); } }
482
+ function move(e) { if (note(e) && !raf) raf = requestAnimationFrame(step); }
483
+ function listen(want) {
484
+ if (want === heard) return;
485
+ heard = want;
486
+ [['pointerenter', enter], ['pointermove', move]].forEach(function (l) {
487
+ if (want) el.addEventListener(l[0], l[1]); else el.removeEventListener(l[0], l[1]);
488
+ });
489
+ if (!want) halt();
490
+ }
491
+ var r = {
492
+ fit: function () { listen(fine.matches && !still.matches); },
493
+ stop: function () {
494
+ listen(false);
495
+ if (!had) t.classList.remove('gw-spot');
496
+ t.style.removeProperty('--gw-spot-x');
497
+ t.style.removeProperty('--gw-spot-y');
498
+ }
499
+ };
500
+ t.classList.add('gw-spot');
501
+ r.fit();
502
+ return r;
503
+ });
504
+ }
505
+
310
506
  // Once per page: what belongs to the document, the window or a media query rather than to one element.
311
507
  function install() {
312
508
  doc = document; root = doc.documentElement; mine = new WeakSet();
@@ -319,8 +515,16 @@ function install() {
319
515
  on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
320
516
  on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
321
517
  on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
518
+ // a drill-down menu that is open keeps the focus (its trap); a click outside a language list closes it
519
+ on(doc, 'keydown', function (e) { if (e.key === 'Tab') records('menu', function (r) { if (r.trap && r.open()) r.trap(e); }); });
520
+ on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
322
521
  watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
323
522
  watch(mid, function () { records('fold', function (r) { r.fit(); }); });
523
+ // the spotlight: a fine pointer, motion welcome, and a browser that can draw the light (@property, color-mix)
524
+ still = mq('(prefers-reduced-motion: reduce)');
525
+ var css = window.CSS;
526
+ lights = !!(css && css.registerProperty && css.supports && css.supports('color', 'color-mix(in srgb, red 50%, blue)'));
527
+ [fine, still].forEach(function (m) { watch(m, function () { records('spot', function (r) { r.fit(); }); }); });
324
528
  on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
325
529
  try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
326
530
  if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
@@ -351,7 +555,7 @@ function init(ctx) {
351
555
  if (!doc) run([install]);
352
556
  // an element removed from the page since the last init is let go
353
557
  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);
558
+ run([mega, mobile, lang, theme, reveal, header, current, segmented, toc, tables, folds, spot], ctx && ctx.querySelectorAll ? ctx : doc);
355
559
  }
356
560
 
357
561
  /** 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.7.0",
3
+ "version": "0.9.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,12 @@ 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.
35
+ - Spotlight (`data-gw-spot`): only on a link, a button or a card with exactly one action (a plan, a link card, the
36
+ mega menu's feature card, a night band's primary button). Never on a heading or text; the kit draws it.
31
37
  - Changed page: keep its kit classes; change words, order and choice of sections.
32
38
  - Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
33
39
  scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
package/system.json CHANGED
@@ -154,6 +154,18 @@
154
154
  "ease": "cubic-bezier(.22,1,.36,1)",
155
155
  "reveal_ms": 700,
156
156
  "spin_ms": 700,
157
- "$comment": "Animate transform and opacity only. Everything is still under prefers-reduced-motion (a busy button's ring turns once every spin_ms, as the dashboard's spinner, and stands still as an ellipsis)."
157
+ "$comment": "Animate transform and opacity only. Everything is still under prefers-reduced-motion (a busy button's ring turns once every spin_ms, as the dashboard's spinner, and stands still as an ellipsis). One exception, the spotlight (spot): its light is one painted layer of one element, so its radius may grow.",
158
+ "spot": {
159
+ "$comment": "The spotlight (0.9.0, Chris 25/10/2026, issue #74): data-gw-spot on a link, a button or a card with one action. A light in the accent follows a fine pointer: each frame it goes trail of the way that is left, and stops within settle px. Its radius grows to radius in ms with ease; its middle is strength percent of the accent over the element's own fill. A .gw-btn takes radius_action and strength_action (a small, filled thing), the night ground of the mega menu's feature card (the Stage) strength_stage. Under reduced motion it stands still, centred, and does not grow.",
160
+ "radius": 260,
161
+ "radius_action": 120,
162
+ "strength": 22,
163
+ "strength_action": 38,
164
+ "strength_stage": 40,
165
+ "ms": 550,
166
+ "ease": "cubic-bezier(.16,1,.3,1)",
167
+ "trail": 0.18,
168
+ "settle": 0.3
169
+ }
158
170
  }
159
171
  }
@@ -3,9 +3,20 @@
3
3
  A flat 64px bar with one hairline. A brand that chose the floating header adds one class to the <header>:
4
4
  class="gw-header gw-header--float" (an inset card from 1080px, docs/theming.md); nothing else changes.
5
5
  Below 1080px the nav and Sign in hide and the menu button opens #gw-mobile-menu,
6
- which repeats the same links. website.js runs the mega menus, the mobile menu and the theme toggle.
6
+ which repeats the same links. website.js runs the mega menus, the mobile menu, the language switcher and the theme
7
+ toggle.
7
8
  Limits: at most five top-level items, one .gw-btn (the primary action), one .gw-btn--secondary. Each mega panel:
8
9
  two or three columns of four or five links, at most one feature card. The announcement bar is optional: one sentence.
10
+ Two options (0.8.0):
11
+ - The mobile menu drills down (data-gw-menu="drill" on #gw-mobile-menu, as gemboss.ai's phone menu): the top level
12
+ lists the sections, a tap on one shows its level alone with Back and its title, and the actions stay in a bar at
13
+ the bottom of the sheet. Each group then carries its head (the Back button and the title, in the page's language).
14
+ Without the attribute and the heads it is the 0.7.0 menu, whose groups open in place; without the script the drill
15
+ menu is that one too.
16
+ - The language switcher (.gw-lang, twice: in the actions from 1080px, in the mobile menu's action bar below). It
17
+ lists the page's translations from its <link rel="alternate" hreflang="…" href="…"> in <head> and stays hidden
18
+ while the page has none: keep `hidden` on it, the script takes it away. Delete both copies only if the site will
19
+ never be translated. Put the word for "Language" in the page's language in data-gw-label-language.
9
20
  Replace every [bracketed] placeholder; delete what you do not use.
10
21
  -->
11
22
  <div class="gw-announce" role="region" aria-label="Announcement">[One sentence of news.] <a href="[link]">[Where the link goes]</a></div>
@@ -36,25 +47,42 @@
36
47
  </nav>
37
48
  <div class="gw-header__actions">
38
49
  <button class="gw-theme-toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16z" fill="currentColor"/></svg></button>
50
+ <!-- optional (0.8.0): the language switcher, shown by the script when <head> lists another language -->
51
+ <div class="gw-lang" data-gw-lang hidden>
52
+ <button class="gw-lang__btn" type="button" aria-expanded="false" aria-controls="gw-lang-menu" data-gw-label-language="Language"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="7"/><path d="M3 10h14M10 3c2 2 2.8 4.3 2.8 7s-.8 5-2.8 7c-2-2-2.8-4.3-2.8-7s.8-5 2.8-7z"/></svg><span class="gw-lang__short"></span></button>
53
+ <ul class="gw-lang__menu" id="gw-lang-menu" hidden></ul>
54
+ </div>
39
55
  <a class="gw-nav__link gw-header__signin" href="[sign-in URL]">Sign in</a>
40
56
  <a class="gw-btn gw-btn--secondary gw-btn--sm" href="[/contact]">[Book a demo]</a>
41
57
  <a class="gw-btn gw-btn--sm" href="[sign-up URL]">[Start free]</a>
42
58
  <button class="gw-menu-toggle" type="button" aria-expanded="false" aria-controls="gw-mobile-menu" data-gw-menu-toggle aria-label="Open menu"><svg class="gw-menu-toggle__open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg><svg class="gw-menu-toggle__close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
43
59
  </div>
44
60
  </div>
45
- <div class="gw-mobile-menu" id="gw-mobile-menu" hidden>
61
+ <!-- data-gw-menu="drill" (0.8.0): leave it out, and the .gw-mobile-menu__head of each group, for the 0.7.0 menu -->
62
+ <div class="gw-mobile-menu" id="gw-mobile-menu" data-gw-menu="drill" hidden>
46
63
  <nav class="gw-wrap" aria-label="Main, mobile">
47
64
  <details class="gw-mobile-menu__group">
48
65
  <summary>[Product menu item]</summary>
66
+ <!-- the level's head: Back and the title (the same words as the summary), focused when the level opens -->
67
+ <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 menu item]</h2></div>
68
+ <!-- optional: a heading over a run of links, as in the mega panel's columns -->
69
+ <p class="gw-mobile-menu__heading">[Group]</p>
49
70
  <ul class="gw-mobile-menu__list">
50
- <li><a class="gw-mobile-menu__link" href="[/page]">[Page title]</a></li>
51
- <li><a class="gw-mobile-menu__link" href="[/page]">[Page title]</a></li>
71
+ <!-- a link with an icon and one line (both optional: <a class="gw-mobile-menu__link" href="…">[Page title]</a> is enough) -->
72
+ <li><a class="gw-mobile-menu__link" href="[/page]"><span class="gw-mobile-menu__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/></svg></span><span><b>[Page title]</b><i>[One line on what it does]</i></span></a></li>
73
+ <li><a class="gw-mobile-menu__link" href="[/page]"><span class="gw-mobile-menu__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 15l4-4 3 3 5-6"/></svg></span><span><b>[Page title]</b><i>[One line on what it does]</i></span></a></li>
52
74
  </ul>
53
75
  </details>
54
76
  <a class="gw-mobile-menu__top" href="[/pricing]">[Pricing]</a>
77
+ <!-- the action bar: what is not a button on the first line, two things at most (the language on the left, Sign in
78
+ on the right), the two buttons under it. Stays at the bottom of the sheet in the drill menu. -->
55
79
  <div class="gw-mobile-menu__actions">
56
- <a class="gw-btn gw-btn--block" href="[sign-up URL]">[Start free]</a>
57
- <a class="gw-btn gw-btn--secondary gw-btn--block" href="[/contact]">[Book a demo]</a>
80
+ <div class="gw-lang" data-gw-lang hidden>
81
+ <button class="gw-lang__btn" type="button" aria-expanded="false" aria-controls="gw-lang-menu-mobile" data-gw-label-language="Language"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="7"/><path d="M3 10h14M10 3c2 2 2.8 4.3 2.8 7s-.8 5-2.8 7c-2-2-2.8-4.3-2.8-7s.8-5 2.8-7z"/></svg><span class="gw-lang__short"></span></button>
82
+ <ul class="gw-lang__menu" id="gw-lang-menu-mobile" hidden></ul>
83
+ </div>
84
+ <a class="gw-btn gw-btn--lg gw-btn--block" href="[sign-up URL]">[Start free]</a>
85
+ <a class="gw-btn gw-btn--secondary gw-btn--lg gw-btn--block" href="[/contact]">[Book a demo]</a>
58
86
  <a class="gw-mobile-menu__link" href="[sign-in URL]">Sign in</a>
59
87
  </div>
60
88
  </nav>