@gemboss/website-ui 0.7.0 → 0.9.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 +18 -3
- package/CHANGELOG.md +129 -0
- package/README.md +8 -8
- package/bin/lib/check.mjs +72 -1
- package/css/effects.css +71 -0
- package/css/header.css +176 -0
- package/css/tokens.css +7 -0
- package/demo/header.html +356 -0
- package/demo/index.html +2 -0
- package/demo/spotlight.html +291 -0
- package/docs/components.md +116 -4
- package/docs/markup-contract.md +135 -5
- package/docs/theming.md +9 -2
- package/js/website.js +168 -4
- package/js/website.mjs +208 -4
- package/package.json +1 -1
- package/skill/SKILL.md +6 -0
- package/system.json +13 -1
- package/templates/header.html +34 -6
- package/website.css +256 -1
- package/website.min.css +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,10 +10,11 @@ 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, the footer's folds; no dependencies; `window.gwWebsite.init(root)` |
|
|
13
|
+
| `js/website.js` | header, mega menu, mobile menu (and its drill-down), language switcher, theme toggle, scroll reveal, segmented control, busy buttons, an article's contents and its tables' scroll box, the footer's folds, the spotlight; 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/header.html` | the header of every site, with its two options (the drill-down mobile menu, the language switcher): fill it once, in the site's shared partial |
|
|
17
18
|
| `templates/footer.html` | the footer of every site (the footer of gemboss.ai): fill it once, in the site's shared partial |
|
|
18
19
|
| `templates/sections/*.html` | ready sections: hero, features, split, showcase, logos, stats, steps, pricing, FAQ, CTA, prose, article, post list, a short form, testimonials… |
|
|
19
20
|
| `demo/index.html` | every component on one page: open it to see what exists before you build anything |
|
|
@@ -21,6 +22,8 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
21
22
|
| `demo/article.html`, `demo/blog.html` | a whole blog post (breadcrumbs, meta, contents, prose, callouts, FAQ) and the blog index (post list, pagination) |
|
|
22
23
|
| `demo/forms.html` | fields in every state, choices, the footer newsletter, a short form, stars, the testimonial wall and the rating badge |
|
|
23
24
|
| `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 |
|
|
25
|
+
| `demo/header.html` | the header of gemboss.ai: the mobile menu that drills down with its bar of actions, and the language switcher, and the same for a GemPages-style brand |
|
|
26
|
+
| `demo/spotlight.html` | the spotlight on plan cards, link cards, the feature card and a night band's button, and the same for a GemPages-style brand |
|
|
24
27
|
| `docs/components.md` | every class, what it is for, its variants |
|
|
25
28
|
| `docs/theming.md` | what a brand other than GemBoss chooses (accent, wash, logo, three named profiles), and what it must not change |
|
|
26
29
|
|
|
@@ -101,6 +104,11 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
101
104
|
`<summary>`: rows that fold on phones, open columns from 768px, no script needed. A link that leaves the site gets the
|
|
102
105
|
↗ (`.gw-footer__ext`); one with `target="_blank"` also `<span class="gw-visually-hidden"> (opens in a new tab)</span>`.
|
|
103
106
|
The wordmark is `aria-hidden="true"`.
|
|
107
|
+
- **Header** (0.8.0): `templates/header.html`. Two options. The mobile menu drills down (`data-gw-menu="drill"`): each
|
|
108
|
+
group carries its head, `<button class="gw-mobile-menu__back">` and `<h2 class="gw-mobile-menu__title" tabindex="-1">`
|
|
109
|
+
in the page's language, and the actions sit in a bar at the bottom. The language switcher (`.gw-lang`, kept `hidden`,
|
|
110
|
+
its list empty) lists the page's `<link rel="alternate" hreflang>` from `<head>` and shows itself when there is
|
|
111
|
+
another language; its word for "Language" goes in `data-gw-label-language`.
|
|
104
112
|
- **Ratings** (0.6.0): stars `.gw-stars` (`role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"`), the
|
|
105
113
|
wall `.gw-testimonials` of `.gw-card.gw-testimonial`, the badge `.gw-rating-badge` (`templates/sections/testimonials.html`).
|
|
106
114
|
- **One choice out of two to four** (Monthly / Yearly) is `.gw-segmented`, a radio group; what each option shows
|
|
@@ -115,6 +123,9 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
115
123
|
else on white or `--gw-bg-soft`.
|
|
116
124
|
- **Motion**: only `transform` and `opacity`, the kit's ease, nothing moves under `prefers-reduced-motion`. Scroll
|
|
117
125
|
reveal is `.gw-reveal` (the JS adds `.is-in`, and a failsafe shows it anyway).
|
|
126
|
+
- **Spotlight** (0.9.0): `data-gw-spot` only on a link, a button or a card with exactly one action (a plan, a link
|
|
127
|
+
card, the mega menu's feature card, a night band's primary button). The kit draws the light in the accent; never on a
|
|
128
|
+
heading or text, never in another colour, never your own script.
|
|
118
129
|
|
|
119
130
|
## 4. Don't (each of these has happened, and each one made a page look cheap or broke it)
|
|
120
131
|
|
|
@@ -147,6 +158,8 @@ Files you will use, all inside `node_modules/@gemboss/website-ui/`:
|
|
|
147
158
|
- Don't make a callout whose kind only its colour tells: `.gw-callout` starts with its `.gw-callout__label` in words.
|
|
148
159
|
- Don't type separators (›, /, >) between breadcrumbs, and don't build a contents list whose links point at ids the
|
|
149
160
|
page does not have: build it from the article's own h2s.
|
|
161
|
+
- Don't build your own language menu or phone menu, don't type the switcher's links (it reads `<link rel="alternate"
|
|
162
|
+
hreflang>`), and don't show it on a page that has one language. Don't leave Back in English on a translated page.
|
|
150
163
|
- Don't add a footer part the template does not have, and don't build footer columns that fold with your own script:
|
|
151
164
|
fill `templates/footer.html` (its family rows take another brand's siblings: GemBoss, GemX, GemUpsells on a
|
|
152
165
|
GemPages site). Don't put the assistants' logos in the Ask AI row.
|
|
@@ -262,8 +275,10 @@ a waiting button not marked with `aria-busy`, a segmented control without its ra
|
|
|
262
275
|
with numbers and no caption, an unknown brand profile or one set off `<html>`, `.gw-header--float` off the header,
|
|
263
276
|
words on the accent button below 4.5:1, breadcrumbs without their markup, a contents link to an id that is not on
|
|
264
277
|
the page, a callout without its label, stars without their value in words, a rating or a testimonial without a link to
|
|
265
|
-
its source, a footer fold without its heading in the summary or a wordmark a screen reader reads,
|
|
266
|
-
|
|
278
|
+
its source, a footer fold without its heading in the summary or a wordmark a screen reader reads, a drill-down level
|
|
279
|
+
without its Back or its title, a language switcher shown by the markup or a translation without `hreflang`, a
|
|
280
|
+
spotlight on a heading or on something that is not one action, and patterns that repeat across pages (likely gaps). It
|
|
281
|
+
does not replace looking at the page at 320px and 1366px.
|
|
267
282
|
|
|
268
283
|
## 11. Upgrading the kit
|
|
269
284
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,135 @@ 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.9.0 (2026-10-25)
|
|
8
|
+
|
|
9
|
+
The spotlight, a named hover effect for the things you click (Chris, 25/10/2026, chosen over a heading that glows
|
|
10
|
+
under the pointer): a soft light in the brand's accent follows the pointer over a plan card, a link card, the mega menu's
|
|
11
|
+
feature card or the primary button of a night band. Opt-in per element: a page without `data-gw-spot` looks exactly
|
|
12
|
+
as before. The demo index (without its new link), `brand-profiles.html`, `article.html`, `blog.html`, `forms.html`,
|
|
13
|
+
`footer.html`, `header.html` and `theme-example.html` are pixel-identical to 0.8.0 at 390 and 1366px, light and dark.
|
|
14
|
+
|
|
15
|
+
Closes #74
|
|
16
|
+
|
|
17
|
+
- `data-gw-spot` on an `<a href>`, a `<button>`, or a card (`<div>`, `<article>`, `<li>`) that holds exactly one of
|
|
18
|
+
them: a `.gw-plan` with its one `.gw-btn`, a `.gw-card--link`, the `.gw-mega__feature`, the `.gw-btn` of a
|
|
19
|
+
`.gw-band--night`. `data-gw-spot="<selector>"` lights the first element the selector names inside it instead (the art
|
|
20
|
+
of a card). Never on a heading or running text: the script refuses a heading, and the kit still forbids gradient or
|
|
21
|
+
accent-coloured words in a heading, and lights of more than one colour.
|
|
22
|
+
- The light: a radial gradient on the `::before` of the element that carries it, under its words, in its own corners
|
|
23
|
+
(`border-radius: inherit`, so both radius profiles hold), no layout change. One colour, `--gw-accent` mixed with
|
|
24
|
+
`color-mix()`: 22% on a card, 38% on a button, 40% on the Stage's night ground; the `-dark` family in dark mode and in
|
|
25
|
+
night bands, the brand's own accent everywhere (a GemPages-style page lights blue). Its radius, a registered
|
|
26
|
+
`@property --gw-spot-r` that inherits, grows to 260px (120px on a button) in 0.55s with `cubic-bezier(.16,1,.3,1)`:
|
|
27
|
+
the one animated property that is neither transform nor opacity (`system.json` `motion` says so).
|
|
28
|
+
- The motion, as Chris asked after the prototype: the light starts where the pointer comes in, then trails it, going
|
|
29
|
+
18% of the way that is left on each frame (one `requestAnimationFrame` loop per element, stopped within 0.3px), so a
|
|
30
|
+
jump of the pointer is followed over several frames, never snapped.
|
|
31
|
+
- Pointer devices only: the CSS is all inside `(hover: hover) and (pointer: fine)`, and `website.js` adds no listener
|
|
32
|
+
without a fine pointer (again on every change). Keyboard focus shows the same light, centred and as large, on the
|
|
33
|
+
focused host or on a card whose one action has the focus (`:has(:focus-visible)`, in a rule of its own). Reduced
|
|
34
|
+
motion: no listener, a still light in the middle that appears without a transition.
|
|
35
|
+
- Fallback: without `@property` or `color-mix()` the script wires nothing, so nothing is drawn, not half a light.
|
|
36
|
+
- Tokens (`system.json` `motion.spot`): `--gw-spot-radius`, `--gw-spot-radius-action`, `--gw-spot-strength`,
|
|
37
|
+
`--gw-spot-strength-action`, `--gw-spot-strength-stage`, `--gw-spot-ms`, `--gw-spot-ease`; the trail and the settle
|
|
38
|
+
distance are the script's, held equal to `motion.spot.trail` and `.settle` by the tests.
|
|
39
|
+
- CSS: `css/effects.css`, last in `website.css`. Every rule of it selects a wired spot (`data-gw-ready~="spot"`) or the
|
|
40
|
+
element carrying its light (`.gw-spot`, set by the script), so no page without the attribute can change.
|
|
41
|
+
- `website.js`: the `spot` part in the `init(root)`/`destroy(root)` registry (`data-gw-ready="spot"`), a
|
|
42
|
+
`pointerenter` and a `pointermove` listener per element, `--gw-spot-x` and `--gw-spot-y` on the element that carries
|
|
43
|
+
the light, nothing else and no event. `destroy` takes the listeners, the frame, the class and the place back.
|
|
44
|
+
- `check`: `spot-target` (an error): `data-gw-spot` on a heading, with a selector that names a heading, on running text
|
|
45
|
+
or a section, on an `<a>` without `href`, or on a card with no link or button or with more than one.
|
|
46
|
+
- Words under the light: while a thing is lit (hover, keyboard focus), its muted words read `--gw-ink-2`: the host sets
|
|
47
|
+
`--gw-muted: var(--gw-ink-2)`, so every part that uses the muted token follows, with no class of its own. Muted would
|
|
48
|
+
dip to 4.0:1 at the light's middle on a white card (5.3:1 at rest); ink-2 there is 8.2:1 measured in Chromium (7.6:1
|
|
49
|
+
in dark), and 4.5:1 or more on every ground by the tokens (a card, a soft card, a night band). The words turn ink-2 at
|
|
50
|
+
once and turn back only once the light has gone (a colour that waits 0.55s, no movement; at once under reduced
|
|
51
|
+
motion). Ink keeps 10:1 or more and a button's words 4.5:1 in every profile, light and dark.
|
|
52
|
+
- Demo: `demo/spotlight.html`, plan cards, link cards, the feature card (also in the Product menu) and the night band's
|
|
53
|
+
CTA, with a switch to a GemPages-style brand (`data-gw-action="accent"`, `data-gw-radius="soft"`, `#3c67ff`), linked
|
|
54
|
+
from `demo/index.html`. No other demo page opts in.
|
|
55
|
+
- Budget: `js/website.js` is 23.1 KB unminified, 7.1 KB gzipped (0.8.0: 20.8 KB, 6.4 KB); the test's budget is now
|
|
56
|
+
24 KB and 8 KB. `website.min.css` is 73.8 KB, 12.7 KB gzipped (0.8.0: 71.8 KB, 12.3 KB).
|
|
57
|
+
- Tests: `test/run.mjs` holds the CSS (every rule opt-in, fine pointer only, the accent only, the reduced-motion and
|
|
58
|
+
focus rules, the tokens), the contrast under the light, the script in its fake DOM (entry point, trailing frame by
|
|
59
|
+
frame, settling, touch, inner element, init twice, destroy, media changes, no `@property`, no `color-mix()`) and the
|
|
60
|
+
check rule; `test/browser.mjs` drives `demo/spotlight.html` in Chromium (the radius read on the `::before` while
|
|
61
|
+
hovering, the light following a jump over several frames, no layout shift, the Stage and the night band, Tab to a
|
|
62
|
+
card and to a plan's button, reduced motion, a touch screen with no listener counted by DevTools and the same
|
|
63
|
+
pixels after a tap, no `@property` or `color-mix()`, init twice and destroy counted by DevTools, a heading refused
|
|
64
|
+
by the script and flagged by `check`, the GemPages-style accent and corners, light and dark, and a muted line at the
|
|
65
|
+
light's middle measured on the drawn pixel: 4.5:1 or more, light and dark, ink-2 while the light fades, muted after).
|
|
66
|
+
|
|
67
|
+
## 0.8.0 (2026-10-25)
|
|
68
|
+
|
|
69
|
+
The header of gemboss.ai on a phone, so the site's header can move onto the kit: a mobile menu that drills down into
|
|
70
|
+
each section with its actions in a bar at the bottom, and the language switcher. Both are options: a header without them
|
|
71
|
+
looks and works exactly as before. The demo index (without its new link), `brand-profiles.html`, `article.html`,
|
|
72
|
+
`blog.html`, `forms.html`, `footer.html` and `theme-example.html` are pixel-identical to 0.7.0 at 390 and 1366px, light
|
|
73
|
+
and dark.
|
|
74
|
+
|
|
75
|
+
- Drill-down mobile menu, opt-in: `data-gw-menu="drill"` on `.gw-mobile-menu`, as gemboss.ai's phone menu (v4, approved
|
|
76
|
+
15/10/2026). The markup is the 0.7.0 menu, plus a head in each group: `<div class="gw-mobile-menu__head">` holding
|
|
77
|
+
`<button class="gw-mobile-menu__back" type="button">Back</button>` and `<h2 class="gw-mobile-menu__title"
|
|
78
|
+
tabindex="-1">`, in the page's language. The top level lists the sections in 64px rows (24px words, a chevron that
|
|
79
|
+
points right); a press on one shows its level alone, with Back on the left and its title in the middle; a level may
|
|
80
|
+
carry headings over runs of links (`.gw-mobile-menu__heading`), a 32px icon tile and a line under a link's name
|
|
81
|
+
(`.gw-mobile-menu__icon`, `<b>`, `<i>`), and a last link to the section's own page (`.gw-mobile-menu__all`, in the
|
|
82
|
+
accent, with its arrow). The actions sit in a bar that stays at the bottom of the sheet while the list scrolls: what is
|
|
83
|
+
not a button on its first line (the language on the left, Sign in on the right), the two buttons side by side under it
|
|
84
|
+
(`gw-btn--lg`: 48px), one hairline above, the safe area below.
|
|
85
|
+
Closes gap #41
|
|
86
|
+
- Its script, wired like every other part (one record, `init` once per element, `destroy` takes it back): a press on a
|
|
87
|
+
section (tap, click, Enter, Space) opens its level and moves the focus to the level's title; Back shows the top level
|
|
88
|
+
and puts the focus back on the section; Escape closes the sheet from anywhere in it, back on the top level, the focus on
|
|
89
|
+
the toggle; while it is open, Tab and Shift+Tab go round the toggle and the sheet (the focus trap); the page under it
|
|
90
|
+
is locked (as in 0.7.0); the sheet starts where the header ends (measured as it opens, `--gw-menu-top`) and the header
|
|
91
|
+
stays in place, so an announcement bar above it no longer jumps away. A level slides in from the right (0.24s, the
|
|
92
|
+
kit's ease, transform and opacity), the sections slide back from the left; under `prefers-reduced-motion` they switch
|
|
93
|
+
at once. Without the script (or before `init`) the groups are the 0.7.0 `<details>` that open in place. `.is-drill`
|
|
94
|
+
(wired), `.is-drilled` (a level in view) and `.is-back` (the slide back) are the script's states.
|
|
95
|
+
- `gw:menu`, a new event on the menu (`.gw-mobile-menu`), `detail: { open, level }`: `level` is the title of the level
|
|
96
|
+
in view, `''` for the top level. It fires when a menu opens or closes (every mobile menu, the 0.7.0 one too) and when
|
|
97
|
+
a drill-down menu moves to a level or back; closing from a level says only `{ open: false, level: '' }`.
|
|
98
|
+
- Language switcher `.gw-lang` (`data-gw-lang`, `hidden` in the markup): a ghost button with the globe and the current
|
|
99
|
+
language's short code (`.gw-lang__btn`, `.gw-lang__short`), and the list of the other languages (`.gw-lang__menu`), a
|
|
100
|
+
small popover card with the card radius of the site's profile. `website.js` fills the list from the page's own
|
|
101
|
+
`<link rel="alternate" hreflang href>` (not `x-default`): the links search engines read, so the switcher offers
|
|
102
|
+
exactly the translations they are told about, and a page with no other language keeps the switcher hidden (gemboss.ai
|
|
103
|
+
today, while `liveLangs` is empty). The current language is the one whose `hreflang` is `<html lang>` (else the same
|
|
104
|
+
base language: `zh` for `zh-Hans`). Each other one is a real link (`.gw-lang__link`, with `hreflang` and `lang`), named
|
|
105
|
+
in its own language: the `<link>`'s `title`, else the kit's name (English, Tiếng Việt, 中文), else the browser's. A link
|
|
106
|
+
to the host of `<link rel="canonical">` keeps only its path, so a preview deployment stays on itself. Every `init`
|
|
107
|
+
reads the links again (a route change). Keyboard: the button opens and closes it; ArrowDown and ArrowUp on it open the
|
|
108
|
+
list on its first or last link, move in it (wrapping), Home and End; Escape closes it with the focus back on the
|
|
109
|
+
button, and goes no further; focus leaving it or a click outside closes it. Its words come from the markup:
|
|
110
|
+
`data-gw-label-language` (else the button's `aria-label`, else "Language") names the button "Language: English" and
|
|
111
|
+
the list "Language". In the header's actions it shows from 1080px; below, a second one sits in the mobile menu's bar
|
|
112
|
+
and its list opens upward. No new dependency: it replaces loading `@gemboss/ui`'s plain switcher next to the kit.
|
|
113
|
+
Closes gap #29
|
|
114
|
+
- Templates: `templates/header.html` shows both options, with comments: the drill-down menu (and how to leave it out),
|
|
115
|
+
the switcher in the actions and in the bar. `templates/page.html` keeps the 0.7.0 menu.
|
|
116
|
+
- Demo: `demo/header.html`, the header of gemboss.ai with its words, sections, links and icons, the drill-down menu, an
|
|
117
|
+
announcement bar, and the switcher with three languages (en, vi, zh-Hans) whose alternates point at the demo itself
|
|
118
|
+
(`?hl=`), with a switch to the same header for a GemPages-style brand (soft corners, the accent button, the floating
|
|
119
|
+
header from 1080px, GemPages' blue). Linked from `demo/index.html`.
|
|
120
|
+
- `check`: `menu-markup` (an error): in a `data-gw-menu="drill"` menu, a group that is not a `<details>`, has no
|
|
121
|
+
`<summary>`, no back button, no title, or a title without `tabindex="-1"`; `data-gw-menu` with another value.
|
|
122
|
+
`lang-markup` (an error): a `.gw-lang` not `hidden` in the markup, without `data-gw-lang`, without its button, a button
|
|
123
|
+
naming no list inside it or without `aria-expanded`, links typed into its list; and, on a page with a switcher, a
|
|
124
|
+
translation's `<link rel="alternate">` (no `type`) without `hreflang` or `href`.
|
|
125
|
+
- `AGENTS.md`, the skill and the docs (`components.md`, `markup-contract.md`): the two options, their markup, the
|
|
126
|
+
script's behaviour and `gw:menu`.
|
|
127
|
+
- Budget: `website.min.css` is 71.8 KB, 12.2 KB gzipped (0.7.0: 65.8 KB, 11.4 KB). `js/website.js` is 20.8 KB
|
|
128
|
+
unminified, 6.4 KB gzipped (0.7.0: 14.5 KB, 4.7 KB): the test's budget is now 22 KB and 7 KB.
|
|
129
|
+
- Tests: `test/run.mjs` holds the 0.7.0 rules of `header.css` byte for byte and every 0.8.0 rule to a 0.8.0 part, and
|
|
130
|
+
covers the drill-down menu, the trap, `gw:menu` and the switcher in its fake DOM (which now sets text, has
|
|
131
|
+
`getClientRects`, style properties, `URL` and `Intl`) and the two rules; `test/browser.mjs` drives `demo/header.html`
|
|
132
|
+
in Chromium (by touch and by keyboard, the focus, Escape, the trap, the scroll lock, the bar at the bottom of a long
|
|
133
|
+
level, reduced motion, without the script, the switcher shown and hidden by the head's links, its keys and a real
|
|
134
|
+
switch to the Vietnamese page, init twice, destroy then init, 320 and 390px light and dark, the GemPages-style switch).
|
|
135
|
+
|
|
7
136
|
## 0.7.0 (2026-10-25)
|
|
8
137
|
|
|
9
138
|
The footer as gemboss.ai has it today (gap #71): the kit's footer, built on 24/10 from gemboss.ai, had fallen behind
|
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, cột footer gập/mở theo khổ màn
|
|
15
|
+
| `website.css`, `website.min.css` | toàn bộ kit trong một tệp (khoảng 107 KB, 74 KB bản min, 12,7 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 (tuỳ chọn đi sâu từng mục kèm thanh nút dính đáy, như gemboss.ai) + nút đổi ngôn ngữ, 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), đốm sáng theo con trỏ trên thứ bấm được (`data-gw-spot`, bật từng chỗ), 16 section, 3 hồ sơ thương hiệu |
|
|
16
|
+
| `js/website.js`, `js/website.mjs` | mega menu, menu điện thoại (kể cả bản đi sâu từng mục: Back, giữ focus trong menu, sự kiện `gw:menu`), nút đổi ngôn ngữ (đọc `<link rel="alternate" hreflang>` của trang, ẩn khi trang chỉ có một ngôn ngữ), đổ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, đốm sáng theo con trỏ; không phụ thuộc thư viện nào, khoảng 7,1 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, 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 |
|
|
18
|
+
| `templates/` | khung một trang, header (`header.html`: hai tuỳ chọn, menu điện thoại đi sâu và nút ngôn ngữ), 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; `header.html` là header của gemboss.ai trên điện thoại (menu đi sâu từng mục, thanh nút dính đáy, nút đổi ngôn ngữ với ba ngôn ngữ), cũng có bản kiểu GemPages; `spotlight.html` là đốm sáng trên thẻ gói giá, thẻ link, thẻ nổi bật của mega menu và nút trong dải tối, cũng có 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.9.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.9.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, cột footer gập thiếu tiêu đề trong summary, wordmark không ẩn với trình đọc màn hình
|
|
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, mục của menu đi sâu thiếu nút Back hay tiêu đề, nút ngôn ngữ không ẩn sẵn hay bản dịch thiếu `hreflang`… 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), form, sao, review
|
|
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, footer (gập, Tab, nút sáng/tối) và header (menu đi sâu bằng chạm và bàn phím, giữ focus, nút ngôn ngữ) (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
|
@@ -149,6 +149,21 @@ export const RULES = {
|
|
|
149
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
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
151
|
},
|
|
152
|
+
"menu-markup": {
|
|
153
|
+
level: "error", title: "Drill-down menu level without its head",
|
|
154
|
+
why: "In a mobile menu with data-gw-menu=\"drill\" each <details class=\"gw-mobile-menu__group\"> is a level the reader opens on its own, from its <summary>. Its head is the only way back (a <button class=\"gw-mobile-menu__back\">) and its title (.gw-mobile-menu__title with tabindex=\"-1\") is where website.js puts the focus. Without them a phone visitor is stuck in the level, or a keyboard and a screen reader lose their place. data-gw-menu has one value: drill.",
|
|
155
|
+
fix: "Copy the group from templates/header.html: <details class=\"gw-mobile-menu__group\"><summary>Product</summary><div class=\"gw-mobile-menu__head\"><button class=\"gw-mobile-menu__back\" type=\"button\">Back</button><h2 class=\"gw-mobile-menu__title\" tabindex=\"-1\">Product</h2></div><ul class=\"gw-mobile-menu__list\">…</ul></details>, with Back in the page's language.",
|
|
156
|
+
},
|
|
157
|
+
"lang-markup": {
|
|
158
|
+
level: "error", title: "Language switcher without its markup",
|
|
159
|
+
why: "website.js fills .gw-lang from the page's <link rel=\"alternate\" hreflang href> and shows it only when there is another language: it starts hidden, its button (.gw-lang__btn) names its list with aria-controls and says aria-expanded, and the list is empty in the markup. A switcher shown by the markup is an empty button on a page with one language; links typed into the list are replaced; a translation's <link rel=\"alternate\"> without hreflang is one the switcher (and search engines) cannot list.",
|
|
160
|
+
fix: "Copy the switcher from templates/header.html: <div class=\"gw-lang\" data-gw-lang hidden><button class=\"gw-lang__btn\" type=\"button\" aria-expanded=\"false\" aria-controls=\"gw-lang-menu\">…</button><ul class=\"gw-lang__menu\" id=\"gw-lang-menu\" hidden></ul></div>, and give each translation <link rel=\"alternate\" hreflang=\"vi\" href=\"…\"> in <head>.",
|
|
161
|
+
},
|
|
162
|
+
"spot-target": {
|
|
163
|
+
level: "error", title: "Spotlight on something that is not one action",
|
|
164
|
+
why: "data-gw-spot lights what you can click: the light says \"this goes somewhere\". On a heading or running text it promises a link that is not there (and a heading stays ink: no gradient, no accent words, no light); on a box with no action, or several, the light cannot say which one it means.",
|
|
165
|
+
fix: "Put data-gw-spot on the <a href> or the <button> itself, or on a card (a <div>, <article> or <li>) that holds exactly one of them, such as a .gw-plan with its one .gw-btn. Never on a heading, a paragraph or a section, and never with a selector that names a heading (docs/components.md, Spotlight).",
|
|
166
|
+
},
|
|
152
167
|
"uppercase-text": {
|
|
153
168
|
level: "warning", title: "Uppercase text",
|
|
154
169
|
why: "Headings, buttons, labels and menu items are sentence case.",
|
|
@@ -499,6 +514,9 @@ function elementEnd(text, start, tag) {
|
|
|
499
514
|
}
|
|
500
515
|
return -1;
|
|
501
516
|
}
|
|
517
|
+
// The spotlight (0.9.0): a card lit as a whole is one of these, holding exactly one link or button.
|
|
518
|
+
const SPOT_BOX = new Set(["div", "article", "li"]);
|
|
519
|
+
const HEADING_SEL = /(?:^|[\s>+~,(])h[1-6](?![\w-])/i;
|
|
502
520
|
const LOADING_CLASS = /^(?:is-)?(?:loading|busy)$|--(?:loading|busy)$/;
|
|
503
521
|
const SPINNER_CLASS = /\bclass\s*=\s*["'][^"']*(?:spinner|loader|loading)/i;
|
|
504
522
|
|
|
@@ -552,7 +570,8 @@ function analyzeHtml(file, text, out) {
|
|
|
552
570
|
}
|
|
553
571
|
|
|
554
572
|
const classes = new Set();
|
|
555
|
-
const ids = new Set(), tocs = [];
|
|
573
|
+
const ids = new Set(), tocs = [], drills = [], alts = [];
|
|
574
|
+
let langs = 0;
|
|
556
575
|
let h1 = 0;
|
|
557
576
|
for (const m of scanText.matchAll(TAG_RE)) {
|
|
558
577
|
const tag = m[1].toLowerCase();
|
|
@@ -647,6 +666,51 @@ function analyzeHtml(file, text, out) {
|
|
|
647
666
|
].filter(Boolean);
|
|
648
667
|
if (missing.length) add("footer-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
649
668
|
}
|
|
669
|
+
if (cls.includes("gw-mobile-menu") && "data-gw-menu" in a) {
|
|
670
|
+
if (a["data-gw-menu"] !== "drill") add("menu-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: data-gw-menu="drill" (the one option; leave the attribute out for the 0.7.0 menu)`));
|
|
671
|
+
else drills.push([m.index, elementEnd(scanText, m.index, tag)]);
|
|
672
|
+
}
|
|
673
|
+
if (cls.includes("gw-mobile-menu__group") && drills.some(([s0, e0]) => m.index > s0 && (e0 < 0 || m.index < e0))) {
|
|
674
|
+
const body = inner();
|
|
675
|
+
const t = /<[a-z][\w-]*\b([^>]*\bclass\s*=\s*["'][^"']*\bgw-mobile-menu__title\b[^>]*)>/i.exec(body);
|
|
676
|
+
const missing = [
|
|
677
|
+
tag !== "details" && `a <details> (the class is on <${tag}>)`,
|
|
678
|
+
!/<summary\b/i.test(body) && "a <summary> (the row that opens the level)",
|
|
679
|
+
!/<button\b[^>]*\bclass\s*=\s*["'][^"']*\bgw-mobile-menu__back\b/i.test(body) && 'a back button (<button class="gw-mobile-menu__back" type="button">)',
|
|
680
|
+
!t && 'the level\'s title (<h2 class="gw-mobile-menu__title" tabindex="-1">)',
|
|
681
|
+
t && parseAttrs(t[1]).tabindex !== "-1" && 'tabindex="-1" on the title (the script puts the focus there)',
|
|
682
|
+
].filter(Boolean);
|
|
683
|
+
if (missing.length) add("menu-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
684
|
+
}
|
|
685
|
+
if (cls.includes("gw-lang")) {
|
|
686
|
+
langs++;
|
|
687
|
+
const body = inner();
|
|
688
|
+
const b = /<button\b([^>]*\bclass\s*=\s*["'][^"']*\bgw-lang__btn\b[^>]*)>/i.exec(body);
|
|
689
|
+
const ba = b ? parseAttrs(b[1]) : {};
|
|
690
|
+
const list = ba["aria-controls"] && new RegExp(`<([a-z]+)\\b[^>]*\\bid\\s*=\\s*["']${ba["aria-controls"].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'][^>]*>`, "i").exec(body);
|
|
691
|
+
const missing = [
|
|
692
|
+
!("hidden" in a) && "hidden (the script shows it when the page has another language)",
|
|
693
|
+
!("data-gw-lang" in a) && "data-gw-lang",
|
|
694
|
+
!b && 'a <button class="gw-lang__btn" type="button">',
|
|
695
|
+
b && !ba["aria-controls"] && "aria-controls on the button, naming its list",
|
|
696
|
+
b && ba["aria-controls"] && !list && `the list the button names (id="${ba["aria-controls"]}") inside the switcher`,
|
|
697
|
+
b && !("aria-expanded" in ba) && 'aria-expanded="false" on the button',
|
|
698
|
+
/<a\b/i.test(body) && "an empty list (the script writes the links from <link rel=\"alternate\" hreflang>)",
|
|
699
|
+
].filter(Boolean);
|
|
700
|
+
if (missing.length) add("lang-markup", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} missing: ${missing.join(", ")}`));
|
|
701
|
+
}
|
|
702
|
+
if (tag === "link" && /(?:^|\s)alternate(?:\s|$)/i.test(a.rel || "") && !a.type) alts.push({ a, where: where() });
|
|
703
|
+
if ("data-gw-spot" in a) {
|
|
704
|
+
const sel = (a["data-gw-spot"] || "").trim();
|
|
705
|
+
const acts = (inner().match(/<a\b[^>]*\bhref\s*=|<button\b/gi) || []).length;
|
|
706
|
+
const why = /^h[1-6]$/.test(tag) ? `on a heading (<${tag}>)`
|
|
707
|
+
: HEADING_SEL.test(sel) ? `its selector names a heading (data-gw-spot="${sel}")`
|
|
708
|
+
: (tag === "a" && "href" in a) || tag === "button" ? ""
|
|
709
|
+
: tag === "a" ? "on an <a> without href"
|
|
710
|
+
: !SPOT_BOX.has(tag) ? `on a <${tag}>, which is not a link, a button or a card`
|
|
711
|
+
: acts !== 1 ? `on a <${tag}> that holds ${acts} links or buttons, not exactly one` : "";
|
|
712
|
+
if (why) add("spot-target", where(`${m[0].replace(/\s+/g, " ").slice(0, 80)} ${why}`));
|
|
713
|
+
}
|
|
650
714
|
if (cls.includes("gw-rating-badge") || cls.includes("gw-testimonial")) {
|
|
651
715
|
const linked = (tag === "a" && "href" in a) || /<a\b[^>]*\bhref\s*=/i.test(inner());
|
|
652
716
|
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)}`));
|
|
@@ -688,6 +752,13 @@ function analyzeHtml(file, text, out) {
|
|
|
688
752
|
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}"` });
|
|
689
753
|
}
|
|
690
754
|
}
|
|
755
|
+
// A page with a switcher: each translation it links in <head> needs its hreflang and its href.
|
|
756
|
+
if (langs) {
|
|
757
|
+
for (const x of alts) {
|
|
758
|
+
const missing = [!(x.a.hreflang || "").trim() && "hreflang (the switcher lists a translation by it)", !(x.a.href || "").trim() && "href"].filter(Boolean);
|
|
759
|
+
if (missing.length) add("lang-markup", { ...x.where, excerpt: `${x.where.excerpt} missing: ${missing.join(", ")}` });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
691
762
|
out.pageClasses.push({ file: relFile, classes });
|
|
692
763
|
}
|
|
693
764
|
|
package/css/effects.css
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* effects: the spotlight (0.9.0, Chris 25/10/2026, issue #74), a named hover effect for the things you click.
|
|
2
|
+
data-gw-spot on an <a href>, a <button>, or a card that holds exactly one of them (a plan card, the mega menu's
|
|
3
|
+
feature card, the primary button of a night band, a .gw-card--link): a soft light in the brand's accent follows the
|
|
4
|
+
pointer over it. Opt-in per element: a page without the attribute does not change.
|
|
5
|
+
website.js wires the host (data-gw-ready="spot") and puts .gw-spot on the element that carries the light: the host,
|
|
6
|
+
or the one its data-gw-spot="<selector>" names inside it. It writes the pointer's place there (--gw-spot-x,
|
|
7
|
+
--gw-spot-y, trailing it a little every frame); this file draws the light on its ::before, under its words, in its
|
|
8
|
+
own corners (border-radius: inherit, so the radius profile holds), and grows the light's radius on hover.
|
|
9
|
+
- Pointer devices only: nothing here applies without (hover: hover) and (pointer: fine).
|
|
10
|
+
- Keyboard focus (on the host, or on the one action of a card) shows the same light, centred.
|
|
11
|
+
- Reduced motion: the light stands still, centred, and appears without a transition.
|
|
12
|
+
- Words under the light: while a thing is lit, its muted words read --gw-ink-2 (the token, set as --gw-muted on the
|
|
13
|
+
host), so they keep 4.5:1 at the light's middle; they turn back once the light has gone. A colour, no movement.
|
|
14
|
+
- Without @property or color-mix(), website.js puts .gw-spot nowhere: no light at all, nothing half drawn.
|
|
15
|
+
- One colour, the accent family, as the page has it (the -dark family in dark mode and in night bands). Never on a
|
|
16
|
+
heading or running text: a heading stays ink, with no gradient and no accent words (check: spot-target).
|
|
17
|
+
The radius is the one animated property that is neither transform nor opacity: it repaints one layer of one
|
|
18
|
+
element. Registered, so it can transition; inherited, so the ::before (and an inner element) reads the host's. */
|
|
19
|
+
@property --gw-spot-r {
|
|
20
|
+
syntax: "<length>";
|
|
21
|
+
inherits: true;
|
|
22
|
+
initial-value: 0px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (hover: hover) and (pointer: fine) {
|
|
26
|
+
/* the host: no light at rest (a lit card does not light a lit button inside it), the light's size on hover and focus */
|
|
27
|
+
[data-gw-ready~="spot"] { --gw-spot-r: 0px; }
|
|
28
|
+
[data-gw-ready~="spot"]:is(:hover, :focus-visible) { --gw-spot-r: var(--gw-spot-radius); --gw-muted: var(--gw-ink-2); }
|
|
29
|
+
/* a card whose one action has the keyboard focus; a rule of its own, so a browser without :has() drops only it */
|
|
30
|
+
[data-gw-ready~="spot"]:has(:focus-visible) { --gw-spot-r: var(--gw-spot-radius); --gw-muted: var(--gw-ink-2); }
|
|
31
|
+
.gw-btn[data-gw-ready~="spot"]:is(:hover, :focus-visible) { --gw-spot-r: var(--gw-spot-radius-action); }
|
|
32
|
+
|
|
33
|
+
/* the element that carries the light: positioned for its layer, isolated so the layer sits under its words.
|
|
34
|
+
:where(), so a site's own position for an inner element wins. */
|
|
35
|
+
:where(.gw-spot) { position: relative; isolation: isolate; }
|
|
36
|
+
/* the light. Not on a busy button: its ::before is the ring. */
|
|
37
|
+
.gw-spot:not([aria-busy="true"])::before {
|
|
38
|
+
--gw-spot-mix: var(--gw-spot-strength);
|
|
39
|
+
content: "";
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset: 0;
|
|
42
|
+
z-index: -1;
|
|
43
|
+
border-radius: inherit;
|
|
44
|
+
background: radial-gradient(circle var(--gw-spot-r) at var(--gw-spot-x, 50%) var(--gw-spot-y, 50%), color-mix(in srgb, var(--gw-accent) var(--gw-spot-mix), transparent), transparent 70%);
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
transition: --gw-spot-r var(--gw-spot-ms) var(--gw-spot-ease);
|
|
47
|
+
}
|
|
48
|
+
/* a button is small and filled: a stronger light. Its words keep their contrast: the light is the accent over the
|
|
49
|
+
button's fill, so under the accent action profile it lies between the accent and the accent-strong, the two fills
|
|
50
|
+
the build checks the words on. */
|
|
51
|
+
.gw-btn.gw-spot:not([aria-busy="true"])::before { --gw-spot-mix: var(--gw-spot-strength-action); }
|
|
52
|
+
/* the mega menu's feature card is the Stage, a night ground: a stronger light, or it would not show */
|
|
53
|
+
.gw-mega__feature.gw-spot:not([aria-busy="true"])::before { --gw-spot-mix: var(--gw-spot-strength-stage); }
|
|
54
|
+
|
|
55
|
+
/* words under the light: lit, muted words are ink-2 at once (rules above); back to muted only once the light has gone
|
|
56
|
+
out (a colour that waits the light's time, no movement). :where(), so a part's own transition wins. */
|
|
57
|
+
:where([data-gw-ready~="spot"] *) { transition: color 0s linear var(--gw-spot-ms); }
|
|
58
|
+
:where([data-gw-ready~="spot"]:is(:hover, :focus-visible) *) { transition-delay: 0s; }
|
|
59
|
+
:where([data-gw-ready~="spot"]:has(:focus-visible) *) { transition-delay: 0s; }
|
|
60
|
+
|
|
61
|
+
/* keyboard focus: the light in the middle, so focus is never weaker than hover */
|
|
62
|
+
:is([data-gw-ready~="spot"]:focus-visible, [data-gw-ready~="spot"]:focus-visible *).gw-spot::before { --gw-spot-x: 50%; --gw-spot-y: 50%; }
|
|
63
|
+
:is([data-gw-ready~="spot"]:has(:focus-visible), [data-gw-ready~="spot"]:has(:focus-visible) *).gw-spot::before { --gw-spot-x: 50%; --gw-spot-y: 50%; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* reduced motion: a still light in the middle, there at once and gone at once, the words with it (website.js follows
|
|
67
|
+
no pointer then) */
|
|
68
|
+
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
|
|
69
|
+
.gw-spot:not([aria-busy="true"])::before { --gw-spot-x: 50%; --gw-spot-y: 50%; transition: none; }
|
|
70
|
+
:where([data-gw-ready~="spot"] *) { transition: none; }
|
|
71
|
+
}
|