@gemboss/website-ui 0.8.0 → 0.10.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 +8 -3
- package/CHANGELOG.md +110 -0
- package/README.md +5 -5
- package/bin/lib/check.mjs +19 -0
- package/css/effects.css +71 -0
- package/css/footer.css +74 -58
- package/css/header.css +12 -0
- package/css/tokens.css +9 -2
- package/demo/footer.html +1 -1
- package/demo/index.html +1 -0
- package/demo/spotlight.html +291 -0
- package/docs/components.md +62 -10
- package/docs/markup-contract.md +45 -7
- package/docs/theming.md +6 -0
- package/js/website.js +54 -4
- package/js/website.mjs +70 -7
- package/package.json +1 -1
- package/skill/SKILL.md +2 -0
- package/system.json +16 -4
- package/templates/footer.html +11 -9
- package/website.css +168 -61
- package/website.min.css +1 -1
package/js/website.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.10.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.10.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
|
+
var still, lights;
|
|
9
10
|
var READY = 'data-gw-ready';
|
|
10
11
|
|
|
11
12
|
function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
|
|
@@ -377,12 +378,57 @@ function folds(ctx) {
|
|
|
377
378
|
});
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
var TRAIL = 0.18, SETTLE = 0.3, HEADING = /^H[1-6]$/;
|
|
382
|
+
function spot(ctx) {
|
|
383
|
+
if (lights) each(ctx, '[data-gw-spot]', 'spot', function (el) {
|
|
384
|
+
var sel = attr(el, 'data-gw-spot', ''), t = el;
|
|
385
|
+
if (sel) { try { t = el.querySelector(sel); } catch (e) { t = null; } }
|
|
386
|
+
if (!t || HEADING.test(t.tagName) || HEADING.test(el.tagName)) return;
|
|
387
|
+
var had = t.classList.contains('gw-spot'), x = 0, y = 0, px = 0, py = 0, raf = 0, heard = false;
|
|
388
|
+
function put(nx, ny) {
|
|
389
|
+
x = nx; y = ny;
|
|
390
|
+
t.style.setProperty('--gw-spot-x', x.toFixed(1) + 'px');
|
|
391
|
+
t.style.setProperty('--gw-spot-y', y.toFixed(1) + 'px');
|
|
392
|
+
}
|
|
393
|
+
function note(e) { if (e.pointerType === 'touch') return false; px = e.clientX; py = e.clientY; return true; }
|
|
394
|
+
function aim() { var b = t.getBoundingClientRect(); return [px - b.left - (t.clientLeft || 0), py - b.top - (t.clientTop || 0)]; }
|
|
395
|
+
function step() {
|
|
396
|
+
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;
|
|
397
|
+
if (done) put(a[0], a[1]); else put(nx, ny);
|
|
398
|
+
raf = done ? 0 : requestAnimationFrame(step);
|
|
399
|
+
}
|
|
400
|
+
function halt() { cancelAnimationFrame(raf); raf = 0; }
|
|
401
|
+
function enter(e) { if (note(e)) { halt(); put.apply(null, aim()); } }
|
|
402
|
+
function move(e) { if (note(e) && !raf) raf = requestAnimationFrame(step); }
|
|
403
|
+
function listen(want) {
|
|
404
|
+
if (want === heard) return;
|
|
405
|
+
heard = want;
|
|
406
|
+
[['pointerenter', enter], ['pointermove', move]].forEach(function (l) {
|
|
407
|
+
if (want) el.addEventListener(l[0], l[1]); else el.removeEventListener(l[0], l[1]);
|
|
408
|
+
});
|
|
409
|
+
if (!want) halt();
|
|
410
|
+
}
|
|
411
|
+
var r = {
|
|
412
|
+
fit: function () { listen(fine.matches && !still.matches); },
|
|
413
|
+
stop: function () {
|
|
414
|
+
listen(false);
|
|
415
|
+
if (!had) t.classList.remove('gw-spot');
|
|
416
|
+
t.style.removeProperty('--gw-spot-x');
|
|
417
|
+
t.style.removeProperty('--gw-spot-y');
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
t.classList.add('gw-spot');
|
|
421
|
+
r.fit();
|
|
422
|
+
return r;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
380
426
|
function install() {
|
|
381
427
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
382
428
|
fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
|
|
383
429
|
var stored = null;
|
|
384
430
|
wide = mq('(min-width: 1080px)');
|
|
385
|
-
mid = mq('(min-width:
|
|
431
|
+
mid = mq('(min-width: 621px)');
|
|
386
432
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
387
433
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
388
434
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
@@ -390,6 +436,10 @@ function install() {
|
|
|
390
436
|
on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
|
|
391
437
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
392
438
|
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
439
|
+
still = mq('(prefers-reduced-motion: reduce)');
|
|
440
|
+
var css = window.CSS;
|
|
441
|
+
lights = !!(css && css.registerProperty && css.supports && css.supports('color', 'color-mix(in srgb, red 50%, blue)'));
|
|
442
|
+
[fine, still].forEach(function (m) { watch(m, function () { records('spot', function (r) { r.fit(); }); }); });
|
|
393
443
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
394
444
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
395
445
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -414,7 +464,7 @@ function init(ctx) {
|
|
|
414
464
|
window.gwWebsite = api;
|
|
415
465
|
if (!doc) run([install]);
|
|
416
466
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
417
|
-
run([mega, mobile, lang, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
467
|
+
run([mega, mobile, lang, theme, reveal, header, current, segmented, toc, tables, folds, spot], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
418
468
|
}
|
|
419
469
|
|
|
420
470
|
/** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
|
package/js/website.mjs
CHANGED
|
@@ -6,10 +6,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.
|
|
9
|
+
var version = '0.10.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 }; }
|
|
@@ -424,11 +425,11 @@ function tables(ctx) {
|
|
|
424
425
|
});
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
// Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row).
|
|
428
|
-
// the markup's own state (folded unless the markup says open) and the browser opens and closes it. From
|
|
428
|
+
// Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row). Up to 620px it keeps
|
|
429
|
+
// the markup's own state (folded unless the markup says open) and the browser opens and closes it. From 621px it is a
|
|
429
430
|
// plain column: the script sets `open`, takes the <summary> out of the Tab order (tabindex="-1") and ignores a click on
|
|
430
431
|
// it, so the heading cannot fold the list away. Again on every change of width; destroy gives back the markup's state.
|
|
431
|
-
// The CSS shows the list from
|
|
432
|
+
// The CSS shows the list from 621px without the script too (::details-content), so this is for state and for browsers
|
|
432
433
|
// without that.
|
|
433
434
|
function folds(ctx) {
|
|
434
435
|
each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
|
|
@@ -445,6 +446,63 @@ function folds(ctx) {
|
|
|
445
446
|
});
|
|
446
447
|
}
|
|
447
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
|
+
|
|
448
506
|
// Once per page: what belongs to the document, the window or a media query rather than to one element.
|
|
449
507
|
function install() {
|
|
450
508
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
@@ -452,8 +510,8 @@ function install() {
|
|
|
452
510
|
var stored = null;
|
|
453
511
|
// 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
|
|
454
512
|
wide = mq('(min-width: 1080px)');
|
|
455
|
-
//
|
|
456
|
-
mid = mq('(min-width:
|
|
513
|
+
// 621px: the footer's columns stand open from here, and fold up to 620px, as on gemboss.ai (1:1, 26/10/2026)
|
|
514
|
+
mid = mq('(min-width: 621px)');
|
|
457
515
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
458
516
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
459
517
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
@@ -462,6 +520,11 @@ function install() {
|
|
|
462
520
|
on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
|
|
463
521
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
464
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(); }); }); });
|
|
465
528
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
466
529
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
467
530
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -492,7 +555,7 @@ function init(ctx) {
|
|
|
492
555
|
if (!doc) run([install]);
|
|
493
556
|
// an element removed from the page since the last init is let go
|
|
494
557
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
495
|
-
run([mega, mobile, lang, 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);
|
|
496
559
|
}
|
|
497
560
|
|
|
498
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.
|
|
3
|
+
"version": "0.10.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
|
@@ -32,6 +32,8 @@ the kit and its rules are how the page ends up looking designed. Compose from th
|
|
|
32
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
33
|
more than one language keeps its language switcher (`.gw-lang`, `hidden`, empty) and gives every page its
|
|
34
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.
|
|
35
37
|
- Changed page: keep its kit classes; change words, order and choice of sections.
|
|
36
38
|
- Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
|
|
37
39
|
scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
|
package/system.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"surface": "#ffffff",
|
|
9
9
|
"ink": "#1b1c1e",
|
|
10
10
|
"ink-2": "#3b3d42",
|
|
11
|
-
"muted": "#
|
|
11
|
+
"muted": "#6a6d73",
|
|
12
12
|
"line": "#e5e7ea",
|
|
13
13
|
"line-strong": "#d5d8dd",
|
|
14
14
|
"line-control": "#85888e",
|
|
@@ -89,14 +89,14 @@
|
|
|
89
89
|
"type": {
|
|
90
90
|
"family": "Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
|
|
91
91
|
"mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
92
|
-
"weights": [400, 500, 600, 650, 700],
|
|
92
|
+
"weights": [400, 500, 600, 650, 700, 800],
|
|
93
93
|
"scale": [11, 12, 13, 14, 16, 18, 20, 22, 24, 30],
|
|
94
94
|
"display_above": 30,
|
|
95
95
|
"display": {
|
|
96
96
|
"h1": "clamp(40px, 4.6vw, 68px)",
|
|
97
97
|
"h2": "clamp(32px, 3.4vw, 52px)",
|
|
98
98
|
"wordmark": "clamp(40px, 9vw, 128px)",
|
|
99
|
-
"$comment": "wordmark (0.7.0): the footer's last line on gemboss.ai, the brand's name, faint, at the end of the page. Decoration only, never a heading.",
|
|
99
|
+
"$comment": "wordmark (0.7.0): the footer's last line on gemboss.ai, the brand's name, faint, at the end of the page. Decoration only, never a heading. Weight 800 (0.10.0, as gemboss.ai draws it): the one use of 800 in the kit.",
|
|
100
100
|
"weight": 600,
|
|
101
101
|
"tracking": "-0.03em"
|
|
102
102
|
},
|
|
@@ -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
|
}
|
package/templates/footer.html
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Footer: one per site, the same on every page, built from this template (AGENTS.md: brands fill its rows, they never
|
|
3
3
|
invent a footer part). It is the footer of gemboss.ai (0.7.0). In order:
|
|
4
4
|
1. the brand block: logo, one sentence; OPTIONAL the Ask AI row; OPTIONAL the newsletter form (0.6.0)
|
|
5
|
-
2. three to six link columns, each a <details class="gw-footer__fold">: folded rows on phones (
|
|
6
|
-
columns from
|
|
5
|
+
2. three to six link columns, each a <details class="gw-footer__fold">: folded rows on phones (up to 620px), open
|
|
6
|
+
columns from 621px. Headings one or two words in sentence case, four to seven links each.
|
|
7
7
|
3. OPTIONAL product-family rows: a label and two to eight products, name and one line each; repeat the <nav> for a
|
|
8
8
|
second family
|
|
9
9
|
4. the bottom row: copyright, legal links, OPTIONAL theme toggle, social links. Nothing else.
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
Replace every [bracketed] placeholder and delete the optional parts you do not use. website.js wires the folds and
|
|
12
12
|
the toggle; without it the footer still works (the browser folds the <details> by itself).
|
|
13
13
|
|
|
14
|
-
Links that leave the site carry <span class="gw-footer__ext" aria-hidden="true"
|
|
14
|
+
Links that leave the site carry <span class="gw-footer__ext" aria-hidden="true">↗</span> (the glyph, set muted, as
|
|
15
|
+
gemboss.ai writes it; an empty span draws it instead). A link with
|
|
15
16
|
target="_blank" also carries <span class="gw-visually-hidden"> (opens in a new tab)</span>; a link without it does not.
|
|
16
17
|
-->
|
|
17
18
|
<footer class="gw-footer">
|
|
18
19
|
<div class="gw-wrap">
|
|
19
20
|
<div class="gw-footer__grid">
|
|
20
21
|
<div class="gw-footer__brand">
|
|
21
|
-
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="
|
|
22
|
+
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="34" height="34"><span class="gw-brand__name">[Brand]</span></a>
|
|
22
23
|
<p>[One sentence on what the brand does for the reader.]</p>
|
|
23
24
|
<!-- OPTIONAL Ask AI: each link opens an assistant with the same neutral question already typed, URL-encoded after
|
|
24
25
|
?q= (spaces as %20). Ask what the brand is and what it does for the reader, never a leading question. Words,
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
<li><a href="[/blog]">[Blog]</a></li>
|
|
90
91
|
<li><a href="[/docs]">[Docs]</a></li>
|
|
91
92
|
<li><a href="[/page]">[Page title]</a></li>
|
|
92
|
-
<li><a href="[https://another site of the brand]" rel="noopener">[Changelog]<span class="gw-footer__ext" aria-hidden="true"
|
|
93
|
+
<li><a href="[https://another site of the brand]" rel="noopener">[Changelog] <span class="gw-footer__ext" aria-hidden="true">↗</span></a></li>
|
|
93
94
|
</ul>
|
|
94
95
|
</details>
|
|
95
96
|
</nav>
|
|
@@ -114,8 +115,8 @@
|
|
|
114
115
|
<ul>
|
|
115
116
|
<li><a href="[/product]"><b>[Product]</b> <i>[One line on what it is]</i></a></li>
|
|
116
117
|
<li><a href="[/product]"><b>[Product]</b> <i>[One line on what it is]</i></a></li>
|
|
117
|
-
<li><a href="[https://another site of the brand]" rel="noopener"><b>[Product]<span class="gw-footer__ext" aria-hidden="true"
|
|
118
|
-
<li><a href="[https://a sibling brand's site]" target="_blank" rel="noopener"><b>[Product]<span class="gw-footer__ext" aria-hidden="true"
|
|
118
|
+
<li><a href="[https://another site of the brand]" rel="noopener"><b>[Product] <span class="gw-footer__ext" aria-hidden="true">↗</span></b> <i>[One line on what it is]</i></a></li>
|
|
119
|
+
<li><a href="[https://a sibling brand's site]" target="_blank" rel="noopener"><b>[Product] <span class="gw-footer__ext" aria-hidden="true">↗</span></b> <i>[One line on what it is]</i><span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
|
|
119
120
|
</ul>
|
|
120
121
|
</details>
|
|
121
122
|
</nav>
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
<li><a href="[/terms]">Terms</a></li>
|
|
127
128
|
</ul>
|
|
128
129
|
<!-- OPTIONAL theme toggle: the header's toggle, here instead of (or as well as) in the header -->
|
|
129
|
-
<button class="gw-theme-toggle gw-footer__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="
|
|
130
|
+
<button class="gw-theme-toggle gw-footer__toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme"><svg class="gw-theme-toggle__moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M21 12.8A8 8 0 1111.2 3a6 6 0 009.8 9.8z"/></svg><svg class="gw-theme-toggle__sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19"/></svg></button>
|
|
130
131
|
<ul aria-label="Social">
|
|
131
132
|
<li><a href="[https://social profile]" rel="noopener">[Network]</a></li>
|
|
132
133
|
<li><a href="[https://social profile]" rel="noopener">[Network]</a></li>
|
|
@@ -134,7 +135,8 @@
|
|
|
134
135
|
</div>
|
|
135
136
|
<!-- OPTIONAL wordmark: decoration (aria-hidden="true", the name is already in the brand block). The mark is an
|
|
136
137
|
inline SVG drawn in currentColor, so it takes the faint colour; an <img> cannot, so leave the mark out and keep
|
|
137
|
-
the name. Not shown
|
|
138
|
+
the name. Not shown up to 620px. The mark is 7vw wide; give the <svg> width and height attributes for its
|
|
139
|
+
shape (gemboss.ai's gem: width="86" height="100"). -->
|
|
138
140
|
<div class="gw-footer__wordmark" aria-hidden="true">
|
|
139
141
|
<svg viewBox="[0 0 width height]">[the brand's mark, every fill and stroke "currentColor"]</svg>
|
|
140
142
|
<span>[Brand]</span>
|