@gemboss/website-ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +15 -2
- package/CHANGELOG.md +61 -0
- package/README.md +8 -8
- package/bin/lib/check.mjs +17 -0
- package/css/base.css +12 -0
- package/css/footer.css +178 -0
- package/css/tokens.css +1 -0
- package/demo/footer.html +339 -0
- package/demo/index.html +1 -0
- package/docs/components.md +57 -5
- package/docs/markup-contract.md +59 -4
- package/docs/theming.md +8 -2
- package/js/website.js +21 -4
- package/js/website.mjs +27 -3
- package/package.json +1 -1
- package/skill/SKILL.md +3 -0
- package/system.json +2 -0
- package/templates/footer.html +108 -20
- package/templates/page.html +16 -11
- package/website.css +192 -1
- package/website.min.css +1 -1
package/js/website.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.7.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
|
|
2
2
|
(function () {
|
|
3
3
|
'use strict';
|
|
4
4
|
if (window.gwWebsite) return;
|
|
5
5
|
|
|
6
|
-
var version = '0.
|
|
6
|
+
var version = '0.7.0';
|
|
7
7
|
var api = { version: version, init: init, destroy: destroy };
|
|
8
|
-
var doc, root, mine, fine, sys, wide, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
8
|
+
var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
9
9
|
var READY = 'data-gw-ready';
|
|
10
10
|
|
|
11
11
|
function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
|
|
@@ -250,15 +250,32 @@ function tables(ctx) {
|
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
function folds(ctx) {
|
|
254
|
+
each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
|
|
255
|
+
var s = d.querySelector('summary'), own = d.open, tab = s && s.getAttribute('tabindex');
|
|
256
|
+
if (!s) return;
|
|
257
|
+
function tabs(v) { if (v === null) s.removeAttribute('tabindex'); else s.setAttribute('tabindex', v); }
|
|
258
|
+
var r = {
|
|
259
|
+
fit: function () { d.open = mid.matches || own; tabs(mid.matches ? '-1' : tab); },
|
|
260
|
+
stop: function () { d.open = own; tabs(tab); }
|
|
261
|
+
};
|
|
262
|
+
on(s, 'click', function (e) { if (mid.matches) e.preventDefault(); });
|
|
263
|
+
r.fit();
|
|
264
|
+
return r;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
253
268
|
function install() {
|
|
254
269
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
255
270
|
fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
|
|
256
271
|
var stored = null;
|
|
257
272
|
wide = mq('(min-width: 1080px)');
|
|
273
|
+
mid = mq('(min-width: 768px)');
|
|
258
274
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
259
275
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
260
276
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
261
277
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
278
|
+
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
262
279
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
263
280
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
264
281
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -283,7 +300,7 @@ function init(ctx) {
|
|
|
283
300
|
window.gwWebsite = api;
|
|
284
301
|
if (!doc) run([install]);
|
|
285
302
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
286
|
-
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
303
|
+
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
/** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
|
package/js/website.mjs
CHANGED
|
@@ -6,10 +6,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.7.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, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
12
|
+
var doc, root, mine, fine, sys, wide, mid, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
13
13
|
var READY = 'data-gw-ready';
|
|
14
14
|
|
|
15
15
|
function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
|
|
@@ -286,6 +286,27 @@ function tables(ctx) {
|
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// Footer folds (0.7.0, gap #40): each <details class="gw-footer__fold"> (a column, a family row). Below 768px it keeps
|
|
290
|
+
// the markup's own state (folded unless the markup says open) and the browser opens and closes it. From 768px it is a
|
|
291
|
+
// plain column: the script sets `open`, takes the <summary> out of the Tab order (tabindex="-1") and ignores a click on
|
|
292
|
+
// it, so the heading cannot fold the list away. Again on every change of width; destroy gives back the markup's state.
|
|
293
|
+
// The CSS shows the list from 768px without the script too (::details-content), so this is for state and for browsers
|
|
294
|
+
// without that.
|
|
295
|
+
function folds(ctx) {
|
|
296
|
+
each(ctx, '.gw-footer__fold', 'fold', function (d, on) {
|
|
297
|
+
var s = d.querySelector('summary'), own = d.open, tab = s && s.getAttribute('tabindex');
|
|
298
|
+
if (!s) return;
|
|
299
|
+
function tabs(v) { if (v === null) s.removeAttribute('tabindex'); else s.setAttribute('tabindex', v); }
|
|
300
|
+
var r = {
|
|
301
|
+
fit: function () { d.open = mid.matches || own; tabs(mid.matches ? '-1' : tab); },
|
|
302
|
+
stop: function () { d.open = own; tabs(tab); }
|
|
303
|
+
};
|
|
304
|
+
on(s, 'click', function (e) { if (mid.matches) e.preventDefault(); });
|
|
305
|
+
r.fit();
|
|
306
|
+
return r;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
289
310
|
// Once per page: what belongs to the document, the window or a media query rather than to one element.
|
|
290
311
|
function install() {
|
|
291
312
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
@@ -293,10 +314,13 @@ function install() {
|
|
|
293
314
|
var stored = null;
|
|
294
315
|
// 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
|
|
295
316
|
wide = mq('(min-width: 1080px)');
|
|
317
|
+
// 768px: the footer's columns stand open from here, and fold below it
|
|
318
|
+
mid = mq('(min-width: 768px)');
|
|
296
319
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
297
320
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
298
321
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
299
322
|
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
323
|
+
watch(mid, function () { records('fold', function (r) { r.fit(); }); });
|
|
300
324
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
301
325
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
302
326
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -327,7 +351,7 @@ function init(ctx) {
|
|
|
327
351
|
if (!doc) run([install]);
|
|
328
352
|
// an element removed from the page since the last init is let go
|
|
329
353
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
330
|
-
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
354
|
+
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables, folds], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
331
355
|
}
|
|
332
356
|
|
|
333
357
|
/** Takes back what init() wired under root (default: the whole document): listeners and data-gw-ready marks. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/website-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/skill/SKILL.md
CHANGED
|
@@ -25,6 +25,9 @@ the kit and its rules are how the page ends up looking designed. Compose from th
|
|
|
25
25
|
words under it with `aria-invalid="true"`; the page decides when, the kit draws it.
|
|
26
26
|
- Ratings and testimonials (`KIT/templates/sections/testimonials.html`): only real ones, each linked to the page it
|
|
27
27
|
comes from, the reviewer's words exactly; stars carry their value in words (`aria-label="4.9 out of 5"`).
|
|
28
|
+
- The footer is `KIT/templates/footer.html` (the footer of gemboss.ai), filled once in the site's shared partial:
|
|
29
|
+
fill its rows (columns, family rows, words), delete the optional parts you do not use (Ask AI, newsletter, family
|
|
30
|
+
rows, theme toggle, wordmark), never invent a footer part. Columns fold on phones by themselves (`<details>`).
|
|
28
31
|
- Changed page: keep its kit classes; change words, order and choice of sections.
|
|
29
32
|
- Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
|
|
30
33
|
scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
|
package/system.json
CHANGED
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"display": {
|
|
96
96
|
"h1": "clamp(40px, 4.6vw, 68px)",
|
|
97
97
|
"h2": "clamp(32px, 3.4vw, 52px)",
|
|
98
|
+
"wordmark": "clamp(40px, 9vw, 128px)",
|
|
99
|
+
"$comment": "wordmark (0.7.0): the footer's last line on gemboss.ai, the brand's name, faint, at the end of the page. Decoration only, never a heading.",
|
|
98
100
|
"weight": 600,
|
|
99
101
|
"tracking": "-0.03em"
|
|
100
102
|
},
|
package/templates/footer.html
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Footer: one per site, the same on every page
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
Footer: one per site, the same on every page, built from this template (AGENTS.md: brands fill its rows, they never
|
|
3
|
+
invent a footer part). It is the footer of gemboss.ai (0.7.0). In order:
|
|
4
|
+
1. the brand block: logo, one sentence; OPTIONAL the Ask AI row; OPTIONAL the newsletter form (0.6.0)
|
|
5
|
+
2. three to six link columns, each a <details class="gw-footer__fold">: folded rows on phones (below 768px), open
|
|
6
|
+
columns from 768px. Headings one or two words in sentence case, four to seven links each.
|
|
7
|
+
3. OPTIONAL product-family rows: a label and two to eight products, name and one line each; repeat the <nav> for a
|
|
8
|
+
second family
|
|
9
|
+
4. the bottom row: copyright, legal links, OPTIONAL theme toggle, social links. Nothing else.
|
|
10
|
+
5. OPTIONAL the wordmark: the brand's mark and name, large and faint, decoration only
|
|
11
|
+
Replace every [bracketed] placeholder and delete the optional parts you do not use. website.js wires the folds and
|
|
12
|
+
the toggle; without it the footer still works (the browser folds the <details> by itself).
|
|
13
|
+
|
|
14
|
+
Links that leave the site carry <span class="gw-footer__ext" aria-hidden="true"></span> (a drawn ↗). A link with
|
|
15
|
+
target="_blank" also carries <span class="gw-visually-hidden"> (opens in a new tab)</span>; a link without it does not.
|
|
9
16
|
-->
|
|
10
17
|
<footer class="gw-footer">
|
|
11
18
|
<div class="gw-wrap">
|
|
@@ -13,6 +20,22 @@
|
|
|
13
20
|
<div class="gw-footer__brand">
|
|
14
21
|
<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>
|
|
15
22
|
<p>[One sentence on what the brand does for the reader.]</p>
|
|
23
|
+
<!-- OPTIONAL Ask AI: each link opens an assistant with the same neutral question already typed, URL-encoded after
|
|
24
|
+
?q= (spaces as %20). Ask what the brand is and what it does for the reader, never a leading question. Words,
|
|
25
|
+
not the assistants' logos. -->
|
|
26
|
+
<div class="gw-footer__ask">
|
|
27
|
+
<p class="gw-footer__ask-label" id="footer-ask">Ask AI about [Brand]</p>
|
|
28
|
+
<ul aria-labelledby="footer-ask">
|
|
29
|
+
<li><a href="https://chatgpt.com/?q=[URL-encoded question]" target="_blank" rel="noopener">ChatGPT<span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
|
|
30
|
+
<li><a href="https://claude.ai/new?q=[URL-encoded question]" target="_blank" rel="noopener">Claude<span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
|
|
31
|
+
<li><a href="https://www.perplexity.ai/search/new?q=[URL-encoded question]" target="_blank" rel="noopener">Perplexity<span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
|
|
32
|
+
<li><a href="https://www.google.com/search?udm=50&q=[URL-encoded question]" target="_blank" rel="noopener">Google AI Mode<span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
|
|
33
|
+
</ul>
|
|
34
|
+
</div>
|
|
35
|
+
<!-- OPTIONAL newsletter (0.6.0): keep it only when it is wired to a list that keeps the address and sends what the
|
|
36
|
+
note promises. The note says how often and how to stop, and links to the privacy policy. No box ticked for
|
|
37
|
+
the reader, no "No thanks, I hate saving money". On send: the page sets aria-invalid="true" and the error's
|
|
38
|
+
words, or aria-busy on the button and then a role="status" line. -->
|
|
16
39
|
<form class="gw-footer__newsletter" action="[/newsletter endpoint]" method="post" novalidate>
|
|
17
40
|
<div class="gw-field">
|
|
18
41
|
<label class="gw-field__label" for="footer-email">[Product news by email]</label>
|
|
@@ -25,31 +48,96 @@
|
|
|
25
48
|
</div>
|
|
26
49
|
</form>
|
|
27
50
|
</div>
|
|
28
|
-
<nav
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
<!-- the columns: three to six. Each is a <nav> named like its heading, and the heading sits inside the <summary>. -->
|
|
52
|
+
<nav class="gw-footer__col" aria-label="[Product]">
|
|
53
|
+
<details class="gw-footer__fold">
|
|
54
|
+
<summary><h2 class="gw-footer__heading">[Product]</h2></summary>
|
|
55
|
+
<ul>
|
|
56
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
57
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
58
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
59
|
+
<li><a href="[/pricing]">[Pricing]</a></li>
|
|
60
|
+
</ul>
|
|
61
|
+
</details>
|
|
35
62
|
</nav>
|
|
36
|
-
<nav class="gw-footer__col" aria-label="[
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
<nav class="gw-footer__col" aria-label="[Use cases]">
|
|
64
|
+
<details class="gw-footer__fold">
|
|
65
|
+
<summary><h2 class="gw-footer__heading">[Use cases]</h2></summary>
|
|
66
|
+
<ul>
|
|
67
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
68
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
69
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
70
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
71
|
+
</ul>
|
|
72
|
+
</details>
|
|
73
|
+
</nav>
|
|
74
|
+
<nav class="gw-footer__col" aria-label="[Solutions]">
|
|
75
|
+
<details class="gw-footer__fold">
|
|
76
|
+
<summary><h2 class="gw-footer__heading">[Solutions]</h2></summary>
|
|
77
|
+
<ul>
|
|
78
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
79
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
80
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
81
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
82
|
+
</ul>
|
|
83
|
+
</details>
|
|
84
|
+
</nav>
|
|
85
|
+
<nav class="gw-footer__col" aria-label="[Resources]">
|
|
86
|
+
<details class="gw-footer__fold">
|
|
87
|
+
<summary><h2 class="gw-footer__heading">[Resources]</h2></summary>
|
|
88
|
+
<ul>
|
|
89
|
+
<li><a href="[/blog]">[Blog]</a></li>
|
|
90
|
+
<li><a href="[/docs]">[Docs]</a></li>
|
|
91
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
92
|
+
<li><a href="[https://another site of the brand]" rel="noopener">[Changelog]<span class="gw-footer__ext" aria-hidden="true"></span></a></li>
|
|
93
|
+
</ul>
|
|
94
|
+
</details>
|
|
95
|
+
</nav>
|
|
96
|
+
<nav class="gw-footer__col" aria-label="[Company]">
|
|
97
|
+
<details class="gw-footer__fold">
|
|
98
|
+
<summary><h2 class="gw-footer__heading">[Company]</h2></summary>
|
|
99
|
+
<ul>
|
|
100
|
+
<li><a href="[/about]">[About]</a></li>
|
|
101
|
+
<li><a href="[/careers]">[Careers]</a></li>
|
|
102
|
+
<li><a href="[/partners]">[Partners]</a></li>
|
|
103
|
+
<li><a href="[/contact]">[Contact]</a></li>
|
|
104
|
+
</ul>
|
|
105
|
+
</details>
|
|
42
106
|
</nav>
|
|
43
107
|
</div>
|
|
108
|
+
<!-- OPTIONAL product family: one <nav> per row, named like its label. Each item is the product's name (<b>) and one
|
|
109
|
+
line (<i>), a space between them. Another GemCommerce brand lists its siblings here (a GemPages site: GemBoss,
|
|
110
|
+
GemX, GemUpsells). -->
|
|
111
|
+
<nav class="gw-footer__family" aria-label="[More from Brand]">
|
|
112
|
+
<details class="gw-footer__fold">
|
|
113
|
+
<summary><h2 class="gw-footer__heading">[More from Brand]</h2></summary>
|
|
114
|
+
<ul>
|
|
115
|
+
<li><a href="[/product]"><b>[Product]</b> <i>[One line on what it is]</i></a></li>
|
|
116
|
+
<li><a href="[/product]"><b>[Product]</b> <i>[One line on what it is]</i></a></li>
|
|
117
|
+
<li><a href="[https://another site of the brand]" rel="noopener"><b>[Product]<span class="gw-footer__ext" aria-hidden="true"></span></b> <i>[One line on what it is]</i></a></li>
|
|
118
|
+
<li><a href="[https://a sibling brand's site]" target="_blank" rel="noopener"><b>[Product]<span class="gw-footer__ext" aria-hidden="true"></span></b> <i>[One line on what it is]</i><span class="gw-visually-hidden"> (opens in a new tab)</span></a></li>
|
|
119
|
+
</ul>
|
|
120
|
+
</details>
|
|
121
|
+
</nav>
|
|
44
122
|
<div class="gw-footer__bottom">
|
|
45
123
|
<span>© [Year] [Company]. All rights reserved.</span>
|
|
46
124
|
<ul aria-label="Legal">
|
|
47
125
|
<li><a href="[/privacy]">Privacy</a></li>
|
|
48
126
|
<li><a href="[/terms]">Terms</a></li>
|
|
49
127
|
</ul>
|
|
128
|
+
<!-- OPTIONAL theme toggle: the header's toggle, here instead of (or as well as) in the header -->
|
|
129
|
+
<button class="gw-theme-toggle gw-footer__toggle" type="button" data-gw-theme-toggle aria-label="Switch to dark theme"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16z" fill="currentColor"/></svg></button>
|
|
50
130
|
<ul aria-label="Social">
|
|
51
131
|
<li><a href="[https://social profile]" rel="noopener">[Network]</a></li>
|
|
132
|
+
<li><a href="[https://social profile]" rel="noopener">[Network]</a></li>
|
|
52
133
|
</ul>
|
|
53
134
|
</div>
|
|
135
|
+
<!-- OPTIONAL wordmark: decoration (aria-hidden="true", the name is already in the brand block). The mark is an
|
|
136
|
+
inline SVG drawn in currentColor, so it takes the faint colour; an <img> cannot, so leave the mark out and keep
|
|
137
|
+
the name. Not shown on phones. -->
|
|
138
|
+
<div class="gw-footer__wordmark" aria-hidden="true">
|
|
139
|
+
<svg viewBox="[0 0 width height]">[the brand's mark, every fill and stroke "currentColor"]</svg>
|
|
140
|
+
<span>[Brand]</span>
|
|
141
|
+
</div>
|
|
54
142
|
</div>
|
|
55
143
|
</footer>
|
package/templates/page.html
CHANGED
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
</section>
|
|
96
96
|
</main>
|
|
97
97
|
|
|
98
|
+
<!-- the short footer; the full one (Ask AI, family rows, theme toggle, wordmark) is templates/footer.html -->
|
|
98
99
|
<footer class="gw-footer">
|
|
99
100
|
<div class="gw-wrap">
|
|
100
101
|
<div class="gw-footer__grid">
|
|
@@ -103,19 +104,23 @@
|
|
|
103
104
|
<p>[One sentence on what the brand does for the reader.]</p>
|
|
104
105
|
</div>
|
|
105
106
|
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
<details class="gw-footer__fold">
|
|
108
|
+
<summary><h2 class="gw-footer__heading">[Column heading]</h2></summary>
|
|
109
|
+
<ul>
|
|
110
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
111
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
112
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
113
|
+
</ul>
|
|
114
|
+
</details>
|
|
112
115
|
</nav>
|
|
113
116
|
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
<details class="gw-footer__fold">
|
|
118
|
+
<summary><h2 class="gw-footer__heading">[Column heading]</h2></summary>
|
|
119
|
+
<ul>
|
|
120
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
121
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
122
|
+
</ul>
|
|
123
|
+
</details>
|
|
119
124
|
</nav>
|
|
120
125
|
</div>
|
|
121
126
|
<div class="gw-footer__bottom">
|
package/website.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.7.0 | generated by scripts/build.mjs from the files in css/. Edit those and not this file */
|
|
2
2
|
/* @gemboss/website-ui tokens. GENERATED from system.json by scripts/build.mjs: do not edit by hand. */
|
|
3
3
|
:root {
|
|
4
4
|
color-scheme: light;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
--gw-text-30: 30px;
|
|
17
17
|
--gw-display-h1: clamp(40px, 4.6vw, 68px);
|
|
18
18
|
--gw-display-h2: clamp(32px, 3.4vw, 52px);
|
|
19
|
+
--gw-display-wordmark: clamp(40px, 9vw, 128px);
|
|
19
20
|
--gw-display-weight: 600;
|
|
20
21
|
--gw-display-tracking: -0.03em;
|
|
21
22
|
--gw-heading-weight: 650;
|
|
@@ -337,6 +338,18 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
|
|
|
337
338
|
}
|
|
338
339
|
.gw-skip:focus { left: 16px; }
|
|
339
340
|
|
|
341
|
+
/* words for a screen reader only (0.7.0): " (opens in a new tab)" in a link with target="_blank" */
|
|
342
|
+
.gw-visually-hidden {
|
|
343
|
+
position: absolute;
|
|
344
|
+
width: 1px;
|
|
345
|
+
height: 1px;
|
|
346
|
+
padding: 0;
|
|
347
|
+
overflow: hidden;
|
|
348
|
+
clip-path: inset(50%);
|
|
349
|
+
white-space: nowrap;
|
|
350
|
+
border: 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
340
353
|
/* scroll reveal: the script adds .is-in; the animation is a failsafe that shows it anyway after 2.5s */
|
|
341
354
|
.gw-reveal {
|
|
342
355
|
opacity: 0;
|
|
@@ -1083,6 +1096,184 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
|
|
|
1083
1096
|
.gw-footer__bottom a { min-height: var(--gw-tap-min); }
|
|
1084
1097
|
}
|
|
1085
1098
|
|
|
1099
|
+
/* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
|
|
1100
|
+
Every part below is optional, per site, and styles only its own markup: a 0.6.0 footer looks exactly as before.
|
|
1101
|
+
In page order: the Ask AI row in the brand block, columns that fold on phones (gap #40), product-family rows, the
|
|
1102
|
+
theme toggle in the bottom row, and the wordmark at the very end. Values measured on gemboss.ai on 25/10/2026 and
|
|
1103
|
+
put on the kit's scales; the full markup is templates/footer.html. */
|
|
1104
|
+
|
|
1105
|
+
/* Ask AI: a label and links that open an assistant with one neutral question already typed (a URL-encoded ?q=), so
|
|
1106
|
+
the answer comes from what the web says, not from a claim the brand wrote. Words, not logos. 28px rows, 44px on
|
|
1107
|
+
touch. */
|
|
1108
|
+
.gw-footer__ask { margin-top: 24px; }
|
|
1109
|
+
/* the row may run wider than the brand's sentence (four names on one line at 390px) */
|
|
1110
|
+
.gw-footer__brand:has(> .gw-footer__ask) { max-width: none; }
|
|
1111
|
+
.gw-footer__brand:has(> .gw-footer__ask) > :not(.gw-footer__ask) { max-width: 320px; }
|
|
1112
|
+
.gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; }
|
|
1113
|
+
.gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
|
|
1114
|
+
.gw-footer__ask a { display: block; padding-block: 4px; color: var(--gw-ink); line-height: 20px; }
|
|
1115
|
+
.gw-footer__ask a:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
|
|
1116
|
+
|
|
1117
|
+
/* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
|
|
1118
|
+
.gw-visually-hidden span, only when the link has target="_blank" */
|
|
1119
|
+
.gw-footer__ext {
|
|
1120
|
+
display: inline-block;
|
|
1121
|
+
width: 12px;
|
|
1122
|
+
height: 12px;
|
|
1123
|
+
margin-left: 4px;
|
|
1124
|
+
vertical-align: -1px;
|
|
1125
|
+
background: var(--gw-muted);
|
|
1126
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
1127
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
|
|
1131
|
+
.gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
|
|
1132
|
+
`open`; the browser opens and closes it, no script needed. From 768px it is the column as before: the summary is
|
|
1133
|
+
only the heading (display: contents: no box, no Tab stop), and the list shows even while the <details> is closed
|
|
1134
|
+
(::details-content; website.js also sets `open` there, for browsers without it). */
|
|
1135
|
+
.gw-footer__fold > summary { list-style: none; }
|
|
1136
|
+
.gw-footer__fold > summary::-webkit-details-marker { display: none; }
|
|
1137
|
+
.gw-footer__fold .gw-footer__heading { margin: 0; line-height: 20px; }
|
|
1138
|
+
.gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
|
|
1139
|
+
.gw-footer__col > .gw-footer__fold a { line-height: 20px; }
|
|
1140
|
+
@media (min-width: 768px) {
|
|
1141
|
+
.gw-footer__fold > summary { display: contents; }
|
|
1142
|
+
.gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
|
|
1143
|
+
}
|
|
1144
|
+
@media (min-width: 768px) {
|
|
1145
|
+
.gw-footer__fold::details-content { content-visibility: visible; }
|
|
1146
|
+
}
|
|
1147
|
+
@media (max-width: 767px) {
|
|
1148
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
|
|
1149
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
|
|
1150
|
+
.gw-footer__col:has(> .gw-footer__fold),
|
|
1151
|
+
.gw-footer__family { border-bottom: 1px solid var(--gw-line); }
|
|
1152
|
+
.gw-footer__brand + .gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
|
|
1153
|
+
.gw-footer__fold > summary {
|
|
1154
|
+
display: flex;
|
|
1155
|
+
align-items: center;
|
|
1156
|
+
justify-content: space-between;
|
|
1157
|
+
gap: 16px;
|
|
1158
|
+
min-height: var(--gw-tap-min);
|
|
1159
|
+
padding-block: 16px;
|
|
1160
|
+
cursor: pointer;
|
|
1161
|
+
}
|
|
1162
|
+
.gw-footer__fold > summary::after {
|
|
1163
|
+
content: "";
|
|
1164
|
+
flex: none;
|
|
1165
|
+
width: 16px;
|
|
1166
|
+
height: 16px;
|
|
1167
|
+
background: var(--gw-muted);
|
|
1168
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
1169
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
1170
|
+
transition: transform 0.18s var(--gw-ease);
|
|
1171
|
+
}
|
|
1172
|
+
.gw-footer__fold[open] > summary::after { transform: rotate(180deg); }
|
|
1173
|
+
.gw-footer__fold > ul { padding-bottom: 12px; }
|
|
1174
|
+
.gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
|
|
1175
|
+
}
|
|
1176
|
+
/* desktop: the brand's track is as wide as gemboss.ai draws it beside five columns (1.2 parts of 6.2, after five 24px
|
|
1177
|
+
gaps), whatever the number of columns, so a family row can line up with it; the columns share the rest */
|
|
1178
|
+
@media (min-width: 1080px) {
|
|
1179
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/* Product family: a labelled row of products, name and one line each (<b> and <i>), as many rows as the brand needs
|
|
1183
|
+
("More from GemBoss", "From the same team, on the Shopify App Store"). A <nav aria-label> holding a fold whose
|
|
1184
|
+
heading is the label. Two items a row on phones, four from 768px; from 1080px the label sits in the brand's track and
|
|
1185
|
+
the items start where the first column starts. */
|
|
1186
|
+
.gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
|
|
1187
|
+
.gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
1188
|
+
.gw-footer__family a {
|
|
1189
|
+
display: flex;
|
|
1190
|
+
flex-direction: column;
|
|
1191
|
+
justify-content: center;
|
|
1192
|
+
gap: 2px;
|
|
1193
|
+
min-height: var(--gw-tap-min);
|
|
1194
|
+
padding: 8px 12px 8px 0;
|
|
1195
|
+
border-radius: var(--gw-radius-action);
|
|
1196
|
+
color: var(--gw-ink);
|
|
1197
|
+
}
|
|
1198
|
+
.gw-footer__family a:hover { background: var(--gw-bg-soft); }
|
|
1199
|
+
.gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
|
|
1200
|
+
.gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
|
|
1201
|
+
@media (min-width: 768px) {
|
|
1202
|
+
.gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
|
|
1203
|
+
.gw-footer__family .gw-footer__heading { margin-bottom: 12px; }
|
|
1204
|
+
.gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px; margin-left: -12px; }
|
|
1205
|
+
.gw-footer__family a { min-height: 0; padding: 8px 12px; }
|
|
1206
|
+
.gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
|
|
1207
|
+
}
|
|
1208
|
+
@media (min-width: 1080px) {
|
|
1209
|
+
.gw-footer__family { display: flow-root; }
|
|
1210
|
+
.gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
|
|
1211
|
+
.gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
|
|
1215
|
+
square with one hairline, its hit area 40px, 44px on touch. It pushes itself and the social links to the right.
|
|
1216
|
+
Below 768px the copyright and the toggle share the first line, legal and social links the second. */
|
|
1217
|
+
.gw-footer__toggle {
|
|
1218
|
+
position: relative;
|
|
1219
|
+
width: 32px;
|
|
1220
|
+
height: 32px;
|
|
1221
|
+
margin-inline: auto 16px;
|
|
1222
|
+
border: 1px solid var(--gw-line-strong);
|
|
1223
|
+
background: var(--gw-surface);
|
|
1224
|
+
}
|
|
1225
|
+
.gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
|
|
1226
|
+
.gw-footer__toggle svg { width: 18px; height: 18px; }
|
|
1227
|
+
@media (max-width: 768px), (pointer: coarse) {
|
|
1228
|
+
.gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
|
|
1229
|
+
}
|
|
1230
|
+
@media (max-width: 767px) {
|
|
1231
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) { column-gap: 16px; }
|
|
1232
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - var(--gw-tap-min) - 16px); }
|
|
1233
|
+
.gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
|
|
1234
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
|
|
1235
|
+
.gw-footer__family + .gw-footer__bottom,
|
|
1236
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) + .gw-footer__bottom { margin-top: 24px; }
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
|
|
1240
|
+
the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
|
|
1241
|
+
page scroll sideways: a name too long for the line is cut. The mark is drawn in currentColor (an inline SVG); an
|
|
1242
|
+
<img> mark keeps its own colours, so leave it out and keep the name. Not on phones, as on gemboss.ai. 24px more room
|
|
1243
|
+
under it than gemboss.ai gives: the kit's footer is a painted band (white like the page), and the breathing gate
|
|
1244
|
+
wants half the letters' size between them and its edge (67px under 123px letters at 1366px). */
|
|
1245
|
+
.gw-footer__wordmark {
|
|
1246
|
+
display: flex;
|
|
1247
|
+
align-items: center;
|
|
1248
|
+
justify-content: center;
|
|
1249
|
+
gap: 24px;
|
|
1250
|
+
padding-block: 48px 24px;
|
|
1251
|
+
color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
|
|
1252
|
+
font-size: var(--gw-display-wordmark);
|
|
1253
|
+
font-weight: 700;
|
|
1254
|
+
letter-spacing: var(--gw-display-tracking);
|
|
1255
|
+
line-height: 1;
|
|
1256
|
+
white-space: nowrap;
|
|
1257
|
+
user-select: none;
|
|
1258
|
+
}
|
|
1259
|
+
.gw-footer__wordmark > svg,
|
|
1260
|
+
.gw-footer__wordmark > img { flex: none; width: auto; height: 0.9em; }
|
|
1261
|
+
.gw-footer__wordmark > span { min-width: 0; overflow-x: clip; }
|
|
1262
|
+
@media (max-width: 767px) { .gw-footer__wordmark { gap: 16px; } }
|
|
1263
|
+
@media (max-width: 479px) { .gw-footer__wordmark { display: none; } }
|
|
1264
|
+
|
|
1265
|
+
@media (max-width: 768px), (pointer: coarse) {
|
|
1266
|
+
.gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
|
|
1267
|
+
}
|
|
1268
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1269
|
+
.gw-footer__fold > summary::after { transition: none; }
|
|
1270
|
+
}
|
|
1271
|
+
/* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
|
|
1272
|
+
@media (forced-colors: active) {
|
|
1273
|
+
.gw-footer__ext,
|
|
1274
|
+
.gw-footer__fold > summary::after { forced-color-adjust: none; background: CanvasText; }
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1086
1277
|
/* sections: hero, logos, stats, features, split, showcase, steps, quote, pricing, CTA. */
|
|
1087
1278
|
|
|
1088
1279
|
/* hero: the first screen. __wash is the one place the wash hues may appear. */
|