@gemboss/website-ui 0.2.0 → 0.4.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 +52 -23
- package/CHANGELOG.md +63 -0
- package/README.md +45 -15
- package/bin/lib/check.mjs +76 -7
- package/bin/lib/contrast.mjs +67 -0
- package/css/components.css +8 -6
- package/css/header.css +37 -5
- package/css/prose.css +3 -2
- package/css/tokens.css +19 -0
- package/demo/brand-profiles.html +199 -0
- package/docs/components.md +56 -11
- package/docs/markup-contract.md +106 -18
- package/docs/philosophy.md +14 -9
- package/docs/theming.md +85 -29
- package/js/website.js +222 -168
- package/js/website.mjs +269 -0
- package/package.json +2 -1
- package/skill/SKILL.md +5 -1
- package/system.json +31 -7
- package/templates/header.html +3 -1
- package/templates/page.html +2 -0
- package/website.css +68 -14
- package/website.min.css +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,23 +10,31 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
10
10
|
| file | what it is |
|
|
11
11
|
|---|---|
|
|
12
12
|
| `website.css` | the whole kit, one file (tokens, base, layout, components, sections) |
|
|
13
|
-
| `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons; no dependencies |
|
|
13
|
+
| `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons; no dependencies; `window.gwWebsite.init(root)` |
|
|
14
|
+
| `js/website.mjs` | the same script as an ES module for apps: `import { init, destroy } from '@gemboss/website-ui/website.mjs'` |
|
|
14
15
|
| `system.json` | every value of the system: colours, spacing, type, radii, shadows, widths |
|
|
15
16
|
| `templates/page.html` | the page skeleton: start every new page from it |
|
|
16
17
|
| `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose… |
|
|
17
18
|
| `demo/index.html` | every component on one page: open it to see what exists before you build anything |
|
|
19
|
+
| `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
|
|
18
20
|
| `docs/components.md` | every class, what it is for, its variants |
|
|
19
|
-
| `docs/theming.md` |
|
|
21
|
+
| `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
|
|
20
22
|
|
|
21
23
|
## 1. The ten rules
|
|
22
24
|
|
|
23
25
|
1. **Compose, do not invent.** A page is the page skeleton plus kit sections plus your words. Look in `demo/` and
|
|
24
26
|
`docs/components.md` first. A new class is a last resort (section 9).
|
|
25
|
-
2. **White canvas,
|
|
26
|
-
(`.gw-btn`)
|
|
27
|
-
accent
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
2. **White canvas, one action colour, one accent.** The background is white (`--gw-bg`). The thing you click
|
|
28
|
+
(`.gw-btn`) is near-black ink, or the brand's accent when the site chose the `accent` action profile
|
|
29
|
+
(`data-gw-action="accent"` on `<html>`, section 7): one colour for every primary button of the site, never chosen
|
|
30
|
+
per button. Why the choice (0.3.0): the site and the product speak one language, and GemPages' product clicks in
|
|
31
|
+
blue while GemBoss's clicks in ink. Otherwise the accent is for links, focus, the active item, the 8px label
|
|
32
|
+
square and a chart line. **The accent is never body text, never words in a heading, never a section band, and
|
|
33
|
+
never a fill you set yourself.**
|
|
34
|
+
3. **Actions are rectangles, chips are pills.** Buttons: 40px tall, sentence case, 14px/600, the action radius of the
|
|
35
|
+
site's profile (8px; 12px in `soft`). Chips, toggles, badges: fully rounded. Cards: the card radius (12px; 20px in
|
|
36
|
+
`soft`), large panels the panel radius (16px; 24px). No other radii: the site picks its profile once
|
|
37
|
+
(`data-gw-radius`), components follow.
|
|
30
38
|
4. **A card sits.** A card (`.gw-card`, a plan) is a white box with a 1px `--gw-line` border and the resting shadow
|
|
31
39
|
`--gw-shadow`, the same as the GemBoss dashboard. It never lifts on hover. To separate anything else, use space or a
|
|
32
40
|
hairline before a shadow. The only shadows are `--gw-shadow` (rest), `--gw-shadow-sm` and `--gw-shadow-raised`
|
|
@@ -66,6 +74,8 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
66
74
|
it works; a hard-coded colour breaks it.
|
|
67
75
|
- **Accent as text** (links, the active menu item, step numbers) is `--gw-accent-strong`; the plain accent is for
|
|
68
76
|
the label square, the focus ring, check marks and borders. Night bands switch to the dark palette by themselves.
|
|
77
|
+
- **Brand profiles** (section 7): `data-gw-action` and `data-gw-radius` on `<html>`, `.gw-header--float` on the
|
|
78
|
+
header. Without them a page is GemBoss: ink buttons, tight corners, the flat header.
|
|
69
79
|
- **Controls** are 40px tall on desktop and at least 44px on touch screens; `.gw-btn--sm` is 36px with a 46px hit area on desktop and 44px tall on touch screens.
|
|
70
80
|
- **A button that waits** (a form sent, an audit started) gets `aria-busy="true"` from the page's script until the
|
|
71
81
|
answer comes: the kit keeps its label and width, draws the ring and ignores a second press. Nothing else: no
|
|
@@ -85,16 +95,21 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
85
95
|
|
|
86
96
|
## 4. Don't (each of these has happened, and each one made a page look cheap or broke it)
|
|
87
97
|
|
|
88
|
-
- Don't fill a button with the accent
|
|
98
|
+
- Don't fill a button yourself: not with the accent (that is the site's action profile, set once on `<html>`), a
|
|
99
|
+
gradient or a wash hue. No raised or inset shadow on a button.
|
|
100
|
+
- Don't colour the words of a heading with the accent or a gradient, and don't fill a section band with the accent.
|
|
89
101
|
- Don't use uppercase labels with letter spacing ("FEATURES").
|
|
90
102
|
- Don't add shadows beyond the three tokens, and don't lift cards on hover.
|
|
91
103
|
- Don't round a button into a pill, and don't square a chip.
|
|
92
104
|
- Don't use cream, beige or grey page backgrounds; white or `--gw-bg-soft` only.
|
|
93
|
-
- Don't use colours, sizes, spacings or radii outside the tokens and scales
|
|
94
|
-
|
|
105
|
+
- Don't use colours, sizes, spacings or radii outside the tokens and scales, and don't redefine a kit token to get
|
|
106
|
+
another look: choose a profile.
|
|
107
|
+
- Don't build your own floating, blurred or "pill" header. The header is the kit's flat bar with one hairline, or
|
|
108
|
+
its inset card `.gw-header--float` when the brand chose it.
|
|
95
109
|
- Don't tuck the hero under the header with a negative margin.
|
|
96
110
|
- Don't use emoji as icons; use inline stroke SVG (1.7–2px stroke, `currentColor`, `aria-hidden="true"`).
|
|
97
|
-
- Don't use gradient washes behind body text, glassmorphism on chrome, or neon glows
|
|
111
|
+
- Don't use gradient washes behind body text, glassmorphism on chrome, or neon glows (the floating header's blur is
|
|
112
|
+
the kit's, not a style to copy).
|
|
98
113
|
- Don't put two primary buttons side by side.
|
|
99
114
|
- Don't write "Click here", "Learn more" as the only words of a link; say where it goes.
|
|
100
115
|
- Don't use the em dash (—) in visible text. Use a comma, a colon or a full stop.
|
|
@@ -105,6 +120,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
105
120
|
- Don't ship a page you have not seen at 320px.
|
|
106
121
|
- Don't write CSS that overrides a `.gw-*` class to change how it looks. If the kit's version is wrong for you,
|
|
107
122
|
that is a gap (section 9).
|
|
123
|
+
- Don't wire kit components with your own JS (no click handler on a trigger, no class toggled by hand). After you
|
|
124
|
+
insert kit markup (a route change, a block rendered later), call `gwWebsite.init(root)` on it, or `init(root)` from
|
|
125
|
+
`website.mjs`.
|
|
108
126
|
|
|
109
127
|
## 5. Accessibility
|
|
110
128
|
|
|
@@ -139,14 +157,24 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
139
157
|
|
|
140
158
|
## 7. Theming for another brand
|
|
141
159
|
|
|
142
|
-
A brand other than GemBoss
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
accent-strong must reach 4.5:1 on
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
A brand other than GemBoss chooses **only** these, and nothing else:
|
|
161
|
+
|
|
162
|
+
- **the accent family**: eight variables in one `:root {}` block after the kit, `--gw-accent-light`,
|
|
163
|
+
`--gw-accent-strong-light`, `--gw-accent-soft-light`, `--gw-on-accent-light` and the same four ending in `-dark`.
|
|
164
|
+
Do not set `--gw-accent` itself: the kit points it at those per theme, and a direct override loses as soon as the
|
|
165
|
+
theme toggle is pressed. Accent text uses `--gw-accent-strong`, so it must reach 4.5:1 on `--gw-bg` and
|
|
166
|
+
`--gw-bg-soft`;
|
|
167
|
+
- **the wash hues**, **the logo** in the header and footer, and **the favicon**;
|
|
168
|
+
- **three named profiles**, once for the site, never free values:
|
|
169
|
+
- action colour: `data-gw-action="accent"` on `<html>` fills the primary button with the accent (default `ink`).
|
|
170
|
+
Its words (`--gw-on-accent`) must reach 4.5:1 on `--gw-accent` and `--gw-accent-strong`, light and dark, or
|
|
171
|
+
`check` fails;
|
|
172
|
+
- radius: `data-gw-radius="soft"` on `<html>` (12 / 20 / 24) instead of `tight` (8 / 12 / 16, default);
|
|
173
|
+
- header: `<header class="gw-header gw-header--float">`, an inset card from 1080px, instead of the flat bar.
|
|
174
|
+
|
|
175
|
+
A brand does not change the neutrals, the type, the spacing, the shadows or the widths, and the profiles do not open
|
|
176
|
+
accent-coloured heading words, accent bands or uppercase labels. Recipe and checks: `docs/theming.md`; working
|
|
177
|
+
examples: `demo/theme-example.html` (accent only) and `demo/brand-profiles.html` (all three profiles).
|
|
150
178
|
|
|
151
179
|
## 8. Where things go in a consuming site
|
|
152
180
|
|
|
@@ -180,10 +208,11 @@ npx gemboss-website check # every .html and .css in the site against
|
|
|
180
208
|
npx gemboss-website check --fix-list # the same, as a to-do list
|
|
181
209
|
```
|
|
182
210
|
|
|
183
|
-
It reports colours, radii, spacing and type outside the system, shadows on chrome,
|
|
184
|
-
labels, em dashes in visible text, images without `alt`, overrides of `.gw-*` classes,
|
|
185
|
-
with `aria-busy`, a segmented control without its radio-group markup, a drawn mock-up
|
|
186
|
-
|
|
211
|
+
It reports colours, radii, spacing and type outside the system, shadows on chrome, buttons filled from site CSS,
|
|
212
|
+
uppercase labels, em dashes in visible text, images without `alt`, overrides of `.gw-*` classes, redefined kit tokens,
|
|
213
|
+
a waiting button not marked with `aria-busy`, a segmented control without its radio-group markup, a drawn mock-up
|
|
214
|
+
with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
|
|
215
|
+
words on the accent button below 4.5:1, and patterns that repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
|
|
187
216
|
|
|
188
217
|
## 11. Upgrading the kit
|
|
189
218
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,69 @@ Every release lists what changed for a site that uses the kit. A release that ad
|
|
|
4
4
|
says so with `Closes gap #N` (the GitHub issue number), so `npx gemboss-website upgrade` can tell that site which
|
|
5
5
|
stopgap (`kitgap-*`) it can now delete.
|
|
6
6
|
|
|
7
|
+
## 0.4.0 (2026-10-25)
|
|
8
|
+
|
|
9
|
+
The website script, standardised like the CSS: it now wires markup that arrives after the page loaded (a Next.js
|
|
10
|
+
route change, a GemPages block rendered later, a CMS preview), and comes as an ES module too. A page that loads
|
|
11
|
+
`js/website.js` with `defer` and changes nothing works exactly as in 0.3.0 (the demo is pixel-identical at 390 and
|
|
12
|
+
1366px, light and dark).
|
|
13
|
+
|
|
14
|
+
Closes #64
|
|
15
|
+
|
|
16
|
+
- `gwWebsite.init(root = document)`: wires the kit's markup under `root`, `root` included. Idempotent: each element
|
|
17
|
+
is wired once (and marked `data-gw-ready`), so call it after every navigation or insert and only the new elements
|
|
18
|
+
are wired; on a fragment it wires that fragment only. The page's own listeners (Escape, the outside click, the
|
|
19
|
+
busy-button guard, the system theme, the scroll) are installed once. Each call lets go of elements removed from
|
|
20
|
+
the page meanwhile and moves the script's `aria-current` mark to the new path (a mark in the markup stays).
|
|
21
|
+
- `gwWebsite.destroy(root = document)`: removes what `init` wired under `root`, its listeners and its marks, for
|
|
22
|
+
frameworks that unmount. An open mega or mobile menu closes first, so the page is never left locked.
|
|
23
|
+
- ESM entry `@gemboss/website-ui/website.mjs` (`js/website.mjs`): `export { init, destroy, version }`, no side
|
|
24
|
+
effects on import (safe to import on a server), no auto-init. `js/website.js` keeps its path, its export
|
|
25
|
+
`./website.js` and its behaviour: it starts by itself on `DOMContentLoaded` and exposes `window.gwWebsite`.
|
|
26
|
+
- One source: `scripts/build.mjs` writes `js/website.js` from `js/website.mjs` (the code in an IIFE, without the
|
|
27
|
+
comments), and `--check` fails when they differ or when the version is not `package.json`'s.
|
|
28
|
+
- One copy per page: the first copy that runs owns `window.gwWebsite`; another one (the classic script next to a
|
|
29
|
+
bundle) hands its calls to it, so nothing is wired twice.
|
|
30
|
+
- Events, all bubbling, listed in `docs/markup-contract.md`: `gw:mega` on the trigger (`{ id, open }`) and
|
|
31
|
+
`gw:theme` on `<html>` (`{ theme: 'light' | 'dark' }`), next to `gw:segmented`.
|
|
32
|
+
- Budget: `js/website.js` is 11.3 KB unminified, 3.8 KB gzipped (0.3.0: 9.2 KB, 3.3 KB). The test now holds it under
|
|
33
|
+
12 KB unminified and 4.5 KB gzipped (was 10 KB unminified): the comments live in `website.mjs`, the classic file
|
|
34
|
+
carries none.
|
|
35
|
+
- `AGENTS.md`, the skill and `docs/components.md`: never wire a kit part with your own JS; call `init(root)` after
|
|
36
|
+
inserting kit markup. README: the ESM import and the Next.js recipe (a client effect on each route change).
|
|
37
|
+
|
|
38
|
+
## 0.3.0 (2026-10-25)
|
|
39
|
+
|
|
40
|
+
Brand profiles: three knobs so that another GemCommerce brand (GemPages first, Gemians later) can use the kit and
|
|
41
|
+
still look like itself. Each one is a named profile chosen once for the site, never a free value. Markup that worked
|
|
42
|
+
in 0.2.0 still works, and a page without the new attributes looks exactly as before (ink buttons, tight corners, the
|
|
43
|
+
flat header: the demo is pixel-identical to 0.2.0 at 390 and 1366px, light and dark).
|
|
44
|
+
|
|
45
|
+
Closes #61
|
|
46
|
+
|
|
47
|
+
- Action colour: `data-gw-action="accent"` on `<html>` fills the primary `.gw-btn` with the brand's accent
|
|
48
|
+
(`--gw-on-accent` words, `--gw-accent-strong` on hover and press, the `-dark` family in dark mode and night bands).
|
|
49
|
+
The default is `ink`. No button shadow. The build fails when the kit's own colours put the button's words below
|
|
50
|
+
4.5:1, light or dark, and `check` does the same with a brand's colours (`action-contrast`).
|
|
51
|
+
- Radius profiles: `data-gw-radius="soft"` on `<html>` gives 12 / 20 / 24 (action, card, panel) and 24 for the mega
|
|
52
|
+
menu; the default `tight` keeps 8 / 12 / 16 and 20. Every component reads `--gw-radius-action|card|panel|popover`;
|
|
53
|
+
no component keeps a radius of its own.
|
|
54
|
+
- Floating header: `<header class="gw-header gw-header--float">` is, from 1080px, an inset card (the panel radius, the
|
|
55
|
+
surface at 85% over a 10px blur, one hairline, the raised shadow, 16px from the top). Same bar, mega menu and
|
|
56
|
+
script; the flat bar below 1080px, so the phone menu is unchanged; solid under `prefers-reduced-transparency` and
|
|
57
|
+
where the browser cannot blur.
|
|
58
|
+
- Still forbidden with every profile: accent or gradient words in a heading, accent section bands, uppercase labels.
|
|
59
|
+
`AGENTS.md` (rules 2 and 3, section 7), `docs/theming.md` (a brand chooses six things, nothing else) and
|
|
60
|
+
`docs/philosophy.md` say what changed and why: the site speaks the language of the brand's own product.
|
|
61
|
+
- `check`: `profile-unknown` (a name that does not exist, or a profile set off `<html>`), `header-float-markup` (the
|
|
62
|
+
float class off the header) and `action-contrast`, all errors; `token-redefined` and `radius-off-scale` point at
|
|
63
|
+
the profiles.
|
|
64
|
+
- `system.json` version 2: `profiles` (action, radius, header). `radius.action`, `radius.card` and `radius.panel`
|
|
65
|
+
moved to `profiles.radius.options.tight`; `radius.scale` gains 24. New tokens: `--gw-radius-popover`,
|
|
66
|
+
`--gw-radius-24`, `--gw-header-gap`, `--gw-header-surface`, `--gw-header-blur`.
|
|
67
|
+
- `demo/brand-profiles.html`: a GemPages-style brand with all three profiles and the accent `#3c67ff`; its
|
|
68
|
+
accent-strong `#2f52d6` is an example the GemPages team has still to confirm.
|
|
69
|
+
|
|
7
70
|
## 0.2.0 (2026-10-25)
|
|
8
71
|
|
|
9
72
|
Four gaps reported while the GemBoss Design System was written. Markup that worked in 0.1.0 still works.
|
package/README.md
CHANGED
|
@@ -12,11 +12,11 @@ it. Made so that a team working with Claude Code, even without a designer, ships
|
|
|
12
12
|
|
|
13
13
|
| | |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng
|
|
16
|
-
| `js/website.js` | mega menu, menu điện thoại, đổi giao diện sáng/tối, hiện dần khi cuộn, đánh dấu trang hiện tại, segmented control, chặn bấm lần hai khi nút đang chờ; không phụ thuộc thư viện nào, khoảng
|
|
15
|
+
| `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 52 KB, 40 KB bản min): token sáng và tối, chữ, bố cục, nút (cả trạng thái nhấn và đang chờ), link, nhãn, chip, segmented control, badge, card, khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), header + mega menu + menu điện thoại, footer, 12 section, 3 hồ sơ thương hiệu |
|
|
16
|
+
| `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại, đổi giao diện sáng/tối, hiện dần khi cuộn, đánh dấu trang hiện tại, segmented control, chặn bấm lần hai khi nút đang chờ; không phụ thuộc thư viện nào, khoảng 4 KB khi nén gzip. `website.js` là script thường (tự chạy khi trang tải xong), `website.mjs` là bản ES module cho app; cả hai có `init(root)` để gắn lại phần markup chèn sau (`docs/markup-contract.md`, mục "Script") |
|
|
17
17
|
| `AGENTS.md` | luật mà Claude Code (và người) phải theo: 10 luật, cách dựng trang, điều cấm, chữ, khả năng truy cập, đổi thương hiệu, khi kit thiếu |
|
|
18
18
|
| `templates/` | khung một trang và 12 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose) |
|
|
19
|
-
| `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn |
|
|
19
|
+
| `demo/` | mọi thành phần trên một trang; `theme-example.html` cho thấy một thương hiệu khác đổi màu nhấn; `brand-profiles.html` là một thương hiệu kiểu GemPages bật cả 3 hồ sơ |
|
|
20
20
|
| `docs/` | danh mục class (`components.md`), đổi thương hiệu (`theming.md`), triết lý (`philosophy.md`), hợp đồng markup |
|
|
21
21
|
| `system.json` | mọi giá trị của hệ thống (màu, khoảng cách, chữ, bo góc, bóng, khung): nguồn duy nhất |
|
|
22
22
|
| CLI `gemboss-website` | `init`, `check`, `gap`, `upgrade` |
|
|
@@ -40,33 +40,55 @@ nó đọc `AGENTS.md` của kit trước, dựng từ mẫu, và chạy `check`
|
|
|
40
40
|
**Site HTML tĩnh hoặc không có bước build:** dùng CDN của npm, ghim đúng phiên bản.
|
|
41
41
|
|
|
42
42
|
```html
|
|
43
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.
|
|
43
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.4.0/website.min.css">
|
|
44
44
|
<link rel="stylesheet" href="/site.css"> <!-- CSS riêng của site, luôn SAU kit -->
|
|
45
|
-
<script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.4.0/js/website.js" defer></script>
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Chèn markup của kit sau khi trang đã tải (block của builder, bản xem trước CMS): gọi `gwWebsite.init(phầnMới)` ngay
|
|
49
|
+
sau đó. Đừng tự viết JS cho mega menu, nút đổi giao diện hay segmented control.
|
|
50
|
+
|
|
51
|
+
**Next.js (App Router):** nạp CSS trong layout gốc; script là bản ES module, gọi `init()` trong một effect mỗi lần
|
|
52
|
+
đổi trang (chuyển trang phía client không tải lại trang, nên phần markup mới phải được gắn lại).
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { init, destroy } from "@gemboss/website-ui/website.mjs";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// app/kit-script.jsx
|
|
60
|
+
"use client";
|
|
61
|
+
import { useEffect } from "react";
|
|
62
|
+
import { usePathname } from "next/navigation";
|
|
63
|
+
import { init } from "@gemboss/website-ui/website.mjs";
|
|
64
|
+
|
|
65
|
+
export default function KitScript() {
|
|
66
|
+
const pathname = usePathname();
|
|
67
|
+
useEffect(() => { init(); }, [pathname]);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
49
71
|
|
|
50
72
|
```js
|
|
51
73
|
// app/layout.js
|
|
52
74
|
import "@gemboss/website-ui/website.css";
|
|
53
75
|
import "./site.css";
|
|
54
|
-
import
|
|
76
|
+
import KitScript from "./kit-script";
|
|
55
77
|
|
|
56
78
|
export default function RootLayout({ children }) {
|
|
57
79
|
return (
|
|
58
80
|
<html lang="vi">
|
|
59
81
|
<body className="gw-page">
|
|
60
82
|
{children}
|
|
61
|
-
<
|
|
83
|
+
<KitScript />
|
|
62
84
|
</body>
|
|
63
85
|
</html>
|
|
64
86
|
);
|
|
65
87
|
}
|
|
66
88
|
```
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
`init()` gọi lại bao nhiêu lần cũng được: phần đã gắn thì bỏ qua, chỉ gắn phần mới. Font Inter nạp từ Google Fonts
|
|
91
|
+
như trong `templates/page.html`.
|
|
70
92
|
|
|
71
93
|
**Bắt đầu một trang:** chép `templates/page.html`, điền header và footer một lần (chung cho mọi trang), ghép các
|
|
72
94
|
section trong `templates/sections/` theo thứ tự trang cần, thay mọi chỗ `[trong ngoặc]`, xoá phần không dùng. Mở
|
|
@@ -74,16 +96,24 @@ section trong `templates/sections/` theo thứ tự trang cần, thay mọi ch
|
|
|
74
96
|
|
|
75
97
|
## Thương hiệu khác GemBoss
|
|
76
98
|
|
|
77
|
-
Một thương hiệu chỉ
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
Một thương hiệu chỉ chọn sáu thứ: màu nhấn (8 biến cho sáng và tối, trong một khối `:root {}` đặt sau kit), màu
|
|
100
|
+
wash, logo (kèm favicon), và ba hồ sơ có tên, chọn một lần cho cả site:
|
|
101
|
+
|
|
102
|
+
- màu nút chính: `data-gw-action="accent"` trên `<html>` cho nút chính dùng màu nhấn (mặc định `ink`, nút màu mực);
|
|
103
|
+
chữ trên nút phải đạt 4,5:1 cả sáng lẫn tối, không thì `check` báo lỗi;
|
|
104
|
+
- bo góc: `data-gw-radius="soft"` trên `<html>` (12 / 20 / 24) thay cho `tight` (8 / 12 / 16, mặc định);
|
|
105
|
+
- header: `<header class="gw-header gw-header--float">`, một thẻ nổi từ 1080px; điện thoại giữ thanh phẳng.
|
|
106
|
+
|
|
107
|
+
Nền trắng, màu trung tính, chữ, khoảng cách, bóng giữ nguyên; chữ màu nhấn trong tiêu đề, dải nền màu nhấn, nhãn
|
|
108
|
+
viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs/theming.md`; ví dụ chạy được:
|
|
109
|
+
`demo/theme-example.html` (chỉ màu nhấn) và `demo/brand-profiles.html` (cả ba hồ sơ).
|
|
80
110
|
|
|
81
111
|
## Lệnh
|
|
82
112
|
|
|
83
113
|
| lệnh | làm gì |
|
|
84
114
|
|---|---|
|
|
85
115
|
| `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
|
|
86
|
-
| `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tô màu
|
|
116
|
+
| `npx gemboss-website check` | kiểm mọi `.html` và `.css` của site: màu, khoảng cách, chữ, bo góc ngoài thang, bóng, nút tự tô màu, nhãn viết hoa giãn chữ, đè class `gw-`, đổi token của kit, hồ sơ thương hiệu sai tên hoặc sai chỗ, chữ trên nút màu nhấn dưới 4,5:1, dấu gạch dài, ảnh thiếu `alt`, link mơ hồ… và gợi ý chỗ kit có thể đang thiếu |
|
|
87
117
|
| `npx gemboss-website gap` | báo một chỗ kit còn thiếu: mở GitHub Issue ở repo của kit (nếu `gh` đã đăng nhập và có quyền), không thì ghi `kit-gaps/<tên>.md` để gửi |
|
|
88
118
|
| `npx gemboss-website upgrade` | so bản đang dùng với bản mới nhất, in thay đổi, gap đã có trong kit, và lệnh nâng cấp |
|
|
89
119
|
|
|
@@ -109,7 +139,7 @@ gemboss-www; mọi thay đổi đi qua PR, giá trị của hệ thống chỉ s
|
|
|
109
139
|
|
|
110
140
|
```sh
|
|
111
141
|
cd packages/website-ui
|
|
112
|
-
node scripts/build.mjs # sinh css/tokens.css từ system.json, ghép website.css và website.min.css
|
|
142
|
+
node scripts/build.mjs # sinh css/tokens.css từ system.json, ghép website.css và website.min.css, sinh js/website.js từ js/website.mjs
|
|
113
143
|
node test/run.mjs # tự kiểm CLI
|
|
114
144
|
node scripts/gate-demo.mjs # cổng thị giác và khoảng thở của gemboss.ai trên demo (cần .ci của repo)
|
|
115
145
|
```
|
package/bin/lib/check.mjs
CHANGED
|
@@ -3,8 +3,19 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { basename, resolve } from "node:path";
|
|
5
5
|
import { PKG, SYSTEM, UsageError, blank, c, isKitPath, lineIndex, parseArgs, rel, walkFiles } from "./util.mjs";
|
|
6
|
+
import { MIN_CONTRAST, actionPairs, ratio } from "./contrast.mjs";
|
|
6
7
|
|
|
7
8
|
const RADII = new Set(SYSTEM.radius.scale);
|
|
9
|
+
// A brand's named profiles (system.json `profiles`): one attribute on <html> each, and the floating header's class.
|
|
10
|
+
const PROFILE = Object.fromEntries(["action", "radius"].map((k) => {
|
|
11
|
+
const p = SYSTEM.profiles[k];
|
|
12
|
+
return [p.attribute, { kind: k, names: Object.keys(p.options).filter((n) => !n.startsWith("$")) }];
|
|
13
|
+
}));
|
|
14
|
+
const FLOAT_CLASS = SYSTEM.profiles.header.options.float.class;
|
|
15
|
+
const ACCENT_ACTION = SYSTEM.profiles.action.options.accent;
|
|
16
|
+
const profileList = () => Object.entries(PROFILE).map(([attr, p]) => `${attr}="${p.names.join("|")}"`).join(" and ");
|
|
17
|
+
const radiusText = () => Object.entries(SYSTEM.profiles.radius.options).filter(([n]) => !n.startsWith("$"))
|
|
18
|
+
.map(([n, o]) => `${n} ${o.action}/${o.card}/${o.panel}`).join(", ");
|
|
8
19
|
const TYPE = new Set(SYSTEM.type.scale);
|
|
9
20
|
// The page gutter (layout.gutter) is a system value too: horizontal padding and the negative bleed that undoes it.
|
|
10
21
|
const SPACE = new Set([...SYSTEM.space.scale, SYSTEM.layout.gutter]);
|
|
@@ -20,13 +31,13 @@ export const RULES = {
|
|
|
20
31
|
},
|
|
21
32
|
"token-redefined": {
|
|
22
33
|
level: "error", title: "Kit token redefined",
|
|
23
|
-
why: "
|
|
24
|
-
fix:
|
|
34
|
+
why: "A brand chooses its accent family and wash hues (variables), its logo, and three named profiles; nothing else. Changing another --gw-* token changes every component on the site.",
|
|
35
|
+
fix: `Remove it. The action colour and the corners are profiles, not tokens: ${profileList()} on <html>; the floating header is the class ${FLOAT_CLASS} (docs/theming.md). If the brand needs something else, report a gap: npx gemboss-website gap --kind token.`,
|
|
25
36
|
},
|
|
26
37
|
"radius-off-scale": {
|
|
27
38
|
level: "error", title: "Radius not on the scale",
|
|
28
|
-
why:
|
|
29
|
-
fix: `Use one of ${list(RADII)} (px), or a kit class that already has the shape.`,
|
|
39
|
+
why: `Actions, cards and panels take the radius of the site's profile (${radiusText()}), chips 999px. Other radii make a page look improvised.`,
|
|
40
|
+
fix: `Use var(--gw-radius-action), var(--gw-radius-card) or var(--gw-radius-panel) (they follow the profile), one of ${list(RADII)} (px), or a kit class that already has the shape.`,
|
|
30
41
|
},
|
|
31
42
|
"type-off-scale": {
|
|
32
43
|
level: "error", title: "Font size not on the type scale",
|
|
@@ -55,8 +66,8 @@ export const RULES = {
|
|
|
55
66
|
},
|
|
56
67
|
"accent-button": {
|
|
57
68
|
level: "error", title: "Button filled with the accent, a wash or a gradient",
|
|
58
|
-
why: "The thing you click
|
|
59
|
-
fix: "Use .gw-btn
|
|
69
|
+
why: "The thing you click has one colour on the whole site, the action profile: ink, or the brand's accent when <html> has data-gw-action=\"accent\". A fill set in site CSS breaks that, and a gradient or a wash hue is never one.",
|
|
70
|
+
fix: "Use .gw-btn and .gw-btn--secondary as they are. For an accent button, put data-gw-action=\"accent\" on <html> (docs/theming.md).",
|
|
60
71
|
},
|
|
61
72
|
"gw-override": {
|
|
62
73
|
level: "error", title: "Site CSS overrides a kit class",
|
|
@@ -103,6 +114,21 @@ export const RULES = {
|
|
|
103
114
|
why: "website.js and screen readers find the choice through the markup: a radiogroup with a name and data-gw-segmented, two to four <button role=\"radio\"> options, exactly one aria-checked=\"true\". Without it the arrows do nothing and the choice is not announced.",
|
|
104
115
|
fix: "Copy the markup from docs/components.md (Segmented control) or templates/sections/pricing.html.",
|
|
105
116
|
},
|
|
117
|
+
"profile-unknown": {
|
|
118
|
+
level: "error", title: "Brand profile that does not exist, or not on <html>",
|
|
119
|
+
why: "A brand chooses its action colour and its corners once for the site, from a fixed list, with one attribute on <html>. An unknown name falls back to the default without a word, and on any other element the profile reaches only part of the page.",
|
|
120
|
+
fix: `Use ${profileList()} on <html>, and nowhere else (docs/theming.md).`,
|
|
121
|
+
},
|
|
122
|
+
"header-float-markup": {
|
|
123
|
+
level: "error", title: "Floating header class not on the header",
|
|
124
|
+
why: `.${FLOAT_CLASS} reshapes the <header class="gw-header"> itself (an inset card from 1080px). On another element it does nothing, or draws a card inside the bar.`,
|
|
125
|
+
fix: `Put both classes on the header: <header class="gw-header ${FLOAT_CLASS}" data-gw-header>.`,
|
|
126
|
+
},
|
|
127
|
+
"action-contrast": {
|
|
128
|
+
level: "error", title: "Words on the accent button below 4.5:1",
|
|
129
|
+
why: `With data-gw-action="accent" the primary button is filled with the brand's accent. Its words (--gw-on-accent) must reach ${MIN_CONTRAST}:1 on --gw-accent and on --gw-accent-strong (hover and pressed), in light and in dark, or the main action of every page is hard to read.`,
|
|
130
|
+
fix: "Darken --gw-accent-light and --gw-accent-strong-light (or change --gw-on-accent-light), lighten the -dark ones (dark words on them), until every pair passes; write them as #rrggbb. Or keep the ink button: remove data-gw-action.",
|
|
131
|
+
},
|
|
106
132
|
"uppercase-text": {
|
|
107
133
|
level: "warning", title: "Uppercase text",
|
|
108
134
|
why: "Headings, buttons, labels and menu items are sentence case.",
|
|
@@ -382,6 +408,39 @@ function checkFontSize(d, value, where, add) {
|
|
|
382
408
|
}
|
|
383
409
|
}
|
|
384
410
|
|
|
411
|
+
/**
|
|
412
|
+
* The brand's accent primitives (--gw-accent-light, --gw-on-accent-dark…) as one page sees them: the plain :root
|
|
413
|
+
* blocks of the site's CSS files, then the page's own <style> blocks, over the kit's values. { name: { value, at } }
|
|
414
|
+
*/
|
|
415
|
+
function brandAccent(cssRules, page) {
|
|
416
|
+
const vals = {};
|
|
417
|
+
const take = ({ file, rules }) => {
|
|
418
|
+
for (const r of rules) {
|
|
419
|
+
if (r.atBlock || r.at.length || !r.selector || !isRootish(r.selector) || /data-theme/i.test(r.selector)) continue;
|
|
420
|
+
for (const d of r.decls) {
|
|
421
|
+
if (/^--gw-(?:on-)?accent(?:-strong|-soft)?-(?:light|dark)$/.test(d.prop)) vals[d.prop] = { value: d.value.replace(/!important\s*$/i, "").trim(), at: `${file}:${d.line}` };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
for (const c of cssRules.filter((x) => !x.inline)) take(c);
|
|
426
|
+
for (const c of cssRules.filter((x) => x.inline && x.file === page)) take(c);
|
|
427
|
+
return vals;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Pages with data-gw-action="accent": the words of the primary button on its fills, light and dark (action-contrast). */
|
|
431
|
+
function checkActionContrast(out, add) {
|
|
432
|
+
for (const pg of out.pageProfiles.filter((p) => p.action === "accent")) {
|
|
433
|
+
const brand = brandAccent(out.cssRules, pg.file);
|
|
434
|
+
const colours = (theme, role) => (brand[`--gw-${role}-${theme}`] || { value: SYSTEM.color[theme][role] }).value;
|
|
435
|
+
const src = (theme, role) => { const b = brand[`--gw-${role}-${theme}`]; return b ? ` (${b.at})` : " (kit default)"; };
|
|
436
|
+
for (const p of actionPairs(ACCENT_ACTION, colours)) {
|
|
437
|
+
if (p.ok) continue;
|
|
438
|
+
const pair = `--gw-${p.textRole}-${p.theme} ${p.text}${src(p.theme, p.textRole)} on --gw-${p.fillRole}-${p.theme} ${p.fill}${src(p.theme, p.fillRole)}`;
|
|
439
|
+
add("action-contrast", { file: pg.file, line: pg.line, excerpt: `${p.theme}, ${p.state === "rest" ? "at rest" : "hover"}: ${pair} ${p.ratio === null ? "cannot be read as a colour" : `is ${ratio(p.ratio)}, needs ${MIN_CONTRAST}:1`}` });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
385
444
|
// --------------------------------------------------------------------------------------------- HTML scanning
|
|
386
445
|
|
|
387
446
|
const TAG_RE = /<([a-zA-Z][\w:-]*)((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
|
|
@@ -469,6 +528,13 @@ function analyzeHtml(file, text, out) {
|
|
|
469
528
|
if (tag === "img" && !("alt" in a)) add("img-alt", where());
|
|
470
529
|
if (tag === "h1" && ++h1 === 2) add("multiple-h1", where());
|
|
471
530
|
if (tag === "html" && !(a.lang || "").trim()) add("missing-lang", where());
|
|
531
|
+
for (const [attr, p] of Object.entries(PROFILE)) {
|
|
532
|
+
if (!(attr in a)) continue;
|
|
533
|
+
const v = (a[attr] || "").trim();
|
|
534
|
+
if (tag !== "html") add("profile-unknown", where(`${attr}="${v}" on <${tag}>: only on <html>`));
|
|
535
|
+
else if (!p.names.includes(v)) add("profile-unknown", where(`${attr}="${v}": no such ${p.kind} profile (${p.names.join(", ")})`));
|
|
536
|
+
else if (p.kind === "action") out.pageProfiles.push({ file: relFile, line, action: v });
|
|
537
|
+
}
|
|
472
538
|
if ((tag === "div" || tag === "span") && "onclick" in a) add("clickable-div", where());
|
|
473
539
|
for (const k of ["alt", "title", "aria-label", "placeholder"]) {
|
|
474
540
|
if (a[k] && hasEmDash(a[k])) add("em-dash", where(`${k}="${a[k]}"`));
|
|
@@ -481,6 +547,7 @@ function analyzeHtml(file, text, out) {
|
|
|
481
547
|
classes.add(k);
|
|
482
548
|
if (k.startsWith("kitgap-") && !out.kitgapUse.has(gapBlock(k))) out.kitgapUse.set(gapBlock(k), { file: relFile, line });
|
|
483
549
|
}
|
|
550
|
+
if (cls.includes(FLOAT_CLASS) && !cls.includes("gw-header")) add("header-float-markup", where());
|
|
484
551
|
const inner = () => { const end = elementEnd(scanText, m.index, tag); return end < 0 ? "" : scanText.slice(m.index + m[0].length, end); };
|
|
485
552
|
if (cls.includes("gw-btn")) {
|
|
486
553
|
const busy = (a["aria-busy"] || "").trim() === "true";
|
|
@@ -569,7 +636,7 @@ export function runCheck(paths, { ignore = [], includeKit = false } = {}) {
|
|
|
569
636
|
const r = RULES[rule];
|
|
570
637
|
findings.push({ rule, level: r.level, file: w.file, line: w.line, excerpt: w.excerpt, title: r.title, why: r.why, fix: r.fix });
|
|
571
638
|
};
|
|
572
|
-
const out = { add, cssRules: [], pageClasses: [], kitgapUse: new Map() };
|
|
639
|
+
const out = { add, cssRules: [], pageClasses: [], kitgapUse: new Map(), pageProfiles: [] };
|
|
573
640
|
const counts = { html: 0, css: 0, skipped: [] };
|
|
574
641
|
for (const f of files) {
|
|
575
642
|
let text;
|
|
@@ -587,6 +654,8 @@ export function runCheck(paths, { ignore = [], includeKit = false } = {}) {
|
|
|
587
654
|
}
|
|
588
655
|
}
|
|
589
656
|
|
|
657
|
+
checkActionContrast(out, add);
|
|
658
|
+
|
|
590
659
|
// Classes styled in site CSS: first definition of each.
|
|
591
660
|
const styled = new Map();
|
|
592
661
|
for (const { file, rules, text } of [...out.cssRules].sort((a, b) => (a.inline ? 1 : 0) - (b.inline ? 1 : 0))) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// WCAG 2 contrast for the kit's action profiles (system.json `profiles.action`). One implementation for the build
|
|
2
|
+
// (scripts/build.mjs checks the kit's own colours) and for `gemboss-website check` (a brand's accent family).
|
|
3
|
+
// No dependencies, Node 18 or later.
|
|
4
|
+
|
|
5
|
+
export const MIN_CONTRAST = 4.5;
|
|
6
|
+
|
|
7
|
+
/** "#rgb", "#rrggbb" (alpha ignored only when fully opaque), "rgb(r g b)" / "rgb(r, g, b)" -> [r, g, b], else null. */
|
|
8
|
+
export function parseColor(value) {
|
|
9
|
+
const v = String(value || "").trim().toLowerCase();
|
|
10
|
+
let m = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(v);
|
|
11
|
+
if (m) {
|
|
12
|
+
let h = m[1];
|
|
13
|
+
if (h.length <= 4) h = [...h].map((c) => c + c).join("");
|
|
14
|
+
if (h.length === 8 && h.slice(6) !== "ff") return null;
|
|
15
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
16
|
+
}
|
|
17
|
+
m = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})\s*(?:[,/]\s*(1|1\.0+|100%)\s*)?\)$/.exec(v);
|
|
18
|
+
if (m) {
|
|
19
|
+
const rgb = m.slice(1, 4).map(Number);
|
|
20
|
+
return rgb.every((n) => n <= 255) ? rgb : null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const channel = (c) => { const s = c / 255; return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; };
|
|
26
|
+
const luminance = ([r, g, b]) => 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
27
|
+
|
|
28
|
+
/** Contrast ratio of two colours (any order), or null when one cannot be read. */
|
|
29
|
+
export function contrast(a, b) {
|
|
30
|
+
const x = parseColor(a), y = parseColor(b);
|
|
31
|
+
if (!x || !y) return null;
|
|
32
|
+
const [hi, lo] = [luminance(x), luminance(y)].sort((p, q) => q - p);
|
|
33
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const ratio = (n) => `${(Math.floor(n * 100) / 100).toFixed(2)}:1`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every pair a primary button paints in one action profile: its label (`on-action`) on the fill (`action`) and on the
|
|
40
|
+
* hover and pressed fill (`action-hover`), in light and in dark (a night band uses the dark palette).
|
|
41
|
+
* `colours(theme, role)` returns the colour of a role ("accent", "on-accent", "action"…) in "light" or "dark".
|
|
42
|
+
* Returns [{ theme, state, text, fill, textRole, fillRole, ratio, ok }]; `ratio` is null when a colour is unreadable.
|
|
43
|
+
*/
|
|
44
|
+
export function actionPairs(profile, colours) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const theme of ["light", "dark"]) {
|
|
47
|
+
for (const [state, fillRole] of [["rest", profile.action], ["hover", profile["action-hover"]]]) {
|
|
48
|
+
const textRole = profile["on-action"];
|
|
49
|
+
const text = colours(theme, textRole), fill = colours(theme, fillRole);
|
|
50
|
+
const r = contrast(text, fill);
|
|
51
|
+
out.push({ theme, state, text, fill, textRole, fillRole, ratio: r, ok: r !== null && r >= MIN_CONTRAST });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The kit's own colours for a role, from system.json `color.light` / `color.dark`. */
|
|
58
|
+
export const kitColours = (system) => (theme, role) => system.color[theme][role];
|
|
59
|
+
|
|
60
|
+
/** Every action profile of system.json against the kit's own colours: [{ profile, ...pair }]. Used by the build. */
|
|
61
|
+
export function kitActionContrast(system) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const [name, profile] of Object.entries(system.profiles.action.options).filter(([k]) => !k.startsWith("$"))) {
|
|
64
|
+
for (const p of actionPairs(profile, kitColours(system))) out.push({ profile: name, ...p });
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
package/css/components.css
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* components: buttons, links, chips, badges, cards, windows, icons, lists, tables, FAQ. */
|
|
2
2
|
|
|
3
|
-
/* actions are rectangles: 40px, radius 8, 14px/600, sentence case
|
|
3
|
+
/* actions are rectangles: 40px, the action radius of the site's profile (8, 12 in soft), 14px/600, sentence case.
|
|
4
|
+
The fill is the action colour: ink, or the brand's accent under data-gw-action="accent" (tokens.css). Never a
|
|
5
|
+
gradient, a wash hue or a shadow. */
|
|
4
6
|
.gw-btn {
|
|
5
7
|
position: relative;
|
|
6
8
|
display: inline-flex;
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
@media (prefers-reduced-motion: reduce) {
|
|
65
67
|
.gw-btn[aria-busy="true"]::before {
|
|
66
68
|
border: 0;
|
|
67
|
-
border-radius: 0;
|
|
69
|
+
border-radius: var(--gw-radius-0);
|
|
68
70
|
background:
|
|
69
71
|
radial-gradient(circle at 3px 50%, currentColor 1.5px, transparent 2px),
|
|
70
72
|
radial-gradient(circle at 8px 50%, currentColor 1.5px, transparent 2px),
|
|
@@ -110,8 +112,8 @@
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/* segmented control: one choice out of two to four (Monthly / Yearly), a radio group of buttons. The dashboard's look
|
|
113
|
-
(@gemboss/ui .ui-seg): one hairline group, radius
|
|
114
|
-
so it follows a brand's accent. The weight never changes with the choice: the labels do not move. */
|
|
115
|
+
(@gemboss/ui .ui-seg): one hairline group, the action radius, a hairline between options, the chosen one on the
|
|
116
|
+
accent tint, so it follows a brand's accent. The weight never changes with the choice: the labels do not move. */
|
|
115
117
|
.gw-segmented {
|
|
116
118
|
display: inline-flex;
|
|
117
119
|
max-width: 100%;
|
|
@@ -170,8 +172,8 @@
|
|
|
170
172
|
.gw-badge--success::before, .gw-badge--warning::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-success); }
|
|
171
173
|
.gw-badge--warning::before { background: var(--gw-warning); }
|
|
172
174
|
|
|
173
|
-
/* cards sit: white, 1px hairline, radius
|
|
174
|
-
app); no hover lift. A soft card is a tint and carries no shadow. */
|
|
175
|
+
/* cards sit: white, 1px hairline, the card radius of the site's profile (12, 20 in soft), the dashboard's resting
|
|
176
|
+
shadow (Chris 24/10/2026: one language with the app); no hover lift. A soft card is a tint and carries no shadow. */
|
|
175
177
|
.gw-card {
|
|
176
178
|
padding: 24px;
|
|
177
179
|
border: 1px solid var(--gw-line);
|