@gemboss/website-ui 0.0.0-stage → 0.1.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 +174 -0
- package/CHANGELOG.md +19 -0
- package/README.md +117 -2
- package/bin/gemboss-website.mjs +78 -0
- package/bin/lib/check.mjs +642 -0
- package/bin/lib/gap.mjs +189 -0
- package/bin/lib/init.mjs +99 -0
- package/bin/lib/upgrade.mjs +155 -0
- package/bin/lib/util.mjs +147 -0
- package/css/base.css +107 -0
- package/css/components.css +236 -0
- package/css/footer.css +53 -0
- package/css/header.css +231 -0
- package/css/layout.css +59 -0
- package/css/sections.css +173 -0
- package/css/tokens.css +198 -0
- package/demo/index.html +629 -0
- package/demo/theme-example.html +133 -0
- package/docs/components.md +299 -0
- package/docs/markup-contract.md +115 -0
- package/docs/philosophy.md +69 -0
- package/docs/theming.md +95 -0
- package/js/website.js +149 -0
- package/package.json +50 -4
- package/skill/SKILL.md +48 -0
- package/system.json +120 -0
- package/templates/footer.html +40 -0
- package/templates/header.html +60 -0
- package/templates/page.html +134 -0
- package/templates/sections/cta.html +15 -0
- package/templates/sections/faq.html +24 -0
- package/templates/sections/features.html +32 -0
- package/templates/sections/hero.html +25 -0
- package/templates/sections/logos.html +15 -0
- package/templates/sections/pricing.html +39 -0
- package/templates/sections/quote.html +13 -0
- package/templates/sections/showcase.html +24 -0
- package/templates/sections/split.html +25 -0
- package/templates/sections/stats.html +26 -0
- package/templates/sections/steps.html +17 -0
- package/website.css +1064 -0
- package/website.min.css +1 -0
package/docs/theming.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Theming for a brand other than GemBoss
|
|
2
|
+
|
|
3
|
+
A GemCommerce brand (GemPages, Gemians…) uses the kit as it is and changes four things: **its accent colour**, **the
|
|
4
|
+
wash hues**, **its logo** and **its favicon**. Everything else stays the kit's: the shapes, the scales, the ink
|
|
5
|
+
buttons, the white canvas, the type. That is what makes the sites look like one family, and it is why a site built
|
|
6
|
+
by someone with no design training still looks careful.
|
|
7
|
+
|
|
8
|
+
Working example: `demo/theme-example.html` (the demo's hero, buttons and cards with a GemPages-style blue).
|
|
9
|
+
|
|
10
|
+
## What you may override
|
|
11
|
+
|
|
12
|
+
Put these in the site's own CSS (`site.css`, linked **after** `website.css`), in **one `:root {}` block**, and nowhere
|
|
13
|
+
else.
|
|
14
|
+
|
|
15
|
+
| variable | what it is | GemBoss value |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `--gw-accent-light` | the accent on light pages: links, focus ring, active item, label square, chart line | `#5661f5` |
|
|
18
|
+
| `--gw-accent-strong-light` | the accent on hover and as text on `--gw-accent-soft` | `#3a44de` |
|
|
19
|
+
| `--gw-accent-soft-light` | the accent's tint: active chip, accent badge, icon hover | `#eef0fe` |
|
|
20
|
+
| `--gw-on-accent-light` | text on a solid accent (rare; the kit never fills a button with it) | `#ffffff` |
|
|
21
|
+
| `--gw-accent-dark` | the accent in dark mode **and inside night bands** | `#7c84ff` |
|
|
22
|
+
| `--gw-accent-strong-dark` | its hover / strong form | `#a3a9ff` |
|
|
23
|
+
| `--gw-accent-soft-dark` | its tint | `#1f2140` |
|
|
24
|
+
| `--gw-on-accent-dark` | text on it | `#0d0d12` |
|
|
25
|
+
| `--gw-wash-pink`, `--gw-wash-violet`, `--gw-wash-sky`, `--gw-wash-mint` | the secondary hues of the hero wash and illustrations | `#ff6f5e`, `#7c8af7`, `#43b0ff`, `#1fcf9f` |
|
|
26
|
+
|
|
27
|
+
Why the `-light` / `-dark` names and not `--gw-accent` itself: the kit sets `--gw-accent` from these in four places
|
|
28
|
+
(the light default, dark from the system setting, dark from the toggle, light from the toggle) and in every night
|
|
29
|
+
band. Override the eight primitives once and all of them follow. Overriding `--gw-accent` directly works only until
|
|
30
|
+
someone presses the theme toggle.
|
|
31
|
+
|
|
32
|
+
The logo and the favicon are files, not variables: the `<img class="gw-brand__mark">` (26×26, SVG preferred, with
|
|
33
|
+
`alt=""` because the name is written next to it) and the `<span class="gw-brand__name">` in the header and footer
|
|
34
|
+
partials, and `<link rel="icon">` in the page head.
|
|
35
|
+
|
|
36
|
+
## What you may not change
|
|
37
|
+
|
|
38
|
+
- **Neutrals**: `--gw-bg`, `--gw-bg-soft`, `--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`,
|
|
39
|
+
`--gw-line-strong`, and the night colours. No cream or grey page, no brand-coloured ink.
|
|
40
|
+
- **The action**: `--gw-action`, `--gw-action-hover`, `--gw-on-action`. Buttons stay ink. Your accent never fills a
|
|
41
|
+
button, a section or a header.
|
|
42
|
+
- **State**: `--gw-success`, `--gw-warning` and their tints. Green and amber mean state on every GemCommerce site.
|
|
43
|
+
- **Scales and shapes**: type sizes and weights, spacing, radii, shadows, widths, the header height, the
|
|
44
|
+
font family. No rounder buttons, no bigger shadows, no other font.
|
|
45
|
+
- **Kit classes**: never restyle a `.gw-*` class. If the kit cannot do what your brand needs, that is a gap
|
|
46
|
+
(`AGENTS.md` section 9), not a theme.
|
|
47
|
+
|
|
48
|
+
`npx gemboss-website check` reports colours outside the tokens, overrides of `.gw-*` classes and accent-filled
|
|
49
|
+
buttons.
|
|
50
|
+
|
|
51
|
+
## A full example
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
/* site.css, linked after website.css. The whole theme of a GemPages-style site. */
|
|
55
|
+
:root {
|
|
56
|
+
/* accent, light pages: #1e5eff on #ffffff is 5.1:1 */
|
|
57
|
+
--gw-accent-light: #1e5eff;
|
|
58
|
+
--gw-accent-strong-light: #1547d1;
|
|
59
|
+
--gw-accent-soft-light: #eaf0ff;
|
|
60
|
+
--gw-on-accent-light: #ffffff;
|
|
61
|
+
/* accent, dark mode and night bands: #6b98ff on #0d0d12 is 7.0:1 */
|
|
62
|
+
--gw-accent-dark: #6b98ff;
|
|
63
|
+
--gw-accent-strong-dark: #a8c0ff;
|
|
64
|
+
--gw-accent-soft-dark: #16213d;
|
|
65
|
+
--gw-on-accent-dark: #0d0d12;
|
|
66
|
+
/* wash: only if the brand has its own secondary hues; otherwise leave the kit's */
|
|
67
|
+
--gw-wash-violet: #7b9cff;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
73
|
+
…
|
|
74
|
+
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="/logo-mark.svg" alt="" width="26" height="26"><span class="gw-brand__name">GemPages</span></a>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Check the contrast before you ship
|
|
78
|
+
|
|
79
|
+
The accent family is read as text: the kit draws links, step numbers, active menu items and badge words in
|
|
80
|
+
`--gw-accent-strong`, and the label square, focus ring, check marks and the featured plan's hairline in `--gw-accent`.
|
|
81
|
+
So:
|
|
82
|
+
|
|
83
|
+
1. **Accent text on white at least 4.5:1**: `--gw-accent-light` and `--gw-accent-strong-light` on `#ffffff`.
|
|
84
|
+
Check `--gw-accent-strong-light` on `--gw-bg-soft` (`#f2f3f5`) too, where links sit in soft sections.
|
|
85
|
+
2. `--gw-accent-strong-light` on `--gw-accent-soft-light`: at least 4.5:1 (the accent badge and the active chip).
|
|
86
|
+
3. `--gw-accent-dark` and `--gw-accent-strong-dark` on `#0d0d12` (dark page) and on `#0c0e14` (night band): at
|
|
87
|
+
least 4.5:1.
|
|
88
|
+
4. `--gw-accent-strong-dark` on `--gw-accent-soft-dark`: at least 4.5:1.
|
|
89
|
+
|
|
90
|
+
Any WCAG contrast checker works (paste the two hex values). A bright brand colour (yellow, light green, cyan) almost
|
|
91
|
+
never passes on white: darken it for `-light` and keep the bright one for `-dark`. If you cannot get 4.5:1 without
|
|
92
|
+
changing the brand colour beyond recognition, keep the kit's accent and use the brand colour only in the logo and
|
|
93
|
+
illustrations.
|
|
94
|
+
|
|
95
|
+
Then open the site in light and dark (the toggle in the header), at 320px and at 1366px, and look at a night band.
|
package/js/website.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*! @gemboss/website-ui website.js | spec: docs/markup-contract.md | load with defer */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
if (window.gwWebsite) return;
|
|
5
|
+
window.gwWebsite = { version: '0.1.0' };
|
|
6
|
+
var doc = document, root = doc.documentElement;
|
|
7
|
+
function mq(q) { return window.matchMedia ? matchMedia(q) : { matches: false }; }
|
|
8
|
+
function watch(m, fn) { if (m.addEventListener) m.addEventListener('change', fn); }
|
|
9
|
+
function all(sel, ctx) { return [].slice.call((ctx || doc).querySelectorAll(sel)); }
|
|
10
|
+
function on(el, type, fn, opt) { el.addEventListener(type, fn, opt || false); }
|
|
11
|
+
function attr(el, name, dflt) { return el.getAttribute(name) || dflt; }
|
|
12
|
+
function esc(e) { return e.key === 'Escape'; }
|
|
13
|
+
function link(e) { return e.target.closest('a[href]'); }
|
|
14
|
+
|
|
15
|
+
// Mega menu. Opened by hover then clicked, a panel stays open: the click meant "open".
|
|
16
|
+
function mega() {
|
|
17
|
+
var fine = mq('(hover: hover) and (pointer: fine)'), open = null, timer = 0;
|
|
18
|
+
var menus = all('[data-gw-mega]').map(function (t) {
|
|
19
|
+
var p = doc.getElementById(attr(t, 'data-gw-mega', attr(t, 'aria-controls', '')));
|
|
20
|
+
return p && { t: t, p: p, item: t.closest('.gw-nav__item') || t.parentNode };
|
|
21
|
+
}).filter(Boolean);
|
|
22
|
+
if (!menus.length) return;
|
|
23
|
+
function set(m, show, mode) {
|
|
24
|
+
m.p.hidden = !show;
|
|
25
|
+
m.t.setAttribute('aria-expanded', String(show));
|
|
26
|
+
m.item.classList.toggle('is-open', show);
|
|
27
|
+
m.mode = mode;
|
|
28
|
+
open = show ? m : open === m ? null : open;
|
|
29
|
+
}
|
|
30
|
+
function show(m, mode) {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
if (open === m) { if (mode === 'click') m.mode = mode; return; }
|
|
33
|
+
if (open) set(open, false);
|
|
34
|
+
set(m, true, mode);
|
|
35
|
+
}
|
|
36
|
+
function hide(focus) {
|
|
37
|
+
var m = open;
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
if (!m) return;
|
|
40
|
+
set(m, false);
|
|
41
|
+
if (focus === true) m.t.focus();
|
|
42
|
+
}
|
|
43
|
+
function inside(m, el) { return !!el && (m.item.contains(el) || m.p.contains(el)); }
|
|
44
|
+
menus.forEach(function (m) {
|
|
45
|
+
on(m.t, 'click', function () { if (open === m && m.mode !== 'hover') hide(); else show(m, 'click'); });
|
|
46
|
+
(m.item.contains(m.p) ? [m.item] : [m.item, m.p]).forEach(function (el) {
|
|
47
|
+
on(el, 'mouseenter', function () {
|
|
48
|
+
if (fine.matches) { clearTimeout(timer); if (open !== m) timer = setTimeout(function () { show(m, 'hover'); }, 80); }
|
|
49
|
+
});
|
|
50
|
+
on(el, 'mouseleave', function () {
|
|
51
|
+
if (fine.matches) { clearTimeout(timer); if (open === m && m.mode === 'hover') timer = setTimeout(hide, 160); }
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
on(m.item, 'focusout', function (e) { if (open === m && e.relatedTarget && !inside(m, e.relatedTarget)) hide(); });
|
|
55
|
+
on(m.p, 'click', function (e) { if (link(e)) hide(); });
|
|
56
|
+
});
|
|
57
|
+
on(doc, 'click', function (e) { if (open && !inside(open, e.target)) hide(); });
|
|
58
|
+
on(doc, 'keydown', function (e) { if (open && esc(e)) hide(inside(open, doc.activeElement) || open.mode === 'click'); });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Mobile menu. Labels: data-gw-label-open/-close, else the markup's aria-label, else English.
|
|
62
|
+
function mobile() {
|
|
63
|
+
var btn = doc.querySelector('[data-gw-menu-toggle]');
|
|
64
|
+
var menu = btn && doc.getElementById(attr(btn, 'aria-controls', 'gw-mobile-menu'));
|
|
65
|
+
if (!menu) return;
|
|
66
|
+
var label = [attr(btn, 'data-gw-label-open', attr(btn, 'aria-label', 'Open menu')), attr(btn, 'data-gw-label-close', 'Close menu')];
|
|
67
|
+
var wide = mq('(min-width: 1080px)');
|
|
68
|
+
function isOpen() { return !menu.hidden; }
|
|
69
|
+
function set(o, focus) {
|
|
70
|
+
menu.hidden = !o;
|
|
71
|
+
btn.setAttribute('aria-expanded', String(o));
|
|
72
|
+
btn.setAttribute('aria-label', label[+o]);
|
|
73
|
+
root.classList.toggle('is-menu-open', o);
|
|
74
|
+
if (focus) btn.focus();
|
|
75
|
+
}
|
|
76
|
+
on(btn, 'click', function () { set(!isOpen()); });
|
|
77
|
+
on(doc, 'keydown', function (e) { if (esc(e) && isOpen()) set(false, true); });
|
|
78
|
+
on(menu, 'click', function (e) { if (link(e)) set(false); }); // same-page anchors do not reload
|
|
79
|
+
watch(wide, function () { if (wide.matches && isOpen()) set(false); });
|
|
80
|
+
on(window, 'pageshow', function (e) { if (e.persisted && isOpen()) set(false); });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Theme toggle. Labels: data-gw-label-dark/-light, else the markup's aria-label, else English.
|
|
84
|
+
function theme() {
|
|
85
|
+
var sys = mq('(prefers-color-scheme: dark)'), stored = null;
|
|
86
|
+
try { stored = localStorage.getItem('gw-theme'); } catch (e) { /* blocked */ }
|
|
87
|
+
if (!root.hasAttribute('data-theme') && /^(light|dark)$/.test(stored)) root.setAttribute('data-theme', stored);
|
|
88
|
+
var btns = all('[data-gw-theme-toggle]'), labels = btns.map(function (b) {
|
|
89
|
+
return [attr(b, 'data-gw-label-dark', attr(b, 'aria-label', 'Switch to dark theme')), attr(b, 'data-gw-label-light', 'Switch to light theme')];
|
|
90
|
+
});
|
|
91
|
+
function dark() { var t = root.getAttribute('data-theme'); return t ? t === 'dark' : sys.matches; }
|
|
92
|
+
function relabel() { btns.forEach(function (b, i) { b.setAttribute('aria-label', labels[i][+dark()]); }); }
|
|
93
|
+
btns.forEach(function (b) {
|
|
94
|
+
on(b, 'click', function () {
|
|
95
|
+
var next = dark() ? 'light' : 'dark';
|
|
96
|
+
root.setAttribute('data-theme', next);
|
|
97
|
+
try { localStorage.setItem('gw-theme', next); } catch (e) { /* blocked */ }
|
|
98
|
+
relabel();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
watch(sys, relabel);
|
|
102
|
+
relabel();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Reveal at 15% visible, once. Failsafe: all shown after 2.5s, or at once without IntersectionObserver.
|
|
106
|
+
function reveal() {
|
|
107
|
+
var els = all('.gw-reveal'), io = null;
|
|
108
|
+
function showAll() { els.forEach(function (el) { el.classList.add('is-in'); }); if (io) io.disconnect(); }
|
|
109
|
+
if (!els.length) return;
|
|
110
|
+
if (!window.IntersectionObserver || mq('(prefers-reduced-motion: reduce)').matches) return showAll();
|
|
111
|
+
io = new IntersectionObserver(function (list) {
|
|
112
|
+
list.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); } });
|
|
113
|
+
}, { threshold: 0.15 });
|
|
114
|
+
els.forEach(function (el) { io.observe(el); });
|
|
115
|
+
setTimeout(showAll, 2500);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Header: .is-scrolled past 8px, read once per frame.
|
|
119
|
+
function header() {
|
|
120
|
+
var h = doc.querySelector('[data-gw-header]'), state = false, queued = false;
|
|
121
|
+
if (!h) return;
|
|
122
|
+
function update() {
|
|
123
|
+
var s = window.scrollY > 8;
|
|
124
|
+
queued = false;
|
|
125
|
+
if (s !== state) h.classList.toggle('is-scrolled', (state = s));
|
|
126
|
+
}
|
|
127
|
+
on(window, 'scroll', function () { if (!queued) { queued = true; requestAnimationFrame(update); } }, { passive: true });
|
|
128
|
+
update();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// aria-current="page" on nav links to this path, unless the markup marked one.
|
|
132
|
+
function current() {
|
|
133
|
+
var menu = doc.getElementById('gw-mobile-menu');
|
|
134
|
+
var links = all('.gw-nav__link[href], .gw-mega__link[href]').concat(menu ? all('a[href]', menu) : []);
|
|
135
|
+
function norm(p) { return p.replace(/\/index\.html?$/, '/').replace(/\.html?$/, '').replace(/\/+$/, '') || '/'; }
|
|
136
|
+
if (links.some(function (a) { return a.hasAttribute('aria-current'); })) return;
|
|
137
|
+
links.forEach(function (a) {
|
|
138
|
+
if (a.classList.contains('gw-btn') || a.hash || a.host !== location.host) return;
|
|
139
|
+
if (norm(a.pathname) === norm(location.pathname)) a.setAttribute('aria-current', 'page');
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function init() {
|
|
144
|
+
[mega, mobile, theme, reveal, header, current].forEach(function (f) {
|
|
145
|
+
try { f(); } catch (e) { console.error('[gw-website]', e); }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (doc.readyState === 'loading') on(doc, 'DOMContentLoaded', init); else init();
|
|
149
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/website-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"website.css",
|
|
9
|
+
"website.min.css",
|
|
10
|
+
"css/",
|
|
11
|
+
"js/",
|
|
12
|
+
"system.json",
|
|
13
|
+
"AGENTS.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"README.md",
|
|
16
|
+
"templates/",
|
|
17
|
+
"demo/",
|
|
18
|
+
"docs/",
|
|
19
|
+
"bin/",
|
|
20
|
+
"skill/"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
"./website.css": "./website.css",
|
|
24
|
+
"./website.min.css": "./website.min.css",
|
|
25
|
+
"./website.js": "./js/website.js",
|
|
26
|
+
"./system.json": "./system.json",
|
|
27
|
+
"./css/*": "./css/*",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"gemboss-website": "bin/gemboss-website.mjs"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node scripts/build.mjs",
|
|
35
|
+
"test": "node test/run.mjs",
|
|
36
|
+
"prepack": "node scripts/build.mjs --check && node test/run.mjs"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/chriskoh26/gemboss-www.git",
|
|
44
|
+
"directory": "packages/website-ui"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"gemboss": {
|
|
50
|
+
"gapRepo": "chriskoh26/gemboss-www"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gemboss-website-ui
|
|
3
|
+
description: Use when building or changing pages of a website that uses @gemboss/website-ui (the GemCommerce website kit) - a new page, adding or changing a section, header, footer or copy, theming the kit for a brand, or reviewing a page before hand-over. Covers the kit's rules, its templates, the check, reporting a missing kit part (gap) and upgrading the kit.
|
|
4
|
+
---
|
|
5
|
+
<!-- managed by @gemboss/website-ui init -->
|
|
6
|
+
|
|
7
|
+
# Website pages with @gemboss/website-ui
|
|
8
|
+
|
|
9
|
+
The kit lives in `node_modules/@gemboss/website-ui/` (below: `KIT/`). The person asking is often not a designer;
|
|
10
|
+
the kit and its rules are how the page ends up looking designed. Compose from the kit, do not invent.
|
|
11
|
+
|
|
12
|
+
## Every time
|
|
13
|
+
|
|
14
|
+
1. **Read `KIT/AGENTS.md`** completely before the first page change of the session. It wins over habit and over
|
|
15
|
+
generic design advice.
|
|
16
|
+
2. **See what exists**: `KIT/demo/index.html` (every component) and `KIT/docs/components.md` (every class).
|
|
17
|
+
3. **Build**:
|
|
18
|
+
- New page: copy `KIT/templates/page.html`, then add sections from `KIT/templates/sections/` in the order the page
|
|
19
|
+
needs. Fill the words, delete what you did not use.
|
|
20
|
+
- Changed page: keep its kit classes; change words, order and choice of sections.
|
|
21
|
+
- Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
|
|
22
|
+
scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
|
|
23
|
+
- Another brand: change only the accent family, wash hues, logo and favicon (`KIT/docs/theming.md`).
|
|
24
|
+
4. **Words**: real facts only. A missing price, number, name or date becomes a visible `[PLACEHOLDER]` and a
|
|
25
|
+
question to the person. No em dash in visible text, no "Learn more" as a whole link.
|
|
26
|
+
5. **Check**: `npx gemboss-website check` and fix every error (`--fix-list` gives a to-do list). Then look at the
|
|
27
|
+
page at 320px and 1366px. Only then say it is done.
|
|
28
|
+
|
|
29
|
+
## When the kit is missing something
|
|
30
|
+
|
|
31
|
+
1. Make sure it is really missing (demo, components doc, two existing parts together, a variant).
|
|
32
|
+
2. A one-off illustration is a page thing: site CSS with tokens, stop.
|
|
33
|
+
3. A reusable thing is a gap: build a stopgap in `kit-gaps.css` (classes `kitgap-…`, tokens only, a comment naming
|
|
34
|
+
the gap), then report it:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx gemboss-website gap --title "…" --kind component --need "…" --why "…" \
|
|
38
|
+
--page https://… --stopgap kit-gaps.css
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It files a GitHub issue (gh) or writes `kit-gaps/<slug>.md` to send. Put the printed `/* gap #N */` comment
|
|
42
|
+
above the stopgap. Tell the person what you reported.
|
|
43
|
+
|
|
44
|
+
## Upgrading the kit
|
|
45
|
+
|
|
46
|
+
`npx gemboss-website upgrade` shows what changed, the gaps the new version closes and the install command. Upgrade,
|
|
47
|
+
replace the listed `kitgap-` stopgaps with the kit's classes, run `npx gemboss-website init` (refreshes this skill)
|
|
48
|
+
and the check, and look at the pages again.
|
package/system.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "The GemBoss website system in one file. css/tokens.css, the docs and `gemboss-website check` all read these values: change a value here, never in one of them alone. Values measured on gemboss.ai and the GemBoss dashboard (DESIGN-LANGUAGE.md of gemboss-www, 15/10/2026).",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"color": {
|
|
5
|
+
"light": {
|
|
6
|
+
"bg": "#ffffff",
|
|
7
|
+
"bg-soft": "#f2f3f5",
|
|
8
|
+
"surface": "#ffffff",
|
|
9
|
+
"ink": "#1b1c1e",
|
|
10
|
+
"ink-2": "#3b3d42",
|
|
11
|
+
"muted": "#686b71",
|
|
12
|
+
"line": "#e5e7ea",
|
|
13
|
+
"line-strong": "#d5d8dd",
|
|
14
|
+
"action": "#1b1c1e",
|
|
15
|
+
"action-hover": "#000000",
|
|
16
|
+
"on-action": "#ffffff",
|
|
17
|
+
"accent": "#5661f5",
|
|
18
|
+
"accent-strong": "#3a44de",
|
|
19
|
+
"accent-soft": "#eef0fe",
|
|
20
|
+
"on-accent": "#ffffff",
|
|
21
|
+
"success": "#007f5f",
|
|
22
|
+
"success-soft": "#e3f1df",
|
|
23
|
+
"warning": "#9a6700",
|
|
24
|
+
"night": "#0c0e14",
|
|
25
|
+
"night-2": "#141722",
|
|
26
|
+
"on-night": "#e3e4e8",
|
|
27
|
+
"on-night-muted": "#9aa3ff"
|
|
28
|
+
},
|
|
29
|
+
"dark": {
|
|
30
|
+
"bg": "#0d0d12",
|
|
31
|
+
"bg-soft": "#121318",
|
|
32
|
+
"surface": "#16171d",
|
|
33
|
+
"ink": "#e3e4e8",
|
|
34
|
+
"ink-2": "#c4c6cc",
|
|
35
|
+
"muted": "#9a9ca6",
|
|
36
|
+
"line": "#2a2b33",
|
|
37
|
+
"line-strong": "#383944",
|
|
38
|
+
"action": "#f2f3f5",
|
|
39
|
+
"action-hover": "#ffffff",
|
|
40
|
+
"on-action": "#0d0d12",
|
|
41
|
+
"accent": "#7c84ff",
|
|
42
|
+
"accent-strong": "#a3a9ff",
|
|
43
|
+
"accent-soft": "#1f2140",
|
|
44
|
+
"on-accent": "#0d0d12",
|
|
45
|
+
"success": "#34c38f",
|
|
46
|
+
"success-soft": "#132a22",
|
|
47
|
+
"warning": "#e0b341",
|
|
48
|
+
"night": "#0a0b10",
|
|
49
|
+
"night-2": "#12141c",
|
|
50
|
+
"on-night": "#e3e4e8",
|
|
51
|
+
"on-night-muted": "#9aa3ff"
|
|
52
|
+
},
|
|
53
|
+
"wash": {
|
|
54
|
+
"$comment": "Secondary hues. ONE job: the hero wash and illustrations. Never chrome, text, borders or buttons.",
|
|
55
|
+
"pink": "#ff6f5e",
|
|
56
|
+
"violet": "#7c8af7",
|
|
57
|
+
"sky": "#43b0ff",
|
|
58
|
+
"mint": "#1fcf9f"
|
|
59
|
+
},
|
|
60
|
+
"roles": {
|
|
61
|
+
"action": "The thing you click. Ink, not the accent.",
|
|
62
|
+
"accent": "Links, focus rings, active state, a chart line, the 8px label square. Never a button fill, never body text.",
|
|
63
|
+
"success/warning": "State only.",
|
|
64
|
+
"wash": "The hero wash and illustrations only."
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"radius": {
|
|
68
|
+
"scale": [0, 4, 6, 8, 12, 16, 20, 999],
|
|
69
|
+
"action": 8,
|
|
70
|
+
"chip": 999,
|
|
71
|
+
"card": 12,
|
|
72
|
+
"panel": 16
|
|
73
|
+
},
|
|
74
|
+
"shadow": {
|
|
75
|
+
"rest": "0 1px 2px rgba(31,30,29,.04), 0 2px 6px rgba(31,30,29,.06)",
|
|
76
|
+
"sm": "0 1px 2px rgba(31,30,29,.04)",
|
|
77
|
+
"raised": "0 4px 16px rgba(31,30,29,.10)",
|
|
78
|
+
"$comment": "The whole budget for chrome. A card sits; it does not hover. An illustration may light itself."
|
|
79
|
+
},
|
|
80
|
+
"type": {
|
|
81
|
+
"family": "Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
|
|
82
|
+
"weights": [400, 500, 600, 650, 700],
|
|
83
|
+
"scale": [11, 12, 13, 14, 16, 18, 20, 22, 24, 30],
|
|
84
|
+
"display_above": 30,
|
|
85
|
+
"display": {
|
|
86
|
+
"h1": "clamp(40px, 4.6vw, 68px)",
|
|
87
|
+
"h2": "clamp(32px, 3.4vw, 52px)",
|
|
88
|
+
"weight": 600,
|
|
89
|
+
"tracking": "-0.03em"
|
|
90
|
+
},
|
|
91
|
+
"heading": { "weight": 650, "tracking": "-0.022em", "line_height": 1.08 },
|
|
92
|
+
"subhead": { "weight": 600 },
|
|
93
|
+
"body": { "size": 18, "line_height": 1.6 },
|
|
94
|
+
"label": { "size": 13, "weight": 500, "case": "sentence", "$comment": "muted, led by the 8px accent square; never uppercase tracking" },
|
|
95
|
+
"button": { "size": 14, "weight": 600, "case": "sentence" }
|
|
96
|
+
},
|
|
97
|
+
"space": {
|
|
98
|
+
"scale": [0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 120],
|
|
99
|
+
"section": "clamp(56px, 38px + 5vw, 110px)",
|
|
100
|
+
"$comment": "4px base; 2px steps only below 8. Section rhythm comes from --space-section; the scale is for everything inside a section."
|
|
101
|
+
},
|
|
102
|
+
"layout": {
|
|
103
|
+
"wrap": 1120,
|
|
104
|
+
"wrap_narrow": 780,
|
|
105
|
+
"gutter": 22,
|
|
106
|
+
"header_height": 64,
|
|
107
|
+
"breakpoints": [480, 768, 1080],
|
|
108
|
+
"test_widths": [320, 390, 768, 1366]
|
|
109
|
+
},
|
|
110
|
+
"control": {
|
|
111
|
+
"button_height": 40,
|
|
112
|
+
"tap_target_min": 44
|
|
113
|
+
},
|
|
114
|
+
"motion": {
|
|
115
|
+
"properties": ["transform", "opacity"],
|
|
116
|
+
"ease": "cubic-bezier(.22,1,.36,1)",
|
|
117
|
+
"reveal_ms": 700,
|
|
118
|
+
"$comment": "Animate transform and opacity only. Everything is still under prefers-reduced-motion."
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Footer: one per site, the same on every page. A brand block, three to five link columns, a bottom row.
|
|
3
|
+
Limits: column headings are one or two words in sentence case; four to seven links per column; the bottom row
|
|
4
|
+
holds the copyright, the legal links and the social links, nothing else. Replace every [bracketed] placeholder.
|
|
5
|
+
-->
|
|
6
|
+
<footer class="gw-footer">
|
|
7
|
+
<div class="gw-wrap">
|
|
8
|
+
<div class="gw-footer__grid">
|
|
9
|
+
<div class="gw-footer__brand">
|
|
10
|
+
<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
|
+
<p>[One sentence on what the brand does for the reader.]</p>
|
|
12
|
+
</div>
|
|
13
|
+
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
14
|
+
<h2 class="gw-footer__heading">[Column heading]</h2>
|
|
15
|
+
<ul>
|
|
16
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
17
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
18
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
19
|
+
</ul>
|
|
20
|
+
</nav>
|
|
21
|
+
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
22
|
+
<h2 class="gw-footer__heading">[Column heading]</h2>
|
|
23
|
+
<ul>
|
|
24
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
25
|
+
<li><a href="[/page]">[Page title]</a></li>
|
|
26
|
+
</ul>
|
|
27
|
+
</nav>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="gw-footer__bottom">
|
|
30
|
+
<span>© [Year] [Company]. All rights reserved.</span>
|
|
31
|
+
<ul aria-label="Legal">
|
|
32
|
+
<li><a href="[/privacy]">Privacy</a></li>
|
|
33
|
+
<li><a href="[/terms]">Terms</a></li>
|
|
34
|
+
</ul>
|
|
35
|
+
<ul aria-label="Social">
|
|
36
|
+
<li><a href="[https://social profile]" rel="noopener">[Network]</a></li>
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</footer>
|
|
@@ -0,0 +1,60 @@
|
|
|
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. Below 1080px the nav and Sign in hide and the menu button opens #gw-mobile-menu,
|
|
4
|
+
which repeats the same links. website.js runs the mega menus, the mobile menu and the theme toggle.
|
|
5
|
+
Limits: at most five top-level items, one .gw-btn (the primary action), one .gw-btn--secondary. Each mega panel:
|
|
6
|
+
two or three columns of four or five links, at most one feature card. The announcement bar is optional: one sentence.
|
|
7
|
+
Replace every [bracketed] placeholder; delete what you do not use.
|
|
8
|
+
-->
|
|
9
|
+
<div class="gw-announce" role="region" aria-label="Announcement">[One sentence of news.] <a href="[link]">[Where the link goes]</a></div>
|
|
10
|
+
<header class="gw-header" data-gw-header>
|
|
11
|
+
<div class="gw-wrap gw-header__bar">
|
|
12
|
+
<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>
|
|
13
|
+
<nav class="gw-nav" aria-label="Main">
|
|
14
|
+
<ul class="gw-nav__list">
|
|
15
|
+
<li class="gw-nav__item">
|
|
16
|
+
<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>
|
|
17
|
+
<div class="gw-mega" id="gw-mega-product" hidden>
|
|
18
|
+
<div class="gw-wrap gw-mega__grid">
|
|
19
|
+
<div class="gw-mega__col">
|
|
20
|
+
<p class="gw-mega__heading">[Group]</p>
|
|
21
|
+
<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>
|
|
22
|
+
<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>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="gw-mega__col">
|
|
25
|
+
<p class="gw-mega__heading">[Group]</p>
|
|
26
|
+
<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>
|
|
27
|
+
</div>
|
|
28
|
+
<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>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</li>
|
|
32
|
+
<li class="gw-nav__item"><a class="gw-nav__link" href="[/pricing]">[Pricing]</a></li>
|
|
33
|
+
</ul>
|
|
34
|
+
</nav>
|
|
35
|
+
<div class="gw-header__actions">
|
|
36
|
+
<button class="gw-theme-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>
|
|
37
|
+
<a class="gw-nav__link gw-header__signin" href="[sign-in URL]">Sign in</a>
|
|
38
|
+
<a class="gw-btn gw-btn--secondary gw-btn--sm" href="[/contact]">[Book a demo]</a>
|
|
39
|
+
<a class="gw-btn gw-btn--sm" href="[sign-up URL]">[Start free]</a>
|
|
40
|
+
<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>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="gw-mobile-menu" id="gw-mobile-menu" hidden>
|
|
44
|
+
<nav class="gw-wrap" aria-label="Main, mobile">
|
|
45
|
+
<details class="gw-mobile-menu__group">
|
|
46
|
+
<summary>[Product menu item]</summary>
|
|
47
|
+
<ul class="gw-mobile-menu__list">
|
|
48
|
+
<li><a class="gw-mobile-menu__link" href="[/page]">[Page title]</a></li>
|
|
49
|
+
<li><a class="gw-mobile-menu__link" href="[/page]">[Page title]</a></li>
|
|
50
|
+
</ul>
|
|
51
|
+
</details>
|
|
52
|
+
<a class="gw-mobile-menu__top" href="[/pricing]">[Pricing]</a>
|
|
53
|
+
<div class="gw-mobile-menu__actions">
|
|
54
|
+
<a class="gw-btn gw-btn--block" href="[sign-up URL]">[Start free]</a>
|
|
55
|
+
<a class="gw-btn gw-btn--secondary gw-btn--block" href="[/contact]">[Book a demo]</a>
|
|
56
|
+
<a class="gw-mobile-menu__link" href="[sign-in URL]">Sign in</a>
|
|
57
|
+
</div>
|
|
58
|
+
</nav>
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|