@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/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.10.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 = [], megaOpen = null, megaTimer = 0, queued = false;
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. Opened by hover then clicked, a panel stays open: the click meant "open". One open on the page, across
60
- // every header init() wired. Each change fires gw:mega on the trigger.
61
- function setMega(m, show, mode) {
62
- m.p.hidden = !show;
63
- m.t.setAttribute('aria-expanded', String(show));
64
- m.item.classList.toggle('is-open', show);
65
- m.mode = mode;
66
- megaOpen = show ? m : megaOpen === m ? null : megaOpen;
67
- emit(m.t, 'gw:mega', { id: m.p.id, open: show });
68
- }
69
- function showMega(m, mode) {
70
- clearTimeout(megaTimer);
71
- if (megaOpen === m) { if (mode === 'click') m.mode = mode; return; }
72
- if (megaOpen) setMega(megaOpen, false);
73
- setMega(m, true, mode);
74
- }
75
- function hideMega(focus) {
76
- var m = megaOpen;
77
- clearTimeout(megaTimer);
78
- if (!m) return;
79
- setMega(m, false);
80
- if (focus === true) m.t.focus();
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, '[data-gw-mega]', 'mega', function (t, on) {
85
- var p = doc.getElementById(attr(t, 'data-gw-mega', attr(t, 'aria-controls', '')));
86
- if (!p) return;
87
- var m = { t: t, p: p, item: t.closest('.gw-nav__item') || t.parentNode };
88
- on(t, 'click', function () { if (megaOpen === m && m.mode !== 'hover') hideMega(); else showMega(m, 'click'); });
89
- (m.item.contains(p) ? [m.item] : [m.item, p]).forEach(function (el) {
90
- on(el, 'mouseenter', function () {
91
- if (fine.matches) { clearTimeout(megaTimer); if (megaOpen !== m) megaTimer = setTimeout(function () { showMega(m, 'hover'); }, 80); }
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
- on(el, 'mouseleave', function () {
94
- if (fine.matches) { clearTimeout(megaTimer); if (megaOpen === m && m.mode === 'hover') megaTimer = setTimeout(hideMega, 160); }
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(m.item, 'focusout', function (e) { if (megaOpen === m && e.relatedTarget && !inside(m, e.relatedTarget)) hideMega(); });
98
- on(p, 'click', function (e) { if (link(e)) hideMega(); });
99
- m.stop = function () { if (megaOpen === m) hideMega(); };
100
- return m;
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 (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
516
- on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
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 () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
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.10.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": "#f2f3f5",
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": {
@@ -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
- A flat 64px bar with one hairline. A brand that chose the floating header adds one class to the <header>:
4
- class="gw-header gw-header--float" (an inset card from 1080px, docs/theming.md); nothing else changes.
5
- Below 1080px the nav and Sign in hide and the menu button opens #gw-mobile-menu,
6
- which repeats the same links. website.js runs the mega menus, the mobile menu, the language switcher and the theme
7
- toggle.
8
- Limits: at most five top-level items, one .gw-btn (the primary action), one .gw-btn--secondary. Each mega panel:
9
- two or three columns of four or five links, at most one feature card. The announcement bar is optional: one sentence.
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 1080px, in the mobile menu's action bar below). It
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-wrap gw-header__bar">
25
- <a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="26" height="26"><span class="gw-brand__name">[Brand]</span></a>
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
- <button class="gw-nav__trigger" type="button" aria-expanded="false" aria-controls="gw-mega-product" data-gw-mega="gw-mega-product">[Product menu item]</button>
30
- <div class="gw-mega" id="gw-mega-product" hidden>
31
- <div class="gw-wrap gw-mega__grid">
32
- <div class="gw-mega__col">
33
- <p class="gw-mega__heading">[Group]</p>
34
- <a class="gw-mega__link" href="[/page]"><span class="gw-mega__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>
35
- <a class="gw-mega__link" href="[/page]"><span class="gw-mega__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>
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
- <div class="gw-mega__col">
38
- <p class="gw-mega__heading">[Group]</p>
39
- <a class="gw-mega__link" href="[/page]"><span class="gw-mega__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg></span><span><b>[Page title]</b><i>[One line on what it does]</i></span></a>
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
- <a class="gw-mega__feature" href="[/page]"><b>[Feature title]</b><i>[One line on why to open it]</i><span class="gw-link gw-link--arrow">[Where it goes]</span></a>
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-btn gw-btn--secondary gw-btn--sm" href="[/contact]">[Book a demo]</a>
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="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg><svg class="gw-menu-toggle__close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
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>