@gemboss/website-ui 0.6.0 → 0.7.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 +15 -2
- package/CHANGELOG.md +61 -0
- package/README.md +8 -8
- package/bin/lib/check.mjs +17 -0
- package/css/base.css +12 -0
- package/css/footer.css +178 -0
- package/css/tokens.css +1 -0
- package/demo/footer.html +339 -0
- package/demo/index.html +1 -0
- package/docs/components.md +57 -5
- package/docs/markup-contract.md +59 -4
- package/docs/theming.md +8 -2
- package/js/website.js +21 -4
- package/js/website.mjs +27 -3
- package/package.json +1 -1
- package/skill/SKILL.md +3 -0
- package/system.json +2 -0
- package/templates/footer.html +108 -20
- package/templates/page.html +16 -11
- package/website.css +192 -1
- package/website.min.css +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,15 +10,17 @@ 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, an article's contents and its tables' scroll box; no dependencies; `window.gwWebsite.init(root)` |
|
|
13
|
+
| `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box, the footer's folds; no dependencies; `window.gwWebsite.init(root)` |
|
|
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/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
|
|
17
18
|
| `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
|
|
18
19
|
| `demo/index.html` | every component on one page: open it to see what exists before you build anything |
|
|
19
20
|
| `demo/brand-profiles.html` | a GemPages-style brand: its accent, the accent button, soft corners, the floating header |
|
|
20
21
|
| `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
|
|
21
22
|
| `demo/forms.html` | fields in every state, choices, the footer newsletter, a short form, stars, the testimonial wall and the rating badge |
|
|
23
|
+
| `demo/footer.html` | the full footer (Ask AI, columns that fold on phones, family rows, theme toggle, wordmark), and the same for a GemPages-style brand |
|
|
22
24
|
| `docs/components.md` | every class, what it is for, its variants |
|
|
23
25
|
| `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
|
|
24
26
|
|
|
@@ -57,6 +59,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
57
59
|
|
|
58
60
|
1. Copy `templates/page.html` into the site. It links `website.css` and `website.js`, has the skip link, the
|
|
59
61
|
header, the `<main>` and the footer. Fill the header and footer once (they are the same on every page).
|
|
62
|
+
**The footer comes from `templates/footer.html`.** A brand fills its rows (the columns, the family rows, the
|
|
63
|
+
words) and deletes the optional parts it does not use (Ask AI, newsletter, family rows, theme toggle, wordmark);
|
|
64
|
+
it never invents a footer part (a logo strip, badges, a map, a second bottom row). Something missing is a gap.
|
|
60
65
|
2. Pick sections from `templates/sections/` in the order the page needs. A typical page: hero → proof (logos or
|
|
61
66
|
stats) → 2 to 4 feature sections → one showcase or split → FAQ → final CTA. Not every page needs every section;
|
|
62
67
|
fewer, clearer sections beat more. A page that is read rather than scanned (a blog post, docs, terms, privacy) is
|
|
@@ -92,6 +97,10 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
92
97
|
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
98
|
form keeps two or three fields (`templates/sections/form.html`). A field in error is `aria-invalid="true"` plus its
|
|
94
99
|
`.gw-field__error` in words; the page decides when, the kit draws it.
|
|
100
|
+
- **Footer** (0.7.0): `templates/footer.html`. Columns are `<details class="gw-footer__fold">` with the heading in the
|
|
101
|
+
`<summary>`: rows that fold on phones, open columns from 768px, no script needed. A link that leaves the site gets the
|
|
102
|
+
↗ (`.gw-footer__ext`); one with `target="_blank"` also `<span class="gw-visually-hidden"> (opens in a new tab)</span>`.
|
|
103
|
+
The wordmark is `aria-hidden="true"`.
|
|
95
104
|
- **Ratings** (0.6.0): stars `.gw-stars` (`role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"`), the
|
|
96
105
|
wall `.gw-testimonials` of `.gw-card.gw-testimonial`, the badge `.gw-rating-badge` (`templates/sections/testimonials.html`).
|
|
97
106
|
- **One choice out of two to four** (Monthly / Yearly) is `.gw-segmented`, a radio group; what each option shows
|
|
@@ -138,6 +147,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
138
147
|
- Don't make a callout whose kind only its colour tells: `.gw-callout` starts with its `.gw-callout__label` in words.
|
|
139
148
|
- Don't type separators (›, /, >) between breadcrumbs, and don't build a contents list whose links point at ids the
|
|
140
149
|
page does not have: build it from the article's own h2s.
|
|
150
|
+
- Don't add a footer part the template does not have, and don't build footer columns that fold with your own script:
|
|
151
|
+
fill `templates/footer.html` (its family rows take another brand's siblings: GemBoss, GemX, GemUpsells on a
|
|
152
|
+
GemPages site). Don't put the assistants' logos in the Ask AI row.
|
|
141
153
|
- Don't animate layout properties (width, height, top, margin), and don't autoplay motion that cannot be stopped.
|
|
142
154
|
- Don't ship a page you have not seen at 320px.
|
|
143
155
|
- Don't write CSS that overrides a `.gw-*` class to change how it looks. If the kit's version is wrong for you,
|
|
@@ -250,7 +262,8 @@ a waiting button not marked with `aria-busy`, a segmented control without its ra
|
|
|
250
262
|
with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
|
|
251
263
|
words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
|
|
252
264
|
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,
|
|
265
|
+
its source, a footer fold without its heading in the summary or a wordmark a screen reader reads, and patterns that
|
|
266
|
+
repeat across pages (likely gaps). It does not replace looking at the page at 320px and 1366px.
|
|
254
267
|
|
|
255
268
|
## 11. Upgrading the kit
|
|
256
269
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,67 @@ 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.7.0 (2026-10-25)
|
|
8
|
+
|
|
9
|
+
The footer as gemboss.ai has it today (gap #71): the kit's footer, built on 24/10 from gemboss.ai, had fallen behind
|
|
10
|
+
the site's. Every new part is optional, per site, and selects only its own markup: a page with the 0.6.0 footer looks
|
|
11
|
+
exactly as before. The demo index (without its new link), `brand-profiles.html`, `article.html`, `blog.html`,
|
|
12
|
+
`forms.html` and `theme-example.html` are pixel-identical to 0.6.0 at 390 and 1366px, light and dark.
|
|
13
|
+
|
|
14
|
+
Closes #71
|
|
15
|
+
|
|
16
|
+
- Ask AI `.gw-footer__ask` (+ `__ask-label`) in the brand block: "Ask AI about [Brand]" and four links (ChatGPT,
|
|
17
|
+
Claude, Perplexity, Google AI Mode) that open the assistant with one neutral question already typed, URL-encoded
|
|
18
|
+
after `?q=` (Google: `search?udm=50&q=`), as gemboss.ai builds them. Words, not the assistants' logos. 28px rows,
|
|
19
|
+
44px on touch; the row may run wider than the brand's sentence (four names on one line at 390px).
|
|
20
|
+
- Columns that fold: each `.gw-footer__col` holds a `<details class="gw-footer__fold">` whose `<summary>` is the
|
|
21
|
+
column's `<h2 class="gw-footer__heading">`. Below 768px the columns are one track of 52px rows with a chevron,
|
|
22
|
+
folded, and the browser opens and closes them (no script needed); the open list's links are 44px. From 768px they
|
|
23
|
+
are the columns as before: the summary is only its heading (`display: contents`: no box, no Tab stop) and the list
|
|
24
|
+
shows even while the `<details>` is closed (`::details-content`, in a rule of its own so an older browser drops only
|
|
25
|
+
it). `website.js` sets `open` from 768px, takes the summary out of the Tab order there and ignores a click on it,
|
|
26
|
+
gives back the markup's state below 768px (again on every change of width), and is wired like every other part
|
|
27
|
+
(`data-gw-ready="fold"`, `init` once per element, `destroy` takes it back). From 1080px the brand's track is as
|
|
28
|
+
wide as gemboss.ai draws it beside five columns (193.5px at 1120px), whatever the number of columns, and the columns
|
|
29
|
+
share the rest: three to six fit. With the fold markup only; the 0.6.0 columns keep their 1.6fr.
|
|
30
|
+
Closes gap #40 (footer accordion on phones)
|
|
31
|
+
- Product family `.gw-footer__family`: a `<nav aria-label>` holding a fold whose heading is the row's label, and a
|
|
32
|
+
list of products, name and one line each (`<b>` and `<i>`, a space between). Repeat it for a second family ("More
|
|
33
|
+
from GemBoss", "From the same team, on the Shopify App Store"). Two items a line on phones (one more folded row),
|
|
34
|
+
four from 768px, and from 1080px the label in the brand's track and the items starting where the first column
|
|
35
|
+
starts. Items take the action radius (12px in `soft`) and the soft ground on hover.
|
|
36
|
+
- External links: `.gw-footer__ext`, a drawn ↗ after a link that leaves the site (aria-hidden). A link with
|
|
37
|
+
`target="_blank"` also carries `<span class="gw-visually-hidden"> (opens in a new tab)</span>`; one without it does
|
|
38
|
+
not. `.gw-visually-hidden` is new in `base.css`: words a screen reader hears and the page does not show.
|
|
39
|
+
- Theme toggle in the bottom row: `.gw-theme-toggle.gw-footer__toggle` with `data-gw-theme-toggle`, wired by
|
|
40
|
+
`website.js` like the header's (both relabel together). A 32px square with one hairline and the action radius, a
|
|
41
|
+
40px hit area, 44px on touch; it pushes itself and the social links to the right. Below 768px the copyright and the
|
|
42
|
+
toggle share the first line, the legal and social links the second.
|
|
43
|
+
- Wordmark `.gw-footer__wordmark` (`aria-hidden="true"`): the brand's mark (an inline SVG in `currentColor`) and name,
|
|
44
|
+
ink at 10%, at `--gw-display-wordmark` (`clamp(40px, 9vw, 128px)`, new in `system.json` `type.display`), the
|
|
45
|
+
display tracking, weight 700, centred, 48px under the bottom row. A name too long for the line is cut, never a
|
|
46
|
+
sideways scroll. Not shown below 480px, as gemboss.ai hides it on phones.
|
|
47
|
+
- Spacing as measured on gemboss.ai and on the kit's scale: family rows 48px apart with 32px under their hairline,
|
|
48
|
+
32px from the last family row to the bottom row (24px on phones), headings 12px above their links, 32px link rows.
|
|
49
|
+
- Templates: `templates/footer.html` is now the full footer, every optional part marked (Ask AI, the 0.6.0
|
|
50
|
+
newsletter, the family row, the toggle, the wordmark), five columns; `templates/page.html` uses the folding columns.
|
|
51
|
+
Demo: `demo/footer.html`, the gemboss.ai footer with gemboss.ai's words, and a switch to the same footer for a
|
|
52
|
+
GemPages-style brand (`data-gw-radius="soft"`, `data-gw-action="accent"`, the GemPages accent of
|
|
53
|
+
`docs/theming.md`, a family row listing GemBoss, GemX and GemUpsells, the newsletter instead of Ask AI), linked
|
|
54
|
+
from `demo/index.html`.
|
|
55
|
+
- `check`: `footer-markup` (an error): a `.gw-footer__fold` that is not a `<details>`, has no `<summary>`, or whose
|
|
56
|
+
summary does not hold the `.gw-footer__heading`; a `.gw-footer__wordmark` without `aria-hidden="true"`.
|
|
57
|
+
- `AGENTS.md`, the skill and the docs: the footer comes from `templates/footer.html`; a brand fills its rows and
|
|
58
|
+
never invents a footer part.
|
|
59
|
+
- Budget: `website.min.css` is 65.8 KB, 11.4 KB gzipped (0.6.0: 59.3 KB, 10.5 KB), most of it the three drawn icons'
|
|
60
|
+
masks. `js/website.js` is 14.5 KB unminified, 4.7 KB gzipped (0.6.0: 13.8 KB, 4.5 KB), under the test's 15 KB and
|
|
61
|
+
5 KB.
|
|
62
|
+
- Tests: `test/run.mjs` holds the 0.6.0 rules of `footer.css` byte for byte and every 0.7.0 rule to a 0.7.0 part, and
|
|
63
|
+
covers the folds in its fake DOM and the new rule; `test/browser.mjs` drives `demo/footer.html` in Chromium (folded
|
|
64
|
+
at 390px, open at 1366px, Tab, Enter and Space, a click on a desktop heading, resizing both ways, without the
|
|
65
|
+
script, the wordmark out of the accessibility tree, no sideways scroll at 320 and 390px, the toggle, the
|
|
66
|
+
GemPages-style switch light and dark, and the column widths measured on gemboss.ai).
|
|
67
|
+
|
|
7
68
|
## 0.6.0 (2026-10-25)
|
|
8
69
|
|
|
9
70
|
Forms and ratings: the fields a contact page, a sign-up or a footer newsletter needs, with their error state, and the
|
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`, `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,
|
|
15
|
+
| `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 92 KB, 66 KB bản min, 11,4 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 như gemboss.ai (cột gập lại trên điện thoại, hàng Ask AI, hàng sản phẩm cùng nhà, nút sáng/tối, wordmark; ô newsletter tuỳ chọn), 16 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ờ, đá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, cột footer gập/mở theo khổ màn; không phụ thuộc thư viện nào, khoảng 4,7 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à 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) |
|
|
18
|
+
| `templates/` | khung một trang, header, footer (`footer.html`: footer của gemboss.ai, mọi phần tuỳ chọn đều ghi rõ) 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); `footer.html` là footer đầy đủ như gemboss.ai, kèm nút chuyển sang bản kiểu GemPages |
|
|
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.7.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.7.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ồ, 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 |
|
|
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, cột footer gập thiếu tiêu đề trong summary, wordmark không ẩn với trình đọc màn hình… 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 # demo trong Chromium thật: bài viết (cuộn, Tab, mục lục, bảng)
|
|
144
|
+
node test/browser.mjs # demo trong Chromium thật: bài viết (cuộn, Tab, mục lục, bảng), form, sao, review và footer (gập, Tab, nút sáng/tối) (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
|
@@ -144,6 +144,11 @@ export const RULES = {
|
|
|
144
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
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
146
|
},
|
|
147
|
+
"footer-markup": {
|
|
148
|
+
level: "error", title: "Footer fold or wordmark without its markup",
|
|
149
|
+
why: "A footer column folds on phones as a <details class=\"gw-footer__fold\"> whose <summary> holds the column's heading (<h2 class=\"gw-footer__heading\">): the summary is the row a reader taps, and a screen reader names the row by that heading. The wordmark (.gw-footer__wordmark) repeats the brand's name as decoration; without aria-hidden=\"true\" a screen reader reads the name a second time.",
|
|
150
|
+
fix: "Copy the markup from templates/footer.html: <details class=\"gw-footer__fold\"><summary><h2 class=\"gw-footer__heading\">Product</h2></summary><ul>…</ul></details>, and <div class=\"gw-footer__wordmark\" aria-hidden=\"true\">.",
|
|
151
|
+
},
|
|
147
152
|
"uppercase-text": {
|
|
148
153
|
level: "warning", title: "Uppercase text",
|
|
149
154
|
why: "Headings, buttons, labels and menu items are sentence case.",
|
|
@@ -630,6 +635,18 @@ function analyzeHtml(file, text, out) {
|
|
|
630
635
|
].filter(Boolean);
|
|
631
636
|
if (missing.length) add("stars-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
|
|
632
637
|
}
|
|
638
|
+
if (cls.includes("gw-footer__wordmark") && (a["aria-hidden"] || "").trim() !== "true") {
|
|
639
|
+
add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: aria-hidden="true" (the wordmark is decoration)`));
|
|
640
|
+
}
|
|
641
|
+
if (cls.includes("gw-footer__fold")) {
|
|
642
|
+
const sum = /<summary\b[^>]*>([\s\S]*?)<\/summary\s*>/i.exec(inner());
|
|
643
|
+
const missing = [
|
|
644
|
+
tag !== "details" && `a <details> (the class is on <${tag}>)`,
|
|
645
|
+
!sum && "a <summary> as its first child",
|
|
646
|
+
sum && !/\bclass\s*=\s*["'][^"']*\bgw-footer__heading\b/i.test(sum[1]) && "the column's heading inside the <summary> (<h2 class=\"gw-footer__heading\">)",
|
|
647
|
+
].filter(Boolean);
|
|
648
|
+
if (missing.length) add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
649
|
+
}
|
|
633
650
|
if (cls.includes("gw-rating-badge") || cls.includes("gw-testimonial")) {
|
|
634
651
|
const linked = (tag === "a" && "href" in a) || /<a\b[^>]*\bhref\s*=/i.test(inner());
|
|
635
652
|
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)}`));
|
package/css/base.css
CHANGED
|
@@ -91,6 +91,18 @@ h5, h6 { font-size: 16px; font-weight: 600; letter-spacing: 0; }
|
|
|
91
91
|
}
|
|
92
92
|
.gw-skip:focus { left: 16px; }
|
|
93
93
|
|
|
94
|
+
/* words for a screen reader only (0.7.0): " (opens in a new tab)" in a link with target="_blank" */
|
|
95
|
+
.gw-visually-hidden {
|
|
96
|
+
position: absolute;
|
|
97
|
+
width: 1px;
|
|
98
|
+
height: 1px;
|
|
99
|
+
padding: 0;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
clip-path: inset(50%);
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
border: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
/* scroll reveal: the script adds .is-in; the animation is a failsafe that shows it anyway after 2.5s */
|
|
95
107
|
.gw-reveal {
|
|
96
108
|
opacity: 0;
|
package/css/footer.css
CHANGED
|
@@ -54,3 +54,181 @@
|
|
|
54
54
|
@media (max-width: 768px), (pointer: coarse) {
|
|
55
55
|
.gw-footer__bottom a { min-height: var(--gw-tap-min); }
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/* ---- 0.7.0: the footer as gemboss.ai has it (issue #71) ---------------------------------------------------------------
|
|
59
|
+
Every part below is optional, per site, and styles only its own markup: a 0.6.0 footer looks exactly as before.
|
|
60
|
+
In page order: the Ask AI row in the brand block, columns that fold on phones (gap #40), product-family rows, the
|
|
61
|
+
theme toggle in the bottom row, and the wordmark at the very end. Values measured on gemboss.ai on 25/10/2026 and
|
|
62
|
+
put on the kit's scales; the full markup is templates/footer.html. */
|
|
63
|
+
|
|
64
|
+
/* Ask AI: a label and links that open an assistant with one neutral question already typed (a URL-encoded ?q=), so
|
|
65
|
+
the answer comes from what the web says, not from a claim the brand wrote. Words, not logos. 28px rows, 44px on
|
|
66
|
+
touch. */
|
|
67
|
+
.gw-footer__ask { margin-top: 24px; }
|
|
68
|
+
/* the row may run wider than the brand's sentence (four names on one line at 390px) */
|
|
69
|
+
.gw-footer__brand:has(> .gw-footer__ask) { max-width: none; }
|
|
70
|
+
.gw-footer__brand:has(> .gw-footer__ask) > :not(.gw-footer__ask) { max-width: 320px; }
|
|
71
|
+
.gw-footer__ask > .gw-footer__ask-label { margin: 0; color: var(--gw-muted); font-size: 13px; }
|
|
72
|
+
.gw-footer__ask > ul { display: flex; flex-wrap: wrap; gap: 0 16px; margin: 4px 0 0; padding: 0; list-style: none; }
|
|
73
|
+
.gw-footer__ask a { display: block; padding-block: 4px; color: var(--gw-ink); line-height: 20px; }
|
|
74
|
+
.gw-footer__ask a:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
|
|
75
|
+
|
|
76
|
+
/* the mark after a link that leaves the site (↗, drawn), aria-hidden; the words "(opens in a new tab)" are a separate
|
|
77
|
+
.gw-visually-hidden span, only when the link has target="_blank" */
|
|
78
|
+
.gw-footer__ext {
|
|
79
|
+
display: inline-block;
|
|
80
|
+
width: 12px;
|
|
81
|
+
height: 12px;
|
|
82
|
+
margin-left: 4px;
|
|
83
|
+
vertical-align: -1px;
|
|
84
|
+
background: var(--gw-muted);
|
|
85
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
86
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.5 8.5l5-5M4.5 3.5h4v4' fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Folds: <details class="gw-footer__fold"> inside a .gw-footer__col (or a .gw-footer__family), its <summary> holding the
|
|
90
|
+
.gw-footer__heading, then the <ul>. Below 768px each is a 52px row with a chevron, folded unless the markup says
|
|
91
|
+
`open`; the browser opens and closes it, no script needed. From 768px it is the column as before: the summary is
|
|
92
|
+
only the heading (display: contents: no box, no Tab stop), and the list shows even while the <details> is closed
|
|
93
|
+
(::details-content; website.js also sets `open` there, for browsers without it). */
|
|
94
|
+
.gw-footer__fold > summary { list-style: none; }
|
|
95
|
+
.gw-footer__fold > summary::-webkit-details-marker { display: none; }
|
|
96
|
+
.gw-footer__fold .gw-footer__heading { margin: 0; line-height: 20px; }
|
|
97
|
+
.gw-footer__fold > ul { margin: 0; padding: 0; list-style: none; }
|
|
98
|
+
.gw-footer__col > .gw-footer__fold a { line-height: 20px; }
|
|
99
|
+
@media (min-width: 768px) {
|
|
100
|
+
.gw-footer__fold > summary { display: contents; }
|
|
101
|
+
.gw-footer__col > .gw-footer__fold .gw-footer__heading { margin-bottom: 12px; }
|
|
102
|
+
}
|
|
103
|
+
@media (min-width: 768px) {
|
|
104
|
+
.gw-footer__fold::details-content { content-visibility: visible; }
|
|
105
|
+
}
|
|
106
|
+
@media (max-width: 767px) {
|
|
107
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
|
|
108
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) > .gw-footer__brand { padding-bottom: 24px; }
|
|
109
|
+
.gw-footer__col:has(> .gw-footer__fold),
|
|
110
|
+
.gw-footer__family { border-bottom: 1px solid var(--gw-line); }
|
|
111
|
+
.gw-footer__brand + .gw-footer__col:has(> .gw-footer__fold) { border-top: 1px solid var(--gw-line); }
|
|
112
|
+
.gw-footer__fold > summary {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
gap: 16px;
|
|
117
|
+
min-height: var(--gw-tap-min);
|
|
118
|
+
padding-block: 16px;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
.gw-footer__fold > summary::after {
|
|
122
|
+
content: "";
|
|
123
|
+
flex: none;
|
|
124
|
+
width: 16px;
|
|
125
|
+
height: 16px;
|
|
126
|
+
background: var(--gw-muted);
|
|
127
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
128
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 5.5L8 11l5.5-5.5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 16px no-repeat;
|
|
129
|
+
transition: transform 0.18s var(--gw-ease);
|
|
130
|
+
}
|
|
131
|
+
.gw-footer__fold[open] > summary::after { transform: rotate(180deg); }
|
|
132
|
+
.gw-footer__fold > ul { padding-bottom: 12px; }
|
|
133
|
+
.gw-footer__col > .gw-footer__fold a { display: flex; align-items: center; min-height: var(--gw-tap-min); }
|
|
134
|
+
}
|
|
135
|
+
/* desktop: the brand's track is as wide as gemboss.ai draws it beside five columns (1.2 parts of 6.2, after five 24px
|
|
136
|
+
gaps), whatever the number of columns, so a family row can line up with it; the columns share the rest */
|
|
137
|
+
@media (min-width: 1080px) {
|
|
138
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) { grid-template-columns: minmax(0, calc((100% - 120px) * 6 / 31)); }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Product family: a labelled row of products, name and one line each (<b> and <i>), as many rows as the brand needs
|
|
142
|
+
("More from GemBoss", "From the same team, on the Shopify App Store"). A <nav aria-label> holding a fold whose
|
|
143
|
+
heading is the label. Two items a row on phones, four from 768px; from 1080px the label sits in the brand's track and
|
|
144
|
+
the items start where the first column starts. */
|
|
145
|
+
.gw-footer__family .gw-footer__heading { color: var(--gw-muted); font-weight: 500; }
|
|
146
|
+
.gw-footer__family .gw-footer__fold > ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
147
|
+
.gw-footer__family a {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
gap: 2px;
|
|
152
|
+
min-height: var(--gw-tap-min);
|
|
153
|
+
padding: 8px 12px 8px 0;
|
|
154
|
+
border-radius: var(--gw-radius-action);
|
|
155
|
+
color: var(--gw-ink);
|
|
156
|
+
}
|
|
157
|
+
.gw-footer__family a:hover { background: var(--gw-bg-soft); }
|
|
158
|
+
.gw-footer__family b { font-size: 14px; font-weight: 600; line-height: 20px; }
|
|
159
|
+
.gw-footer__family i { color: var(--gw-muted); font-size: 13px; font-style: normal; line-height: 18px; }
|
|
160
|
+
@media (min-width: 768px) {
|
|
161
|
+
.gw-footer__family { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--gw-line); }
|
|
162
|
+
.gw-footer__family .gw-footer__heading { margin-bottom: 12px; }
|
|
163
|
+
.gw-footer__family .gw-footer__fold > ul { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 16px; margin-left: -12px; }
|
|
164
|
+
.gw-footer__family a { min-height: 0; padding: 8px 12px; }
|
|
165
|
+
.gw-footer__family + .gw-footer__bottom { margin-top: 32px; }
|
|
166
|
+
}
|
|
167
|
+
@media (min-width: 1080px) {
|
|
168
|
+
.gw-footer__family { display: flow-root; }
|
|
169
|
+
.gw-footer__family .gw-footer__heading { float: left; width: calc((100% - 120px) * 6 / 31); margin: 8px 0 0; }
|
|
170
|
+
.gw-footer__family .gw-footer__fold > ul { margin-left: calc((100% - 120px) * 6 / 31 + 12px); }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* The theme toggle in the bottom row (.gw-theme-toggle with data-gw-theme-toggle, wired like the header's): a 32px
|
|
174
|
+
square with one hairline, its hit area 40px, 44px on touch. It pushes itself and the social links to the right.
|
|
175
|
+
Below 768px the copyright and the toggle share the first line, legal and social links the second. */
|
|
176
|
+
.gw-footer__toggle {
|
|
177
|
+
position: relative;
|
|
178
|
+
width: 32px;
|
|
179
|
+
height: 32px;
|
|
180
|
+
margin-inline: auto 16px;
|
|
181
|
+
border: 1px solid var(--gw-line-strong);
|
|
182
|
+
background: var(--gw-surface);
|
|
183
|
+
}
|
|
184
|
+
.gw-footer__toggle::after { content: ""; position: absolute; inset: -5px; }
|
|
185
|
+
.gw-footer__toggle svg { width: 18px; height: 18px; }
|
|
186
|
+
@media (max-width: 768px), (pointer: coarse) {
|
|
187
|
+
.gw-footer__toggle { width: var(--gw-tap-min); height: var(--gw-tap-min); }
|
|
188
|
+
}
|
|
189
|
+
@media (max-width: 767px) {
|
|
190
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) { column-gap: 16px; }
|
|
191
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) > :first-child { flex: 1 1 calc(100% - var(--gw-tap-min) - 16px); }
|
|
192
|
+
.gw-footer__bottom > .gw-footer__toggle { order: 1; margin: 0; }
|
|
193
|
+
.gw-footer__bottom:has(> .gw-footer__toggle) > ul { order: 2; }
|
|
194
|
+
.gw-footer__family + .gw-footer__bottom,
|
|
195
|
+
.gw-footer__grid:has(> .gw-footer__col > .gw-footer__fold) + .gw-footer__bottom { margin-top: 24px; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* The wordmark: the brand's mark and name, large and faint, the footer's last line. Decoration (aria-hidden="true"):
|
|
199
|
+
the name is already in the brand block. Its size follows the viewport (--gw-display-wordmark) and it never makes the
|
|
200
|
+
page scroll sideways: a name too long for the line is cut. The mark is drawn in currentColor (an inline SVG); an
|
|
201
|
+
<img> mark keeps its own colours, so leave it out and keep the name. Not on phones, as on gemboss.ai. 24px more room
|
|
202
|
+
under it than gemboss.ai gives: the kit's footer is a painted band (white like the page), and the breathing gate
|
|
203
|
+
wants half the letters' size between them and its edge (67px under 123px letters at 1366px). */
|
|
204
|
+
.gw-footer__wordmark {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
gap: 24px;
|
|
209
|
+
padding-block: 48px 24px;
|
|
210
|
+
color: color-mix(in srgb, var(--gw-ink) 10%, transparent);
|
|
211
|
+
font-size: var(--gw-display-wordmark);
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
letter-spacing: var(--gw-display-tracking);
|
|
214
|
+
line-height: 1;
|
|
215
|
+
white-space: nowrap;
|
|
216
|
+
user-select: none;
|
|
217
|
+
}
|
|
218
|
+
.gw-footer__wordmark > svg,
|
|
219
|
+
.gw-footer__wordmark > img { flex: none; width: auto; height: 0.9em; }
|
|
220
|
+
.gw-footer__wordmark > span { min-width: 0; overflow-x: clip; }
|
|
221
|
+
@media (max-width: 767px) { .gw-footer__wordmark { gap: 16px; } }
|
|
222
|
+
@media (max-width: 479px) { .gw-footer__wordmark { display: none; } }
|
|
223
|
+
|
|
224
|
+
@media (max-width: 768px), (pointer: coarse) {
|
|
225
|
+
.gw-footer__ask a { display: flex; align-items: center; min-height: var(--gw-tap-min); padding-block: 0; }
|
|
226
|
+
}
|
|
227
|
+
@media (prefers-reduced-motion: reduce) {
|
|
228
|
+
.gw-footer__fold > summary::after { transition: none; }
|
|
229
|
+
}
|
|
230
|
+
/* a forced-colours screen drops backgrounds: draw the chevron and the ↗ in its text colour */
|
|
231
|
+
@media (forced-colors: active) {
|
|
232
|
+
.gw-footer__ext,
|
|
233
|
+
.gw-footer__fold > summary::after { forced-color-adjust: none; background: CanvasText; }
|
|
234
|
+
}
|
package/css/tokens.css
CHANGED