@gemboss/website-ui 0.10.0 → 0.12.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 +99 -0
- package/README.md +2 -2
- package/bin/lib/check.mjs +7 -2
- package/css/base.css +25 -19
- package/css/components.css +22 -9
- package/css/effects.css +1 -1
- package/css/forms.css +1 -1
- package/css/header.css +374 -120
- package/css/layout.css +7 -3
- package/css/sections.css +1 -1
- package/css/tokens.css +33 -3
- package/demo/article.html +58 -70
- package/demo/blog.html +58 -70
- package/demo/brand-profiles.html +57 -27
- package/demo/footer.html +58 -70
- package/demo/forms.html +58 -70
- package/demo/header.html +59 -109
- package/demo/index.html +58 -70
- package/demo/spotlight.html +58 -71
- package/demo/theme-example.html +1 -1
- package/docs/components.md +37 -26
- package/docs/markup-contract.md +54 -24
- package/docs/theming.md +1 -1
- package/js/website.js +315 -48
- package/js/website.mjs +350 -51
- package/package.json +1 -1
- package/system.json +47 -8
- package/templates/header.html +49 -25
- package/templates/page.html +55 -21
- package/website.css +465 -158
- 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.12.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';
|
|
92
213
|
});
|
|
93
|
-
|
|
94
|
-
|
|
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();
|
|
95
310
|
});
|
|
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;
|
|
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;
|
|
@@ -285,19 +554,46 @@ function theme(ctx) {
|
|
|
285
554
|
relabel();
|
|
286
555
|
}
|
|
287
556
|
|
|
288
|
-
// Reveal
|
|
289
|
-
//
|
|
557
|
+
// Reveal (0.12.0, as gemboss.ai; system.json motion.reveal): once, when 12% of it is in view with the bottom 6% of the
|
|
558
|
+
// view taken off. Each .gw-reveal after the first among its .gw-reveal siblings waits 70ms more (at most 380ms), so a
|
|
559
|
+
// grid cascades. A [data-gw-count] number inside counts up from 0 when its reveal shows. Failsafe: all shown 1.6s after
|
|
560
|
+
// the init that found them, or at once without IntersectionObserver or under reduced motion (and then nothing counts).
|
|
561
|
+
var REVEAL = { threshold: 0.12, margin: '0px 0px -6% 0px', stagger: 70, staggerMax: 380, failsafe: 1600, count: 1100 };
|
|
562
|
+
// the number in the element's first words: "$1,200+" counts from 0 to 1,200 with its sign, grouping and decimals kept
|
|
563
|
+
function countUp(el) {
|
|
564
|
+
var n = el.firstChild, m = n && n.nodeType === 3 && /^(\s*\D*?)(\d[\d,]*(?:\.\d+)?)(.*)$/.exec(n.nodeValue), t0;
|
|
565
|
+
if (!m) return;
|
|
566
|
+
var num = m[2].replace(/,/g, ''), dec = (num.split('.')[1] || '').length;
|
|
567
|
+
function fmt(v) { v = v.toFixed(dec); return m[1] + (num !== m[2] ? Number(v).toLocaleString('en-US') : v) + m[3]; }
|
|
568
|
+
n.nodeValue = fmt(0);
|
|
569
|
+
requestAnimationFrame(function step(ts) {
|
|
570
|
+
var p = Math.min((ts - (t0 = t0 || ts)) / REVEAL.count, 1);
|
|
571
|
+
n.nodeValue = fmt(p < 1 ? num * (1 - Math.pow(1 - p, 3)) : +num);
|
|
572
|
+
if (p < 1) requestAnimationFrame(step);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
290
575
|
function reveal(ctx) {
|
|
291
|
-
var els = [], io =
|
|
292
|
-
each(ctx, '.gw-reveal', 'reveal', function (el) {
|
|
293
|
-
|
|
576
|
+
var els = [], io, calm = still.matches;
|
|
577
|
+
each(ctx, '.gw-reveal', 'reveal', function (el) {
|
|
578
|
+
els.push(el);
|
|
579
|
+
return { stop: function () { if (io) io.unobserve(el); el.style.transitionDelay = ''; } };
|
|
580
|
+
});
|
|
581
|
+
function show(el) {
|
|
582
|
+
if (el.classList.contains('is-in')) return;
|
|
583
|
+
el.classList.add('is-in');
|
|
584
|
+
if (!calm) [].forEach.call(el.querySelectorAll('[data-gw-count]'), countUp);
|
|
585
|
+
}
|
|
294
586
|
if (!els.length) return;
|
|
295
|
-
if (!window.IntersectionObserver ||
|
|
587
|
+
if (!window.IntersectionObserver || calm) return els.forEach(show);
|
|
588
|
+
els.forEach(function (el) {
|
|
589
|
+
var i = [].filter.call(el.parentNode.children, function (c) { return c.classList.contains('gw-reveal'); }).indexOf(el);
|
|
590
|
+
if (i > 0) el.style.transitionDelay = Math.min(i * REVEAL.stagger, REVEAL.staggerMax) + 'ms';
|
|
591
|
+
});
|
|
296
592
|
io = new IntersectionObserver(function (list) {
|
|
297
|
-
list.forEach(function (en) { if (en.isIntersecting) { en.target
|
|
298
|
-
}, { threshold:
|
|
593
|
+
list.forEach(function (en) { if (en.isIntersecting) { show(en.target); io.unobserve(en.target); } });
|
|
594
|
+
}, { threshold: REVEAL.threshold, rootMargin: REVEAL.margin });
|
|
299
595
|
els.forEach(function (el) { io.observe(el); });
|
|
300
|
-
setTimeout(
|
|
596
|
+
setTimeout(function () { els.forEach(show); io.disconnect(); }, REVEAL.failsafe);
|
|
301
597
|
}
|
|
302
598
|
|
|
303
599
|
// Header: .is-scrolled past 8px, read once per frame (one scroll listener for every header).
|
|
@@ -510,15 +806,18 @@ function install() {
|
|
|
510
806
|
var stored = null;
|
|
511
807
|
// 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
|
|
512
808
|
wide = mq('(min-width: 1080px)');
|
|
809
|
+
// 1081px: the header's desktop, as gemboss.ai (its phone menu runs to 1080px)
|
|
810
|
+
desk = mq('(min-width: 1081px)');
|
|
513
811
|
// 621px: the footer's columns stand open from here, and fold up to 620px, as on gemboss.ai (1:1, 26/10/2026)
|
|
514
812
|
mid = mq('(min-width: 621px)');
|
|
515
|
-
on(doc, 'click', function (e) { if (
|
|
516
|
-
on(doc, 'keydown', function (e) { if (
|
|
813
|
+
on(doc, 'click', function (e) { records('mega', function (r) { if (r.open() && !r.has(e.target)) r.close(); }); });
|
|
814
|
+
on(doc, 'keydown', function (e) { if (esc(e)) records('mega', function (r) { if (r.open()) r.close(true); }); });
|
|
517
815
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
518
816
|
// a drill-down menu that is open keeps the focus (its trap); a click outside a language list closes it
|
|
519
817
|
on(doc, 'keydown', function (e) { if (e.key === 'Tab') records('menu', function (r) { if (r.trap && r.open()) r.trap(e); }); });
|
|
520
818
|
on(doc, 'click', function (e) { records('lang', function (r) { if (r.open() && !r.el.contains(e.target)) r.set(false); }); });
|
|
521
|
-
watch(wide, function () {
|
|
819
|
+
watch(wide, function () { records('toc', function (r) { r.fit(); }); });
|
|
820
|
+
watch(desk, function () { if (desk.matches) closeMenus(); });
|
|
522
821
|
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
523
822
|
// the spotlight: a fine pointer, motion welcome, and a browser that can draw the light (@property, color-mix)
|
|
524
823
|
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.12.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
|
@@ -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,38 @@
|
|
|
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
|
+
},
|
|
81
|
+
"announce": {
|
|
82
|
+
"$comment": "The announcement bar (0.12.0): one sentence over the header, as gemboss.ai draws it, the same in light and dark. bg its ground, ink its words, line its hairline at the bottom; tag and tag-ink the small pill before the sentence; link the link's words and link-line their underline. A brand sets its own bar here.",
|
|
83
|
+
"bg": "#171a38",
|
|
84
|
+
"ink": "#b9beda",
|
|
85
|
+
"line": "rgba(255, 255, 255, 0.06)",
|
|
86
|
+
"tag": "rgba(122, 132, 255, 0.2)",
|
|
87
|
+
"tag-ink": "#a7aeff",
|
|
88
|
+
"link": "#ffffff",
|
|
89
|
+
"link-line": "rgba(255, 255, 255, 0.28)"
|
|
90
|
+
},
|
|
70
91
|
"roles": {
|
|
71
92
|
"action": "The thing you click: ink, or the brand's accent when the site chose the accent action profile (profiles.action).",
|
|
72
93
|
"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
94
|
"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
95
|
"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."
|
|
96
|
+
"wash": "The hero wash and illustrations only.",
|
|
97
|
+
"scrim": "The veil over the page while a mega menu is open (0.11.0): a dim and a 4px blur, as gemboss.ai.",
|
|
98
|
+
"stage": "The ground of a mega menu's pictures and nothing else (0.11.0).",
|
|
99
|
+
"announce": "The announcement bar over the header and nothing else (0.12.0)."
|
|
76
100
|
}
|
|
77
101
|
},
|
|
78
102
|
"radius": {
|
|
79
|
-
"scale": [0, 4, 6, 8, 12, 16, 20, 24, 999],
|
|
103
|
+
"scale": [0, 2, 4, 6, 8, 12, 16, 20, 24, 999],
|
|
80
104
|
"chip": 999,
|
|
81
105
|
"$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
106
|
},
|
|
@@ -84,6 +108,8 @@
|
|
|
84
108
|
"rest": "0 1px 2px rgba(31,30,29,.04), 0 2px 6px rgba(31,30,29,.06)",
|
|
85
109
|
"sm": "0 1px 2px rgba(31,30,29,.04)",
|
|
86
110
|
"raised": "0 4px 16px rgba(31,30,29,.10)",
|
|
111
|
+
"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)" },
|
|
112
|
+
"lift": "0 16px 32px -12px rgba(4,5,26,.72), 0 4px 8px -2px rgba(4,5,26,.36)",
|
|
87
113
|
"$comment": "The whole budget for chrome. A card sits; it does not hover. An illustration may light itself."
|
|
88
114
|
},
|
|
89
115
|
"type": {
|
|
@@ -98,13 +124,16 @@
|
|
|
98
124
|
"wordmark": "clamp(40px, 9vw, 128px)",
|
|
99
125
|
"$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
126
|
"weight": 600,
|
|
101
|
-
"tracking": "-0.03em"
|
|
127
|
+
"tracking": "-0.03em",
|
|
128
|
+
"h1_leading": 1.1,
|
|
129
|
+
"$comment_leading": "1.1 and 1.06 (0.12.0, gemboss.ai's): at 1.02 the descenders of a first line touched the capitals of the second (Chris, 15/10/2026).",
|
|
130
|
+
"h2_leading": 1.06
|
|
102
131
|
},
|
|
103
132
|
"heading": { "weight": 650, "tracking": "-0.022em", "line_height": 1.08 },
|
|
104
133
|
"subhead": { "weight": 600 },
|
|
105
134
|
"body": { "size": 18, "line_height": 1.6 },
|
|
106
|
-
"label": { "size": 13, "weight": 500, "case": "sentence", "$comment": "muted, led by the 8px
|
|
107
|
-
"button": { "size": 14, "weight": 600, "case": "sentence" }
|
|
135
|
+
"label": { "size": 13, "weight": 500, "case": "sentence", "$comment": "muted, led by the 8px square in ink (0.12.0, as gemboss.ai; the input --gw-label-mark paints it another colour); never uppercase tracking" },
|
|
136
|
+
"button": { "size": 14, "weight": 600, "case": "sentence", "family": "Inter", "$comment": "family (0.12.0): a button names the brand face alone, as gemboss.ai does. A glyph the face does not carry (the arrow → is not in Inter's web subset) is then drawn by the system's own fallback, the width gemboss.ai's buttons have (with the full stack it is a different face, 1px narrower, and a pair of buttons fits where gemboss.ai wraps it)." }
|
|
108
137
|
},
|
|
109
138
|
"space": {
|
|
110
139
|
"scale": [0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 120],
|
|
@@ -153,6 +182,16 @@
|
|
|
153
182
|
"properties": ["transform", "opacity"],
|
|
154
183
|
"ease": "cubic-bezier(.22,1,.36,1)",
|
|
155
184
|
"reveal_ms": 700,
|
|
185
|
+
"reveal": {
|
|
186
|
+
"$comment": "Scroll reveal (0.12.0, as gemboss.ai): a .gw-reveal rises distance px and fades in over reveal_ms when threshold of it is in view, the bottom margin of the view taken off; each one after the first among its .gw-reveal siblings waits stagger_ms more (at most stagger_max_ms); all are shown failsafe_ms after the script starts. A [data-gw-count] number inside counts up from 0 over count_ms when its reveal shows. The script's numbers are held equal to these by the tests.",
|
|
187
|
+
"distance": 20,
|
|
188
|
+
"threshold": 0.12,
|
|
189
|
+
"margin": "0px 0px -6% 0px",
|
|
190
|
+
"stagger_ms": 70,
|
|
191
|
+
"stagger_max_ms": 380,
|
|
192
|
+
"failsafe_ms": 1600,
|
|
193
|
+
"count_ms": 1100
|
|
194
|
+
},
|
|
156
195
|
"spin_ms": 700,
|
|
157
196
|
"$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
197
|
"spot": {
|