@gemboss/website-ui 0.10.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 +59 -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/header.css +335 -115
- package/css/tokens.css +22 -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 +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 +17 -12
- package/docs/markup-contract.md +45 -19
- package/js/website.js +284 -41
- package/js/website.mjs +314 -42
- package/package.json +1 -1
- package/system.json +20 -5
- package/templates/header.html +47 -24
- package/templates/page.html +54 -20
- package/website.css +361 -121
- 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;
|
|
@@ -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)');
|
|
782
|
+
// 1081px: the header's desktop, as gemboss.ai (its phone menu runs to 1080px)
|
|
783
|
+
desk = mq('(min-width: 1081px)');
|
|
513
784
|
// 621px: the footer's columns stand open from here, and fold up to 620px, as on gemboss.ai (1:1, 26/10/2026)
|
|
514
785
|
mid = mq('(min-width: 621px)');
|
|
515
|
-
on(doc, 'click', function (e) { if (
|
|
516
|
-
on(doc, 'keydown', function (e) { if (
|
|
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
|
@@ -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,6 +97,8 @@
|
|
|
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": {
|
package/templates/header.html
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Header: one per site, the same on every page. Keep it in one partial or component, never copied per page.
|
|
3
|
-
|
|
4
|
-
class="gw-header gw-header--float" (an inset card from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
Header: one per site, the same on every page. Keep it in one partial or component, never copied per page. It is
|
|
3
|
+
gemboss.ai's header (0.11.0, 1:1): a flat 64px bar with one hairline, 12px wider than the page's wrap. A brand that
|
|
4
|
+
chose the floating header adds one class to the <header>: class="gw-header gw-header--float" (an inset card from
|
|
5
|
+
1081px, docs/theming.md). Up to 1080px the nav and Sign in hide and the menu button opens #gw-mobile-menu, which
|
|
6
|
+
repeats the same links. website.js runs the mega menus, the mobile menu, the language switcher and the theme toggle.
|
|
7
|
+
The mega menu (0.11.0): a section's name is a link to its page that opens its panel (the pointer resting on it, the
|
|
8
|
+
keyboard's focus, ArrowDown steps into the first row). A panel holds its columns of rows and, beside them, the
|
|
9
|
+
pictures (.gw-mega__aside): its own card, and a preview per row while the pointer is on the row (data-gw-preview names
|
|
10
|
+
a <template> at the end of the header). Under it, an optional strip of links. Its width and its pictures' width are
|
|
11
|
+
the panel's (--gw-mega-w, --gw-mega-pv; 1000 and 320 when it says nothing), its columns too (--gw-mega-cols, 3).
|
|
12
|
+
Limits: at most five top-level items, one .gw-btn (the primary action), one second text action (.gw-header__link).
|
|
13
|
+
Each panel: two or three columns of four or five rows, at most one card, three links in its strip. The announcement
|
|
14
|
+
bar is optional: one sentence.
|
|
10
15
|
Two options (0.8.0):
|
|
11
16
|
- The mobile menu drills down (data-gw-menu="drill" on #gw-mobile-menu, as gemboss.ai's phone menu): the top level
|
|
12
17
|
lists the sections, a tap on one shows its level alone with Back and its title, and the actions stay in a bar at
|
|
13
18
|
the bottom of the sheet. Each group then carries its head (the Back button and the title, in the page's language).
|
|
19
|
+
A level may end with its panel's card and a link to the section's page (.gw-mobile-menu__all).
|
|
14
20
|
Without the attribute and the heads it is the 0.7.0 menu, whose groups open in place; without the script the drill
|
|
15
21
|
menu is that one too.
|
|
16
|
-
- The language switcher (.gw-lang, twice: in the actions from
|
|
22
|
+
- The language switcher (.gw-lang, twice: in the actions from 1081px, in the mobile menu's action bar below). It
|
|
17
23
|
lists the page's translations from its <link rel="alternate" hreflang="…" href="…"> in <head> and stays hidden
|
|
18
24
|
while the page has none: keep `hidden` on it, the script takes it away. Delete both copies only if the site will
|
|
19
25
|
never be translated. Put the word for "Language" in the page's language in data-gw-label-language.
|
|
@@ -21,24 +27,35 @@
|
|
|
21
27
|
-->
|
|
22
28
|
<div class="gw-announce" role="region" aria-label="Announcement">[One sentence of news.] <a href="[link]">[Where the link goes]</a></div>
|
|
23
29
|
<header class="gw-header" data-gw-header>
|
|
24
|
-
<div class="gw-
|
|
25
|
-
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="
|
|
30
|
+
<div class="gw-header__bar">
|
|
31
|
+
<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>
|
|
26
32
|
<nav class="gw-nav" aria-label="Main">
|
|
27
33
|
<ul class="gw-nav__list">
|
|
28
34
|
<li class="gw-nav__item">
|
|
29
|
-
<
|
|
30
|
-
<div class="gw-mega" id="gw-mega-product" hidden>
|
|
31
|
-
<div class="gw-
|
|
32
|
-
<div class="gw-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
<a class="gw-nav__trigger" href="[/product]" aria-expanded="false" aria-controls="gw-mega-product">[Product menu item] <svg class="gw-nav__chev" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6"><path d="M6 9l6 6 6-6"/></svg></a>
|
|
36
|
+
<div class="gw-mega" id="gw-mega-product" style="--gw-mega-w: 1120px; --gw-mega-pv: 380px" hidden>
|
|
37
|
+
<div class="gw-mega__body">
|
|
38
|
+
<div class="gw-mega__grid">
|
|
39
|
+
<div class="gw-mega__col">
|
|
40
|
+
<p class="gw-mega__heading">[Group]</p>
|
|
41
|
+
<a class="gw-mega__link" href="[/page]" data-gw-preview="pv-page-1"><span><b>[Page title]</b><i>[One line on what it does]</i></span></a>
|
|
42
|
+
<a class="gw-mega__link" href="[/page]" data-gw-preview="pv-page-2"><span><b>[Page title]</b><i>[One line on what it does]</i></span></a>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="gw-mega__col">
|
|
45
|
+
<p class="gw-mega__heading">[Group]</p>
|
|
46
|
+
<!-- data-gw-preview="feature": this row shows the panel's own card -->
|
|
47
|
+
<a class="gw-mega__link" href="[/page]" data-gw-preview="feature"><span><b>[Page title]</b><i>[One line on what it does]</i></span></a>
|
|
48
|
+
</div>
|
|
36
49
|
</div>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
<a class="gw-
|
|
50
|
+
<!-- the panel's own card: a picture on the Stage (an <img> fills it; or your own drawing), its words under it -->
|
|
51
|
+
<div class="gw-mega__aside">
|
|
52
|
+
<a class="gw-mega__card" href="[/page]"><span class="gw-mega__art"><img src="[/picture.webp]" alt=""></span><span class="gw-mega__card-text"><b>[Card title]</b><i>[One line on why to open it]</i><span class="gw-mega__go">[Where it goes] →</span></span></a>
|
|
40
53
|
</div>
|
|
41
|
-
|
|
54
|
+
</div>
|
|
55
|
+
<!-- optional: the strip under the panel, three links at most, the last one on the right -->
|
|
56
|
+
<div class="gw-mega__foot">
|
|
57
|
+
<div class="gw-mega__foot-start"><a href="[changelog URL]" rel="noopener"><span class="gw-mega__new">New</span> [What's new on the changelog] <span aria-hidden="true">↗</span></a><a href="[/product]">[Everything in the product]</a></div>
|
|
58
|
+
<div class="gw-mega__foot-end"><a class="gw-mega__cta" href="[/contact]">[Book a demo] <span aria-hidden="true">→</span></a></div>
|
|
42
59
|
</div>
|
|
43
60
|
</div>
|
|
44
61
|
</li>
|
|
@@ -53,9 +70,9 @@
|
|
|
53
70
|
<ul class="gw-lang__menu" id="gw-lang-menu" hidden></ul>
|
|
54
71
|
</div>
|
|
55
72
|
<a class="gw-nav__link gw-header__signin" href="[sign-in URL]">Sign in</a>
|
|
56
|
-
<a class="gw-
|
|
73
|
+
<a class="gw-nav__link gw-header__link" href="[/contact]">[Book a demo]</a>
|
|
57
74
|
<a class="gw-btn gw-btn--sm" href="[sign-up URL]">[Start free]</a>
|
|
58
|
-
<button class="gw-menu-toggle" type="button" aria-expanded="false" aria-controls="gw-mobile-menu" data-gw-menu-toggle aria-label="Open menu"><svg class="gw-menu-toggle__open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
75
|
+
<button class="gw-menu-toggle" type="button" aria-expanded="false" aria-controls="gw-mobile-menu" data-gw-menu-toggle aria-label="Open menu"><svg class="gw-menu-toggle__open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg><svg class="gw-menu-toggle__close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
|
|
59
76
|
</div>
|
|
60
77
|
</div>
|
|
61
78
|
<!-- data-gw-menu="drill" (0.8.0): leave it out, and the .gw-mobile-menu__head of each group, for the 0.7.0 menu -->
|
|
@@ -72,6 +89,8 @@
|
|
|
72
89
|
<li><a class="gw-mobile-menu__link" href="[/page]"><span class="gw-mobile-menu__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/></svg></span><span><b>[Page title]</b><i>[One line on what it does]</i></span></a></li>
|
|
73
90
|
<li><a class="gw-mobile-menu__link" href="[/page]"><span class="gw-mobile-menu__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 15l4-4 3 3 5-6"/></svg></span><span><b>[Page title]</b><i>[One line on what it does]</i></span></a></li>
|
|
74
91
|
</ul>
|
|
92
|
+
<!-- optional: the level ends with its panel's card (the same markup as in the panel) and the section's own page -->
|
|
93
|
+
<a class="gw-mobile-menu__all" href="[/product]">All of [Product menu item]</a>
|
|
75
94
|
</details>
|
|
76
95
|
<a class="gw-mobile-menu__top" href="[/pricing]">[Pricing]</a>
|
|
77
96
|
<!-- the action bar: what is not a button on the first line, two things at most (the language on the left, Sign in
|
|
@@ -83,8 +102,12 @@
|
|
|
83
102
|
</div>
|
|
84
103
|
<a class="gw-btn gw-btn--lg gw-btn--block" href="[sign-up URL]">[Start free]</a>
|
|
85
104
|
<a class="gw-btn gw-btn--secondary gw-btn--lg gw-btn--block" href="[/contact]">[Book a demo]</a>
|
|
105
|
+
<a class="gw-mobile-menu__link gw-mobile-menu__link--muted" href="[changelog URL]" rel="noopener">[What's new] <span aria-hidden="true">↗</span></a>
|
|
86
106
|
<a class="gw-mobile-menu__link" href="[sign-in URL]">Sign in</a>
|
|
87
107
|
</div>
|
|
88
108
|
</nav>
|
|
89
109
|
</div>
|
|
110
|
+
<!-- the rows' previews (data-gw-preview): the script clones one into the panel's pictures while its row is pointed at -->
|
|
111
|
+
<template id="pv-page-1"><img src="[/preview-1.webp]" alt=""></template>
|
|
112
|
+
<template id="pv-page-2"><img src="[/preview-2.webp]" alt=""></template>
|
|
90
113
|
</header>
|