@gemboss/website-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -14,10 +14,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
14
14
  | `js/website.mjs` | the same script as an ES module for apps: `import { init, destroy } from '@gemboss/website-ui/website.mjs'` |
15
15
  | `system.json` | every value of the system: colours, spacing, type, radii, shadows, widths |
16
16
  | `templates/page.html` | the page skeleton: start every new page from it |
17
- | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list… |
17
+ | `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
18
18
  | `demo/index.html` | every component on one page: open it to see what exists before you build anything |
19
19
  | `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
20
20
  | `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
21
+ | `demo/forms.html` | fields in every state, choices, the footer newsletter, a short form, stars, the testimonial wall and the rating badge |
21
22
  | `docs/components.md` | every class, what it is for, its variants |
22
23
  | `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
23
24
 
@@ -74,7 +75,8 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
74
75
  - **Colour roles** (CSS variables, light and dark): `--gw-bg`, `--gw-bg-soft` (the neutral raised fill),
75
76
  `--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`, `--gw-line-strong`, `--gw-action`,
76
77
  `--gw-on-action`, `--gw-accent`, `--gw-accent-strong`, `--gw-accent-soft`, `--gw-success`, `--gw-warning` (and
77
- their tints `--gw-success-soft`, `--gw-warning-soft`), `--gw-night` (the dark band). Wash hues `--gw-wash-pink|violet|sky|mint` exist for the hero wash and illustrations only.
78
+ their tints `--gw-success-soft`, `--gw-warning-soft`), `--gw-danger` (an error, a destructive button; `--gw-danger-soft`,
79
+ `--gw-on-danger`), `--gw-line-control` (a form control's border, 3:1), `--gw-night` (the dark band). Wash hues `--gw-wash-pink|violet|sky|mint` exist for the hero wash and illustrations only.
78
80
  - **Dark mode** follows the visitor's system and the theme toggle (`data-theme` on `<html>`). Use the variables and
79
81
  it works; a hard-coded colour breaks it.
80
82
  - **Accent as text** (links, the active menu item, step numbers) is `--gw-accent-strong`; the plain accent is for
@@ -85,6 +87,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
85
87
  - **A button that waits** (a form sent, an audit started) gets `aria-busy="true"` from the page's script until the
86
88
  answer comes: the kit keeps its label and width, draws the ring and ignores a second press. Nothing else: no
87
89
  `disabled`, no loading class, no spinner of your own, no "Loading…" label.
90
+ - **Forms** (0.6.0): a field is `.gw-field` (its `<label>` above, the control, the error, the hint), the controls
91
+ `.gw-input`, `.gw-select`, `.gw-textarea`, `.gw-check`, `.gw-radio` (a group in `.gw-choices`), one field and its
92
+ button in a row `.gw-form-inline`, a short form `.gw-form`. Ask as little as you can: one field beats a form, and a
93
+ form keeps two or three fields (`templates/sections/form.html`). A field in error is `aria-invalid="true"` plus its
94
+ `.gw-field__error` in words; the page decides when, the kit draws it.
95
+ - **Ratings** (0.6.0): stars `.gw-stars` (`role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"`), the
96
+ wall `.gw-testimonials` of `.gw-card.gw-testimonial`, the badge `.gw-rating-badge` (`templates/sections/testimonials.html`).
88
97
  - **One choice out of two to four** (Monthly / Yearly) is `.gw-segmented`, a radio group; what each option shows
89
98
  carries `data-gw-show="group:value"`. Markup: `docs/components.md`, `templates/sections/pricing.html`.
90
99
  - **Type**: Inter. Headings weight 650, tracking −0.022em; display h1/h2 weight 600, tracking −0.03em. Body 18px,
@@ -119,6 +128,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
119
128
  - Don't write "Click here", "Learn more" as the only words of a link; say where it goes.
120
129
  - Don't use the em dash (—) in visible text. Use a comma, a colon or a full stop.
121
130
  - Don't invent numbers, customer names, logos, testimonials, ratings, awards or certifications.
131
+ - Don't show a rating or a testimonial without a link to the page it comes from, and don't reword a review, round a
132
+ score up or average one yourself (section 6).
133
+ - Don't build a form of many fields where one question does; don't use the placeholder as the label; don't tick a
134
+ consent box for the reader or word a refusal to shame them ("No thanks, I like losing sales").
135
+ - Don't tell an error by its colour alone: `aria-invalid="true"` and words in `.gw-field__error` that say how to fix it.
122
136
  - Don't write fake numbers on a mock-up without the caption "Illustrative data." under it, and don't reword it.
123
137
  - Don't style an article's headings, lists or code in site CSS; wrap the article in `.gw-prose`.
124
138
  - Don't make a callout whose kind only its colour tells: `.gw-callout` starts with its `.gw-callout__label` in words.
@@ -144,6 +158,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
144
158
  (a `.gw-note` or a `role="status"` line). Under `prefers-reduced-motion` the kit's ring stands still.
145
159
  - A single choice is a radio group (`.gw-segmented`): one Tab stop, the arrows move and choose. Never a row of
146
160
  `aria-pressed` buttons for it.
161
+ - A form field has a visible `<label for>` above it; its error and hint sit under it and are named by
162
+ `aria-describedby` (the error first). An error is `aria-invalid="true"` on the control plus words that say how to
163
+ fix it ("Enter your work email, like name@example.com"); after a failed send, move the focus to the first field in
164
+ error. Mark the optional fields "(optional)". A group of checkboxes or radios is a `<fieldset class="gw-choices">`
165
+ with its question in the `<legend>`.
166
+ - Stars are `role="img"` with the value in words (`aria-label="4.9 out of 5"`); a number shown beside them is
167
+ `aria-hidden="true"`.
147
168
  - A wide table gets a scroll box a keyboard can reach: the `.gw-table` wrapper, or, for a bare table in `.gw-prose`,
148
169
  the box `website.js` adds. Give the table a `<caption>`: it names the box.
149
170
 
@@ -159,6 +180,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
159
180
  - Made-up numbers in a drawn mock-up: `.gw-window__caption` says exactly "Illustrative data." (vi: "Dữ liệu minh
160
181
  hoạ.", zh: "示意数据。"), plus one sentence only when a demo brand needs naming ("Vital Glow is a GemBoss demo
161
182
  brand."). Real numbers in a window: the caption names their source.
183
+ - **Ratings and testimonials**: every one links to the page it comes from (the store's review page, the review
184
+ itself), and says only what that page says: the score and the count as shown there on the day you copy them, the
185
+ reviewer's own words (cut with an ellipsis, never reworded), their name and store as they wrote them. No made-up
186
+ review, no quote you wrote for a customer, no score you averaged yourself, no rating without its link (`check`
187
+ warns: `rating-source`). Do not name internal tools as the source.
162
188
  - Image sources: record where each image comes from (the brand's own screen, a licensed photo). No photos of real
163
189
  people or other brands' logos without permission.
164
190
  - Other languages: rewrite each page for its reader (transcreation), do not translate sentence by sentence. Facts
@@ -223,7 +249,8 @@ uppercase labels, em dashes in visible text, images without `alt`, overrides of
223
249
  a waiting button not marked with `aria-busy`, a segmented control without its radio-group markup, a drawn mock-up
224
250
  with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
225
251
  words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
226
- the page, a callout without its label, and patterns that repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
252
+ the page, a callout without its label, stars without their value in words, a rating or a testimonial without a link to
253
+ its source, and patterns that repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
227
254
 
228
255
  ## 11. Upgrading the kit
229
256
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,61 @@ 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.6.0 (2026-10-25)
8
+
9
+ Forms and ratings: the fields a contact page, a sign-up or a footer newsletter needs, with their error state, and the
10
+ proof a product site on the Shopify App Store shows (stars, a wall of short reviews, the store's rating badge). Markup
11
+ that worked in 0.5.0 still works, and a page without the new parts looks exactly as before: the demo index (without its
12
+ new link), `brand-profiles.html`, `article.html`, `blog.html` and `theme-example.html` are pixel-identical to 0.5.0 at
13
+ 390 and 1366px, light and dark, with and without reduced motion.
14
+
15
+ - Fields `.gw-field` (`__label`, `__error`, `__hint`): the label above, the control, the error in words after an icon,
16
+ then the hint, 8px apart, tied with `for`/`id` and `aria-describedby` (the error first). Controls `.gw-input`,
17
+ `.gw-select` (a drawn caret that follows dark mode) and `.gw-textarea`: the dashboard's field family at the
18
+ website's sizes, as tall as a button (40px, 44px on touch), 16px words so a phone does not zoom, the action radius of
19
+ the site's profile, a 1px border that reaches 3:1. States by attribute: hover, focus (the accent ring and an ink
20
+ border), `readonly` (the soft ground), `disabled` (faded, its reason in the hint), `aria-invalid="true"` (the danger
21
+ border and tint, and the message in the danger colour with its icon: never the colour alone).
22
+ Closes gap #28 (form fields and the error state)
23
+ - Choices `.gw-choices` (a `<fieldset>` and its `<legend>`), `.gw-check` and `.gw-radio` (the `<label>` is the row and
24
+ the target: 32px, 44px on touch; chosen, the box takes the accent and the mark `--gw-on-accent`), and a filter's
25
+ count `.gw-check__count`, for the template gallery filters the gempages.net research found.
26
+ - One field and its button `.gw-form-inline` (one line from 480px, each a full line below it or in a narrow box), and
27
+ a short form's stack `.gw-form` (fields 24px apart, actions 32px below, 560px at most; on a card too).
28
+ - Footer newsletter: `templates/footer.html` has an optional `<form class="gw-footer__newsletter">` in the brand block
29
+ (label, the inline row with a secondary button, a note that says how often and how to stop and links to the privacy
30
+ policy). Delete it unless it is wired to a list. Existing footers are unchanged.
31
+ - `.gw-btn--danger` (the danger fill, `--gw-on-danger` words, hover and pressed one step towards the ink) for the action
32
+ of a page that destroys something, and `.gw-badge--danger` (a danger dot, ink words on the danger tint), as the gap
33
+ asked.
34
+ - Tokens: `--gw-danger` (`#b42318`, dark `#ff6b5a`: the dashboard's `--gemboss-danger` family; 5.92:1 or more on every
35
+ ground, light, dark and in night bands), `--gw-danger-soft` (`#fff0e9`, dark `#2e1714`), `--gw-on-danger` (`#ffffff`,
36
+ dark `#0d0d12`; 6.57:1 and 6.92:1 on the fill) and `--gw-line-control` (`#85888e`, dark `#6a6d78`): a form control's
37
+ border, 3.20:1 or more on `bg`, `bg-soft` and `surface` and in night bands, where `--gw-line-strong` (1.28:1) is a
38
+ hairline. A brand does not change them.
39
+ - Stars `.gw-stars`: a value from 0 to 5 in `style="--gw-stars: 4.9"`, drawn as five 16px ink stars with a partial
40
+ last one (a mask over a hard-stop gradient; system colours on a forced-colours screen), and heard as its name:
41
+ `role="img"`, `aria-label="4.9 out of 5"`.
42
+ - Testimonial wall `.gw-testimonials` (one, two from 768px, three from 1080px; no masonry, a row's cards share its
43
+ height) of `.gw-card.gw-testimonial` (`__quote`, `__by` with the name, the store and the link to the review,
44
+ `__avatar` optional), and the rating badge `.gw-rating-badge` (an `<a>` to the store's review page: `__icon`,
45
+ `__score` with the stars, `__count`).
46
+ Closes gap #56 (testimonial wall with star rating, App Store rating badge)
47
+ - Content rule (`AGENTS.md` sections 4 to 6, `docs/components.md`): every rating and testimonial links to the page it
48
+ comes from and copies only what it shows; no made-up review, no reworded quote, no score averaged by hand.
49
+ - `check`: `stars-markup` (an error: `.gw-stars` without `role="img"` or its name, a value outside 0 to 5, a name that
50
+ does not carry the drawn number) and `rating-source` (a warning: a `.gw-rating-badge` or a `.gw-testimonial` without
51
+ a link). `--gw-stars` set on the element is an input, not a redefined token.
52
+ - No script: the page owns validation (the kit ships no validator), so `website.js` only carries the new version.
53
+ - Templates: `templates/sections/form.html` (a short form beside its reason: two fields and a choice) and
54
+ `templates/sections/testimonials.html` (the badge and the wall). Demo: `demo/forms.html` (every field state, the
55
+ choices, the inline row, the danger button, stars from 0 to 5, a short form, the badge and the wall with sample data,
56
+ the footer newsletter), linked from `demo/index.html`.
57
+ - Budget: `website.min.css` is 59.3 KB, 10.5 KB gzipped (0.5.0: 50.5 KB, 9.1 KB). `js/website.js` is unchanged in size.
58
+ - Tests: `test/run.mjs` covers the tokens and their contrast, the new CSS and the two `check` rules; `test/browser.mjs`
59
+ drives `demo/forms.html` in Chromium (Tab through a form, a label click, the error in the accessibility tree,
60
+ 44px targets and the newsletter row at 390px, the stars' names, the soft and accent profiles).
61
+
7
62
  ## 0.5.0 (2026-10-25)
8
63
 
9
64
  The blog release: the parts around a long article and the blog index, so a blog (gempages.net's 1,290 posts first,
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 68 KB, 51 KB bản min, 9 KB khi nén gzip): 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 (kèm ảnh trên đầu thẻ), khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), bộ khung bài blog (breadcrumb, dòng tác giả và ngày, mục lục dính, hộp lưu ý, danh sách bài, phân trang), header + mega menu + menu điện thoại, footer, 14 section, 3 hồ sơ thương hiệu |
15
+ | `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 82 KB, 59 KB bản min, 10,5 KB khi nén gzip): token sáng và tối, chữ, bố cục, nút (cả trạng thái nhấn, đang chờ và nút xoá), link, nhãn, chip, segmented control, badge, card (kèm ảnh trên đầu thẻ), khung cửa sổ kèm chú thích "Illustrative data.", bảng, FAQ, chữ bài dài (`.gw-prose`), bộ khung bài blog (breadcrumb, dòng tác giả và ngày, mục lục dính, hộp lưu ý, danh sách bài, phân trang), ô nhập và trạng thái lỗi (ô chữ, danh sách chọn, ô nhiều dòng, checkbox, radio, hàng một ô một nút cho newsletter), sao đánh giá, tường review, huy hiệu điểm App Store, header + mega menu + menu điện thoại, footer (kèm ô newsletter tuỳ chọn), 16 section, 3 hồ sơ thương hiệu |
16
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ờ, đánh dấu mục đang đọc trong mục lục bài viết, khung cuộn cho bảng trong bài; không phụ thuộc thư viện nào, khoảng 4,5 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
- | `templates/` | khung một trang và 14 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose, article, post-list) |
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ơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài |
18
+ | `templates/` | khung một trang và 16 section (hero, logos, stats, features, split, showcase, steps, quote, pricing, faq, cta, prose, article, post-list, form, testimonials) |
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ơ; `article.html` là một bài blog trọn vẹn, `blog.html` là trang danh sách bài; `forms.html` là mọi trạng thái của ô nhập, newsletter ở footer, tường review và huy hiệu điểm (dữ liệu mẫu) |
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.5.0/website.min.css">
43
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.6.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.5.0/js/website.js" defer></script>
45
+ <script src="https://cdn.jsdelivr.net/npm/@gemboss/website-ui@0.6.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
@@ -113,7 +113,7 @@ viết hoa vẫn cấm. Công thức và các phép kiểm tương phản: `docs
113
113
  | lệnh | làm gì |
114
114
  |---|---|
115
115
  | `npx gemboss-website init` | nối luật của kit vào Claude Code của dự án (CLAUDE.md + skill) |
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 |
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ồ, sao thiếu giá trị bằng chữ, điểm hay review không có link nguồn… và gợi ý chỗ kit có thể đang thiếu |
117
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 |
118
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 |
119
119
 
@@ -141,7 +141,7 @@ gemboss-www; mọi thay đổi đi qua PR, giá trị của hệ thống chỉ s
141
141
  cd packages/website-ui
142
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
143
143
  node test/run.mjs # tự kiểm CLI, CSS và website.js (DOM giả)
144
- node test/browser.mjs # bài viết demo trong Chromium thật: cuộn, Tab, mục lục gập, bảng (cần .ci của repo)
144
+ node test/browser.mjs # demo trong Chromium thật: bài viết (cuộn, Tab, mục lục, bảng) và form, sao, review (cần .ci của repo)
145
145
  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)
146
146
  ```
147
147
 
package/bin/lib/check.mjs CHANGED
@@ -139,6 +139,11 @@ export const RULES = {
139
139
  why: "A screen reader announces the trail from its markup: a <nav> with a name around an <ol>, the last item marked aria-current=\"page\". The kit draws the separators; a › or / typed between the links is read out as a word.",
140
140
  fix: "Copy the markup from docs/components.md (Breadcrumbs) or templates/sections/article.html: <nav class=\"gw-breadcrumbs\" aria-label=\"Breadcrumb\"><ol><li><a href=\"/blog\">Blog</a></li><li><a href=\"…\" aria-current=\"page\">This page</a></li></ol></nav>.",
141
141
  },
142
+ "stars-markup": {
143
+ level: "error", title: "Stars without their value in words",
144
+ why: "The kit draws .gw-stars from style=\"--gw-stars: N\" (a number from 0 to 5), and a screen reader hears only its name: role=\"img\" and aria-label=\"N out of 5\". Without them the stars are empty or silent, and a label that names another number than the drawing says two things at once.",
145
+ fix: "Write <span class=\"gw-stars\" role=\"img\" aria-label=\"4.9 out of 5\" style=\"--gw-stars: 4.9\"></span>: the same number in the style and in the label, from 0 to 5 (docs/components.md, Ratings).",
146
+ },
142
147
  "uppercase-text": {
143
148
  level: "warning", title: "Uppercase text",
144
149
  why: "Headings, buttons, labels and menu items are sentence case.",
@@ -154,6 +159,11 @@ export const RULES = {
154
159
  why: "A note, a tip and a warning differ by their tint, which a colour-blind reader, a screen reader and a printed page do not get. The kind has to be in words.",
155
160
  fix: "Start the callout with <p class=\"gw-callout__label\">Note</p> (Tip, Warning, or the same word in the page's language).",
156
161
  },
162
+ "rating-source": {
163
+ level: "warning", title: "Rating or testimonial without a link to its source",
164
+ why: "A score, a review count or a quote is a claim. Linked to the page it comes from (the store's review page, the review itself), a reader can check it; without the link it reads as made up, and a made-up review makes every other claim on the page doubtful.",
165
+ fix: "Make the .gw-rating-badge itself the <a href> to the review page, and give every .gw-testimonial a link to its review (in the byline: <a href=\"…\">Shopify App Store</a>). If there is no source to link, do not show the rating or the quote.",
166
+ },
157
167
  "possible-gap": {
158
168
  level: "note", title: "Possible kit gap: a site class used on several pages",
159
169
  why: "A site class styled in site CSS and used on 3 or more pages is often a reusable part the kit does not have yet.",
@@ -305,6 +315,7 @@ export function subjectOf(sel) {
305
315
  const ATTR_THEME = String.raw`\[\s*data-theme(?:\s*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[\w-]+))?\s*\]`;
306
316
  const ROOTISH = new RegExp(`^(?::root|html)?(?:${ATTR_THEME}|:not\\(\\s*${ATTR_THEME}\\s*\\))*$`, "i");
307
317
  const isRootish = (sel) => splitTop(sel).every((s) => s && ROOTISH.test(s));
318
+ const INPUTS = new Set(["--gw-stars"]); // set on one element in the markup (style="--gw-stars: 4.9"), read by the kit's CSS
308
319
  const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?)$/; // docs/theming.md: the eight accent variables (-light/-dark) + wash
309
320
 
310
321
  const COLOR_PROPS = /^(?:color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left|block|inline)(?:-(?:start|end))?)?(?:-color)?|outline(?:-color)?|fill|stroke|text-decoration(?:-color)?|caret-color|accent-color|column-rule(?:-color)?|text-emphasis(?:-color)?|-webkit-text-fill-color|-webkit-text-stroke(?:-color)?|scrollbar-color|stop-color|flood-color|lighting-color)$/;
@@ -361,6 +372,8 @@ function checkRule(rule, ctx, add) {
361
372
  const where = { file: ctx.file, line: d.line, excerpt: excerptOf(sel, d) };
362
373
  if (d.prop.startsWith("--")) {
363
374
  if (d.prop.startsWith("--gw-")) {
375
+ // an input a component reads from its own markup (the stars' value), not a token: stars-markup checks it
376
+ if (INPUTS.has(d.prop)) continue;
364
377
  if (!(THEMABLE.test(d.prop) && rootish)) add("token-redefined", where);
365
378
  } else if (!ctx.svg && colorLiterals(value).length) add("color-literal", where);
366
379
  continue;
@@ -604,6 +617,23 @@ function analyzeHtml(file, text, out) {
604
617
  if (cls.includes("gw-callout") && !/\bclass\s*=\s*["'][^"']*\bgw-callout__label\b/i.test(inner())) {
605
618
  add("callout-label", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${decode(visible.slice(m.index, elementEnd(scanText, m.index, tag))).replace(/\s+/g, " ").trim().slice(0, 60)}`));
606
619
  }
620
+ if (cls.includes("gw-stars")) {
621
+ const v = (/(?:^|;)\s*--gw-stars\s*:\s*([^;]*)/.exec(decode(a.style || "")) || [])[1];
622
+ const n = v !== undefined && /^\s*\d+(?:\.\d+)?\s*$/.test(v) ? Number(v) : null;
623
+ const label = a["aria-label"] || "";
624
+ const said = [...label.replace(/(\d),(\d)/g, "$1.$2").matchAll(/\d+(?:\.\d+)?/g)].map((x) => Number(x[0]));
625
+ const missing = [
626
+ a.role !== "img" && 'role="img"',
627
+ !(label.trim() || a["aria-labelledby"]) && 'aria-label with the value in words ("4.9 out of 5")',
628
+ v === undefined ? 'style="--gw-stars: N"' : (n === null || n > 5) && `a value from 0 to 5 (--gw-stars: ${v.trim()})`,
629
+ n !== null && n <= 5 && label.trim() && !said.includes(n) && `the same number in aria-label as in --gw-stars (${n})`,
630
+ ].filter(Boolean);
631
+ if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
632
+ }
633
+ if (cls.includes("gw-rating-badge") || cls.includes("gw-testimonial")) {
634
+ const linked = (tag === "a" && "href" in a) || /<a\b[^>]*\bhref\s*=/i.test(inner());
635
+ if (!linked) add("rating-source", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${decode(visible.slice(m.index, elementEnd(scanText, m.index, tag))).replace(/\s+/g, " ").trim().slice(0, 60)}`));
636
+ }
607
637
  if (cls.includes("gw-window")) {
608
638
  const end = elementEnd(scanText, m.index, tag);
609
639
  const body = end < 0 ? "" : scanText.slice(m.index, end);
@@ -43,6 +43,11 @@
43
43
  .gw-btn--secondary:active { background: var(--gw-bg-soft); border-color: var(--gw-ink); }
44
44
  .gw-btn--ghost:active { background: var(--gw-line); }
45
45
  .gw-btn--on-night:active { background: var(--gw-on-night); opacity: 0.76; }
46
+ /* destructive (0.6.0, gap #28): delete, cancel a plan, erase my data. The danger fill with on-danger words (6.5:1 or
47
+ more, light and dark); hover and pressed one step stronger (towards the ink: darker in light, lighter in dark). It is
48
+ the action of a page that destroys something, never a second primary beside .gw-btn. */
49
+ .gw-btn--danger { background: var(--gw-danger); color: var(--gw-on-danger); }
50
+ .gw-btn--danger:hover, .gw-btn--danger:active { background: color-mix(in srgb, var(--gw-danger) 84%, var(--gw-ink)); }
46
51
  /* loading: aria-busy="true" on the <button>. The label stays (it keeps the width and the accessible name) but is not
47
52
  painted; a 16px ring in the button's own ink turns in its place, the dashboard's spinner. ::before, because
48
53
  .gw-btn--sm draws its hit area with ::after. website.js ignores presses on a busy button (no double submit). */
@@ -171,6 +176,9 @@
171
176
  .gw-badge--success { background: var(--gw-success-soft); color: var(--gw-ink); }
172
177
  .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); }
173
178
  .gw-badge--warning::before { background: var(--gw-warning); }
179
+ /* an error (0.6.0, gap #28): the danger dot, ink words on the danger tint, as success's */
180
+ .gw-badge--danger { background: var(--gw-danger-soft); color: var(--gw-ink); }
181
+ .gw-badge--danger::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: var(--gw-radius-chip); background: var(--gw-danger); }
174
182
 
175
183
  /* cards sit: white, 1px hairline, the card radius of the site's profile (12, 20 in soft), the dashboard's resting
176
184
  shadow (Chris 24/10/2026: one language with the app); no hover lift. A soft card is a tint and carries no shadow. */
package/css/footer.css CHANGED
@@ -14,6 +14,9 @@
14
14
  .gw-footer__brand { grid-column: 1 / -1; max-width: 320px; }
15
15
  .gw-footer__brand p { margin-top: 16px; color: var(--gw-muted); }
16
16
  .gw-footer__heading { margin: 0 0 8px; color: var(--gw-ink); font-size: 13px; font-weight: 600; letter-spacing: 0; }
17
+ /* an optional newsletter form in the brand block (0.6.0): label, the inline row (field and button), a note on what the
18
+ reader agrees to; only when the form is wired to a list that keeps the address */
19
+ .gw-footer__newsletter { margin-top: 24px; }
17
20
  .gw-footer__col ul { margin: 0; padding: 0; list-style: none; }
18
21
  .gw-footer__col a { display: block; padding-block: 6px; color: var(--gw-muted); }
19
22
  .gw-footer__col a:hover { color: var(--gw-ink); }
package/css/forms.css ADDED
@@ -0,0 +1,154 @@
1
+ /* forms (0.6.0, gap #28): a field (label, control, error, hint), the controls, a group of choices, the inline row and
2
+ the form stack. A public site asks as little as it can: one field beats a form, and a form keeps two or three fields
3
+ (AGENTS.md section 5). The look is the dashboard's field family (@gemboss/ui gemboss-field, ui-select, ui-textarea,
4
+ ui-check, ui-help, ui-error) at the website's sizes: as tall as a button (40px, 44px on touch), 16px words (a phone
5
+ does not zoom the page), the action radius of the site's profile, and a 1px border that reaches 3:1
6
+ (--gw-line-control). The page owns validation: it sets aria-invalid="true" and writes the message; the kit draws it. */
7
+
8
+ /* a field: the label above the control, the error and then the hint under it, 8px apart */
9
+ .gw-field { display: grid; gap: 8px; min-width: 0; }
10
+ .gw-field__label { display: block; color: var(--gw-ink); font-size: 14px; font-weight: 600; line-height: 1.5; }
11
+ /* two classes, so a context that styles its paragraphs (the footer's brand block) does not move or recolour them */
12
+ :is(.gw-field, .gw-choices) > :is(.gw-field__hint, .gw-field__error) { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.5; }
13
+ :is(.gw-field, .gw-choices) > .gw-field__hint { color: var(--gw-muted); }
14
+ /* the error: an icon and words, in the danger colour; never the colour alone. Empty, it takes no room, so a page can
15
+ keep it in the markup (named by aria-describedby) and fill it when the visitor sends the form. */
16
+ :is(.gw-field, .gw-choices) > .gw-field__error { display: flex; gap: 6px; color: var(--gw-danger); font-weight: 500; }
17
+ :is(.gw-field, .gw-choices) > .gw-field__error:empty { display: none; }
18
+ .gw-field__error::before {
19
+ content: "";
20
+ flex: none;
21
+ width: 16px;
22
+ height: 16px;
23
+ margin-top: 2px;
24
+ margin-top: calc((1lh - 16px) / 2);
25
+ background: currentColor;
26
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
27
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
28
+ }
29
+
30
+ /* the controls: one box for a line of text, a choice from a list and a few lines. Full width of their field. */
31
+ .gw-input,
32
+ .gw-select,
33
+ .gw-textarea {
34
+ display: block;
35
+ width: 100%;
36
+ min-width: 0;
37
+ height: var(--gw-button-height);
38
+ margin: 0;
39
+ padding: 0 12px;
40
+ border: 1px solid var(--gw-line-control);
41
+ border-radius: var(--gw-radius-action);
42
+ background-color: var(--gw-surface);
43
+ color: var(--gw-ink);
44
+ font-size: 16px;
45
+ line-height: 1.5;
46
+ -webkit-appearance: none;
47
+ appearance: none;
48
+ }
49
+ :is(.gw-input, .gw-textarea)::placeholder { color: var(--gw-muted); opacity: 1; }
50
+ /* a list: the caret is drawn with the muted ink (two gradients, so it follows dark mode), with room for it */
51
+ .gw-select {
52
+ padding-right: 40px;
53
+ background-image: linear-gradient(45deg, transparent 50%, var(--gw-muted) 50%), linear-gradient(135deg, var(--gw-muted) 50%, transparent 50%);
54
+ background-position: calc(100% - 20px) 50%, calc(100% - 15px) 50%;
55
+ background-size: 5px 5px;
56
+ background-repeat: no-repeat;
57
+ cursor: pointer;
58
+ }
59
+ .gw-textarea { height: auto; min-height: 96px; padding-block: 8px; resize: vertical; }
60
+ /* the states, in this order: hover, focus, read-only, invalid, disabled. Focus is the kit's accent ring. */
61
+ :is(.gw-input, .gw-select, .gw-textarea):hover { border-color: var(--gw-muted); }
62
+ :is(.gw-input, .gw-select, .gw-textarea):focus-visible { border-color: var(--gw-ink); outline: 2px solid var(--gw-accent); outline-offset: 1px; }
63
+ /* read-only: still focusable and readable, visibly not editable (the dashboard's) */
64
+ :is(.gw-input, .gw-textarea)[readonly] { border-color: var(--gw-line-strong); background-color: var(--gw-bg-soft); cursor: default; }
65
+ :is(.gw-input, .gw-select, .gw-textarea)[aria-invalid="true"] { border-color: var(--gw-danger); background-color: var(--gw-danger-soft); }
66
+ :is(.gw-input, .gw-select, .gw-textarea):disabled { opacity: 0.48; cursor: not-allowed; }
67
+
68
+ /* a group of choices: a <fieldset> whose <legend> is the question, one choice per row */
69
+ .gw-choices { min-width: 0; margin: 0; padding: 0; border: 0; }
70
+ .gw-choices > legend { margin-bottom: 4px; padding: 0; }
71
+ .gw-choices > :is(.gw-check, .gw-radio) { display: flex; }
72
+ .gw-choices > :is(.gw-field__hint, .gw-field__error) { margin-top: 4px; }
73
+
74
+ /* a checkbox or a radio: the <label> is the target (32px, 44px on touch), the box sits on the first line of words */
75
+ .gw-check,
76
+ .gw-radio {
77
+ display: inline-flex;
78
+ align-items: flex-start;
79
+ gap: 8px;
80
+ max-width: 100%;
81
+ padding-block: 4px;
82
+ color: var(--gw-ink);
83
+ font-size: 16px;
84
+ line-height: 1.5;
85
+ overflow-wrap: break-word;
86
+ cursor: pointer;
87
+ }
88
+ :is(.gw-check, .gw-radio) > :not(input) { min-width: 0; }
89
+ :is(.gw-check, .gw-radio) > input {
90
+ display: inline-grid;
91
+ flex: none;
92
+ place-content: center;
93
+ width: 18px;
94
+ height: 18px;
95
+ margin: 3px 0 0;
96
+ border: 1px solid var(--gw-line-control);
97
+ border-radius: var(--gw-radius-4);
98
+ background: var(--gw-surface);
99
+ -webkit-appearance: none;
100
+ appearance: none;
101
+ cursor: inherit;
102
+ }
103
+ .gw-radio > input { border-radius: var(--gw-radius-chip); }
104
+ :is(.gw-check, .gw-radio):hover > input { border-color: var(--gw-muted); }
105
+ /* chosen: the accent, as every check mark of the kit; the mark in the words-on-accent colour */
106
+ .gw-check > input:checked { border-color: var(--gw-accent); background: var(--gw-accent); }
107
+ .gw-check > input:checked::before {
108
+ content: "";
109
+ width: 14px;
110
+ height: 14px;
111
+ background: var(--gw-on-accent);
112
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
113
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
114
+ }
115
+ .gw-radio > input:checked { border: 5px solid var(--gw-accent); }
116
+ :is(.gw-check, .gw-radio) > input[aria-invalid="true"]:not(:checked) { border-color: var(--gw-danger); }
117
+ :is(.gw-check, .gw-radio):has(> input:disabled) { opacity: 0.48; cursor: not-allowed; }
118
+ /* a filter's count: a small pill at the end of the row, the neutral badge's colours */
119
+ .gw-check__count {
120
+ flex: none;
121
+ min-width: 24px;
122
+ margin: 1px 0 0 auto;
123
+ padding: 2px 8px;
124
+ border-radius: var(--gw-radius-chip);
125
+ background: color-mix(in srgb, var(--gw-ink) 8%, transparent);
126
+ color: var(--gw-ink-2);
127
+ font-size: 12px;
128
+ font-weight: 500;
129
+ text-align: center;
130
+ font-variant-numeric: tabular-nums;
131
+ }
132
+
133
+ /* the inline row: one field and its button (a newsletter, "get the guide"). One line from 480px, the field taking the
134
+ room; below 480px, and in a box too narrow for both, the field and the button each take a full line. */
135
+ .gw-form-inline { display: flex; flex-wrap: wrap; gap: 8px; }
136
+ .gw-form-inline > :not(.gw-btn) { flex: 999 1 160px; min-width: 0; }
137
+ .gw-form-inline > .gw-btn { flex: 1 0 auto; }
138
+ @media (max-width: 479px) {
139
+ .gw-form-inline > :not(.gw-btn) { flex-basis: 100%; }
140
+ }
141
+
142
+ /* the form stack: fields 24px apart, the actions 32px below the last one, 560px at most (a field's width hints at its
143
+ answer: no field runs across a wide screen) */
144
+ .gw-form { display: grid; gap: 24px; max-width: 560px; }
145
+ /* the gap is the rhythm, also when the form is the card itself (a card spaces its own children otherwise) */
146
+ .gw-form > * { margin: 0; }
147
+ .gw-form > :is(.gw-actions, .gw-btn) { margin-top: 8px; }
148
+ .gw-form > .gw-btn { justify-self: start; }
149
+
150
+ /* touch: every control is at least 44px tall, a choice's row too */
151
+ @media (max-width: 768px), (pointer: coarse) {
152
+ .gw-input, .gw-select { height: var(--gw-tap-min); }
153
+ .gw-check, .gw-radio { padding-block: 10px; }
154
+ }
@@ -0,0 +1,78 @@
1
+ /* ratings (0.6.0, gap #56): stars, the testimonial wall and the rating badge. Every rating and every testimonial
2
+ links to the page it comes from, and says only what that page says (AGENTS.md section 6); `check` warns about one
3
+ without its link (rating-source). */
4
+
5
+ /* stars: a value from 0 to 5 (style="--gw-stars: 4.9") drawn as five 16px stars, the last one cut where the value
6
+ ends. A screen reader hears the value in words: role="img" and aria-label="4.9 out of 5" (the drawing itself is a
7
+ background, nothing to read). Ink on the strong hairline: a rating is proof, not a state or an action. */
8
+ .gw-stars {
9
+ display: inline-block;
10
+ flex: none;
11
+ width: 80px;
12
+ height: 16px;
13
+ vertical-align: middle;
14
+ background: linear-gradient(90deg, var(--gw-ink) calc(clamp(0, var(--gw-stars, 0), 5) * 20%), var(--gw-line-strong) 0);
15
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
16
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
17
+ }
18
+ /* a forced-colours screen drops backgrounds: draw the stars in its own text colours */
19
+ @media (forced-colors: active) {
20
+ .gw-stars { forced-color-adjust: none; background: linear-gradient(90deg, CanvasText calc(clamp(0, var(--gw-stars, 0), 5) * 20%), GrayText 0); }
21
+ }
22
+
23
+ /* the wall: short quotes in cards, one column on phones, two from 768px, three from 1080px. No masonry: the cards of
24
+ a row share its height, and each card's byline sits at its foot. */
25
+ .gw-testimonials {
26
+ display: grid;
27
+ grid-template-columns: minmax(0, 1fr);
28
+ gap: 16px;
29
+ margin: 0;
30
+ padding: 0;
31
+ list-style: none;
32
+ }
33
+ .gw-testimonials > li { display: flex; min-width: 0; }
34
+ @media (min-width: 768px) { .gw-testimonials { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } }
35
+ @media (min-width: 1080px) { .gw-testimonials { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
36
+ /* a quote card (with .gw-card): stars, the reviewer's own words, who wrote them and where */
37
+ .gw-testimonial { display: flex; flex-direction: column; gap: 16px; width: 100%; margin: 0; }
38
+ .gw-testimonial > * { margin: 0; }
39
+ .gw-testimonial__quote,
40
+ .gw-testimonial__quote p { color: var(--gw-ink); font-size: 16px; line-height: 1.6; }
41
+ .gw-testimonial__quote > * + * { margin-top: 8px; }
42
+ .gw-testimonial > .gw-testimonial__by { display: flex; align-items: center; gap: 12px; margin-top: auto; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
43
+ .gw-testimonial__by b { display: block; color: var(--gw-ink); font-weight: 600; }
44
+ .gw-testimonial__by > :not(img) { min-width: 0; }
45
+ .gw-testimonial__avatar { flex: none; width: 40px; height: 40px; border-radius: var(--gw-radius-chip); background: var(--gw-bg-soft); object-fit: cover; }
46
+
47
+ /* the rating badge: one link to the store's review page, with the store's mark, the score, the stars and the count.
48
+ A small card that sits; the border darkens on hover, as a linked card's. */
49
+ .gw-rating-badge {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: 12px;
53
+ max-width: 100%;
54
+ padding: 8px 16px 8px 8px;
55
+ border: 1px solid var(--gw-line);
56
+ border-radius: var(--gw-radius-card);
57
+ background: var(--gw-surface);
58
+ color: var(--gw-ink);
59
+ font-size: 13px;
60
+ line-height: 1.4;
61
+ vertical-align: middle;
62
+ }
63
+ a.gw-rating-badge:hover { border-color: var(--gw-line-strong); }
64
+ .gw-rating-badge__icon {
65
+ display: inline-flex;
66
+ flex: none;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 40px;
70
+ height: 40px;
71
+ border-radius: var(--gw-radius-action);
72
+ background: var(--gw-bg-soft);
73
+ color: var(--gw-ink);
74
+ }
75
+ .gw-rating-badge__icon :is(svg, img) { width: 24px; height: 24px; }
76
+ .gw-rating-badge__body { display: grid; gap: 2px; min-width: 0; }
77
+ .gw-rating-badge__score { display: flex; align-items: center; gap: 8px; color: var(--gw-ink); font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
78
+ .gw-rating-badge__count { color: var(--gw-muted); }
package/css/tokens.css CHANGED
@@ -89,6 +89,7 @@
89
89
  --gw-muted: #686b71;
90
90
  --gw-line: #e5e7ea;
91
91
  --gw-line-strong: #d5d8dd;
92
+ --gw-line-control: #85888e;
92
93
  --gw-action: #1b1c1e;
93
94
  --gw-action-hover: #000000;
94
95
  --gw-on-action: #ffffff;
@@ -100,6 +101,9 @@
100
101
  --gw-success-soft: #e3f1df;
101
102
  --gw-warning: #9a6700;
102
103
  --gw-warning-soft: #fbf1dc;
104
+ --gw-danger: #b42318;
105
+ --gw-danger-soft: #fff0e9;
106
+ --gw-on-danger: #ffffff;
103
107
  --gw-night: #0c0e14;
104
108
  --gw-night-2: #141722;
105
109
  --gw-on-night: #e3e4e8;
@@ -116,6 +120,7 @@
116
120
  --gw-muted: #9a9ca6;
117
121
  --gw-line: #2a2b33;
118
122
  --gw-line-strong: #383944;
123
+ --gw-line-control: #6a6d78;
119
124
  --gw-action: #f2f3f5;
120
125
  --gw-action-hover: #ffffff;
121
126
  --gw-on-action: #0d0d12;
@@ -127,6 +132,9 @@
127
132
  --gw-success-soft: #132a22;
128
133
  --gw-warning: #e0b341;
129
134
  --gw-warning-soft: #2b2410;
135
+ --gw-danger: #ff6b5a;
136
+ --gw-danger-soft: #2e1714;
137
+ --gw-on-danger: #0d0d12;
130
138
  --gw-night: #0a0b10;
131
139
  --gw-night-2: #12141c;
132
140
  --gw-on-night: #e3e4e8;
@@ -143,6 +151,7 @@
143
151
  --gw-muted: #9a9ca6;
144
152
  --gw-line: #2a2b33;
145
153
  --gw-line-strong: #383944;
154
+ --gw-line-control: #6a6d78;
146
155
  --gw-action: #f2f3f5;
147
156
  --gw-action-hover: #ffffff;
148
157
  --gw-on-action: #0d0d12;
@@ -154,6 +163,9 @@
154
163
  --gw-success-soft: #132a22;
155
164
  --gw-warning: #e0b341;
156
165
  --gw-warning-soft: #2b2410;
166
+ --gw-danger: #ff6b5a;
167
+ --gw-danger-soft: #2e1714;
168
+ --gw-on-danger: #0d0d12;
157
169
  --gw-night: #0a0b10;
158
170
  --gw-night-2: #12141c;
159
171
  --gw-on-night: #e3e4e8;
@@ -169,6 +181,7 @@
169
181
  --gw-muted: #686b71;
170
182
  --gw-line: #e5e7ea;
171
183
  --gw-line-strong: #d5d8dd;
184
+ --gw-line-control: #85888e;
172
185
  --gw-action: #1b1c1e;
173
186
  --gw-action-hover: #000000;
174
187
  --gw-on-action: #ffffff;
@@ -180,6 +193,9 @@
180
193
  --gw-success-soft: #e3f1df;
181
194
  --gw-warning: #9a6700;
182
195
  --gw-warning-soft: #fbf1dc;
196
+ --gw-danger: #b42318;
197
+ --gw-danger-soft: #fff0e9;
198
+ --gw-on-danger: #ffffff;
183
199
  --gw-night: #0c0e14;
184
200
  --gw-night-2: #141722;
185
201
  --gw-on-night: #e3e4e8;
@@ -196,6 +212,7 @@
196
212
  --gw-muted: #9a9ca6;
197
213
  --gw-line: #2a2b33;
198
214
  --gw-line-strong: #383944;
215
+ --gw-line-control: #6a6d78;
199
216
  --gw-action: #f2f3f5;
200
217
  --gw-action-hover: #ffffff;
201
218
  --gw-on-action: #0d0d12;
@@ -207,6 +224,9 @@
207
224
  --gw-success-soft: #132a22;
208
225
  --gw-warning: #e0b341;
209
226
  --gw-warning-soft: #2b2410;
227
+ --gw-danger: #ff6b5a;
228
+ --gw-danger-soft: #2e1714;
229
+ --gw-on-danger: #0d0d12;
210
230
  }
211
231
  /* action profile "accent": data-gw-action="accent" on <html>. In night bands too, where the dark palette applies. */
212
232
  :root[data-gw-action="accent"],
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"><rect width="48" height="48" fill="#e5e7ea"/><circle cx="24" cy="19" r="8" fill="#9a9ca6"/><path d="M9 44c1.6-8.4 7.8-13 15-13s13.4 4.6 15 13z" fill="#9a9ca6"/></svg>