@gemboss/website-ui 0.4.0 → 0.6.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 +43 -5
- package/CHANGELOG.md +117 -0
- package/README.md +9 -8
- package/bin/lib/check.mjs +77 -0
- package/css/article.css +237 -0
- package/css/components.css +23 -0
- package/css/footer.css +3 -0
- package/css/forms.css +154 -0
- package/css/prose.css +29 -16
- package/css/ratings.css +78 -0
- package/css/tokens.css +25 -0
- package/demo/article.html +359 -0
- package/demo/assets/article-hero.svg +1 -0
- package/demo/assets/avatar-gemboss.svg +1 -0
- package/demo/assets/avatar-sample.svg +1 -0
- package/demo/assets/post-headless.svg +1 -0
- package/demo/assets/post-motion.svg +1 -0
- package/demo/assets/post-output.svg +1 -0
- package/demo/assets/post-speed.svg +1 -0
- package/demo/assets/post-theme.svg +1 -0
- package/demo/assets/post-voice.svg +1 -0
- package/demo/blog.html +314 -0
- package/demo/forms.html +505 -0
- package/demo/index.html +2 -0
- package/docs/components.md +334 -12
- package/docs/markup-contract.md +123 -7
- package/docs/theming.md +4 -3
- package/js/website.js +68 -8
- package/js/website.mjs +80 -7
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/system.json +12 -1
- package/templates/footer.html +15 -0
- package/templates/sections/article.html +93 -0
- package/templates/sections/form.html +42 -0
- package/templates/sections/post-list.html +65 -0
- package/templates/sections/testimonials.html +58 -0
- package/website.css +553 -17
- 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.6.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.6.0';
|
|
7
7
|
var api = { version: version, init: init, destroy: destroy };
|
|
8
|
-
var doc, root, mine, fine, sys, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
8
|
+
var doc, root, mine, fine, sys, wide, 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 }; }
|
|
@@ -18,8 +18,9 @@ function esc(e) { return e.key === 'Escape'; }
|
|
|
18
18
|
function link(e) { return e.target.closest('a[href]'); }
|
|
19
19
|
function emit(el, type, detail) { if (window.CustomEvent) el.dispatchEvent(new CustomEvent(type, { bubbles: true, detail: detail })); }
|
|
20
20
|
|
|
21
|
-
function each(ctx, sel, kind, fn) {
|
|
22
|
-
|
|
21
|
+
function each(ctx, sel, kind, fn) { wire(within(sel, ctx), kind, fn); }
|
|
22
|
+
function wire(els, kind, fn) {
|
|
23
|
+
els.forEach(function (el) {
|
|
23
24
|
if (live.some(function (r) { return r.el === el && r.kind === kind; })) return;
|
|
24
25
|
var offs = [], r = fn(el, function (t, type, f, opt) {
|
|
25
26
|
on(t, type, f, opt);
|
|
@@ -191,14 +192,73 @@ function segmented(ctx) {
|
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
function toc(ctx) {
|
|
196
|
+
each(ctx, '[data-gw-toc]', 'toc', function (nav, on) {
|
|
197
|
+
var box = nav.querySelector('details'), own = !!(box && box.open), cur = null, io = null;
|
|
198
|
+
var items = all('a[href]', nav).map(function (a) {
|
|
199
|
+
var id = attr(a, 'href', '');
|
|
200
|
+
if (id.charAt(0) !== '#') return null;
|
|
201
|
+
try { id = decodeURIComponent(id.slice(1)); } catch (e) { id = id.slice(1); }
|
|
202
|
+
return { a: a, h: doc.getElementById(id) };
|
|
203
|
+
}).filter(function (x) { return x && x.h; });
|
|
204
|
+
if (!items.length) return;
|
|
205
|
+
function update() {
|
|
206
|
+
var line = window.innerHeight * 0.3, top = -Infinity, next = null;
|
|
207
|
+
items.forEach(function (x) { var t = x.h.getBoundingClientRect().top; if (t <= line && t > top) { top = t; next = x; } });
|
|
208
|
+
if (next === cur) return;
|
|
209
|
+
if (cur) cur.a.removeAttribute('aria-current');
|
|
210
|
+
if (next) next.a.setAttribute('aria-current', 'true');
|
|
211
|
+
cur = next;
|
|
212
|
+
emit(nav, 'gw:toc', { id: next ? next.h.id : '' });
|
|
213
|
+
}
|
|
214
|
+
var r = {
|
|
215
|
+
fit: function () { if (box) box.open = wide.matches || own; },
|
|
216
|
+
stop: function () { if (io) io.disconnect(); if (cur) cur.a.removeAttribute('aria-current'); if (box) box.open = own; }
|
|
217
|
+
};
|
|
218
|
+
on(nav, 'click', function (e) { if (box && !wide.matches && link(e)) box.open = false; });
|
|
219
|
+
if (window.IntersectionObserver) {
|
|
220
|
+
io = new IntersectionObserver(update, { rootMargin: '100000px 0px -70% 0px' });
|
|
221
|
+
items.forEach(function (x) { io.observe(x.h); });
|
|
222
|
+
}
|
|
223
|
+
r.fit();
|
|
224
|
+
return r;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function words(el) { return el ? el.textContent.replace(/\s+/g, ' ').trim() : ''; }
|
|
229
|
+
function tables(ctx) {
|
|
230
|
+
var list = [];
|
|
231
|
+
within('.gw-prose', ctx).forEach(function (p) {
|
|
232
|
+
list = list.concat([].filter.call(p.children, function (t) { return t.tagName === 'TABLE'; }));
|
|
233
|
+
});
|
|
234
|
+
wire(list, 'table', function (t) {
|
|
235
|
+
var name = words(t.querySelector('caption')) || words(t.querySelector('th')) || words(t.querySelector('td'));
|
|
236
|
+
if (!name) return;
|
|
237
|
+
var box = doc.createElement('div'), had = t.classList.contains('is-wrapped');
|
|
238
|
+
box.className = 'gw-prose__table';
|
|
239
|
+
box.setAttribute('role', 'region');
|
|
240
|
+
box.setAttribute('tabindex', '0');
|
|
241
|
+
box.setAttribute('aria-label', name);
|
|
242
|
+
t.parentNode.insertBefore(box, t);
|
|
243
|
+
box.appendChild(t);
|
|
244
|
+
t.classList.add('is-wrapped');
|
|
245
|
+
return { stop: function () {
|
|
246
|
+
var p = box.parentNode;
|
|
247
|
+
if (p) { p.insertBefore(t, box); p.removeChild(box); }
|
|
248
|
+
if (!had) t.classList.remove('is-wrapped');
|
|
249
|
+
} };
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
194
253
|
function install() {
|
|
195
254
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
196
255
|
fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
|
|
197
|
-
var
|
|
256
|
+
var stored = null;
|
|
257
|
+
wide = mq('(min-width: 1080px)');
|
|
198
258
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
199
259
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
200
260
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
201
|
-
watch(wide, function () { if (wide.matches) closeMenus(); });
|
|
261
|
+
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
202
262
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
203
263
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
204
264
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -223,7 +283,7 @@ function init(ctx) {
|
|
|
223
283
|
window.gwWebsite = api;
|
|
224
284
|
if (!doc) run([install]);
|
|
225
285
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
226
|
-
run([mega, mobile, theme, reveal, header, current, segmented], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
286
|
+
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
227
287
|
}
|
|
228
288
|
|
|
229
289
|
/** 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.6.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, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
12
|
+
var doc, root, mine, fine, sys, wide, 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 }; }
|
|
@@ -27,8 +27,10 @@ function emit(el, type, detail) { if (window.CustomEvent) el.dispatchEvent(new C
|
|
|
27
27
|
// with the `on` it is given (so destroy can remove them) and returns a record, or nothing when the element cannot be
|
|
28
28
|
// wired yet (the next init tries again). Records live in `live`. data-gw-ready lists the kinds wired on an element;
|
|
29
29
|
// the record, not the attribute, decides, so markup copied from a live page, attribute and all, is still wired.
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// wire() does the same for a list of elements a selector cannot name (the tables straight inside a .gw-prose).
|
|
31
|
+
function each(ctx, sel, kind, fn) { wire(within(sel, ctx), kind, fn); }
|
|
32
|
+
function wire(els, kind, fn) {
|
|
33
|
+
els.forEach(function (el) {
|
|
32
34
|
if (live.some(function (r) { return r.el === el && r.kind === kind; })) return;
|
|
33
35
|
var offs = [], r = fn(el, function (t, type, f, opt) {
|
|
34
36
|
on(t, type, f, opt);
|
|
@@ -215,15 +217,86 @@ function segmented(ctx) {
|
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
// Contents (.gw-toc, data-gw-toc): links to the #ids of the article's headings, plain links that work without the
|
|
221
|
+
// script. The section in view is the one whose heading last went above the line 30% down the window: its link gets
|
|
222
|
+
// aria-current="true" and the nav fires gw:toc ({ id }, '' above the first heading). One IntersectionObserver per
|
|
223
|
+
// list sees a heading cross that line, both ways; its root runs from far above the window down to the line, so a jump
|
|
224
|
+
// across many sections (a link, End, a dragged scrollbar) still counts. Without IntersectionObserver nothing is
|
|
225
|
+
// marked. The <details> is open from 1080px (the list sits beside the article), below that it keeps the markup's own
|
|
226
|
+
// state (folded) and a link inside folds it before the page moves.
|
|
227
|
+
function toc(ctx) {
|
|
228
|
+
each(ctx, '[data-gw-toc]', 'toc', function (nav, on) {
|
|
229
|
+
var box = nav.querySelector('details'), own = !!(box && box.open), cur = null, io = null;
|
|
230
|
+
var items = all('a[href]', nav).map(function (a) {
|
|
231
|
+
var id = attr(a, 'href', '');
|
|
232
|
+
if (id.charAt(0) !== '#') return null;
|
|
233
|
+
try { id = decodeURIComponent(id.slice(1)); } catch (e) { id = id.slice(1); }
|
|
234
|
+
return { a: a, h: doc.getElementById(id) };
|
|
235
|
+
}).filter(function (x) { return x && x.h; });
|
|
236
|
+
if (!items.length) return;
|
|
237
|
+
function update() {
|
|
238
|
+
var line = window.innerHeight * 0.3, top = -Infinity, next = null;
|
|
239
|
+
items.forEach(function (x) { var t = x.h.getBoundingClientRect().top; if (t <= line && t > top) { top = t; next = x; } });
|
|
240
|
+
if (next === cur) return;
|
|
241
|
+
if (cur) cur.a.removeAttribute('aria-current');
|
|
242
|
+
if (next) next.a.setAttribute('aria-current', 'true');
|
|
243
|
+
cur = next;
|
|
244
|
+
emit(nav, 'gw:toc', { id: next ? next.h.id : '' });
|
|
245
|
+
}
|
|
246
|
+
var r = {
|
|
247
|
+
fit: function () { if (box) box.open = wide.matches || own; },
|
|
248
|
+
stop: function () { if (io) io.disconnect(); if (cur) cur.a.removeAttribute('aria-current'); if (box) box.open = own; }
|
|
249
|
+
};
|
|
250
|
+
on(nav, 'click', function (e) { if (box && !wide.matches && link(e)) box.open = false; });
|
|
251
|
+
if (window.IntersectionObserver) {
|
|
252
|
+
io = new IntersectionObserver(update, { rootMargin: '100000px 0px -70% 0px' });
|
|
253
|
+
items.forEach(function (x) { io.observe(x.h); });
|
|
254
|
+
}
|
|
255
|
+
r.fit();
|
|
256
|
+
return r;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Prose tables (gap #63): each <table> straight inside a .gw-prose goes into a scroll box of its own,
|
|
261
|
+
// div.gw-prose__table, a region named after its caption, else its first header cell, else its first cell, with
|
|
262
|
+
// tabindex="0" so a keyboard can scroll it. The table is marked .is-wrapped and keeps display: table. A table with no
|
|
263
|
+
// words to name it is left as it is (it scrolls by itself, as in 0.2.0). destroy() takes the box away again.
|
|
264
|
+
function words(el) { return el ? el.textContent.replace(/\s+/g, ' ').trim() : ''; }
|
|
265
|
+
function tables(ctx) {
|
|
266
|
+
var list = [];
|
|
267
|
+
within('.gw-prose', ctx).forEach(function (p) {
|
|
268
|
+
list = list.concat([].filter.call(p.children, function (t) { return t.tagName === 'TABLE'; }));
|
|
269
|
+
});
|
|
270
|
+
wire(list, 'table', function (t) {
|
|
271
|
+
var name = words(t.querySelector('caption')) || words(t.querySelector('th')) || words(t.querySelector('td'));
|
|
272
|
+
if (!name) return;
|
|
273
|
+
var box = doc.createElement('div'), had = t.classList.contains('is-wrapped');
|
|
274
|
+
box.className = 'gw-prose__table';
|
|
275
|
+
box.setAttribute('role', 'region');
|
|
276
|
+
box.setAttribute('tabindex', '0');
|
|
277
|
+
box.setAttribute('aria-label', name);
|
|
278
|
+
t.parentNode.insertBefore(box, t);
|
|
279
|
+
box.appendChild(t);
|
|
280
|
+
t.classList.add('is-wrapped');
|
|
281
|
+
return { stop: function () {
|
|
282
|
+
var p = box.parentNode;
|
|
283
|
+
if (p) { p.insertBefore(t, box); p.removeChild(box); }
|
|
284
|
+
if (!had) t.classList.remove('is-wrapped');
|
|
285
|
+
} };
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
218
289
|
// Once per page: what belongs to the document, the window or a media query rather than to one element.
|
|
219
290
|
function install() {
|
|
220
291
|
doc = document; root = doc.documentElement; mine = new WeakSet();
|
|
221
292
|
fine = mq('(hover: hover) and (pointer: fine)'); sys = mq('(prefers-color-scheme: dark)');
|
|
222
|
-
var
|
|
293
|
+
var stored = null;
|
|
294
|
+
// 1080px, the kit's desktop width: the header shows its nav, the contents sit beside the article
|
|
295
|
+
wide = mq('(min-width: 1080px)');
|
|
223
296
|
on(doc, 'click', function (e) { if (megaOpen && !inside(megaOpen, e.target)) hideMega(); });
|
|
224
297
|
on(doc, 'keydown', function (e) { if (megaOpen && esc(e)) hideMega(inside(megaOpen, doc.activeElement) || megaOpen.mode === 'click'); });
|
|
225
298
|
on(doc, 'keydown', function (e) { if (esc(e)) closeMenus(true); });
|
|
226
|
-
watch(wide, function () { if (wide.matches) closeMenus(); });
|
|
299
|
+
watch(wide, function () { if (wide.matches) closeMenus(); records('toc', function (r) { r.fit(); }); });
|
|
227
300
|
on(window, 'pageshow', function (e) { if (e.persisted) closeMenus(); });
|
|
228
301
|
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
229
302
|
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
@@ -254,7 +327,7 @@ function init(ctx) {
|
|
|
254
327
|
if (!doc) run([install]);
|
|
255
328
|
// an element removed from the page since the last init is let go
|
|
256
329
|
live.slice().forEach(function (r) { if (r.el.isConnected === false) drop(r); });
|
|
257
|
-
run([mega, mobile, theme, reveal, header, current, segmented], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
330
|
+
run([mega, mobile, theme, reveal, header, current, segmented, toc, tables], ctx && ctx.querySelectorAll ? ctx : doc);
|
|
258
331
|
}
|
|
259
332
|
|
|
260
333
|
/** 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.6.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
|
@@ -17,6 +17,14 @@ the kit and its rules are how the page ends up looking designed. Compose from th
|
|
|
17
17
|
3. **Build**:
|
|
18
18
|
- New page: copy `KIT/templates/page.html`, then add sections from `KIT/templates/sections/` in the order the page
|
|
19
19
|
needs. Fill the words, delete what you did not use.
|
|
20
|
+
- An article page (a blog post, a guide) is `KIT/templates/sections/article.html`: breadcrumbs, the meta row, a
|
|
21
|
+
contents list from the article's h2s, the prose, callouts, the FAQ last. Never a callout told by its colour
|
|
22
|
+
alone (it keeps its label: Note, Tip, Warning). The blog index is `KIT/templates/sections/post-list.html`.
|
|
23
|
+
- A form asks as little as it can: one field beats a form, a form keeps two or three fields
|
|
24
|
+
(`KIT/templates/sections/form.html`; one field and its button: `.gw-form-inline`). Label above, the error in
|
|
25
|
+
words under it with `aria-invalid="true"`; the page decides when, the kit draws it.
|
|
26
|
+
- Ratings and testimonials (`KIT/templates/sections/testimonials.html`): only real ones, each linked to the page it
|
|
27
|
+
comes from, the reviewer's words exactly; stars carry their value in words (`aria-label="4.9 out of 5"`).
|
|
20
28
|
- Changed page: keep its kit classes; change words, order and choice of sections.
|
|
21
29
|
- Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
|
|
22
30
|
scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
|
package/system.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"muted": "#686b71",
|
|
12
12
|
"line": "#e5e7ea",
|
|
13
13
|
"line-strong": "#d5d8dd",
|
|
14
|
+
"line-control": "#85888e",
|
|
14
15
|
"action": "#1b1c1e",
|
|
15
16
|
"action-hover": "#000000",
|
|
16
17
|
"on-action": "#ffffff",
|
|
@@ -21,6 +22,10 @@
|
|
|
21
22
|
"success": "#007f5f",
|
|
22
23
|
"success-soft": "#e3f1df",
|
|
23
24
|
"warning": "#9a6700",
|
|
25
|
+
"warning-soft": "#fbf1dc",
|
|
26
|
+
"danger": "#b42318",
|
|
27
|
+
"danger-soft": "#fff0e9",
|
|
28
|
+
"on-danger": "#ffffff",
|
|
24
29
|
"night": "#0c0e14",
|
|
25
30
|
"night-2": "#141722",
|
|
26
31
|
"on-night": "#e3e4e8",
|
|
@@ -35,6 +40,7 @@
|
|
|
35
40
|
"muted": "#9a9ca6",
|
|
36
41
|
"line": "#2a2b33",
|
|
37
42
|
"line-strong": "#383944",
|
|
43
|
+
"line-control": "#6a6d78",
|
|
38
44
|
"action": "#f2f3f5",
|
|
39
45
|
"action-hover": "#ffffff",
|
|
40
46
|
"on-action": "#0d0d12",
|
|
@@ -45,6 +51,10 @@
|
|
|
45
51
|
"success": "#34c38f",
|
|
46
52
|
"success-soft": "#132a22",
|
|
47
53
|
"warning": "#e0b341",
|
|
54
|
+
"warning-soft": "#2b2410",
|
|
55
|
+
"danger": "#ff6b5a",
|
|
56
|
+
"danger-soft": "#2e1714",
|
|
57
|
+
"on-danger": "#0d0d12",
|
|
48
58
|
"night": "#0a0b10",
|
|
49
59
|
"night-2": "#12141c",
|
|
50
60
|
"on-night": "#e3e4e8",
|
|
@@ -60,7 +70,8 @@
|
|
|
60
70
|
"roles": {
|
|
61
71
|
"action": "The thing you click: ink, or the brand's accent when the site chose the accent action profile (profiles.action).",
|
|
62
72
|
"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.",
|
|
63
|
-
"success/warning": "State only.",
|
|
73
|
+
"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
|
+
"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.",
|
|
64
75
|
"wash": "The hero wash and illustrations only."
|
|
65
76
|
}
|
|
66
77
|
},
|
package/templates/footer.html
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
Footer: one per site, the same on every page. A brand block, three to five link columns, a bottom row.
|
|
3
3
|
Limits: column headings are one or two words in sentence case; four to seven links per column; the bottom row
|
|
4
4
|
holds the copyright, the legal links and the social links, nothing else. Replace every [bracketed] placeholder.
|
|
5
|
+
The newsletter form in the brand block is optional (0.6.0): keep it only when it is wired to a list that keeps the
|
|
6
|
+
address and sends what the note promises; otherwise delete the <form>. The note says how often and how to stop, and
|
|
7
|
+
links to the privacy policy. No box ticked for the reader, no "No thanks, I hate saving money". On send: the page
|
|
8
|
+
sets aria-invalid="true" and the error's words, or aria-busy on the button and then a role="status" line.
|
|
5
9
|
-->
|
|
6
10
|
<footer class="gw-footer">
|
|
7
11
|
<div class="gw-wrap">
|
|
@@ -9,6 +13,17 @@
|
|
|
9
13
|
<div class="gw-footer__brand">
|
|
10
14
|
<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>
|
|
11
15
|
<p>[One sentence on what the brand does for the reader.]</p>
|
|
16
|
+
<form class="gw-footer__newsletter" action="[/newsletter endpoint]" method="post" novalidate>
|
|
17
|
+
<div class="gw-field">
|
|
18
|
+
<label class="gw-field__label" for="footer-email">[Product news by email]</label>
|
|
19
|
+
<div class="gw-form-inline">
|
|
20
|
+
<input class="gw-input" id="footer-email" name="email" type="email" autocomplete="email" required aria-describedby="footer-email-error footer-email-note">
|
|
21
|
+
<button class="gw-btn gw-btn--secondary" type="submit">[Subscribe]</button>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="gw-field__error" id="footer-email-error"></p>
|
|
24
|
+
<p class="gw-field__hint" id="footer-email-note">[How often you write.] Unsubscribe in one click. <a href="[/privacy]">Privacy policy</a></p>
|
|
25
|
+
</div>
|
|
26
|
+
</form>
|
|
12
27
|
</div>
|
|
13
28
|
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
14
29
|
<h2 class="gw-footer__heading">[Column heading]</h2>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Article: a blog post or a guide, the whole <main> of the page. In this order: breadcrumbs, the title (the page's one
|
|
3
|
+
<h1>), the meta row, a hero image, the contents list, the article's own HTML in .gw-prose, the FAQ last.
|
|
4
|
+
- Breadcrumbs: every level above the page, then the page itself with aria-current="page". No separator characters:
|
|
5
|
+
the kit draws them. A BreadcrumbList JSON-LD is up to the site (example: docs/components.md, Breadcrumbs).
|
|
6
|
+
- Meta row: each item a <span>; the date as <time datetime="YYYY-MM-DD">, "Updated" when it changed after it was
|
|
7
|
+
published; the read time at about 200 words a minute. The avatar is optional (alt="": the name is written next
|
|
8
|
+
to it).
|
|
9
|
+
- Hero: the post's own image, with width and height, not lazy (it is the first screen). Leave the figure out when
|
|
10
|
+
the post has none.
|
|
11
|
+
- Contents: one link per <h2> of the article (a nested <ol> for its <h3>s, if any), each to the heading's id, in the
|
|
12
|
+
article's order, with the heading's words. data-gw-toc lets website.js mark the section in view and fold the list
|
|
13
|
+
on phones; the links work without it. check reports a link to an id that is not on the page.
|
|
14
|
+
- Callouts: a note, a tip or a warning, one idea each, at most one every few sections. Always the label in words
|
|
15
|
+
(Note, Tip, Warning, or the page's language): never a callout told by its colour alone.
|
|
16
|
+
- FAQ: the last <h2> of the article, real questions with short answers. FAQPage JSON-LD only if every answer is
|
|
17
|
+
visible on the page (example: docs/components.md, Article).
|
|
18
|
+
Limits: one article per page; headings in order (h2, then h3), each <h2> with an id; tables straight in the prose
|
|
19
|
+
are boxed by website.js (gap #63), give them a <caption>.
|
|
20
|
+
Then a post list (templates/sections/post-list.html, "More from the blog") and the closing CTA, if the page needs one.
|
|
21
|
+
-->
|
|
22
|
+
<article class="gw-wrap gw-article">
|
|
23
|
+
<header class="gw-article__head">
|
|
24
|
+
<nav class="gw-breadcrumbs" aria-label="Breadcrumb">
|
|
25
|
+
<ol>
|
|
26
|
+
<li><a href="[/blog]">[Blog]</a></li>
|
|
27
|
+
<li><a href="[/blog/category]">[Category]</a></li>
|
|
28
|
+
<li><a href="[/blog/this-post]" aria-current="page">[The post's title]</a></li>
|
|
29
|
+
</ol>
|
|
30
|
+
</nav>
|
|
31
|
+
<h1>[The post's title]</h1>
|
|
32
|
+
<p class="gw-article-meta">
|
|
33
|
+
<span><img class="gw-article-meta__avatar" src="[/img/author.webp]" alt="" width="24" height="24">[Author or team]</span>
|
|
34
|
+
<span><a href="[/blog/category]">[Category]</a></span>
|
|
35
|
+
<span>[Updated] <time datetime="[2026-10-25]">[October 25, 2026]</time></span>
|
|
36
|
+
<span>[5] min read</span>
|
|
37
|
+
</p>
|
|
38
|
+
</header>
|
|
39
|
+
<figure class="gw-article__hero">
|
|
40
|
+
<img src="[/img/post-hero.webp]" alt="[What the picture shows]" width="[1200]" height="[675]" fetchpriority="high">
|
|
41
|
+
</figure>
|
|
42
|
+
<nav class="gw-toc" aria-label="[On this page]" data-gw-toc>
|
|
43
|
+
<details>
|
|
44
|
+
<summary>[On this page]</summary>
|
|
45
|
+
<ol>
|
|
46
|
+
<li><a href="#[first-section]">[The first section's heading]</a></li>
|
|
47
|
+
<li><a href="#[second-section]">[The second section's heading]</a>
|
|
48
|
+
<ol>
|
|
49
|
+
<li><a href="#[a-step]">[A step inside it]</a></li>
|
|
50
|
+
</ol>
|
|
51
|
+
</li>
|
|
52
|
+
<li><a href="#faq">[Frequently asked questions]</a></li>
|
|
53
|
+
</ol>
|
|
54
|
+
</details>
|
|
55
|
+
</nav>
|
|
56
|
+
<div class="gw-prose">
|
|
57
|
+
<p>[The first paragraph: what the reader will know or be able to do by the end.]</p>
|
|
58
|
+
<h2 id="[first-section]">[The first section's heading]</h2>
|
|
59
|
+
<p>[Paragraphs of two to five sentences. Links go where they say: <a href="[link]">[the setup guide]</a>.]</p>
|
|
60
|
+
<div class="gw-callout gw-callout--note" role="note">
|
|
61
|
+
<p class="gw-callout__label">[Note]</p>
|
|
62
|
+
<p>[Something the reader should keep in mind, in one or two sentences.]</p>
|
|
63
|
+
</div>
|
|
64
|
+
<h2 id="[second-section]">[The second section's heading]</h2>
|
|
65
|
+
<p>[A paragraph.]</p>
|
|
66
|
+
<h3 id="[a-step]">[A step inside it]</h3>
|
|
67
|
+
<p>[A paragraph.]</p>
|
|
68
|
+
<table>
|
|
69
|
+
<caption>[What the table compares]</caption>
|
|
70
|
+
<thead><tr><th scope="col">[Column]</th><th scope="col">[Column]</th></tr></thead>
|
|
71
|
+
<tbody><tr><th scope="row">[Row]</th><td>[Value]</td></tr></tbody>
|
|
72
|
+
</table>
|
|
73
|
+
<div class="gw-callout gw-callout--tip" role="note">
|
|
74
|
+
<p class="gw-callout__label">[Tip]</p>
|
|
75
|
+
<p>[A shortcut or a better way, in one or two sentences.]</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="gw-callout gw-callout--warning" role="note">
|
|
78
|
+
<p class="gw-callout__label">[Warning]</p>
|
|
79
|
+
<p>[What goes wrong if the reader skips this, in one or two sentences.]</p>
|
|
80
|
+
</div>
|
|
81
|
+
<h2 id="faq">[Frequently asked questions]</h2>
|
|
82
|
+
<div class="gw-faq">
|
|
83
|
+
<details class="gw-faq__item">
|
|
84
|
+
<summary>[A question the reader asks?]</summary>
|
|
85
|
+
<p>[The answer in two to four sentences.]</p>
|
|
86
|
+
</details>
|
|
87
|
+
<details class="gw-faq__item">
|
|
88
|
+
<summary>[A question the reader asks?]</summary>
|
|
89
|
+
<p>[The answer.]</p>
|
|
90
|
+
</details>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</article>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Form: a short form beside the reason to send it (a contact form, "get the guide", a demo request). Ask as little as
|
|
3
|
+
you can: one field beats a form, and a form keeps two or three fields. A question with a good guess is a choice the
|
|
4
|
+
reader confirms (a radio group with the guess checked), not a box to fill.
|
|
5
|
+
- Every field: its <label for> above the control, the error right under it, then the hint; aria-describedby names
|
|
6
|
+
the error first, then the hint. Mark the optional fields "(optional)" in the label; the placeholder is never the label.
|
|
7
|
+
- Validation is the page's: on send, set aria-invalid="true" on each wrong field and write in its .gw-field__error
|
|
8
|
+
what to do ("Enter your work email, like name@example.com"), then move the focus to the first one. The kit adds the
|
|
9
|
+
icon and the colour; the words say it, never the colour alone. Keep the empty error element in the markup.
|
|
10
|
+
- While it is sent: aria-busy="true" on the button. When the answer comes: say what happened in words in the
|
|
11
|
+
role="status" line ("Sent. We'll reply to name@example.com.").
|
|
12
|
+
- Consent boxes start unchecked, say exactly what the reader gets, and are never required to get something else.
|
|
13
|
+
- One field and its button (a newsletter, "email me the guide"): the .gw-form-inline row, see templates/footer.html.
|
|
14
|
+
Limits: three fields, one .gw-btn; no field that the team does not read.
|
|
15
|
+
-->
|
|
16
|
+
<section class="gw-section" aria-labelledby="[form-h]">
|
|
17
|
+
<div class="gw-wrap gw-split">
|
|
18
|
+
<div>
|
|
19
|
+
<p class="gw-label">[Contact]</p>
|
|
20
|
+
<h2 id="[form-h]"><span class="gw-dim">[The reader's question.]</span> [What sending this gets them.]</h2>
|
|
21
|
+
<p class="gw-lead">[One line: who answers and how.]</p>
|
|
22
|
+
</div>
|
|
23
|
+
<form class="gw-card gw-form" action="[/endpoint]" method="post" novalidate>
|
|
24
|
+
<div class="gw-field">
|
|
25
|
+
<label class="gw-field__label" for="[form-email]">[Work email]</label>
|
|
26
|
+
<input class="gw-input" id="[form-email]" name="email" type="email" autocomplete="email" required aria-describedby="[form-email]-error">
|
|
27
|
+
<p class="gw-field__error" id="[form-email]-error"></p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="gw-field">
|
|
30
|
+
<label class="gw-field__label" for="[form-message]">[What are you building?]</label>
|
|
31
|
+
<textarea class="gw-textarea" id="[form-message]" name="message" rows="3" required aria-describedby="[form-message]-error [form-message]-hint"></textarea>
|
|
32
|
+
<p class="gw-field__error" id="[form-message]-error"></p>
|
|
33
|
+
<p class="gw-field__hint" id="[form-message]-hint">[A sentence or two is enough.]</p>
|
|
34
|
+
</div>
|
|
35
|
+
<label class="gw-check"><input type="checkbox" name="[news]" value="yes"><span>[Also send me product news, once a month]</span></label>
|
|
36
|
+
<div class="gw-actions">
|
|
37
|
+
<button class="gw-btn" type="submit">[Send the request]</button>
|
|
38
|
+
<p class="gw-note" role="status"></p>
|
|
39
|
+
</div>
|
|
40
|
+
</form>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Post list: the blog index, a category page, or "More from the blog" under an article. Cards with a picture
|
|
3
|
+
(.gw-card__media, gap #33), the category as a .gw-label, the title, one or two lines of excerpt and the meta row.
|
|
4
|
+
- The title's link covers the whole card, so the card is one target and its name is the title alone: no other link
|
|
5
|
+
or button inside a card. The picture is alt="" here (the title says what the post is) and loading="lazy".
|
|
6
|
+
- The title is one level below the section's heading (h3 under an h2; h2 on the index, under its h1).
|
|
7
|
+
- --wide on the picture for 1200 x 630 share images (1.91:1), --square for 1:1; the default is 16:9.
|
|
8
|
+
- .gw-post-list--compact: rows without cards or pictures, for a long list or a sidebar.
|
|
9
|
+
- Pagination: previous and next as links (rel="prev" / rel="next"), the page number between them as words. Leave
|
|
10
|
+
out the link that does not exist (no "Newer posts" on page 1).
|
|
11
|
+
Limits: three, six or nine cards in the grid; the same picture shape for every card of a list; real posts, real
|
|
12
|
+
dates.
|
|
13
|
+
-->
|
|
14
|
+
<section class="gw-section" aria-labelledby="[posts-h]">
|
|
15
|
+
<div class="gw-wrap">
|
|
16
|
+
<div class="gw-section__head">
|
|
17
|
+
<p class="gw-label">[Blog]</p>
|
|
18
|
+
<h2 id="[posts-h]">[More from the blog]</h2>
|
|
19
|
+
</div>
|
|
20
|
+
<ul class="gw-post-list" role="list">
|
|
21
|
+
<li>
|
|
22
|
+
<article class="gw-card gw-post">
|
|
23
|
+
<div class="gw-card__media"><img src="[/img/post.webp]" alt="" width="[640]" height="[360]" loading="lazy"></div>
|
|
24
|
+
<p class="gw-label">[Category]</p>
|
|
25
|
+
<h3 class="gw-post__title"><a href="[/blog/post]">[The post's title]</a></h3>
|
|
26
|
+
<p>[One or two lines on what the reader gets from it.]</p>
|
|
27
|
+
<p class="gw-article-meta"><span><time datetime="[2026-10-25]">[Oct 25, 2026]</time></span><span>[5] min read</span></p>
|
|
28
|
+
</article>
|
|
29
|
+
</li>
|
|
30
|
+
<li>
|
|
31
|
+
<article class="gw-card gw-post">
|
|
32
|
+
<div class="gw-card__media"><img src="[/img/post.webp]" alt="" width="[640]" height="[360]" loading="lazy"></div>
|
|
33
|
+
<p class="gw-label">[Category]</p>
|
|
34
|
+
<h3 class="gw-post__title"><a href="[/blog/post]">[The post's title]</a></h3>
|
|
35
|
+
<p>[One or two lines on what the reader gets from it.]</p>
|
|
36
|
+
<p class="gw-article-meta"><span><time datetime="[2026-10-25]">[Oct 25, 2026]</time></span><span>[5] min read</span></p>
|
|
37
|
+
</article>
|
|
38
|
+
</li>
|
|
39
|
+
<li>
|
|
40
|
+
<article class="gw-card gw-post">
|
|
41
|
+
<div class="gw-card__media"><img src="[/img/post.webp]" alt="" width="[640]" height="[360]" loading="lazy"></div>
|
|
42
|
+
<p class="gw-label">[Category]</p>
|
|
43
|
+
<h3 class="gw-post__title"><a href="[/blog/post]">[The post's title]</a></h3>
|
|
44
|
+
<p>[One or two lines on what the reader gets from it.]</p>
|
|
45
|
+
<p class="gw-article-meta"><span><time datetime="[2026-10-25]">[Oct 25, 2026]</time></span><span>[5] min read</span></p>
|
|
46
|
+
</article>
|
|
47
|
+
</li>
|
|
48
|
+
</ul>
|
|
49
|
+
<nav class="gw-pagination" aria-label="[Blog pages]">
|
|
50
|
+
<a class="gw-btn gw-btn--secondary" href="[/blog?page=1]" rel="prev">[Newer posts]</a>
|
|
51
|
+
<p class="gw-pagination__status">[Page 2 of 12]</p>
|
|
52
|
+
<a class="gw-btn gw-btn--secondary" href="[/blog?page=3]" rel="next">[Older posts]</a>
|
|
53
|
+
</nav>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<!-- the compact list: rows, no picture -->
|
|
58
|
+
<ul class="gw-post-list gw-post-list--compact" role="list">
|
|
59
|
+
<li>
|
|
60
|
+
<article class="gw-card gw-post">
|
|
61
|
+
<h3 class="gw-post__title"><a href="[/blog/post]">[The post's title]</a></h3>
|
|
62
|
+
<p class="gw-article-meta"><span>[Category]</span><span><time datetime="[2026-10-25]">[Oct 25, 2026]</time></span><span>[5] min read</span></p>
|
|
63
|
+
</article>
|
|
64
|
+
</li>
|
|
65
|
+
</ul>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Testimonials: a rating badge and a wall of short reviews. Every one comes from a page anyone can open, and links to
|
|
3
|
+
it: the badge to the store's review page, each card to its review. Copy only what that page shows: the score and
|
|
4
|
+
the count on the day you copy them (update them when they change), the reviewer's words exactly (cut a long one
|
|
5
|
+
with an ellipsis, never reword it), their name and store as they wrote them. No review you wrote, no score you
|
|
6
|
+
averaged yourself, no source you cannot link: leave the card out. `check` warns about a badge or a card without
|
|
7
|
+
its link (rating-source).
|
|
8
|
+
- Stars: style="--gw-stars: N" and aria-label="N out of 5", the same number (0 to 5, one decimal). A number you also
|
|
9
|
+
show beside them is aria-hidden="true", so it is not read twice.
|
|
10
|
+
- The badge's icon: the generic bag below, or the store's own mark only where its brand rules allow it.
|
|
11
|
+
- Avatars are optional: a photo the reviewer made public with the review, alt="" (the name is beside it).
|
|
12
|
+
Limits: three or six cards; one to three sentences each; one badge.
|
|
13
|
+
-->
|
|
14
|
+
<section class="gw-section gw-section--soft" aria-labelledby="[reviews-h]">
|
|
15
|
+
<div class="gw-wrap">
|
|
16
|
+
<div class="gw-section__head">
|
|
17
|
+
<p class="gw-label">[Reviews]</p>
|
|
18
|
+
<h2 id="[reviews-h]"><span class="gw-dim">[What merchants say.]</span> [In their own words.]</h2>
|
|
19
|
+
<a class="gw-rating-badge" href="[https://apps.shopify.com/your-app/reviews]">
|
|
20
|
+
<span class="gw-rating-badge__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="M5 8h14l-1 12H6z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></span>
|
|
21
|
+
<span class="gw-rating-badge__body">
|
|
22
|
+
<span class="gw-rating-badge__score"><span aria-hidden="true">[4.9]</span><span class="gw-stars" role="img" aria-label="[4.9] out of 5" style="--gw-stars: 4.9"></span></span>
|
|
23
|
+
<span class="gw-rating-badge__count">[1,234] reviews on the [Shopify App Store]</span>
|
|
24
|
+
</span>
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
27
|
+
<ul class="gw-testimonials" role="list">
|
|
28
|
+
<li>
|
|
29
|
+
<figure class="gw-card gw-testimonial">
|
|
30
|
+
<span class="gw-stars" role="img" aria-label="[5] out of 5" style="--gw-stars: 5"></span>
|
|
31
|
+
<blockquote class="gw-testimonial__quote"><p>[The reviewer's exact words.]</p></blockquote>
|
|
32
|
+
<figcaption class="gw-testimonial__by">
|
|
33
|
+
<img class="gw-testimonial__avatar" src="[/img/reviewer.webp]" alt="" width="40" height="40" loading="lazy">
|
|
34
|
+
<span><b>[Name]</b>[Store] · <a href="[https://link to the review]">[Shopify App Store]</a></span>
|
|
35
|
+
</figcaption>
|
|
36
|
+
</figure>
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
<figure class="gw-card gw-testimonial">
|
|
40
|
+
<span class="gw-stars" role="img" aria-label="[5] out of 5" style="--gw-stars: 5"></span>
|
|
41
|
+
<blockquote class="gw-testimonial__quote"><p>[The reviewer's exact words.]</p></blockquote>
|
|
42
|
+
<figcaption class="gw-testimonial__by">
|
|
43
|
+
<span><b>[Name]</b>[Store] · <a href="[https://link to the review]">[Shopify App Store]</a></span>
|
|
44
|
+
</figcaption>
|
|
45
|
+
</figure>
|
|
46
|
+
</li>
|
|
47
|
+
<li>
|
|
48
|
+
<figure class="gw-card gw-testimonial">
|
|
49
|
+
<span class="gw-stars" role="img" aria-label="[5] out of 5" style="--gw-stars: 5"></span>
|
|
50
|
+
<blockquote class="gw-testimonial__quote"><p>[The reviewer's exact words.]</p></blockquote>
|
|
51
|
+
<figcaption class="gw-testimonial__by">
|
|
52
|
+
<span><b>[Name]</b>[Store] · <a href="[https://link to the review]">[Shopify App Store]</a></span>
|
|
53
|
+
</figcaption>
|
|
54
|
+
</figure>
|
|
55
|
+
</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|