@gemboss/website-ui 0.4.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 +43 -5
- package/CHANGELOG.md +117 -0
- package/README.md +9 -8
- package/bin/lib/check.mjs +77 -0
- package/css/article.css +237 -0
- package/css/components.css +23 -0
- package/css/footer.css +3 -0
- package/css/forms.css +154 -0
- package/css/prose.css +29 -16
- package/css/ratings.css +78 -0
- package/css/tokens.css +25 -0
- package/demo/article.html +359 -0
- package/demo/assets/article-hero.svg +1 -0
- package/demo/assets/avatar-gemboss.svg +1 -0
- package/demo/assets/avatar-sample.svg +1 -0
- package/demo/assets/post-headless.svg +1 -0
- package/demo/assets/post-motion.svg +1 -0
- package/demo/assets/post-output.svg +1 -0
- package/demo/assets/post-speed.svg +1 -0
- package/demo/assets/post-theme.svg +1 -0
- package/demo/assets/post-voice.svg +1 -0
- package/demo/blog.html +314 -0
- package/demo/forms.html +505 -0
- package/demo/index.html +2 -0
- package/docs/components.md +334 -12
- package/docs/markup-contract.md +123 -7
- package/docs/theming.md +4 -3
- package/js/website.js +68 -8
- package/js/website.mjs +80 -7
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/system.json +12 -1
- package/templates/footer.html +15 -0
- package/templates/sections/article.html +93 -0
- package/templates/sections/form.html +42 -0
- package/templates/sections/post-list.html +65 -0
- package/templates/sections/testimonials.html +58 -0
- package/website.css +553 -17
- package/website.min.css +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,13 +10,15 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
10
10
|
| file | what it is |
|
|
11
11
|
|---|---|
|
|
12
12
|
| `website.css` | the whole kit, one file (tokens, base, layout, components, sections) |
|
|
13
|
-
| `js/website.js` | header, mega menu, mobile menu, theme toggle, scroll reveal, segmented control, busy buttons; no dependencies; `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; 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/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose… |
|
|
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
|
+
| `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 |
|
|
20
22
|
| `docs/components.md` | every class, what it is for, its variants |
|
|
21
23
|
| `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
|
|
22
24
|
|
|
@@ -59,6 +61,10 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
59
61
|
stats) → 2 to 4 feature sections → one showcase or split → FAQ → final CTA. Not every page needs every section;
|
|
60
62
|
fewer, clearer sections beat more. A page that is read rather than scanned (a blog post, docs, terms, privacy) is
|
|
61
63
|
the prose section: the article's own HTML inside `.gw-prose`, no classes and no site CSS for its type.
|
|
64
|
+
**An article page (a blog post, a guide) is `templates/sections/article.html`**: breadcrumbs, the meta row
|
|
65
|
+
(author, category, date, read time), a contents list (`.gw-toc`) built from the article's h2s, the prose, callouts
|
|
66
|
+
where they help, the FAQ last. Never a callout told by its colour alone: it always carries its label in words
|
|
67
|
+
(Note, Tip, Warning). The blog index and "more from the blog" are `templates/sections/post-list.html`.
|
|
62
68
|
3. Write the copy (section 6), then delete what you did not use. Do not leave empty slots or sample text.
|
|
63
69
|
4. Images: real product screens or photos the brand owns. Give every `<img>` a real `alt` (or `alt=""` when it is
|
|
64
70
|
decoration), `width`, `height`, and `loading="lazy"` below the first screen.
|
|
@@ -68,8 +74,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
68
74
|
|
|
69
75
|
- **Colour roles** (CSS variables, light and dark): `--gw-bg`, `--gw-bg-soft` (the neutral raised fill),
|
|
70
76
|
`--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`, `--gw-line-strong`, `--gw-action`,
|
|
71
|
-
`--gw-on-action`, `--gw-accent`, `--gw-accent-strong`, `--gw-accent-soft`, `--gw-success`, `--gw-warning
|
|
72
|
-
`--gw-
|
|
77
|
+
`--gw-on-action`, `--gw-accent`, `--gw-accent-strong`, `--gw-accent-soft`, `--gw-success`, `--gw-warning` (and
|
|
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.
|
|
73
80
|
- **Dark mode** follows the visitor's system and the theme toggle (`data-theme` on `<html>`). Use the variables and
|
|
74
81
|
it works; a hard-coded colour breaks it.
|
|
75
82
|
- **Accent as text** (links, the active menu item, step numbers) is `--gw-accent-strong`; the plain accent is for
|
|
@@ -80,6 +87,13 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
80
87
|
- **A button that waits** (a form sent, an audit started) gets `aria-busy="true"` from the page's script until the
|
|
81
88
|
answer comes: the kit keeps its label and width, draws the ring and ignores a second press. Nothing else: no
|
|
82
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`).
|
|
83
97
|
- **One choice out of two to four** (Monthly / Yearly) is `.gw-segmented`, a radio group; what each option shows
|
|
84
98
|
carries `data-gw-show="group:value"`. Markup: `docs/components.md`, `templates/sections/pricing.html`.
|
|
85
99
|
- **Type**: Inter. Headings weight 650, tracking −0.022em; display h1/h2 weight 600, tracking −0.03em. Body 18px,
|
|
@@ -114,8 +128,16 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
114
128
|
- Don't write "Click here", "Learn more" as the only words of a link; say where it goes.
|
|
115
129
|
- Don't use the em dash (—) in visible text. Use a comma, a colon or a full stop.
|
|
116
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.
|
|
117
136
|
- Don't write fake numbers on a mock-up without the caption "Illustrative data." under it, and don't reword it.
|
|
118
137
|
- Don't style an article's headings, lists or code in site CSS; wrap the article in `.gw-prose`.
|
|
138
|
+
- Don't make a callout whose kind only its colour tells: `.gw-callout` starts with its `.gw-callout__label` in words.
|
|
139
|
+
- Don't type separators (›, /, >) between breadcrumbs, and don't build a contents list whose links point at ids the
|
|
140
|
+
page does not have: build it from the article's own h2s.
|
|
119
141
|
- Don't animate layout properties (width, height, top, margin), and don't autoplay motion that cannot be stopped.
|
|
120
142
|
- Don't ship a page you have not seen at 320px.
|
|
121
143
|
- Don't write CSS that overrides a `.gw-*` class to change how it looks. If the kit's version is wrong for you,
|
|
@@ -136,6 +158,15 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
136
158
|
(a `.gw-note` or a `role="status"` line). Under `prefers-reduced-motion` the kit's ring stands still.
|
|
137
159
|
- A single choice is a radio group (`.gw-segmented`): one Tab stop, the arrows move and choose. Never a row of
|
|
138
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"`.
|
|
168
|
+
- A wide table gets a scroll box a keyboard can reach: the `.gw-table` wrapper, or, for a bare table in `.gw-prose`,
|
|
169
|
+
the box `website.js` adds. Give the table a `<caption>`: it names the box.
|
|
139
170
|
|
|
140
171
|
## 6. Words
|
|
141
172
|
|
|
@@ -149,6 +180,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
149
180
|
- Made-up numbers in a drawn mock-up: `.gw-window__caption` says exactly "Illustrative data." (vi: "Dữ liệu minh
|
|
150
181
|
hoạ.", zh: "示意数据。"), plus one sentence only when a demo brand needs naming ("Vital Glow is a GemBoss demo
|
|
151
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.
|
|
152
188
|
- Image sources: record where each image comes from (the brand's own screen, a licensed photo). No photos of real
|
|
153
189
|
people or other brands' logos without permission.
|
|
154
190
|
- Other languages: rewrite each page for its reader (transcreation), do not translate sentence by sentence. Facts
|
|
@@ -212,7 +248,9 @@ It reports colours, radii, spacing and type outside the system, shadows on chrom
|
|
|
212
248
|
uppercase labels, em dashes in visible text, images without `alt`, overrides of `.gw-*` classes, redefined kit tokens,
|
|
213
249
|
a waiting button not marked with `aria-busy`, a segmented control without its radio-group markup, a drawn mock-up
|
|
214
250
|
with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
|
|
215
|
-
words on the accent button below 4.5:1,
|
|
251
|
+
words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
|
|
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.
|
|
216
254
|
|
|
217
255
|
## 11. Upgrading the kit
|
|
218
256
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,123 @@ 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
|
+
|
|
62
|
+
## 0.5.0 (2026-10-25)
|
|
63
|
+
|
|
64
|
+
The blog release: the parts around a long article and the blog index, so a blog (gempages.net's 1,290 posts first,
|
|
65
|
+
its Shopify article template the proposed pilot) is built from the kit instead of site code. Markup that worked in
|
|
66
|
+
0.4.0 still works, and a page without the new parts looks exactly as before: the demo index (without its new link),
|
|
67
|
+
`brand-profiles.html` and `theme-example.html` are pixel-identical to 0.4.0 at 390 and 1366px, light and dark, with
|
|
68
|
+
and without reduced motion, the index's bare prose table now in its script-made scroll box included.
|
|
69
|
+
|
|
70
|
+
- Breadcrumbs `.gw-breadcrumbs`: a `<nav>` with a name around an `<ol>`, the last item `aria-current="page"`. The
|
|
71
|
+
separators are a chevron the CSS draws, never a character in the text; the current item stays on one line and is
|
|
72
|
+
cut with an ellipsis on a phone. A BreadcrumbList JSON-LD example is in `docs/components.md` (the kit writes none).
|
|
73
|
+
- Article meta row `.gw-article-meta` (+ `__avatar`): author, category, `<time datetime>`, read time; 14px muted on
|
|
74
|
+
24px lines, a dot between items that never starts a wrapped line.
|
|
75
|
+
- Article layout `.gw-article` (+ `__head`, `__hero`) and contents `.gw-toc` (`data-gw-toc`): one column on phones
|
|
76
|
+
with the contents folded above the prose (a `<details>`); from 1080px (the kit's desktop width) the reading column (68ch) and the contents
|
|
77
|
+
beside it, sticky under the header (the floating header included). The links work without the script.
|
|
78
|
+
`website.js` marks the section in view with `aria-current="true"` (an accent bar; the weight never changes) using
|
|
79
|
+
an IntersectionObserver that sees a heading cross the line 30% down the window, so a jump across sections counts
|
|
80
|
+
too; it opens the list from 1080px, folds it again below, and a link folds it on a phone before the page moves.
|
|
81
|
+
Each change of section fires `gw:toc` on the nav (`{ id }`, `''` above the first heading). A contents link glides
|
|
82
|
+
to its section, except under `prefers-reduced-motion`.
|
|
83
|
+
Closes gap #54 (blog article chrome: breadcrumbs, meta row, sticky contents, callout, post list)
|
|
84
|
+
- Callout `.gw-callout` (+ `--note`, `--tip`, `--warning`, `__label`), `role="note"`: the kind in words (the label),
|
|
85
|
+
a 16px icon before it, on the kit's tints: the accent's, success's and the new `--gw-warning-soft`. Inside
|
|
86
|
+
`.gw-prose` it takes 32px of air like a figure.
|
|
87
|
+
- Card picture `.gw-card__media` (+ `--wide` 1.91:1 for 1200 x 630 share images, `--square`; 16:9 by default): the
|
|
88
|
+
card's first child, edge to edge across its top, its corners the card's radius inside the hairline, the soft
|
|
89
|
+
ground while it loads.
|
|
90
|
+
Closes gap #33 (card with a thumbnail)
|
|
91
|
+
- Post list `.gw-post-list` (+ `--compact`) of `.gw-card.gw-post` (`__title`): a picture, the category as a
|
|
92
|
+
`.gw-label`, the title whose link covers the card (one target, named by the title alone), an excerpt and a meta
|
|
93
|
+
row; one, two (768px) and three (1080px) columns. Pagination `.gw-pagination` (+ `__status`): previous and next as
|
|
94
|
+
links, markup only.
|
|
95
|
+
- Prose tables: `website.js` puts each `<table>` straight inside a `.gw-prose` in a scroll box of its own,
|
|
96
|
+
`div.gw-prose__table`, a region named after the table's caption (else its first header cell, else its first
|
|
97
|
+
cell) with `tabindex="0"`, and marks the table `.is-wrapped`. The table keeps `display: table`, so its rows and
|
|
98
|
+
cells stay a table to assistive tech. Without the script, or before it runs, the table scrolls by itself as in
|
|
99
|
+
0.2.0 (`:not(.is-wrapped)`); the box looks the same. `destroy` takes the box away again. A site's own wrapper for
|
|
100
|
+
bare tables can go. Issue #63 itself stays open until the box is heard with VoiceOver on Safari, as it asks (the
|
|
101
|
+
table semantics are checked in Chromium's accessibility tree only).
|
|
102
|
+
Closes gap #63 (prose tables that scroll without losing table semantics)
|
|
103
|
+
- `init(root)` and `destroy(root)` cover the two new parts like the others: wired once (`data-gw-ready="toc"` and
|
|
104
|
+
`"table"`), taken back by `destroy` (observer disconnected, mark removed, box removed), wired again by a later
|
|
105
|
+
`init`.
|
|
106
|
+
- Templates: `templates/sections/article.html` (breadcrumbs, title, meta, hero, contents, prose with callouts and a
|
|
107
|
+
table, FAQ last) and `templates/sections/post-list.html` (cards, pagination, the compact list). Demo:
|
|
108
|
+
`demo/article.html` (a whole post, then a post list) and `demo/blog.html` (the blog index), linked from
|
|
109
|
+
`demo/index.html`, with their drawn pictures in `demo/assets/`.
|
|
110
|
+
- `check`: `toc-target` (a contents link to an id that is not on the page) and `breadcrumbs-markup` (no `<nav>`,
|
|
111
|
+
no name, no `<ol>`, `aria-current="page"` missing from the last item or set on another, a typed separator) are
|
|
112
|
+
errors; `callout-label` (a callout without its label, told by colour alone) is a warning.
|
|
113
|
+
- Token: `--gw-warning-soft` (`#fbf1dc`, dark `#2b2410`), the warning's tint, next to `--gw-success-soft`.
|
|
114
|
+
- Budget: `js/website.js` is 13.8 KB unminified, 4.5 KB gzipped (0.4.0: 11.3 KB, 3.8 KB); the test now holds it
|
|
115
|
+
under 15 KB and 5 KB. `website.min.css` is 50.5 KB, 9.1 KB gzipped (0.4.0: 40.6 KB, 7.7 KB), most of it the
|
|
116
|
+
icons' masks, which gzip well.
|
|
117
|
+
- Tests: `test/run.mjs` covers the new script parts, rules and CSS in its fake DOM; `test/browser.mjs` drives the
|
|
118
|
+
demo in Chromium (wheel and jump scrolling, Tab and Enter through the contents, the fold below 1080px, the table's
|
|
119
|
+
box and accessibility tree at 390px, init twice, destroy then init, the soft and accent profiles). CI runs it in
|
|
120
|
+
the kit's job.
|
|
121
|
+
- `AGENTS.md`, the skill and the docs: an article page is `templates/sections/article.html` (breadcrumbs, meta,
|
|
122
|
+
contents from the h2s, prose, callouts); never a callout told by its colour alone.
|
|
123
|
+
|
|
7
124
|
## 0.4.0 (2026-10-25)
|
|
8
125
|
|
|
9
126
|
The website script, standardised like the CSS: it now wires markup that arrives after the page loaded (a Next.js
|
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
|
|
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
|
+
| `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à
|
|
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ồ
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -140,7 +140,8 @@ gemboss-www; mọi thay đổi đi qua PR, giá trị của hệ thống chỉ s
|
|
|
140
140
|
```sh
|
|
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
|
-
node test/run.mjs # tự kiểm CLI
|
|
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) và form, sao, review (cần .ci của repo)
|
|
144
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)
|
|
145
146
|
```
|
|
146
147
|
|
package/bin/lib/check.mjs
CHANGED
|
@@ -129,6 +129,21 @@ export const RULES = {
|
|
|
129
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
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
131
|
},
|
|
132
|
+
"toc-target": {
|
|
133
|
+
level: "error", title: "Contents link to a heading that is not on the page",
|
|
134
|
+
why: "A .gw-toc link whose #id names no element on the page jumps nowhere, and website.js cannot mark its section. It happens when a heading is renamed or removed and the list is not.",
|
|
135
|
+
fix: "Give the heading the id the link names (<h2 id=\"…\">), or point the link at the heading's id. Build the list from the article's own h2s (docs/components.md, Contents).",
|
|
136
|
+
},
|
|
137
|
+
"breadcrumbs-markup": {
|
|
138
|
+
level: "error", title: "Breadcrumbs without their markup",
|
|
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
|
+
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
|
+
},
|
|
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
|
+
},
|
|
132
147
|
"uppercase-text": {
|
|
133
148
|
level: "warning", title: "Uppercase text",
|
|
134
149
|
why: "Headings, buttons, labels and menu items are sentence case.",
|
|
@@ -139,6 +154,16 @@ export const RULES = {
|
|
|
139
154
|
why: "A drawn mock-up's numbers are made up. Without a caption a visitor reads them as a claim, and a claim found to be made up makes every other number on the page doubtful.",
|
|
140
155
|
fix: "Add <p class=\"gw-window__caption\">Illustrative data.</p> as the last child of the .gw-window (vi: Dữ liệu minh hoạ. zh: 示意数据。), or the source when the numbers are real.",
|
|
141
156
|
},
|
|
157
|
+
"callout-label": {
|
|
158
|
+
level: "warning", title: "Callout that tells its kind by colour only",
|
|
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.",
|
|
160
|
+
fix: "Start the callout with <p class=\"gw-callout__label\">Note</p> (Tip, Warning, or the same word in the page's language).",
|
|
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
|
+
},
|
|
142
167
|
"possible-gap": {
|
|
143
168
|
level: "note", title: "Possible kit gap: a site class used on several pages",
|
|
144
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.",
|
|
@@ -290,6 +315,7 @@ export function subjectOf(sel) {
|
|
|
290
315
|
const ATTR_THEME = String.raw`\[\s*data-theme(?:\s*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[\w-]+))?\s*\]`;
|
|
291
316
|
const ROOTISH = new RegExp(`^(?::root|html)?(?:${ATTR_THEME}|:not\\(\\s*${ATTR_THEME}\\s*\\))*$`, "i");
|
|
292
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
|
|
293
319
|
const THEMABLE = /^--gw-(?:accent(?:-[\w-]+)?|on-accent(?:-(?:light|dark))?|wash(?:-[\w-]+)?)$/; // docs/theming.md: the eight accent variables (-light/-dark) + wash
|
|
294
320
|
|
|
295
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)$/;
|
|
@@ -346,6 +372,8 @@ function checkRule(rule, ctx, add) {
|
|
|
346
372
|
const where = { file: ctx.file, line: d.line, excerpt: excerptOf(sel, d) };
|
|
347
373
|
if (d.prop.startsWith("--")) {
|
|
348
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;
|
|
349
377
|
if (!(THEMABLE.test(d.prop) && rootish)) add("token-redefined", where);
|
|
350
378
|
} else if (!ctx.svg && colorLiterals(value).length) add("color-literal", where);
|
|
351
379
|
continue;
|
|
@@ -519,6 +547,7 @@ function analyzeHtml(file, text, out) {
|
|
|
519
547
|
}
|
|
520
548
|
|
|
521
549
|
const classes = new Set();
|
|
550
|
+
const ids = new Set(), tocs = [];
|
|
522
551
|
let h1 = 0;
|
|
523
552
|
for (const m of scanText.matchAll(TAG_RE)) {
|
|
524
553
|
const tag = m[1].toLowerCase();
|
|
@@ -536,6 +565,7 @@ function analyzeHtml(file, text, out) {
|
|
|
536
565
|
else if (p.kind === "action") out.pageProfiles.push({ file: relFile, line, action: v });
|
|
537
566
|
}
|
|
538
567
|
if ((tag === "div" || tag === "span") && "onclick" in a) add("clickable-div", where());
|
|
568
|
+
if (a.id) ids.add(a.id);
|
|
539
569
|
for (const k of ["alt", "title", "aria-label", "placeholder"]) {
|
|
540
570
|
if (a[k] && hasEmDash(a[k])) add("em-dash", where(`${k}="${a[k]}"`));
|
|
541
571
|
}
|
|
@@ -565,6 +595,45 @@ function analyzeHtml(file, text, out) {
|
|
|
565
595
|
].filter(Boolean);
|
|
566
596
|
if (missing.length) add("segmented-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 100)} missing: ${missing.join(", ")}`));
|
|
567
597
|
}
|
|
598
|
+
if (cls.includes("gw-toc")) {
|
|
599
|
+
const end = elementEnd(scanText, m.index, tag);
|
|
600
|
+
if (end > 0) tocs.push({ start: m.index, end });
|
|
601
|
+
}
|
|
602
|
+
if (cls.includes("gw-breadcrumbs")) {
|
|
603
|
+
const body = inner();
|
|
604
|
+
const items = [...body.matchAll(/<li\b[^>]*>[\s\S]*?<\/li\s*>/gi)].map((x) => x[0]);
|
|
605
|
+
const current = (li) => /\baria-current\s*=\s*["']?page\b/i.test(li);
|
|
606
|
+
const words = decode(body.replace(/<[^>]*>/g, " "));
|
|
607
|
+
const missing = [
|
|
608
|
+
tag !== "nav" && "a <nav> (the class is on <" + tag + ">)",
|
|
609
|
+
tag === "nav" && !(a["aria-label"] || a["aria-labelledby"]) && "aria-label on the <nav>",
|
|
610
|
+
!/<ol\b/i.test(body) && "an <ol> of <li> items",
|
|
611
|
+
items.length && !current(items[items.length - 1]) && 'aria-current="page" on the last item',
|
|
612
|
+
items.slice(0, -1).some(current) && 'aria-current="page" on the last item only',
|
|
613
|
+
/(?:^|\s)(?:›|»|>|\/|\||·|•|→)(?=\s|$)/.test(words) && "no separator characters in the text (the kit draws them)",
|
|
614
|
+
].filter(Boolean);
|
|
615
|
+
if (missing.length) add("breadcrumbs-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
616
|
+
}
|
|
617
|
+
if (cls.includes("gw-callout") && !/\bclass\s*=\s*["'][^"']*\bgw-callout__label\b/i.test(inner())) {
|
|
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)}`));
|
|
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
|
+
}
|
|
568
637
|
if (cls.includes("gw-window")) {
|
|
569
638
|
const end = elementEnd(scanText, m.index, tag);
|
|
570
639
|
const body = end < 0 ? "" : scanText.slice(m.index, end);
|
|
@@ -594,6 +663,14 @@ function analyzeHtml(file, text, out) {
|
|
|
594
663
|
if (shouty(txt)) add("uppercase-text", where(`${m[0].slice(0, 60)}${txt.trim()}`));
|
|
595
664
|
}
|
|
596
665
|
}
|
|
666
|
+
// Contents links point at ids of this page (an id may come after the list: checked once the page is read).
|
|
667
|
+
for (const t of tocs) {
|
|
668
|
+
for (const x of scanText.slice(t.start, t.end).matchAll(/<a\b[^>]*?\bhref\s*=\s*["']#([^"']*)["']/gi)) {
|
|
669
|
+
let id = x[1];
|
|
670
|
+
try { id = decodeURIComponent(id); } catch { /* as written */ }
|
|
671
|
+
if (!ids.has(id)) add("toc-target", { file: relFile, line: lineAt(t.start + x.index), excerpt: `href="#${x[1]}": no element on the page has id="${id}"` });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
597
674
|
out.pageClasses.push({ file: relFile, classes });
|
|
598
675
|
}
|
|
599
676
|
|