@gemboss/website-ui 0.9.0 → 0.11.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/CHANGELOG.md +109 -0
- package/README.md +2 -2
- package/bin/lib/check.mjs +6 -2
- package/css/base.css +2 -1
- package/css/effects.css +1 -1
- package/css/footer.css +74 -58
- package/css/header.css +347 -115
- package/css/tokens.css +24 -5
- package/demo/article.html +58 -70
- package/demo/blog.html +58 -70
- package/demo/brand-profiles.html +57 -27
- package/demo/footer.html +59 -71
- package/demo/forms.html +58 -70
- package/demo/header.html +58 -108
- package/demo/index.html +58 -70
- package/demo/spotlight.html +58 -71
- package/demo/theme-example.html +1 -1
- package/docs/components.md +31 -21
- package/docs/markup-contract.md +53 -22
- package/js/website.js +285 -42
- package/js/website.mjs +319 -47
- package/package.json +1 -1
- package/system.json +23 -8
- package/templates/footer.html +11 -9
- package/templates/header.html +47 -24
- package/templates/page.html +54 -20
- package/website.css +449 -181
- package/website.min.css +1 -1
package/js/website.mjs
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// the export line and the // comments, plus auto-init): edit this file, then run the build. Keep comments on lines of
|
|
7
7
|
// their own, so the build can leave them out.
|
|
8
8
|
|
|
9
|
-
var version = '0.
|
|
9
|
+
var version = '0.11.0';
|
|
10
10
|
var api = { version: version, init: init, destroy: destroy };
|
|
11
11
|
// Page state. Set up by the first init(), never at import: a server can import this file.
|
|
12
|
-
var doc, root, mine, fine, sys, wide, mid, live = [],
|
|
12
|
+
var doc, root, mine, fine, sys, wide, desk, mid, live = [], queued = false;
|
|
13
13
|
var still, lights;
|
|
14
14
|
var READY = 'data-gw-ready';
|
|
15
15
|
|
|
@@ -56,48 +56,315 @@ function drop(r) {
|
|
|
56
56
|
}
|
|
57
57
|
function records(kind, fn) { live.slice().forEach(function (r) { if (r.kind === kind) fn(r); }); }
|
|
58
58
|
|
|
59
|
-
// Mega menu.
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
// Mega menu (0.11.0): gemboss.ai's own, 1:1 (Chris 26/10/2026). One record per header with panels: each
|
|
60
|
+
// .gw-nav__item whose name (.gw-nav__trigger, or a [data-gw-mega] of a 0.10.0 header) names its panel by
|
|
61
|
+
// data-gw-mega or aria-controls. Timing: open after the pointer rests 200ms, switch after 90ms once one is open,
|
|
62
|
+
// close 350ms after it leaves; keyboard focus opens at once, ArrowDown on a name opens it and steps into its first
|
|
63
|
+
// row, Escape closes and gives the focus back to the name. A <button> name also opens and closes with a click; a
|
|
64
|
+
// link name is its page's link. From 1081px only: the phone menu has its own markup. The script marks the header
|
|
65
|
+
// .has-mega (.is-mega-open while one is open), draws the surface, the caret and the pill in it and the scrim in the
|
|
66
|
+
// body (all aria-hidden), takes `hidden` off the panels and keeps a closed one inert. A row's preview is the
|
|
67
|
+
// <template> its data-gw-preview names (the value "feature": the panel's own card); a card's pictures load the first
|
|
68
|
+
// time it shows (data-gw-bg, template[data-gw-hydrate]). Each open and close fires gw:mega on the name ({ id, open }).
|
|
69
|
+
// destroy gives everything back.
|
|
70
|
+
var hydrated = new WeakSet();
|
|
71
|
+
function hydrate(card) {
|
|
72
|
+
if (!card || hydrated.has(card)) return;
|
|
73
|
+
hydrated.add(card);
|
|
74
|
+
within('[data-gw-bg]', card).forEach(function (el) {
|
|
75
|
+
el.style.backgroundImage = 'url("' + attr(el, 'data-gw-bg', '') + '")';
|
|
76
|
+
el.classList.add('is-hydrated');
|
|
77
|
+
});
|
|
78
|
+
within('template[data-gw-hydrate]', card).forEach(function (t) {
|
|
79
|
+
t.parentNode.appendChild(t.content.cloneNode(true));
|
|
80
|
+
t.parentNode.classList.add('is-hydrated');
|
|
81
|
+
});
|
|
81
82
|
}
|
|
82
|
-
function inside(m, el) { return !!el && (m.item.contains(el) || m.p.contains(el)); }
|
|
83
83
|
function mega(ctx) {
|
|
84
|
-
each(ctx, '
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
each(ctx, '.gw-header', 'mega', function (header, on) {
|
|
85
|
+
var items = [];
|
|
86
|
+
all('.gw-nav__item', header).forEach(function (item) {
|
|
87
|
+
var t = item.querySelector('[data-gw-mega], .gw-nav__trigger');
|
|
88
|
+
var p = t && doc.getElementById(attr(t, 'data-gw-mega', attr(t, 'aria-controls', '')));
|
|
89
|
+
if (p) items.push({ item: item, t: t, p: p, body: p.querySelector('.gw-mega__body'), foot: p.querySelector('.gw-mega__foot'),
|
|
90
|
+
grid: p.querySelector('.gw-mega__grid'), rows: all('.gw-mega__link', p), pane: p.querySelector('.gw-mega__aside') });
|
|
91
|
+
});
|
|
92
|
+
if (!items.length) return;
|
|
93
|
+
var nav = header.querySelector('.gw-nav') || header;
|
|
94
|
+
var bar = header.querySelector('.gw-header__bar') || header;
|
|
95
|
+
var mk = function (tag, cls) { var n = doc.createElement(tag); n.className = cls; n.setAttribute('aria-hidden', 'true'); return n; };
|
|
96
|
+
var scrim = mk('div', 'gw-scrim'), bg = mk('div', 'gw-mega-surface'), caret = mk('div', 'gw-mega-caret'), pill = mk('span', 'gw-nav__pill');
|
|
97
|
+
var made = [scrim, bg, caret, pill], cards = new Map(), cur = null, openT = 0, closeT = 0, pvT = 0, pvTok = 0, quiet = false, rec;
|
|
98
|
+
doc.body.appendChild(scrim);
|
|
99
|
+
header.appendChild(pill);
|
|
100
|
+
header.appendChild(bg);
|
|
101
|
+
header.appendChild(caret);
|
|
102
|
+
header.classList.add('has-mega');
|
|
103
|
+
items.forEach(function (m) {
|
|
104
|
+
m.hid = m.p.hidden;
|
|
105
|
+
m.p.hidden = false;
|
|
106
|
+
m.p.inert = true;
|
|
107
|
+
m.feat = m.pane && m.pane.querySelector('.gw-mega__card');
|
|
108
|
+
});
|
|
109
|
+
var still2 = function () { return still.matches; };
|
|
110
|
+
// a change with transitions off, then on again (a reflow in between commits the jump)
|
|
111
|
+
function snap(els, fn) {
|
|
112
|
+
els.forEach(function (n) { if (n) n.style.transition = 'none'; });
|
|
113
|
+
fn();
|
|
114
|
+
void header.offsetWidth;
|
|
115
|
+
els.forEach(function (n) { if (n) n.style.transition = ''; });
|
|
116
|
+
}
|
|
117
|
+
function cardFor(m, row) {
|
|
118
|
+
var k = attr(row, 'data-gw-preview', '');
|
|
119
|
+
if (k === 'feature') return m.feat || null;
|
|
120
|
+
if (cards.has(row)) return cards.get(row);
|
|
121
|
+
var tpl = k && doc.getElementById(k);
|
|
122
|
+
if (!tpl || !tpl.content || !m.pane) return null;
|
|
123
|
+
var a = doc.createElement('a'), art = doc.createElement('span'), txt = doc.createElement('span'), go = doc.createElement('span');
|
|
124
|
+
var b = row.querySelector('b'), t = b ? b.cloneNode(true) : doc.createElement('b'), line = row.querySelector('i');
|
|
125
|
+
a.className = 'gw-mega__card';
|
|
126
|
+
a.href = attr(row, 'href', '#');
|
|
127
|
+
if (row.rel) a.rel = row.rel;
|
|
128
|
+
// the row is the link; this is its picture
|
|
129
|
+
a.tabIndex = -1;
|
|
130
|
+
a.setAttribute('aria-hidden', 'true');
|
|
131
|
+
within('[aria-hidden="true"]', t).forEach(function (n) { if (n !== t) n.remove(); });
|
|
132
|
+
go.className = 'gw-mega__go';
|
|
133
|
+
go.textContent = /^https?:/.test(a.getAttribute('href')) ? '\u2197' : '\u2192';
|
|
134
|
+
t.appendChild(go);
|
|
135
|
+
art.className = 'gw-mega__art';
|
|
136
|
+
art.appendChild(tpl.content.cloneNode(true));
|
|
137
|
+
txt.className = 'gw-mega__card-text';
|
|
138
|
+
txt.appendChild(t);
|
|
139
|
+
if (line) txt.appendChild(line.cloneNode(true));
|
|
140
|
+
a.appendChild(art);
|
|
141
|
+
a.appendChild(txt);
|
|
142
|
+
m.pane.appendChild(a);
|
|
143
|
+
cards.set(row, a);
|
|
144
|
+
return a;
|
|
145
|
+
}
|
|
146
|
+
function build(m) {
|
|
147
|
+
if (m.built) return;
|
|
148
|
+
m.built = true;
|
|
149
|
+
if (m.grid) { m.hl = mk('span', 'gw-mega__hl'); m.grid.insertBefore(m.hl, m.grid.firstChild); made.push(m.hl); }
|
|
150
|
+
hydrate(m.feat);
|
|
151
|
+
// a panel without its own card shows its first row's preview (gemboss.ai's Solutions)
|
|
152
|
+
if (m.pane && !m.feat && m.rows[0]) m.feat = cardFor(m, m.rows[0]);
|
|
153
|
+
// the other previews are built while the browser is idle, so their pictures are in hand before a row is reached
|
|
154
|
+
var idle = window.requestIdleCallback || function (f) { return setTimeout(f, 200); };
|
|
155
|
+
if (m.pane) idle(function () { if (header.classList.contains('has-mega')) m.rows.forEach(function (row) { cardFor(m, row); }); });
|
|
156
|
+
}
|
|
157
|
+
// a preview fades in once its pictures are decoded (240ms at most); a newer hover cancels one still waiting
|
|
158
|
+
function preview(m, row, now) {
|
|
159
|
+
clearTimeout(pvT);
|
|
160
|
+
var tok = ++pvTok;
|
|
161
|
+
var go = function () {
|
|
162
|
+
var c = row ? cardFor(m, row) : m.feat;
|
|
163
|
+
if (!c || c === m.on) return;
|
|
164
|
+
var swap = function () {
|
|
165
|
+
if (tok !== pvTok || m.on === c) return;
|
|
166
|
+
if (m.on) m.on.classList.remove('is-on');
|
|
167
|
+
c.classList.add('is-on');
|
|
168
|
+
m.on = c;
|
|
169
|
+
};
|
|
170
|
+
var wait = [].filter.call(c.querySelectorAll('img'), function (i) { return !i.complete && i.decode; });
|
|
171
|
+
if (!wait.length || !window.Promise) { swap(); return; }
|
|
172
|
+
var t = setTimeout(swap, 240);
|
|
173
|
+
Promise.all(wait.map(function (i) { return i.decode().catch(function () {}); })).then(function () { clearTimeout(t); swap(); });
|
|
174
|
+
};
|
|
175
|
+
// a sweep across the rows does not flicker the pane
|
|
176
|
+
if (now) go(); else pvT = setTimeout(go, 80);
|
|
177
|
+
}
|
|
178
|
+
function light(m, row) {
|
|
179
|
+
if (!m.hl) return;
|
|
180
|
+
if (!row) { m.hl.classList.remove('is-on'); return; }
|
|
181
|
+
var place = function () {
|
|
182
|
+
m.hl.style.translate = row.offsetLeft + 'px ' + row.offsetTop + 'px';
|
|
183
|
+
m.hl.style.width = row.offsetWidth + 'px';
|
|
184
|
+
m.hl.style.height = row.offsetHeight + 'px';
|
|
185
|
+
};
|
|
186
|
+
if (m.hl.classList.contains('is-on') && !still2()) place(); else snap([m.hl], place);
|
|
187
|
+
m.hl.classList.add('is-on');
|
|
188
|
+
}
|
|
189
|
+
function movePill(t) {
|
|
190
|
+
if (!t) { pill.classList.remove('is-on'); return; }
|
|
191
|
+
var hr = header.getBoundingClientRect(), r = t.getBoundingClientRect();
|
|
192
|
+
var place = function () {
|
|
193
|
+
pill.style.translate = Math.round(r.left - hr.left) + 'px ' + Math.round(r.top - hr.top) + 'px';
|
|
194
|
+
pill.style.width = Math.round(r.width) + 'px';
|
|
195
|
+
pill.style.height = Math.round(r.height) + 'px';
|
|
196
|
+
};
|
|
197
|
+
if (pill.classList.contains('is-on') && !still2()) place(); else snap([pill], place);
|
|
198
|
+
pill.classList.add('is-on');
|
|
199
|
+
}
|
|
200
|
+
// where each panel sits: centred under its name, kept 8px inside the bar's edges, measured as it opens
|
|
201
|
+
function measure() {
|
|
202
|
+
var hr = header.getBoundingClientRect(), inner = bar.getBoundingClientRect();
|
|
203
|
+
var lo = inner.left - hr.left + 8, hi = inner.right - hr.left - 8;
|
|
204
|
+
header.style.setProperty('--gw-mega-top', (header.clientHeight + 8) + 'px');
|
|
205
|
+
items.forEach(function (m) {
|
|
206
|
+
var t = m.t.getBoundingClientRect(), w = m.p.offsetWidth, h = m.p.offsetHeight;
|
|
207
|
+
var cx = t.left + t.width / 2 - hr.left;
|
|
208
|
+
var x = Math.round(Math.max(lo, Math.min(cx - w / 2, hi - w)));
|
|
209
|
+
m.x = x; m.w = w; m.h = h;
|
|
210
|
+
m.cx = Math.round(Math.max(x + 24, Math.min(cx, x + w - 24)));
|
|
211
|
+
m.p.style.left = x + 'px';
|
|
212
|
+
m.p.style.transformOrigin = (m.cx - x) + 'px 0';
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// the part of panel m the surface covers when the surface is at rect r (panel coordinates)
|
|
216
|
+
function clipTo(m, r) {
|
|
217
|
+
if (!r) return 'inset(0px 0px 0px 0px round 20px)';
|
|
218
|
+
var l = Math.max(0, r.x - m.x), rt = Math.max(0, m.x + m.w - (r.x + r.w)), b = Math.max(0, m.h - r.h);
|
|
219
|
+
return 'inset(0px ' + Math.round(rt) + 'px ' + Math.round(b) + 'px ' + Math.round(l) + 'px round 20px)';
|
|
220
|
+
}
|
|
221
|
+
// x, width and height move on the main thread (left, width, height), like the panels' clip-path, so the surface
|
|
222
|
+
// and what it clips never drift apart; only the lift in and out (translate y) runs on the compositor
|
|
223
|
+
function surface(m, y) {
|
|
224
|
+
bg.style.left = m.x + 'px';
|
|
225
|
+
bg.style.translate = '0px ' + y + 'px';
|
|
226
|
+
bg.style.width = m.w + 'px';
|
|
227
|
+
bg.style.height = m.h + 'px';
|
|
228
|
+
bg.style.transformOrigin = (m.cx - m.x) + 'px 0';
|
|
229
|
+
caret.style.translate = (m.cx - 6) + 'px ' + y + 'px';
|
|
230
|
+
}
|
|
231
|
+
function show(m) {
|
|
232
|
+
clearTimeout(closeT);
|
|
233
|
+
if (cur === m) return;
|
|
234
|
+
var prev = cur;
|
|
235
|
+
cur = m;
|
|
236
|
+
// a panel on its way out leaves the Tab order at once (inert), not when its fade ends
|
|
237
|
+
if (prev) {
|
|
238
|
+
prev.pinned = false;
|
|
239
|
+
prev.item.classList.remove('is-open');
|
|
240
|
+
prev.t.setAttribute('aria-expanded', 'false');
|
|
241
|
+
prev.p.inert = true;
|
|
242
|
+
emit(prev.t, 'gw:mega', { id: prev.p.id, open: false });
|
|
243
|
+
}
|
|
244
|
+
if (!m) {
|
|
245
|
+
header.classList.remove('is-mega-open');
|
|
246
|
+
scrim.classList.remove('is-on');
|
|
247
|
+
if (prev) { bg.style.translate = '0px -6px'; caret.style.translate = (prev.cx - 6) + 'px -6px'; }
|
|
248
|
+
movePill(nav.querySelector('.gw-nav__trigger:hover, .gw-nav__link:hover'));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
build(m);
|
|
252
|
+
// one panel open on the page, whichever header holds it
|
|
253
|
+
records('mega', function (o) { if (o !== rec && o.open()) o.close(); });
|
|
254
|
+
// every panel arrives showing its own card, with no row lit
|
|
255
|
+
preview(m, null, true);
|
|
256
|
+
light(m, null);
|
|
257
|
+
if (!prev) {
|
|
258
|
+
measure();
|
|
259
|
+
snap([bg, caret, m.p, m.body, m.foot], function () {
|
|
260
|
+
surface(m, -6);
|
|
261
|
+
m.p.style.clipPath = clipTo(m, null);
|
|
262
|
+
m.p.style.setProperty('--gw-mega-dx', '0px');
|
|
263
|
+
});
|
|
264
|
+
header.classList.add('is-mega-open');
|
|
265
|
+
surface(m, 0);
|
|
266
|
+
} else {
|
|
267
|
+
var hr = header.getBoundingClientRect(), br = bg.getBoundingClientRect();
|
|
268
|
+
var from = { x: br.left - hr.left, w: br.width, h: br.height };
|
|
269
|
+
var dir = items.indexOf(m) > items.indexOf(prev) ? 1 : -1;
|
|
270
|
+
if (!still2()) {
|
|
271
|
+
snap([m.p, m.body, m.foot], function () {
|
|
272
|
+
m.p.style.clipPath = clipTo(m, from);
|
|
273
|
+
m.p.style.setProperty('--gw-mega-dx', (dir * 8) + 'px');
|
|
274
|
+
});
|
|
275
|
+
prev.p.style.clipPath = clipTo(prev, m);
|
|
276
|
+
prev.p.style.setProperty('--gw-mega-dx', (-dir * 8) + 'px');
|
|
277
|
+
}
|
|
278
|
+
m.p.style.clipPath = clipTo(m, null);
|
|
279
|
+
m.p.style.setProperty('--gw-mega-dx', '0px');
|
|
280
|
+
surface(m, 0);
|
|
281
|
+
}
|
|
282
|
+
m.p.inert = false;
|
|
283
|
+
m.item.classList.add('is-open');
|
|
284
|
+
m.t.setAttribute('aria-expanded', 'true');
|
|
285
|
+
scrim.classList.add('is-on');
|
|
286
|
+
movePill(m.t);
|
|
287
|
+
emit(m.t, 'gw:mega', { id: m.p.id, open: true });
|
|
288
|
+
}
|
|
289
|
+
function hideSoon() { clearTimeout(openT); clearTimeout(closeT); closeT = setTimeout(function () { show(null); }, 350); }
|
|
290
|
+
items.forEach(function (m) {
|
|
291
|
+
on(m.p, 'transitionend', function (e) {
|
|
292
|
+
// a panel that has faded out drops its clip, so the next open starts whole
|
|
293
|
+
if (e.target === m.p && e.propertyName === 'opacity' && !m.item.classList.contains('is-open')) m.p.style.clipPath = '';
|
|
294
|
+
});
|
|
295
|
+
on(m.item, 'pointerenter', function (e) {
|
|
296
|
+
if (!desk.matches || e.pointerType === 'touch') return;
|
|
297
|
+
clearTimeout(closeT);
|
|
298
|
+
clearTimeout(openT);
|
|
299
|
+
// once a panel is open, a pointer crossing a neighbour on its way down does not swap it out
|
|
300
|
+
openT = setTimeout(function () { show(m); }, cur ? 90 : 200);
|
|
301
|
+
});
|
|
302
|
+
on(m.item, 'pointerleave', function () { if (desk.matches && !m.pinned) hideSoon(); });
|
|
303
|
+
on(m.item, 'focusin', function () { if (desk.matches && !quiet) show(m); });
|
|
304
|
+
on(m.item, 'focusout', function (e) { if (desk.matches && !m.item.contains(e.relatedTarget)) hideSoon(); });
|
|
305
|
+
on(m.t, 'keydown', function (e) {
|
|
306
|
+
if (!desk.matches || e.key !== 'ArrowDown') return;
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
show(m);
|
|
309
|
+
if (m.rows[0]) m.rows[0].focus();
|
|
92
310
|
});
|
|
93
|
-
|
|
94
|
-
|
|
311
|
+
// a 0.10.0 header's <button> name: a click opens its panel and keeps it open when the pointer leaves (pinned),
|
|
312
|
+
// a click on it again closes it; a panel the pointer opened stays open at the click
|
|
313
|
+
if (m.t.tagName === 'BUTTON') on(m.t, 'click', function () {
|
|
314
|
+
if (!desk.matches) return;
|
|
315
|
+
clearTimeout(openT);
|
|
316
|
+
if (cur === m && m.pinned) { show(null); return; }
|
|
317
|
+
show(m);
|
|
318
|
+
m.pinned = true;
|
|
95
319
|
});
|
|
320
|
+
on(m.p, 'click', function (e) { if (link(e)) show(null); });
|
|
321
|
+
m.rows.forEach(function (row) {
|
|
322
|
+
on(row, 'pointerenter', function () { if (!desk.matches || cur !== m) return; light(m, row); preview(m, row, false); });
|
|
323
|
+
on(row, 'focus', function () { if (!desk.matches) return; if (cur !== m) show(m); light(m, row); preview(m, row, true); });
|
|
324
|
+
});
|
|
325
|
+
// the Tab onto the panel's own card shows that card, whatever row was previewed last
|
|
326
|
+
if (m.feat && m.feat.tagName === 'A') on(m.feat, 'focus', function () { if (!desk.matches) return; light(m, null); preview(m, null, true); });
|
|
327
|
+
});
|
|
328
|
+
all('.gw-nav__trigger, .gw-nav__link', nav).forEach(function (t) {
|
|
329
|
+
on(t, 'pointerenter', function (e) { if (desk.matches && e.pointerType !== 'touch') movePill(t); });
|
|
330
|
+
on(t, 'focus', function () { if (desk.matches) movePill(t); });
|
|
96
331
|
});
|
|
97
|
-
on(
|
|
98
|
-
on(
|
|
99
|
-
|
|
100
|
-
|
|
332
|
+
on(nav, 'pointerleave', function () { if (desk.matches && !cur) movePill(null); });
|
|
333
|
+
on(nav, 'focusout', function (e) { if (desk.matches && !cur && !nav.contains(e.relatedTarget)) movePill(null); });
|
|
334
|
+
on(window, 'resize', function () {
|
|
335
|
+
if (!desk.matches) { show(null); movePill(null); return; }
|
|
336
|
+
if (cur) { measure(); snap([bg, caret], function () { surface(cur, 0); }); movePill(cur.t); }
|
|
337
|
+
});
|
|
338
|
+
rec = {
|
|
339
|
+
open: function () { return !!cur; },
|
|
340
|
+
has: function (el) { return !!cur && !!el && (cur.item.contains(el) || cur.p.contains(el)); },
|
|
341
|
+
close: function (focus) {
|
|
342
|
+
if (!cur) return;
|
|
343
|
+
var t = cur.t;
|
|
344
|
+
quiet = true;
|
|
345
|
+
show(null);
|
|
346
|
+
if (focus) t.focus();
|
|
347
|
+
quiet = false;
|
|
348
|
+
if (focus) movePill(t);
|
|
349
|
+
},
|
|
350
|
+
stop: function () {
|
|
351
|
+
clearTimeout(openT); clearTimeout(closeT); clearTimeout(pvT);
|
|
352
|
+
if (cur) show(null);
|
|
353
|
+
made.forEach(function (n) { n.remove(); });
|
|
354
|
+
cards.forEach(function (c) { c.remove(); });
|
|
355
|
+
items.forEach(function (m) {
|
|
356
|
+
m.p.hidden = m.hid;
|
|
357
|
+
m.p.inert = false;
|
|
358
|
+
['left', 'transformOrigin', 'clipPath'].forEach(function (k) { m.p.style[k] = ''; });
|
|
359
|
+
m.p.style.removeProperty('--gw-mega-dx');
|
|
360
|
+
if (m.on) m.on.classList.remove('is-on');
|
|
361
|
+
m.built = false;
|
|
362
|
+
});
|
|
363
|
+
header.style.removeProperty('--gw-mega-top');
|
|
364
|
+
header.classList.remove('has-mega', 'is-mega-open');
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
return rec;
|
|
101
368
|
});
|
|
102
369
|
}
|
|
103
370
|
|
|
@@ -143,6 +410,8 @@ function mobile(ctx) {
|
|
|
143
410
|
if (g === cur) return;
|
|
144
411
|
cur = g;
|
|
145
412
|
groups.forEach(function (x) { x.open = x === g; });
|
|
413
|
+
// a level's card gets its pictures the first time the level shows (0.11.0)
|
|
414
|
+
if (g) hydrate(g.querySelector('.gw-mega__card'));
|
|
146
415
|
menu.classList.toggle('is-drilled', !!g);
|
|
147
416
|
menu.classList.toggle('is-back', !g && r.open());
|
|
148
417
|
menu.scrollTop = 0;
|
|
@@ -425,11 +694,11 @@ function tables(ctx) {
|
|
|
425
694
|
});
|
|
426
695
|
}
|
|
427
696
|
|
|
428
|
-
// Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row).
|
|
429
|
-
// the markup's own state (folded unless the markup says open) and the browser opens and closes it. From
|
|
697
|
+
// Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row). Up to 620px it keeps
|
|
698
|
+
// the markup's own state (folded unless the markup says open) and the browser opens and closes it. From 621px it is a
|
|
430
699
|
// plain column: the script sets `open`, takes the <summary> out of the Tab order (tabindex="-1") and ignores a click on
|
|
431
700
|
// it, so the heading cannot fold the list away. Again on every change of width; destroy gives back the markup's state.
|
|
432
|
-
// The CSS shows the list from
|
|
701
|
+
// The CSS shows the list from 621px without the script too (::details-content), so this is for state and for browsers
|
|
433
702
|
// without that.
|
|
434
703
|
function folds(ctx) {
|
|
435
704
|
each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
|
|
@@ -510,15 +779,18 @@ function install() {
|
|
|
510
779
|
var stored = null;
|
|
511
780
|
// 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
|
|
512
781
|
wide = mq('(min-width: 1080px)');
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
782
|
+
// 1081px: the header's desktop, as gemboss.ai (its phone menu runs to 1080px)
|
|
783
|
+
desk = mq('(min-width: 1081px)');
|
|
784
|
+
// 621px: the footer's columns stand open from here, and fold up to 620px, as on gemboss.ai (1:1, 26/10/2026)
|
|
785
|
+
mid = mq('(min-width: 621px)');
|
|
786
|
+
on(doc, 'click', function (e) { records('mega', function (r) { if (r.open() && !r.has(e.target)) r.close(); }); });
|
|
787
|
+
on(doc, 'keydown', function (e) { if (esc(e)) records('mega', function (r) { if (r.open()) r.close(true); }); });
|
|
517
788
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
518
789
|
// a drill-down menu that is open keeps the focus (its trap); a click outside a language list closes it
|
|
519
790
|
on(doc, 'keydown', function (e) { if (e.key === 'Tab') records('menu', function (r) { if (r.trap && r.open()) r.trap(e); }); });
|
|
520
791
|
on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
|
|
521
|
-
watch(wide, function () {
|
|
792
|
+
watch(wide, function () { records('toc', function (r) { r.fit(); }); });
|
|
793
|
+
watch(desk, function () { if (desk.matches) closeMenus(); });
|
|
522
794
|
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
523
795
|
// the spotlight: a fine pointer, motion welcome, and a browser that can draw the light (@property, color-mix)
|
|
524
796
|
still = mq('(prefers-reduced-motion: reduce)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/website-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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/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",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"night": "#0c0e14",
|
|
30
30
|
"night-2": "#141722",
|
|
31
31
|
"on-night": "#e3e4e8",
|
|
32
|
-
"on-night-muted": "#9aa3ff"
|
|
32
|
+
"on-night-muted": "#9aa3ff",
|
|
33
|
+
"scrim": "rgba(12, 14, 18, 0.12)"
|
|
33
34
|
},
|
|
34
35
|
"dark": {
|
|
35
36
|
"bg": "#0d0d12",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"line": "#2a2b33",
|
|
42
43
|
"line-strong": "#383944",
|
|
43
44
|
"line-control": "#6a6d78",
|
|
44
|
-
"action": "#
|
|
45
|
+
"action": "#e3e4e8",
|
|
45
46
|
"action-hover": "#ffffff",
|
|
46
47
|
"on-action": "#0d0d12",
|
|
47
48
|
"accent": "#7c84ff",
|
|
@@ -58,7 +59,8 @@
|
|
|
58
59
|
"night": "#0a0b10",
|
|
59
60
|
"night-2": "#12141c",
|
|
60
61
|
"on-night": "#e3e4e8",
|
|
61
|
-
"on-night-muted": "#9aa3ff"
|
|
62
|
+
"on-night-muted": "#9aa3ff",
|
|
63
|
+
"scrim": "rgba(0, 0, 0, 0.35)"
|
|
62
64
|
},
|
|
63
65
|
"wash": {
|
|
64
66
|
"$comment": "Secondary hues. ONE job: the hero wash and illustrations. Never chrome, text, borders or buttons.",
|
|
@@ -67,16 +69,27 @@
|
|
|
67
69
|
"sky": "#43b0ff",
|
|
68
70
|
"mint": "#1fcf9f"
|
|
69
71
|
},
|
|
72
|
+
"stage": {
|
|
73
|
+
"$comment": "The Stage (0.11.0): the deep iris ground a mega menu's pictures sit on, as gemboss.ai draws it, the same in light and dark. glow and glow-2 light it from two corners (with their strength), top and bottom are its gradient, dot its faint grid, ink its words. A brand sets its own stage here.",
|
|
74
|
+
"glow": "rgba(139, 147, 255, 0.5)",
|
|
75
|
+
"glow-2": "rgba(86, 97, 245, 0.34)",
|
|
76
|
+
"top": "#1d2058",
|
|
77
|
+
"bottom": "#0d0f2e",
|
|
78
|
+
"dot": "rgba(255, 255, 255, 0.1)",
|
|
79
|
+
"ink": "#ffffff"
|
|
80
|
+
},
|
|
70
81
|
"roles": {
|
|
71
82
|
"action": "The thing you click: ink, or the brand's accent when the site chose the accent action profile (profiles.action).",
|
|
72
83
|
"accent": "Links, focus rings, active state, a chart line, the 8px label square; the primary button only through the accent action profile. Never body text, never words in a heading, never a section band.",
|
|
73
84
|
"success/warning/danger": "State only. Their -soft tints are grounds: a tip or a warning callout, a success or an error badge, a field marked invalid. danger (0.6.0, gap #28) is the dashboard's --gemboss-danger family: the words of a field's error and the fill of a destructive button (on-danger on it).",
|
|
74
85
|
"line-control": "The border of a form control (a field, a checkbox, a radio), 0.6.0: it must reach 3:1 on bg, bg-soft and surface, light and dark, where line and line-strong are hairlines that do not. Like the dashboard's --gemboss-field-line.",
|
|
75
|
-
"wash": "The hero wash and illustrations only."
|
|
86
|
+
"wash": "The hero wash and illustrations only.",
|
|
87
|
+
"scrim": "The veil over the page while a mega menu is open (0.11.0): a dim and a 4px blur, as gemboss.ai.",
|
|
88
|
+
"stage": "The ground of a mega menu's pictures and nothing else (0.11.0)."
|
|
76
89
|
}
|
|
77
90
|
},
|
|
78
91
|
"radius": {
|
|
79
|
-
"scale": [0, 4, 6, 8, 12, 16, 20, 24, 999],
|
|
92
|
+
"scale": [0, 2, 4, 6, 8, 12, 16, 20, 24, 999],
|
|
80
93
|
"chip": 999,
|
|
81
94
|
"$comment": "Chips stay pills in every profile. The radii of actions, cards, panels and popovers come from the site's radius profile (profiles.radius): components read --gw-radius-action|card|panel|popover, never a number."
|
|
82
95
|
},
|
|
@@ -84,19 +97,21 @@
|
|
|
84
97
|
"rest": "0 1px 2px rgba(31,30,29,.04), 0 2px 6px rgba(31,30,29,.06)",
|
|
85
98
|
"sm": "0 1px 2px rgba(31,30,29,.04)",
|
|
86
99
|
"raised": "0 4px 16px rgba(31,30,29,.10)",
|
|
100
|
+
"dark": { "rest": "0 1px 0 rgba(0,0,0,.3)", "sm": "0 1px 0 rgba(0,0,0,.25)", "raised": "0 4px 16px rgba(0,0,0,.5)" },
|
|
101
|
+
"lift": "0 16px 32px -12px rgba(4,5,26,.72), 0 4px 8px -2px rgba(4,5,26,.36)",
|
|
87
102
|
"$comment": "The whole budget for chrome. A card sits; it does not hover. An illustration may light itself."
|
|
88
103
|
},
|
|
89
104
|
"type": {
|
|
90
105
|
"family": "Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
|
|
91
106
|
"mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
92
|
-
"weights": [400, 500, 600, 650, 700],
|
|
107
|
+
"weights": [400, 500, 600, 650, 700, 800],
|
|
93
108
|
"scale": [11, 12, 13, 14, 16, 18, 20, 22, 24, 30],
|
|
94
109
|
"display_above": 30,
|
|
95
110
|
"display": {
|
|
96
111
|
"h1": "clamp(40px, 4.6vw, 68px)",
|
|
97
112
|
"h2": "clamp(32px, 3.4vw, 52px)",
|
|
98
113
|
"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.",
|
|
114
|
+
"$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
115
|
"weight": 600,
|
|
101
116
|
"tracking": "-0.03em"
|
|
102
117
|
},
|
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>
|