@gemboss/website-ui 0.2.0 → 0.3.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 +47 -22
- package/CHANGELOG.md +32 -0
- package/README.md +16 -8
- 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 +26 -11
- package/docs/markup-contract.md +16 -5
- package/docs/philosophy.md +14 -9
- package/docs/theming.md +85 -29
- package/js/website.js +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +3 -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
|
@@ -15,18 +15,25 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
15
15
|
| `templates/page.html` | the page skeleton: start every new page from it |
|
|
16
16
|
| `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose… |
|
|
17
17
|
| `demo/index.html` | every component on one page: open it to see what exists before you build anything |
|
|
18
|
+
| `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
|
|
18
19
|
| `docs/components.md` | every class, what it is for, its variants |
|
|
19
|
-
| `docs/theming.md` |
|
|
20
|
+
| `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
|
|
20
21
|
|
|
21
22
|
## 1. The ten rules
|
|
22
23
|
|
|
23
24
|
1. **Compose, do not invent.** A page is the page skeleton plus kit sections plus your words. Look in `demo/` and
|
|
24
25
|
`docs/components.md` first. A new class is a last resort (section 9).
|
|
25
|
-
2. **White canvas,
|
|
26
|
-
(`.gw-btn`)
|
|
27
|
-
accent
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
2. **White canvas, one action colour, one accent.** The background is white (`--gw-bg`). The thing you click
|
|
27
|
+
(`.gw-btn`) is near-black ink, or the brand's accent when the site chose the `accent` action profile
|
|
28
|
+
(`data-gw-action="accent"` on `<html>`, section 7): one colour for every primary button of the site, never chosen
|
|
29
|
+
per button. Why the choice (0.3.0): the site and the product speak one language, and GemPages' product clicks in
|
|
30
|
+
blue while GemBoss's clicks in ink. Otherwise the accent is for links, focus, the active item, the 8px label
|
|
31
|
+
square and a chart line. **The accent is never body text, never words in a heading, never a section band, and
|
|
32
|
+
never a fill you set yourself.**
|
|
33
|
+
3. **Actions are rectangles, chips are pills.** Buttons: 40px tall, sentence case, 14px/600, the action radius of the
|
|
34
|
+
site's profile (8px; 12px in `soft`). Chips, toggles, badges: fully rounded. Cards: the card radius (12px; 20px in
|
|
35
|
+
`soft`), large panels the panel radius (16px; 24px). No other radii: the site picks its profile once
|
|
36
|
+
(`data-gw-radius`), components follow.
|
|
30
37
|
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
38
|
`--gw-shadow`, the same as the GemBoss dashboard. It never lifts on hover. To separate anything else, use space or a
|
|
32
39
|
hairline before a shadow. The only shadows are `--gw-shadow` (rest), `--gw-shadow-sm` and `--gw-shadow-raised`
|
|
@@ -66,6 +73,8 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
66
73
|
it works; a hard-coded colour breaks it.
|
|
67
74
|
- **Accent as text** (links, the active menu item, step numbers) is `--gw-accent-strong`; the plain accent is for
|
|
68
75
|
the label square, the focus ring, check marks and borders. Night bands switch to the dark palette by themselves.
|
|
76
|
+
- **Brand profiles** (section 7): `data-gw-action` and `data-gw-radius` on `<html>`, `.gw-header--float` on the
|
|
77
|
+
header. Without them a page is GemBoss: ink buttons, tight corners, the flat header.
|
|
69
78
|
- **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
79
|
- **A button that waits** (a form sent, an audit started) gets `aria-busy="true"` from the page's script until the
|
|
71
80
|
answer comes: the kit keeps its label and width, draws the ring and ignores a second press. Nothing else: no
|
|
@@ -85,16 +94,21 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
85
94
|
|
|
86
95
|
## 4. Don't (each of these has happened, and each one made a page look cheap or broke it)
|
|
87
96
|
|
|
88
|
-
- Don't fill a button with the accent
|
|
97
|
+
- Don't fill a button yourself: not with the accent (that is the site's action profile, set once on `<html>`), a
|
|
98
|
+
gradient or a wash hue. No raised or inset shadow on a button.
|
|
99
|
+
- 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
100
|
- Don't use uppercase labels with letter spacing ("FEATURES").
|
|
90
101
|
- Don't add shadows beyond the three tokens, and don't lift cards on hover.
|
|
91
102
|
- Don't round a button into a pill, and don't square a chip.
|
|
92
103
|
- 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
|
-
|
|
104
|
+
- Don't use colours, sizes, spacings or radii outside the tokens and scales, and don't redefine a kit token to get
|
|
105
|
+
another look: choose a profile.
|
|
106
|
+
- Don't build your own floating, blurred or "pill" header. The header is the kit's flat bar with one hairline, or
|
|
107
|
+
its inset card `.gw-header--float` when the brand chose it.
|
|
95
108
|
- Don't tuck the hero under the header with a negative margin.
|
|
96
109
|
- 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
|
|
110
|
+
- Don't use gradient washes behind body text, glassmorphism on chrome, or neon glows (the floating header's blur is
|
|
111
|
+
the kit's, not a style to copy).
|
|
98
112
|
- Don't put two primary buttons side by side.
|
|
99
113
|
- Don't write "Click here", "Learn more" as the only words of a link; say where it goes.
|
|
100
114
|
- Don't use the em dash (—) in visible text. Use a comma, a colon or a full stop.
|
|
@@ -139,14 +153,24 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
139
153
|
|
|
140
154
|
## 7. Theming for another brand
|
|
141
155
|
|
|
142
|
-
A brand other than GemBoss
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
accent-strong must reach 4.5:1 on
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
A brand other than GemBoss chooses **only** these, and nothing else:
|
|
157
|
+
|
|
158
|
+
- **the accent family**: eight variables in one `:root {}` block after the kit, `--gw-accent-light`,
|
|
159
|
+
`--gw-accent-strong-light`, `--gw-accent-soft-light`, `--gw-on-accent-light` and the same four ending in `-dark`.
|
|
160
|
+
Do not set `--gw-accent` itself: the kit points it at those per theme, and a direct override loses as soon as the
|
|
161
|
+
theme toggle is pressed. Accent text uses `--gw-accent-strong`, so it must reach 4.5:1 on `--gw-bg` and
|
|
162
|
+
`--gw-bg-soft`;
|
|
163
|
+
- **the wash hues**, **the logo** in the header and footer, and **the favicon**;
|
|
164
|
+
- **three named profiles**, once for the site, never free values:
|
|
165
|
+
- action colour: `data-gw-action="accent"` on `<html>` fills the primary button with the accent (default `ink`).
|
|
166
|
+
Its words (`--gw-on-accent`) must reach 4.5:1 on `--gw-accent` and `--gw-accent-strong`, light and dark, or
|
|
167
|
+
`check` fails;
|
|
168
|
+
- radius: `data-gw-radius="soft"` on `<html>` (12 / 20 / 24) instead of `tight` (8 / 12 / 16, default);
|
|
169
|
+
- header: `<header class="gw-header gw-header--float">`, an inset card from 1080px, instead of the flat bar.
|
|
170
|
+
|
|
171
|
+
A brand does not change the neutrals, the type, the spacing, the shadows or the widths, and the profiles do not open
|
|
172
|
+
accent-coloured heading words, accent bands or uppercase labels. Recipe and checks: `docs/theming.md`; working
|
|
173
|
+
examples: `demo/theme-example.html` (accent only) and `demo/brand-profiles.html` (all three profiles).
|
|
150
174
|
|
|
151
175
|
## 8. Where things go in a consuming site
|
|
152
176
|
|
|
@@ -180,10 +204,11 @@ npx gemboss-website check # every .html and .css in the site against
|
|
|
180
204
|
npx gemboss-website check --fix-list # the same, as a to-do list
|
|
181
205
|
```
|
|
182
206
|
|
|
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
|
-
|
|
207
|
+
It reports colours, radii, spacing and type outside the system, shadows on chrome, buttons filled from site CSS,
|
|
208
|
+
uppercase labels, em dashes in visible text, images without `alt`, overrides of `.gw-*` classes, redefined kit tokens,
|
|
209
|
+
a waiting button not marked with `aria-busy`, a segmented control without its radio-group markup, a drawn mock-up
|
|
210
|
+
with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
|
|
211
|
+
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
212
|
|
|
188
213
|
## 11. Upgrading the kit
|
|
189
214
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.3.0 (2026-10-25)
|
|
8
|
+
|
|
9
|
+
Brand profiles: three knobs so that another GemCommerce brand (GemPages first, Gemians later) can use the kit and
|
|
10
|
+
still look like itself. Each one is a named profile chosen once for the site, never a free value. Markup that worked
|
|
11
|
+
in 0.2.0 still works, and a page without the new attributes looks exactly as before (ink buttons, tight corners, the
|
|
12
|
+
flat header: the demo is pixel-identical to 0.2.0 at 390 and 1366px, light and dark).
|
|
13
|
+
|
|
14
|
+
Closes #61
|
|
15
|
+
|
|
16
|
+
- Action colour: `data-gw-action="accent"` on `<html>` fills the primary `.gw-btn` with the brand's accent
|
|
17
|
+
(`--gw-on-accent` words, `--gw-accent-strong` on hover and press, the `-dark` family in dark mode and night bands).
|
|
18
|
+
The default is `ink`. No button shadow. The build fails when the kit's own colours put the button's words below
|
|
19
|
+
4.5:1, light or dark, and `check` does the same with a brand's colours (`action-contrast`).
|
|
20
|
+
- Radius profiles: `data-gw-radius="soft"` on `<html>` gives 12 / 20 / 24 (action, card, panel) and 24 for the mega
|
|
21
|
+
menu; the default `tight` keeps 8 / 12 / 16 and 20. Every component reads `--gw-radius-action|card|panel|popover`;
|
|
22
|
+
no component keeps a radius of its own.
|
|
23
|
+
- Floating header: `<header class="gw-header gw-header--float">` is, from 1080px, an inset card (the panel radius, the
|
|
24
|
+
surface at 85% over a 10px blur, one hairline, the raised shadow, 16px from the top). Same bar, mega menu and
|
|
25
|
+
script; the flat bar below 1080px, so the phone menu is unchanged; solid under `prefers-reduced-transparency` and
|
|
26
|
+
where the browser cannot blur.
|
|
27
|
+
- Still forbidden with every profile: accent or gradient words in a heading, accent section bands, uppercase labels.
|
|
28
|
+
`AGENTS.md` (rules 2 and 3, section 7), `docs/theming.md` (a brand chooses six things, nothing else) and
|
|
29
|
+
`docs/philosophy.md` say what changed and why: the site speaks the language of the brand's own product.
|
|
30
|
+
- `check`: `profile-unknown` (a name that does not exist, or a profile set off `<html>`), `header-float-markup` (the
|
|
31
|
+
float class off the header) and `action-contrast`, all errors; `token-redefined` and `radius-off-scale` point at
|
|
32
|
+
the profiles.
|
|
33
|
+
- `system.json` version 2: `profiles` (action, radius, header). `radius.action`, `radius.card` and `radius.panel`
|
|
34
|
+
moved to `profiles.radius.options.tight`; `radius.scale` gains 24. New tokens: `--gw-radius-popover`,
|
|
35
|
+
`--gw-radius-24`, `--gw-header-gap`, `--gw-header-surface`, `--gw-header-blur`.
|
|
36
|
+
- `demo/brand-profiles.html`: a GemPages-style brand with all three profiles and the accent `#3c67ff`; its
|
|
37
|
+
accent-strong `#2f52d6` is an example the GemPages team has still to confirm.
|
|
38
|
+
|
|
7
39
|
## 0.2.0 (2026-10-25)
|
|
8
40
|
|
|
9
41
|
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
|
|
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
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 9 KB |
|
|
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,9 +40,9 @@ 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.3.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.3.0/js/website.js" defer></script>
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
**Next.js (App Router):** nạp CSS trong layout gốc, nạp script bằng `next/script`.
|
|
@@ -74,16 +74,24 @@ section trong `templates/sections/` theo thứ tự trang cần, thay mọi ch
|
|
|
74
74
|
|
|
75
75
|
## Thương hiệu khác GemBoss
|
|
76
76
|
|
|
77
|
-
Một thương hiệu chỉ
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
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
|
|
78
|
+
wash, logo (kèm favicon), và ba hồ sơ có tên, chọn một lần cho cả site:
|
|
79
|
+
|
|
80
|
+
- 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);
|
|
81
|
+
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;
|
|
82
|
+
- bo góc: `data-gw-radius="soft"` trên `<html>` (12 / 20 / 24) thay cho `tight` (8 / 12 / 16, mặc định);
|
|
83
|
+
- header: `<header class="gw-header gw-header--float">`, một thẻ nổi từ 1080px; điện thoại giữ thanh phẳng.
|
|
84
|
+
|
|
85
|
+
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
|
|
86
|
+
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:
|
|
87
|
+
`demo/theme-example.html` (chỉ màu nhấn) và `demo/brand-profiles.html` (cả ba hồ sơ).
|
|
80
88
|
|
|
81
89
|
## Lệnh
|
|
82
90
|
|
|
83
91
|
| lệnh | làm gì |
|
|
84
92
|
|---|---|
|
|
85
93
|
| `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
|
|
94
|
+
| `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
95
|
| `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
96
|
| `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
97
|
|
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);
|
package/css/header.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* header: announcement, a flat 64px bar with one hairline, mega menu, mobile menu below 1080px.
|
|
2
|
-
Not a floating pill: no radius, no blur, no shadow on the bar. Nothing tucks under it.
|
|
2
|
+
Not a floating pill: no radius, no blur, no shadow on the bar. Nothing tucks under it. A brand may choose the one
|
|
3
|
+
other shape, .gw-header--float (an inset card from 1080px, at the end of this file). */
|
|
3
4
|
.gw-announce {
|
|
4
5
|
padding: 8px var(--gw-gutter);
|
|
5
6
|
border-bottom: 1px solid var(--gw-line);
|
|
@@ -83,9 +84,9 @@
|
|
|
83
84
|
text-underline-offset: 8px;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/* mega menu: a floating card centred under the bar, as on gemboss.ai (Chris 24/10/2026): one hairline,
|
|
87
|
-
the raised shadow (it is a popover), 32px inside. The bridge
|
|
88
|
-
the 8px gap from the trigger. */
|
|
87
|
+
/* mega menu: a floating card centred under the bar, as on gemboss.ai (Chris 24/10/2026): one hairline, the popover
|
|
88
|
+
radius of the site's profile (20, 24 in soft), the raised shadow (it is a popover), 32px inside. The bridge
|
|
89
|
+
(::before) keeps the pointer inside while it crosses the 8px gap from the trigger. */
|
|
89
90
|
.gw-mega {
|
|
90
91
|
position: absolute;
|
|
91
92
|
top: calc(100% + 8px);
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
overflow-y: auto;
|
|
96
97
|
transform: translateX(-50%);
|
|
97
98
|
border: 1px solid var(--gw-line);
|
|
98
|
-
border-radius:
|
|
99
|
+
border-radius: var(--gw-radius-popover);
|
|
99
100
|
background: var(--gw-surface);
|
|
100
101
|
box-shadow: var(--gw-shadow-raised);
|
|
101
102
|
}
|
|
@@ -229,3 +230,34 @@ html.is-menu-open .gw-header { position: fixed; top: 0; left: 0; right: 0; }
|
|
|
229
230
|
@media (max-width: 768px), (pointer: coarse) {
|
|
230
231
|
.gw-theme-toggle, .gw-menu-toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
|
|
231
232
|
}
|
|
233
|
+
|
|
234
|
+
/* floating header (0.3.0), a brand's header profile: <header class="gw-header gw-header--float">. The same bar, mega
|
|
235
|
+
menu and script, drawn from 1080px as an inset card: the panel radius of the site's profile (16, 24 in soft), the
|
|
236
|
+
surface at 85% over a 10px blur, one hairline, the raised shadow, a 16px gap above it and when it sticks. It is
|
|
237
|
+
wrap + 2 x 24px + its border wide, so the bar's content lines up with the page's from 1170px. Below 1080px it is
|
|
238
|
+
the flat bar, so the phone menu is unchanged. Solid for a visitor who asks for less transparency, and where the
|
|
239
|
+
browser cannot blur. No other floating or blurred header: it is a profile, not a style to copy. */
|
|
240
|
+
@media (min-width: 1080px) {
|
|
241
|
+
.gw-header--float {
|
|
242
|
+
top: var(--gw-header-gap);
|
|
243
|
+
width: min(100% - 32px, var(--gw-wrap) + 50px);
|
|
244
|
+
margin: var(--gw-header-gap) auto 0;
|
|
245
|
+
border: 1px solid var(--gw-line);
|
|
246
|
+
border-radius: var(--gw-radius-panel);
|
|
247
|
+
background: color-mix(in srgb, var(--gw-surface) var(--gw-header-surface), transparent);
|
|
248
|
+
-webkit-backdrop-filter: blur(var(--gw-header-blur));
|
|
249
|
+
backdrop-filter: blur(var(--gw-header-blur));
|
|
250
|
+
box-shadow: var(--gw-shadow-raised);
|
|
251
|
+
}
|
|
252
|
+
.gw-header--float.is-scrolled { border-color: var(--gw-line-strong); }
|
|
253
|
+
.gw-header--float > .gw-header__bar { width: auto; padding-inline: 24px; }
|
|
254
|
+
.gw-header--float .gw-mega { max-height: calc(100vh - var(--gw-header-height) - var(--gw-header-gap) - 24px); }
|
|
255
|
+
/* an anchor lands below the card, not under it */
|
|
256
|
+
:root:has(.gw-header--float) [id] { scroll-margin-top: calc(var(--gw-header-height) + var(--gw-header-gap) + 18px); }
|
|
257
|
+
}
|
|
258
|
+
@media (min-width: 1080px) and (prefers-reduced-transparency: reduce) {
|
|
259
|
+
.gw-header--float { background: var(--gw-surface); -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
260
|
+
}
|
|
261
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
262
|
+
.gw-header--float { background: var(--gw-surface); }
|
|
263
|
+
}
|
package/css/prose.css
CHANGED
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
:where(.gw-prose) > hr { height: 0; border: 0; border-top: 1px solid var(--gw-line); }
|
|
60
60
|
:where(.gw-prose) :is(sup, sub) { line-height: 0; }
|
|
61
61
|
|
|
62
|
-
/* code: inline, a tint chip that wraps on a phone
|
|
62
|
+
/* code: inline, a tint chip that wraps on a phone (radius 6 in every profile: it follows the text, not the shapes);
|
|
63
|
+
a block scrolls inside its own box */
|
|
63
64
|
:where(.gw-prose) :is(pre, code, kbd, samp) { font-family: var(--gw-font-mono); }
|
|
64
65
|
:where(.gw-prose) code {
|
|
65
66
|
padding: 2px 6px;
|
|
@@ -87,7 +88,7 @@
|
|
|
87
88
|
line-height: 1.6;
|
|
88
89
|
tab-size: 2;
|
|
89
90
|
}
|
|
90
|
-
:where(.gw-prose) pre code { padding: 0; border-radius: 0; background: none; font-size: inherit; overflow-wrap: normal; }
|
|
91
|
+
:where(.gw-prose) pre code { padding: 0; border-radius: var(--gw-radius-0); background: none; font-size: inherit; overflow-wrap: normal; }
|
|
91
92
|
|
|
92
93
|
/* images and captions (an image inside a .gw-window keeps the window's square edges) */
|
|
93
94
|
:where(.gw-prose) > :is(img, video),
|
package/css/tokens.css
CHANGED
|
@@ -46,11 +46,13 @@
|
|
|
46
46
|
--gw-radius-12: 12px;
|
|
47
47
|
--gw-radius-16: 16px;
|
|
48
48
|
--gw-radius-20: 20px;
|
|
49
|
+
--gw-radius-24: 24px;
|
|
49
50
|
--gw-radius-999: 999px;
|
|
50
51
|
--gw-radius-action: 8px;
|
|
51
52
|
--gw-radius-chip: 999px;
|
|
52
53
|
--gw-radius-card: 12px;
|
|
53
54
|
--gw-radius-panel: 16px;
|
|
55
|
+
--gw-radius-popover: 20px;
|
|
54
56
|
--gw-shadow: 0 1px 2px rgba(31,30,29,.04), 0 2px 6px rgba(31,30,29,.06);
|
|
55
57
|
--gw-shadow-sm: 0 1px 2px rgba(31,30,29,.04);
|
|
56
58
|
--gw-shadow-raised: 0 4px 16px rgba(31,30,29,.10);
|
|
@@ -58,6 +60,9 @@
|
|
|
58
60
|
--gw-wrap-narrow: 780px;
|
|
59
61
|
--gw-gutter: 22px;
|
|
60
62
|
--gw-header-height: 64px;
|
|
63
|
+
--gw-header-gap: 16px;
|
|
64
|
+
--gw-header-surface: 85%;
|
|
65
|
+
--gw-header-blur: 10px;
|
|
61
66
|
--gw-button-height: 40px;
|
|
62
67
|
--gw-tap-min: 44px;
|
|
63
68
|
--gw-ease: cubic-bezier(.22,1,.36,1);
|
|
@@ -198,3 +203,17 @@
|
|
|
198
203
|
--gw-success-soft: #132a22;
|
|
199
204
|
--gw-warning: #e0b341;
|
|
200
205
|
}
|
|
206
|
+
/* action profile "accent": data-gw-action="accent" on <html>. In night bands too, where the dark palette applies. */
|
|
207
|
+
:root[data-gw-action="accent"],
|
|
208
|
+
:root[data-gw-action="accent"] :is(.gw-band--night, .gw-showcase) {
|
|
209
|
+
--gw-action: var(--gw-accent);
|
|
210
|
+
--gw-action-hover: var(--gw-accent-strong);
|
|
211
|
+
--gw-on-action: var(--gw-on-accent);
|
|
212
|
+
}
|
|
213
|
+
/* radius profile "soft": data-gw-radius="soft" on <html> */
|
|
214
|
+
:root[data-gw-radius="soft"] {
|
|
215
|
+
--gw-radius-action: 12px;
|
|
216
|
+
--gw-radius-card: 20px;
|
|
217
|
+
--gw-radius-panel: 24px;
|
|
218
|
+
--gw-radius-popover: 24px;
|
|
219
|
+
}
|