@getvitops/core 8.1.0 → 8.2.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/CHANGELOG.md +244 -11
- package/css/patterns/banner.css +20 -0
- package/css/patterns/copy.css +5 -0
- package/css/patterns/forms.css +5 -0
- package/css/patterns/marquee.css +16 -0
- package/css/patterns/splitter.css +11 -0
- package/css/patterns/table.css +10 -0
- package/dist/consent.js +1 -1
- package/dist/deferred.js +1 -1
- package/dist/editor.js +2 -2
- package/dist/elements.js +5 -5
- package/dist/polyfills/adoptedStyleSheets.js +1 -1
- package/dist/polyfills/css-anchor-positioning-fn.js +4 -4
- package/dist/polyfills/global.esm.js +1 -1
- package/dist/polyfills/invokers-polyfill.js +1 -1
- package/dist/polyfills/popover.js +2 -2
- package/dist/polyfills/webcomponents-bundle.js +9 -9
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,241 @@
|
|
|
1
1
|
# @getvitops/core
|
|
2
2
|
|
|
3
|
+
## 8.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
|
|
8
|
+
designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
|
|
9
|
+
'./site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
|
|
10
|
+
`<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
|
|
11
|
+
offered, now usable by the client directly, no dev server required. An edit is visible
|
|
12
|
+
site-wide immediately (a runtime `:root` override, served via a new plugin API route and
|
|
13
|
+
`page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
|
|
14
|
+
secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
|
|
15
|
+
reaches the git-tracked `site.json` through your repo's normal review path rather than
|
|
16
|
+
becoming a second, unreviewed source of truth. Until that's configured, the live half still
|
|
17
|
+
works; only the auto-commit half is skipped (and says so in the admin page). The save route
|
|
18
|
+
runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
|
|
19
|
+
synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
|
|
20
|
+
|
|
21
|
+
Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
|
|
22
|
+
EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
|
|
23
|
+
Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
|
|
24
|
+
auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
|
|
25
|
+
plugin-install time.
|
|
26
|
+
|
|
27
|
+
**Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
|
|
28
|
+
capability is now also granted whenever `designSystemEditor` is set, independent of the
|
|
29
|
+
existing `scripts: 'fragments'` option (which governs something unrelated — web-component
|
|
30
|
+
script delivery). No change if you don't use `designSystemEditor`.
|
|
31
|
+
|
|
32
|
+
Also new, supporting the above:
|
|
33
|
+
|
|
34
|
+
- `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
|
|
35
|
+
`/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
|
|
36
|
+
`design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
|
|
37
|
+
standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
|
|
38
|
+
into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
|
|
39
|
+
`mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
|
|
40
|
+
(re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
|
|
41
|
+
subtree" implementation shared by the dev endpoint and the new plugin route. New
|
|
42
|
+
`site.deployment.repo`/`branch` config fields.
|
|
43
|
+
- `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
|
|
44
|
+
wrapper over GitHub's Contents API.
|
|
45
|
+
- `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
|
|
46
|
+
unchanged — `/__vitops/design-system`) so the same element can target the new plugin
|
|
47
|
+
route instead of the dev-only Vite endpoint.
|
|
48
|
+
|
|
49
|
+
- 88b465a: **Accessibility and layout fixes for the web components.**
|
|
50
|
+
|
|
51
|
+
- **`<wc-entries>`: the column selector is no longer `aria-hidden`.** Its buttons are real and
|
|
52
|
+
focusable, so hiding the `<nav>` left a keyboard user on a control that assistive tech never
|
|
53
|
+
announced. It is now exposed as a `Column selector` navigation. The generated table stays
|
|
54
|
+
`aria-hidden` (it duplicates the `<dl>`, which is what assistive tech reads) and contains nothing
|
|
55
|
+
focusable.
|
|
56
|
+
- **`prefers-reduced-motion` is honoured by programmatic scrolling.** `<wc-carousel>` (prev/next and
|
|
57
|
+
marker clicks) and `<wc-tree>` (deep-link scroll) always passed `behavior: 'smooth'` to
|
|
58
|
+
`scrollIntoView`, which overrides the stylesheet's `scroll-behavior` and so ignored the setting.
|
|
59
|
+
They now scroll instantly when it is set, reading the preference at call time.
|
|
60
|
+
- **Pre-upgrade `display` for six more elements** — `wc-entries`, `wc-image-compare`,
|
|
61
|
+
`wc-split-panel`, `wc-multi-field`, `wc-dismissable` (all `block`), `wc-copy` (`inline-flex`) and
|
|
62
|
+
`wc-marquee` (`block`, as a fallback under the `.marquee` class). Each is declared in its pattern's
|
|
63
|
+
partial inside `:where()` (zero specificity, so any consumer rule wins) and matches the element's
|
|
64
|
+
upgraded `:host`, so registering no longer shifts layout. `wc-split-panel`, which declared no
|
|
65
|
+
display at all, is now a block after upgrade too — a visible change if a layout relied on it being
|
|
66
|
+
inline. `wc-counter` is deliberately left inline.
|
|
67
|
+
- **A dismiss button no longer shows before it can work.**
|
|
68
|
+
`wc-dismissable:not(:defined)` hides `.banner__close`, `.notification__close` and `[data-dismiss]`
|
|
69
|
+
buttons, so with no JS a banner has no dead ✕. (`:not(:defined)` is also true until `elements.js`
|
|
70
|
+
has run, so the button appears on upgrade.)
|
|
71
|
+
|
|
72
|
+
Formats reached for the CSS changes: all formats (the partials are components-layer patterns, inlined
|
|
73
|
+
by the tailwind format as well). Other packages in the `fixed` group: No consumer-facing change in this
|
|
74
|
+
package.
|
|
75
|
+
|
|
76
|
+
### Patch Changes
|
|
77
|
+
|
|
78
|
+
- 734ab60: EmDash gallery and carousel blocks now derive their element id from the block's `_key`, so two blocks with the same label on one page no longer open each other's dialog. `<wc-entries>` column navigation now honours `prefers-reduced-motion`.
|
|
79
|
+
- 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
|
|
80
|
+
both projects' GA releases.
|
|
81
|
+
|
|
82
|
+
**Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
|
|
83
|
+
`emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
|
|
84
|
+
`^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
|
|
85
|
+
the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
|
|
86
|
+
code here needed to change, only the declared range.
|
|
87
|
+
|
|
88
|
+
The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
|
|
89
|
+
the version bump:
|
|
90
|
+
|
|
91
|
+
- `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
|
|
92
|
+
`css.format: 'tailwind'` without installing either, which threw on build.
|
|
93
|
+
- `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
|
|
94
|
+
`vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
|
|
95
|
+
the build never generated.
|
|
96
|
+
- `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
|
|
97
|
+
`site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
|
|
98
|
+
migration sequence explicitly, rather than relying solely on first-request auto-migration. This
|
|
99
|
+
needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
|
|
100
|
+
which only a human can set — run `emdash migrate --status` against the real deployed database,
|
|
101
|
+
review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
|
|
102
|
+
<value>`. Deploys fail clearly on the migration-apply step until it's set.
|
|
103
|
+
- `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
|
|
104
|
+
not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
|
|
105
|
+
errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
|
|
106
|
+
Uncomment it once scaffolded into its own repo.
|
|
107
|
+
|
|
108
|
+
No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
|
|
109
|
+
to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
|
|
110
|
+
losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
|
|
111
|
+
now names the bad key instead of reporting a bare "Invalid input").
|
|
112
|
+
|
|
113
|
+
Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
|
|
114
|
+
(`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
|
|
115
|
+
has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
|
|
116
|
+
by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
|
|
117
|
+
hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
|
|
118
|
+
`postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
|
|
119
|
+
`node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
|
|
120
|
+
hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
|
|
121
|
+
(the same path `astro sync` would generate for real). Both are gitignored and only ever exist
|
|
122
|
+
inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
|
|
123
|
+
Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
|
|
124
|
+
evolving default) and making the template a real pnpm workspace member (would change how its
|
|
125
|
+
`@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
|
|
126
|
+
|
|
127
|
+
**Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
|
|
128
|
+
`AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
|
|
129
|
+
`.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
|
|
130
|
+
repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
|
|
131
|
+
compose with vite-plus's own `defineConfig`). A type-level shim
|
|
132
|
+
(`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
|
|
133
|
+
runtime `vp test` failure for this one file is still open.
|
|
134
|
+
|
|
135
|
+
- fdc3f7e: `<wc-marquee>` adds a "Pause animation" button beside the track (WCAG 2.2.2), and the marquee also pauses while focus is inside it (`data-paused` and `:focus-within`). Pattern CSS: all formats. `<wc-consent>` moves focus into the banner when a visitor opens it from a settings control and returns it on close; an unprompted banner still doesn't take focus. `<wc-image-compare>`'s before/after panes are `role="group"` so their labels are announced. The conversion route format-checks `phone`/`email` beacon payloads and imports the Worker-safe `@getvitops/utils/content` instead of the utils root barrel.
|
|
136
|
+
|
|
137
|
+
## 8.2.0-alpha.0
|
|
138
|
+
|
|
139
|
+
### Minor Changes
|
|
140
|
+
|
|
141
|
+
- c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
|
|
142
|
+
designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
|
|
143
|
+
'./site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
|
|
144
|
+
`<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
|
|
145
|
+
offered, now usable by the client directly, no dev server required. An edit is visible
|
|
146
|
+
site-wide immediately (a runtime `:root` override, served via a new plugin API route and
|
|
147
|
+
`page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
|
|
148
|
+
secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
|
|
149
|
+
reaches the git-tracked `site.json` through your repo's normal review path rather than
|
|
150
|
+
becoming a second, unreviewed source of truth. Until that's configured, the live half still
|
|
151
|
+
works; only the auto-commit half is skipped (and says so in the admin page). The save route
|
|
152
|
+
runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
|
|
153
|
+
synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
|
|
154
|
+
|
|
155
|
+
Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
|
|
156
|
+
EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
|
|
157
|
+
Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
|
|
158
|
+
auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
|
|
159
|
+
plugin-install time.
|
|
160
|
+
|
|
161
|
+
**Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
|
|
162
|
+
capability is now also granted whenever `designSystemEditor` is set, independent of the
|
|
163
|
+
existing `scripts: 'fragments'` option (which governs something unrelated — web-component
|
|
164
|
+
script delivery). No change if you don't use `designSystemEditor`.
|
|
165
|
+
|
|
166
|
+
Also new, supporting the above:
|
|
167
|
+
- `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
|
|
168
|
+
`/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
|
|
169
|
+
`design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
|
|
170
|
+
standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
|
|
171
|
+
into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
|
|
172
|
+
`mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
|
|
173
|
+
(re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
|
|
174
|
+
subtree" implementation shared by the dev endpoint and the new plugin route. New
|
|
175
|
+
`site.deployment.repo`/`branch` config fields.
|
|
176
|
+
- `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
|
|
177
|
+
wrapper over GitHub's Contents API.
|
|
178
|
+
- `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
|
|
179
|
+
unchanged — `/__vitops/design-system`) so the same element can target the new plugin
|
|
180
|
+
route instead of the dev-only Vite endpoint.
|
|
181
|
+
|
|
182
|
+
### Patch Changes
|
|
183
|
+
|
|
184
|
+
- 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
|
|
185
|
+
both projects' GA releases.
|
|
186
|
+
|
|
187
|
+
**Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
|
|
188
|
+
`emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
|
|
189
|
+
`^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
|
|
190
|
+
the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
|
|
191
|
+
code here needed to change, only the declared range.
|
|
192
|
+
|
|
193
|
+
The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
|
|
194
|
+
the version bump:
|
|
195
|
+
- `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
|
|
196
|
+
`css.format: 'tailwind'` without installing either, which threw on build.
|
|
197
|
+
- `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
|
|
198
|
+
`vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
|
|
199
|
+
the build never generated.
|
|
200
|
+
- `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
|
|
201
|
+
`site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
|
|
202
|
+
migration sequence explicitly, rather than relying solely on first-request auto-migration. This
|
|
203
|
+
needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
|
|
204
|
+
which only a human can set — run `emdash migrate --status` against the real deployed database,
|
|
205
|
+
review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
|
|
206
|
+
<value>`. Deploys fail clearly on the migration-apply step until it's set.
|
|
207
|
+
- `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
|
|
208
|
+
not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
|
|
209
|
+
errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
|
|
210
|
+
Uncomment it once scaffolded into its own repo.
|
|
211
|
+
|
|
212
|
+
No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
|
|
213
|
+
to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
|
|
214
|
+
losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
|
|
215
|
+
now names the bad key instead of reporting a bare "Invalid input").
|
|
216
|
+
|
|
217
|
+
Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
|
|
218
|
+
(`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
|
|
219
|
+
has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
|
|
220
|
+
by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
|
|
221
|
+
hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
|
|
222
|
+
`postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
|
|
223
|
+
`node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
|
|
224
|
+
hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
|
|
225
|
+
(the same path `astro sync` would generate for real). Both are gitignored and only ever exist
|
|
226
|
+
inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
|
|
227
|
+
Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
|
|
228
|
+
evolving default) and making the template a real pnpm workspace member (would change how its
|
|
229
|
+
`@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
|
|
230
|
+
|
|
231
|
+
**Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
|
|
232
|
+
`AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
|
|
233
|
+
`.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
|
|
234
|
+
repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
|
|
235
|
+
compose with vite-plus's own `defineConfig`). A type-level shim
|
|
236
|
+
(`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
|
|
237
|
+
runtime `vp test` failure for this one file is still open.
|
|
238
|
+
|
|
3
239
|
## 8.1.0
|
|
4
240
|
|
|
5
241
|
### Minor Changes
|
|
@@ -68,7 +304,6 @@
|
|
|
68
304
|
|
|
69
305
|
**Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
|
|
70
306
|
contract changed):**
|
|
71
|
-
|
|
72
307
|
- `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
|
|
73
308
|
wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
|
|
74
309
|
is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
|
|
@@ -109,7 +344,6 @@
|
|
|
109
344
|
### Minor Changes
|
|
110
345
|
|
|
111
346
|
- Add Google Business Profile reviews: fetch, cache, render.
|
|
112
|
-
|
|
113
347
|
- **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
|
|
114
348
|
one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
|
|
115
349
|
never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
|
|
@@ -144,7 +378,6 @@
|
|
|
144
378
|
`--site` for now.)
|
|
145
379
|
|
|
146
380
|
**Fixed:**
|
|
147
|
-
|
|
148
381
|
- `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
|
|
149
382
|
`@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
|
|
150
383
|
being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
|
|
@@ -332,7 +565,7 @@
|
|
|
332
565
|
created, with `patterns/counter.css`'s own reduced-motion query as a second belt.
|
|
333
566
|
- The accessible name stays the final value for the whole animation: `.counter__value` is toggled
|
|
334
567
|
to the framework's `.sr-only` utility while running, never hidden — `display: none`/`visibility:
|
|
335
|
-
hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
|
|
568
|
+
hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
|
|
336
569
|
screen reader must never be read ~60 intermediate numbers. The animated text lives in a separate
|
|
337
570
|
`aria-hidden` presentation span it never reaches.
|
|
338
571
|
- `data-from` (default `0`), `data-duration` (ms, default `1200`), `data-easing` (`linear` |
|
|
@@ -986,7 +1219,7 @@ hidden` would pull it out of the accessible-name computation along with the visu
|
|
|
986
1219
|
`animate-view` / `animate-scroll` / `animate-trigger` / `transition`, and every effect
|
|
987
1220
|
gained an `open-<fx>` state variant. Overlays now state _where they start_
|
|
988
1221
|
(`--translate-x-from`) instead of owning a keyframe: `class="drawer drawer--right
|
|
989
|
-
open-fade-in"` composes slide and fade. Applied at zero specificity with identity
|
|
1222
|
+
open-fade-in"` composes slide and fade. Applied at zero specificity with identity
|
|
990
1223
|
defaults, so a popover that sets no effect vars is unchanged.
|
|
991
1224
|
- **One scrim.** `--scrim` / `--scrim-filter` tokens and `.no-scrim` in `popover.css`,
|
|
992
1225
|
replacing 18 `::backdrop` blocks across seven partials. `.drawer--modeless` is aliased.
|
|
@@ -1130,8 +1363,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
|
|
|
1130
1363
|
|
|
1131
1364
|
```js
|
|
1132
1365
|
vitops({
|
|
1133
|
-
analytics: { googleAnalytics:
|
|
1134
|
-
consent: { policyUrl:
|
|
1366
|
+
analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
|
|
1367
|
+
consent: { policyUrl: '/legal/cookies' },
|
|
1135
1368
|
});
|
|
1136
1369
|
```
|
|
1137
1370
|
|
|
@@ -1189,8 +1422,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
|
|
|
1189
1422
|
replace it with `--anim-range`. `--stagger-range-step` is now a viewport length (`5vh`) to match.
|
|
1190
1423
|
|
|
1191
1424
|
**Fixed: every journey ran on a truncated range.** The generator emitted `animation-range: entry
|
|
1192
|
-
exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
|
|
1193
|
-
100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
|
|
1425
|
+
exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
|
|
1426
|
+
100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
|
|
1194
1427
|
`from` state — as the element hits the top of the viewport, which is to say they faded out while
|
|
1195
1428
|
still fully on screen. Journeys now run `entry calc(50% + 10vh) → exit 100%`: they start on the same
|
|
1196
1429
|
midpoint pivot as everything else and keep the full entry → hold → exit arc, so the hold sits in the
|
|
@@ -1203,7 +1436,7 @@ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than th
|
|
|
1203
1436
|
so one knob tunes both. If your own config has an empty `slide` base, add the same line.
|
|
1204
1437
|
|
|
1205
1438
|
**Fixed: `reveal-*` on hover was unreachable.** A `hover-reveal-left` element rests at `clip-path:
|
|
1206
|
-
inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
|
|
1439
|
+
inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
|
|
1207
1440
|
hittable area and could never receive the hover that would reveal it. The state variants now match
|
|
1208
1441
|
the element **or its direct parent**, mirroring what `animation.css` already did for the trigger
|
|
1209
1442
|
driver (`:is(.is-active, [data-active]) > .animate-trigger`):
|
|
@@ -1755,7 +1988,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1755
1988
|
```js
|
|
1756
1989
|
// astro.config.mjs
|
|
1757
1990
|
getvitops({
|
|
1758
|
-
css: { input:
|
|
1991
|
+
css: { input: 'design-system.json', format: 'css', out: 'src/styles' },
|
|
1759
1992
|
editor: true,
|
|
1760
1993
|
});
|
|
1761
1994
|
```
|
package/css/patterns/banner.css
CHANGED
|
@@ -69,3 +69,23 @@
|
|
|
69
69
|
.banner[hidden] {
|
|
70
70
|
display: none;
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* `<wc-dismissable>` wraps a banner or notification — block content — and animates its own
|
|
75
|
+
* opacity/scale on dismiss, so it needs a box of its own: `display: contents` would drop
|
|
76
|
+
* that animation. Pre-upgrade it would otherwise be an inline box around a block child.
|
|
77
|
+
*/
|
|
78
|
+
:where(wc-dismissable) {
|
|
79
|
+
display: block;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* A dismiss button does nothing until `<wc-dismissable>` upgrades — an inert control that
|
|
84
|
+
* looks live is worse than none. `:not(:defined)` is true with no JS (and until elements.js
|
|
85
|
+
* has run), so the close affordance only appears once something can act on it. The banner
|
|
86
|
+
* itself stays fully readable. Markup that puts a `[data-dismiss]` button inside the element
|
|
87
|
+
* gets the same treatment, whatever its class.
|
|
88
|
+
*/
|
|
89
|
+
wc-dismissable:not(:defined) :is(.banner__close, .notification__close, [data-dismiss]) {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
package/css/patterns/copy.css
CHANGED
package/css/patterns/forms.css
CHANGED
package/css/patterns/marquee.css
CHANGED
|
@@ -56,6 +56,13 @@
|
|
|
56
56
|
animation-play-state: paused;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/* Keyboard users can't hover; focusing anything inside (a link in an item) also pauses, and
|
|
60
|
+
`<wc-marquee>`'s pause button sets `data-paused` (WCAG 2.2.2). */
|
|
61
|
+
&:focus-within .marquee__content,
|
|
62
|
+
&[data-paused] .marquee__content {
|
|
63
|
+
animation-play-state: paused;
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
&.marquee--reverse {
|
|
60
67
|
--_marquee-direction: reverse;
|
|
61
68
|
}
|
|
@@ -100,3 +107,12 @@
|
|
|
100
107
|
translate: 0 calc(-1 * var(--_marquee-distance, calc(100% + var(--marquee-gap, 1rem))));
|
|
101
108
|
}
|
|
102
109
|
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* The `.marquee` class carries `display: flex` when present (the Astro component always sets
|
|
113
|
+
* it). This is the fallback for a hand-written `<wc-marquee>` without the class, and it is
|
|
114
|
+
* zero specificity so `.marquee` still wins.
|
|
115
|
+
*/
|
|
116
|
+
:where(wc-marquee) {
|
|
117
|
+
display: block;
|
|
118
|
+
}
|
|
@@ -210,3 +210,14 @@
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* Pre-upgrade display for the two elements that live in this partial — see the note on
|
|
216
|
+
* `wc-entries` in table.css. Both are a framed block of content (two panes; two images),
|
|
217
|
+
* and `wc-image-compare`'s upgraded `:host` is already `display: block`, so declaring it
|
|
218
|
+
* here removes the layout shift when it registers. `wc-split-panel` has no `:host` display
|
|
219
|
+
* at all, so this also fixes it after upgrade.
|
|
220
|
+
*/
|
|
221
|
+
:where(wc-split-panel, wc-image-compare) {
|
|
222
|
+
display: block;
|
|
223
|
+
}
|
package/css/patterns/table.css
CHANGED
|
@@ -104,3 +104,13 @@ table,
|
|
|
104
104
|
min-inline-size: var(--table-min-inline-size, 100%);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Before upgrade (or with elements.js absent) an unregistered custom element is
|
|
110
|
+
* `display: inline`, forming an inline box around block content. The default-view
|
|
111
|
+
* `<h3>` + `<dl>` pairs are block content, as is the table the element builds.
|
|
112
|
+
* Zero specificity, so any consumer rule wins.
|
|
113
|
+
*/
|
|
114
|
+
:where(wc-entries) {
|
|
115
|
+
display: block;
|
|
116
|
+
}
|
package/dist/consent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=[`necessary`,`analytics`,`marketing`,`preferences`],t=`vitops:consent`,n=`vitops:consent-open`,r=`vitops_consent`,i=[`analytics`,`marketing`,`preferences`];function a(e){return{analytics:e,marketing:e,preferences:e}}function o(){return{ts:null,choices:a(null)}}function s(e,t){for(let n of e.split(`;`)){let e=n.trim();if(e.slice(0,t.length+1)===`${t}=`)return e.slice(t.length+1)}return null}function c(e){if(!e)return o();let t;try{t=JSON.parse(decodeURIComponent(e))}catch{return o()}if(typeof t!=`object`||!t)return o();let n=t;if(n.v!==2||typeof n.c!=`object`||n.c===null)return o();let r=n.c,s=a(null);for(let e of i){let t=r[e];typeof t==`boolean`&&(s[e]=t)}return{ts:typeof n.ts==`number`?n.ts:null,choices:s}}function l(e){let t={};for(let n of i){let r=e.choices[n];typeof r==`boolean`&&(t[n]=r)}return encodeURIComponent(JSON.stringify({v:2,ts:e.ts??0,c:t}))}function u(e,t){return t===`necessary
|
|
1
|
+
const e=[`necessary`,`analytics`,`marketing`,`preferences`],t=`vitops:consent`,n=`vitops:consent-open`,r=`vitops_consent`,i=[`analytics`,`marketing`,`preferences`];function a(e){return{analytics:e,marketing:e,preferences:e}}function o(){return{ts:null,choices:a(null)}}function s(e,t){for(let n of e.split(`;`)){let e=n.trim();if(e.slice(0,t.length+1)===`${t}=`)return e.slice(t.length+1)}return null}function c(e){if(!e)return o();let t;try{t=JSON.parse(decodeURIComponent(e))}catch{return o()}if(typeof t!=`object`||!t)return o();let n=t;if(n.v!==2||typeof n.c!=`object`||n.c===null)return o();let r=n.c,s=a(null);for(let e of i){let t=r[e];typeof t==`boolean`&&(s[e]=t)}return{ts:typeof n.ts==`number`?n.ts:null,choices:s}}function l(e){let t={};for(let n of i){let r=e.choices[n];typeof r==`boolean`&&(t[n]=r)}return encodeURIComponent(JSON.stringify({v:2,ts:e.ts??0,c:t}))}function u(e,t){return t===`necessary`||e.choices[t]===!0}function d(e,t){return t===`necessary`||e.choices[t]!==null}function f(e,t){for(let n of t)if(!d(e,n))return!0;return!1}function p(e,t){return{ts:t,choices:a(e)}}function m(e,t,n){let r={...e.choices};for(let e of i){let n=t[e];typeof n==`boolean`&&(r[e]=n)}return{ts:n,choices:r}}function h(e,t){return i.filter(n=>u(e,n)&&!u(t,n))}function g(e){return`; path=/; max-age=15552000; SameSite=Lax${e?`; Secure`:``}`}const _=location.protocol===`https:`;let v=c(s(document.cookie,r));const y=/* @__PURE__ */ new Set,b=/* @__PURE__ */ new WeakSet,x=/* @__PURE__ */ new Set,S=/* @__PURE__ */ new Set,C=/* @__PURE__ */ new Map,w=/* @__PURE__ */ new Set;function T(e){w.has(e)||(w.add(e),queueMicrotask(()=>{document.querySelector(`wc-consent`)||console.warn(`[vitops] something requested the "${e}" consent category, but this page has no <wc-consent> element to ask with — so the request can never be granted and whatever waited on it will not run. Render <CookieConsent /> on every page that gates anything.`)}))}function E(e){S.has(e)||(S.add(e),T(e),B())}function D(){for(let[e,t]of C)if(d(v,e)){C.delete(e);for(let n of t)n(u(v,e))}}const O={idle:[],async:[],interaction:[]},k={idle:!1,async:!0,interaction:!1};function A(e){if(k[e])return;k[e]=!0;let t=O[e];O[e]=[];for(let e of t)e()}function j(e,t){k[e]?t():O[e].push(t)}function M(e){document.readyState===`complete`?e():addEventListener(`load`,e,{once:!0})}M(()=>{let e=globalThis.requestIdleCallback;e?e(()=>A(`idle`),{timeout:3e3}):setTimeout(()=>A(`idle`),1)});{let e=[`pointerdown`,`keydown`,`scroll`,`touchstart`],t=()=>{for(let n of e)removeEventListener(n,t);A(`interaction`)};for(let n of e)addEventListener(n,t,{once:!0,passive:!0});M(()=>setTimeout(()=>A(`interaction`),8e3))}const N=/* @__PURE__ */ new Set([`type`,`src`,`data-vitops-tag`,`data-src`,`data-strategy`,`data-consent`,`data-consent-src`,`data-consent-cookies`]);function P(t){let n=t.getAttribute(`data-consent`);return e.includes(n??``)?n:`analytics`}function F(e){let t=e.textContent??``,n=e.getAttribute(`data-src`),r=t=>{let n=document.createElement(`script`);for(let{name:t,value:r}of Array.from(e.attributes))N.has(t)||n.setAttribute(t,r);t(n),e.parentNode?.insertBefore(n,e.nextSibling)};t.trim()&&r(e=>e.text=t),n&&r(e=>{e.async=!0,e.src=n})}function I(e){if(!b.has(e)){if(b.add(e),x.add(P(e)),e instanceof HTMLScriptElement)F(e);else{let t=e.getAttribute(`data-consent-src`);t&&e.setAttribute(`src`,t)}e.setAttribute(`data-vitops-activated`,``)}}function L(){let e=document.querySelectorAll(`script[data-vitops-tag]:not([data-vitops-activated]),[data-consent-src]:not([data-vitops-activated])`);for(let t of e){let e=t.getAttribute(`data-strategy`);j(e===`async`||e===`interaction`?e:`idle`,()=>{let e=P(t);u(v,e)?I(t):E(e)})}}function R(e){let t=/* @__PURE__ */ new Set;for(let n of e)for(let e of document.querySelectorAll(`[data-consent="${n}"]`))for(let n of(e.getAttribute(`data-consent-cookies`)??``).split(`,`))n.trim()&&t.add(n.trim());if(!t.size)return;let n=document.cookie.split(`;`).map(e=>e.trim().split(`=`)[0]??``),r=location.hostname.split(`.`).slice(-2).join(`.`),i=[``,`; domain=${location.hostname}`,`; domain=.${r}`];for(let e of n)if([...t].some(t=>t.endsWith(`*`)?e.startsWith(t.slice(0,-1)):e===t))for(let t of i)document.cookie=`${e}=; path=/; max-age=0${t}`}function z(e,t){let n=h(v,e),i=v;v=e,t?document.cookie=`${r}=${l(e)}${g(_)}`:document.cookie=`${r}=; path=/; max-age=0`,B(),L(),n.length&&(R(n),n.some(e=>x.has(e)&&u(i,e))&&V.reloadOnRevoke&&location.reload())}function B(){D();for(let e of y)e(v);document.dispatchEvent(new CustomEvent(t,{detail:v}))}const V={categories:e,reloadOnRevoke:!0,get:()=>v,granted:e=>u(v,e),needed:()=>f(v,S),demanded:()=>e.filter(e=>S.has(e)),require(e){return E(e),u(v,e)},request(e){return E(e),d(v,e)?Promise.resolve(u(v,e)):new Promise(t=>{let n=C.get(e);n?n.push(t):C.set(e,[t])})},set:e=>z(m(v,e,Date.now()),!0),acceptAll:()=>z(p(!0,Date.now()),!0),rejectAll:()=>z(p(!1,Date.now()),!0),reset:()=>z(o(),!1),open:()=>document.dispatchEvent(new CustomEvent(n)),subscribe(e){return y.add(e),e(v),()=>void y.delete(e)}},H=window.vitopsConsent?.q;if(window.vitopsConsent=V,H)for(let[e,t]of H)t?V.request(e).then(t):V.require(e);document.addEventListener(`click`,e=>{e.target?.closest?.(`[data-consent-open]`)&&(e.preventDefault(),V.open())}),document.addEventListener(`astro:page-load`,L),L(),B();var U=class extends HTMLElement{#e;#t=`prompt`;#n=!1;get#r(){return window.vitopsConsent}get#i(){return this.querySelector(`[popover]`)}connectedCallback(){let e=this.#r;if(!e)return;let t=this.#i;t&&typeof t.showPopover!=`function`&&t.removeAttribute(`popover`),this.getAttribute(`reload-on-revoke`)===`false`&&(e.reloadOnRevoke=!1),this.addEventListener(`click`,this.#m),this.addEventListener(`submit`,this.#o),document.addEventListener(n,this.#a),this.#e=e.subscribe(()=>{this.#n&&this.#t===`settings`?this.#p():e.needed()?this.#u(`prompt`):this.#f()})}disconnectedCallback(){this.removeEventListener(`click`,this.#m),this.removeEventListener(`submit`,this.#o),document.removeEventListener(n,this.#a),this.#e?.()}#a=()=>this.#u(`settings`);#o=e=>e.preventDefault();#s(e){return this.querySelector(`[data-consent-row="${e}"]`)??this.#c(e)?.closest(`label`)??null}#c(e){return this.querySelector(`input[name="${e}"]`)}#l(e){let t=this.#r;if(!t)return[];if(e===`settings`){let e=i.filter(e=>this.#s(e));return e.length?e:[...i]}let n=t.get();return i.filter(e=>t.demanded().includes(e)&&!d(n,e))}#u(e){if(!this.#l(e).length)return;this.#t=e,this.#p(),this.hidden=!1,this.#n=!0;let t=this.#i;t?.isConnected&&!t.matches(`:popover-open`)&&t.showPopover(),e===`settings`&&t&&(this.#d=document.activeElement instanceof HTMLElement?document.activeElement:null,t.tabIndex=-1,t.focus({preventScroll:!0}))}#d=null;#f(){this.#n=!1,this.#i?.hidePopover?.(),this.hidden=!0;let e=this.#d;this.#d=null,e?.isConnected&&e.focus({preventScroll:!0})}#p(){let e=this.#r;if(!e)return;let t=this.#l(this.#t);for(let n of i){let r=this.#s(n);r&&(r.hidden=!t.includes(n));let i=this.#c(n);i&&(i.checked=e.granted(n))}this.#i?.classList.toggle(`consent--single`,t.length<2)}#m=e=>{let t=this.#r,n=e.target?.closest?.(`[data-consent-accept],[data-consent-reject],[data-consent-save]`);if(!t||!n)return;e.preventDefault();let r=this.#l(this.#t);n.hasAttribute(`data-consent-accept`)?t.set(this.#h(r,()=>!0)):n.hasAttribute(`data-consent-reject`)?t.set(this.#h(r,()=>!1)):t.set(this.#h(r,e=>this.#c(e)?.checked===!0)),this.#f()};#h(e,t){let n={};for(let r of e)n[r]=t(r);return n}};customElements.get(`wc-consent`)||customElements.define(`wc-consent`,U);export{e as CATEGORIES,t as CONSENT_EVENT,n as CONSENT_OPEN_EVENT,r as COOKIE_NAME,V as consent};
|
package/dist/deferred.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){let t=[...e.children];parseFloat(getComputedStyle(e).getPropertyValue(`--stagger-amount`));let n=[],r=e.getBoundingClientRect().top;t.forEach(e=>{let t=Math.round(e.getBoundingClientRect().top-r),i=n.find(e=>Math.abs(e.top-t)<10);i||(i={top:t,items:[]},n.push(i)),i.items.push(e)}),n.forEach(({items:e})=>{e.forEach((e,t)=>{e.style.setProperty(`--_stagger-index`,String(t))})})}let t;function n(){clearTimeout(t),t=setTimeout(()=>{requestAnimationFrame(()=>{document.querySelectorAll(`.stagger`).forEach(e)})},100)}const r=new IntersectionObserver((e,t)=>{e.forEach(e=>{if(!e.isIntersecting)return;let n=e.rootBounds,r=e.boundingClientRect;(!n||r.height>=n.height||r.top+r.height/2<=n.bottom)&&(e.target.classList.add(`is-active`),t.unobserve(e.target))})},{rootMargin:`0px 0px -10% 0px`,threshold:[0,.25,.5,.75,1]});function i(e){let t=e.querySelector(`.horizontal-scroll__track`);if(!t)return;let n=Array.from(t.children).reduce((e,t)=>e+t.offsetWidth,0);t.style.width=`${n}px`}document.addEventListener(`click`,e=>{let t=e.target.closest?.(`a[href^="#"]`);if(!t)return;let n=t.closest(`dialog[open]`);n&&n.close()}),window.addEventListener(`load`,()=>{requestAnimationFrame(()=>{document.querySelectorAll(`.stagger`).forEach(e),window.addEventListener(`resize`,n),document.querySelectorAll(`.horizontal-scroll`).forEach(i),document.querySelectorAll(`.animate-trigger`).forEach(e=>{r.observe(e)});let t=new Date().getFullYear();document.querySelectorAll(`[data-current-year]`).forEach(e=>{e.textContent=t.toString()})})});
|
|
1
|
+
function e(e){let t=[...e.children];parseFloat(getComputedStyle(e).getPropertyValue(`--stagger-amount`));let n=[],r=e.getBoundingClientRect().top;t.forEach(e=>{let t=Math.round(e.getBoundingClientRect().top-r),i=n.find(e=>Math.abs(e.top-t)<10);i||(i={top:t,items:[]},n.push(i)),i.items.push(e)}),n.forEach(({items:e})=>{e.forEach((e,t)=>{e.style.setProperty(`--_stagger-index`,String(t))})})}let t;function n(){clearTimeout(t),t=setTimeout(()=>{requestAnimationFrame(()=>{document.querySelectorAll(`.stagger`).forEach(e)})},100)}const r=new IntersectionObserver((e,t)=>{e.forEach(e=>{if(!e.isIntersecting)return;let n=e.rootBounds,r=e.boundingClientRect;(!n||r.height>=n.height||r.top+r.height/2<=n.bottom)&&(e.target.classList.add(`is-active`),t.unobserve(e.target))})},{rootMargin:`0px 0px -10% 0px`,threshold:[0,.25,.5,.75,1]});function i(e){let t=e.querySelector(`.horizontal-scroll__track`);if(!t)return;let n=Array.from(t.children).reduce((e,t)=>e+t.offsetWidth,0);t.style.width=`${n}px`}document.addEventListener(`click`,e=>{let t=e.target.closest?.(`a[href^="#"]`);if(!t)return;let n=t.closest(`dialog[open]`);n&&n.close()}),window.addEventListener(`load`,()=>{requestAnimationFrame(()=>{document.querySelectorAll(`.stagger`).forEach(e),window.addEventListener(`resize`,n),document.querySelectorAll(`.horizontal-scroll`).forEach(i),document.querySelectorAll(`.animate-trigger`).forEach(e=>{r.observe(e)});let t=(/* @__PURE__ */ new Date()).getFullYear();document.querySelectorAll(`[data-current-year]`).forEach(e=>{e.textContent=t.toString()})})});
|
package/dist/editor.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=`vitops:ds-overrides`,t
|
|
2
|
-
`)}\n}\n`:``}serializeCSS(){return this.block(`:root`,this.store.light)+this.block(`:root[data-brx-theme="dark"], :root[data-theme="dark"]`,this.store.dark)}render(){this.styleEl&&(this.styleEl.textContent=this.serializeCSS())}persist(){try{localStorage.setItem(e,JSON.stringify({light:[...this.store.light],dark:[...this.store.dark],roles:[...this.roles]}))}catch{}}hydrate(){try{let t=localStorage.getItem(e);if(!t)return;let n=JSON.parse(t);this.store.light=new Map(n.light??[]),this.store.dark=new Map(n.dark??[]),this.roles=new Map(n.roles??[]);for(let[e,t]of this.roles)this.markRoleVars(e,t)}catch{}}setOverride(e,t,n){let r=this.store[n];t==null||t===``?r.delete(e):r.set(e,t),this.render(),this.persist()}effective(e,t,n){return this.store[t].has(e)?this.store[t].get(e):t===`dark`&&this.store.light.has(e)?this.store.light.get(e):n}reset(){this.store.light.clear(),this.store.dark.clear(),this.roles.clear(),this.roleVars.clear(),this.render(),this.persist(),this.syncers.forEach(e=>e())}currentScheme(){let e=document.documentElement;return e.dataset.theme===`dark`||e.getAttribute(`data-brx-theme`)===`dark`?`dark`:`light`}setScheme(e){this.scheme=e,document.documentElement.dataset.theme=e,document.documentElement.style.colorScheme=`${e} only`;try{localStorage.setItem(`vitops-color-scheme`,e)}catch{}this.syncers.forEach(e=>e())}roleTokenNames(e,t,r){let i=e.colors.roles.find(e=>e.name===t)?.kind??`chromatic`,a=e.colors.roleTokens?.[r]?.[i];if(!a)return[];let o=[];for(let e of[`light`,`dark`])for(let[r,i]of Object.entries(a[e]))o.push([n(t,r),i,e]);return o}markRoleVars(e,t){if(this.manifest)for(let[n]of this.roleTokenNames(this.manifest,e,t))this.roleVars.add(n)}remapRole(e,t,n){let r=this.roleTokenNames(e,t,n);if(r.length){this.roles.set(t,n);for(let[e,t,n]of r)this.roleVars.add(e),this.store[n].set(e,t);this.render(),this.persist()}}deepSet(e,t,n){let r=t.split(`.`),i=e;for(let e=0;e<r.length-1;e++){let t=r[e];(typeof i[t]!=`object`||i[t]==null)&&(i[t]={}),i=i[t]}i[r[r.length-1]]=n}buildPatch(e){let t={},n=[];for(let[n,r]of this.roles){let i=e.colors.roles.find(e=>e.name===n)?.kind??`chromatic`;this.deepSet(t,`colors.roles.${n}`,{hue:r,kind:i})}for(let[r,i]of this.store.light){if(this.roleVars.has(r))continue;let a=e.reverseIndex[r];a?this.deepSet(t,a,i):n.push(r)}for(let e of this.store.dark.keys())this.roleVars.has(e)||n.push(`${e} (dark)`);return{patch:t,skipped:n}}el(e,t={},n=[]){let r=document.createElement(e),i=r;for(let[e,n]of Object.entries(t))n===!0?i.setAttribute(e,``):n!==!1&&i.setAttribute(e,String(n));for(let e of n)i.append(e);return r}row(e,t,n){return this.syncers.add(n),n(),this.el(`label`,{class:`ed-row`},[this.el(`span`,{class:`ed-label`,title:e},[e.replace(/^--/,``)]),t])}textInput(e,t,n=``){let r=this.el(`input`,{type:`text`,class:`ed-input`});return r.addEventListener(`input`,()=>this.setOverride(e,r.value.trim()||null,this.scheme)),this.row(e,r,()=>{r.value=this.store[this.scheme].get(e)??``,r.placeholder=n||t})}colorInput(e,t){let n=this.el(`input`,{type:`color`,class:`ed-color`}),r=this.el(`input`,{type:`text`,class:`ed-input ed-mono`}),i=t=>this.setOverride(e,t||null,`light`);n.addEventListener(`input`,()=>{r.value=n.value,i(n.value)}),r.addEventListener(`input`,()=>{/^#[0-9a-f]{6}$/i.test(r.value)&&(n.value=r.value),i(r.value.trim())});let a=this.el(`span`,{class:`ed-pair`},[n,r]);return this.row(e,a,()=>{let i=this.store.light.get(e)??t;r.value=i,/^#[0-9a-f]{6}$/i.test(i)&&(n.value=i)})}selectInput(e,t){let n=this.el(`select`,{class:`ed-input`});n.append(this.el(`option`,{value:``},[`— default —`]));for(let e of t)n.append(this.el(`option`,{value:e.value},[e.label]));return n.addEventListener(`change`,()=>this.setOverride(e,n.value||null,this.scheme)),this.row(e,n,()=>{n.value=this.store[this.scheme].get(e)??``})}remSlider(e,t){let n=parseFloat(t)||1,r=this.el(`input`,{type:`range`,class:`ed-range`,min:`0`,max:String(Math.max(4,Math.ceil(n*2))),step:`0.05`}),i=this.el(`output`,{class:`ed-out`});r.addEventListener(`input`,()=>{i.textContent=`${r.value}rem`,this.setOverride(e,`${r.value}rem`,this.scheme)});let a=this.el(`span`,{class:`ed-pair`},[r,i]);return this.row(e,a,()=>{r.value=String(parseFloat(this.effective(e,this.scheme,t))||n),i.textContent=this.store[this.scheme].get(e)??`auto`})}slider(e,t){let n=t.toValue??(e=>`${e}${t.unit??``}`),r=t.fromValue??(e=>Number.isNaN(parseFloat(e))?t.def:parseFloat(e)),i=this.el(`input`,{type:`range`,class:`ed-range`,min:String(t.min),max:String(t.max),step:String(t.step)}),a=this.el(`output`,{class:`ed-out`});i.addEventListener(`input`,()=>{let t=n(parseFloat(i.value));a.textContent=t,this.setOverride(e,t,this.scheme)});let o=this.el(`span`,{class:`ed-pair`},[i,a]);return this.row(e,o,()=>{let n=this.store[this.scheme].get(e);i.value=String(n==null?t.def:r(n)),a.textContent=n??`${t.def}${t.unit??``}`})}section(e,t=!1){let n=this.el(`div`,{class:`ed-body rhythm`});return{details:this.el(`details`,{class:`details details--marker-start ed-section`,...t?{open:!0}:{}},[this.el(`summary`,{class:`font-eyebrow`},[this.el(`span`,{class:`icon-mask details-icon ed-icon ed-icon--caret`,"aria-hidden":`true`}),e]),n]),body:n}}hint(e){return this.el(`p`,{class:`font-footnote text-surface-x-muted`},[e])}sub(e){let t=this.section(e);return t.details.classList.add(`ed-sub`),t}buildPalette(e){let{details:t,body:n}=this.section(`Palette`,!0);n.append(this.hint(`Hues feed the semantic roles below — edit a hue and every role mapped to it follows. Hue edits are scheme-global: dark mode re-points which step a token reads, it never redefines the hue itself.`));for(let t of e.colors.ramps){let i=this.sub(t);for(let n of e.colors.steps){let a=e.colors.palette[t]?.[n];a!=null&&i.body.append(this.colorInput(r(t,n),a))}n.append(i.details)}return n.append(this.buildRoles(e)),t}buildRoles(e){let{details:t,body:n}=this.sub(`Semantic roles`);n.append(this.hint(`Point a role at another hue — its whole functional token set (bg / border / solid / on-solid / text) is rewritten for both appearances.`));let r=e.colors.ramps;for(let{name:t,ramp:i}of e.colors.roles){let a=this.el(`select`,{class:`ed-input`});for(let e of r)a.append(this.el(`option`,{value:e},[e]));a.addEventListener(`change`,()=>this.remapRole(e,t,a.value)),n.append(this.row(`--${t}`,a,()=>{a.value=this.roles.get(t)??i}))}return t}buildTypography(e){let{details:t,body:n}=this.section(`Typography`),r=Object.keys(e.fonts).map(e=>({label:e,value:`var(--font-${e})`})),i=e.scales.text.map(e=>({label:e.name,value:`var(--text-${e.name})`})),a={tt:[`none`,`uppercase`,`lowercase`,`capitalize`],fst:[`normal`,`italic`,`oblique`],td:[`none`,`underline`,`line-through`],tw:[`wrap`,`balance`,`pretty`,`nowrap`]};for(let t of e.typography.roles){let o=e.typography.specs[t]??{},s=this.sub(t);for(let[n,{key:c}]of Object.entries(e.typography.hooks)){let e=`--${t}-${n}`,l=o[c]==null?``:String(o[c]);n===`ff`?s.body.append(this.selectInput(e,r)):n===`fs`?s.body.append(this.selectInput(e,i)):a[n]?s.body.append(this.selectInput(e,a[n].map(e=>({label:e,value:e})))):s.body.append(this.textInput(e,l,l||c))}n.append(s.details)}return t}buildSpacing(e){let{details:t,body:n}=this.section(`Spacing`);n.append(this.hint(`Each step overrides to a fixed rem (dropping the fluid clamp). Reset restores it.`));for(let t of e.scales.space)n.append(this.remSlider(`--space-${t.name}`,t.max));return t}buildLayout(){let{details:e,body:t}=this.section(`Layout`);return t.append(this.hint(`Vertical rhythm and the .centered track widths — global structure.`)),t.append(this.slider(`--rhythm-scale`,{min:.5,max:2.5,step:.05,def:1}),this.slider(`--width-measure`,{min:30,max:110,step:1,def:65,unit:`ch`}),this.slider(`--width-breakout`,{min:40,max:150,step:1,def:90,unit:`ch`}),this.slider(`--width-spotlight`,{min:60,max:200,step:1,def:120,unit:`ch`}),this.slider(`--gutter`,{min:0,max:6,step:.1,def:3,toValue:e=>`clamp(1rem, 4cqi, ${e}rem)`,fromValue:e=>{let t=/clamp\([^,]+,[^,]+,\s*([\d.]+)/.exec(e);return t?parseFloat(t[1]):3}})),e}buildPatterns(e){let t=this.section(`Pattern defaults`);for(let[n,r]of Object.entries(e.patterns.defaults))t.body.append(this.textInput(`--${n}-default`,r,r));let n=this.section(`Radii`);for(let[t,r]of Object.entries(e.patterns.radii))n.body.append(this.textInput(`--br-${t}`,r,r));let r=this.section(`Pattern groups`);for(let[t,n]of Object.entries(e.patterns.groups)){let e=this.sub(t);for(let[r,i]of Object.entries(n))e.body.append(this.textInput(`--${r}-${t}`,i,i));r.body.append(e.details)}let i=this.section(`Components`);i.body.append(this.hint(`Per-component geometry — overrides the group defaults for just this component.`));for(let t of e.patterns.items){if(!t.wrappable.length)continue;let e=this.sub(t.name);for(let{sfx:n,prop:r}of t.wrappable){let i=t.base[r]??``;e.body.append(this.textInput(`--${n}-${t.name}`,i,i))}i.body.append(e.details)}let a=this.section(`Shadows`);for(let[t,n]of Object.entries(e.shadows))a.body.append(this.textInput(`--shadow-${t}`,n,n));return[t.details,n.details,r.details,i.details,a.details]}async flash(e,t){let n=e.textContent;e.textContent=t,setTimeout(()=>e.textContent=n,1400)}async copy(e,t){try{await navigator.clipboard.writeText(e),this.flash(t,`Copied ✓`)}catch{this.flash(t,`Copy failed`)}}async save(e,n){let{patch:r}=this.buildPatch(e);if(!Object.keys(r).length)return void this.flash(n,`Nothing to save`);try{let e=await fetch(t,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({patch:r})});if(!e.ok){let t=await e.json().catch(()=>({}));console.error(`[wc-theme-editor] save rejected`,t.errors??e.statusText),this.flash(n,`Rejected ✗`);return}this.reset(),this.flash(n,`Saved ✓`)}catch(e){console.error(`[wc-theme-editor] save failed`,e),this.flash(n,`Failed ✗`)}}buildToolbar(e){let t=this.el(`div`,{class:`cluster-start ed-toolbar`}),n=this.el(`button`,{type:`button`,class:`btn ed-seg`},[`Light`]),r=this.el(`button`,{type:`button`,class:`btn ed-seg`},[`Dark`]),i=()=>{n.classList.toggle(`is-active`,this.scheme===`light`),r.classList.toggle(`is-active`,this.scheme===`dark`)};this.syncers.add(i),n.addEventListener(`click`,()=>this.setScheme(`light`)),r.addEventListener(`click`,()=>this.setScheme(`dark`)),i(),t.append(this.el(`span`,{class:`ed-segwrap`,role:`group`},[n,r]));let a=this.el(`button`,{type:`button`,class:`btn`},[`Copy CSS`]);a.addEventListener(`click`,()=>this.copy(this.serializeCSS()||`/* no overrides yet */`,a));let o=this.el(`button`,{type:`button`,class:`btn`},[`Copy JSON patch`]);if(o.addEventListener(`click`,()=>{let{patch:t,skipped:n}=this.buildPatch(e),r=n.length?`\n// runtime-only (no source mapping): ${n.join(`, `)}`:``;this.copy(JSON.stringify(t,null,2)+r,o)}),t.append(a,o),this.canSave){let n=this.el(`button`,{type:`button`,class:`cta`},[`Save to source`]);n.addEventListener(`click`,()=>void this.save(e,n)),t.append(n)}let s=this.el(`button`,{type:`button`,class:`btn ed-danger`},[`Reset all`]);return s.addEventListener(`click`,()=>this.reset()),t.append(s),t}build(e){let t=`ds-editor-panel`,n=this.el(`div`,{id:t,popover:`manual`,class:`drawer drawer--right drawer--modeless ed-panel bg-surface text-surface rhythm`,"aria-label":`Design-system editor`});n.append(this.el(`header`,{class:`cluster-between`},[this.el(`strong`,{class:`font-heading`},[`Theme editor`]),(()=>{let e=this.el(`button`,{type:`button`,class:`btn`,popovertarget:t,popovertargetaction:`hide`},[this.el(`span`,{class:`icon-mask ed-icon ed-icon--close`,"aria-hidden":`true`})]);return e.setAttribute(`aria-label`,`Close editor`),e})()]),this.buildToolbar(e),this.buildPalette(e),this.buildTypography(e),this.buildSpacing(e),this.buildLayout(),...this.buildPatterns(e));let r=this.el(`button`,{type:`button`,class:`cta ed-launch`,popovertarget:t},[this.el(`span`,{class:`icon-mask ed-icon ed-icon--theme`,"aria-hidden":`true`}),`Theme`]);document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.matches(`:popover-open`)&&n.hidePopover()}),this.append(r,n)}};customElements.get(`wc-theme-editor`)||customElements.define(`wc-theme-editor`,i);
|
|
1
|
+
const e=`vitops:ds-overrides`,t=(e,t)=>{let n=t.indexOf(`-`),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n+1);return i===`on`?`--color-${r}-on-${e}`:i.startsWith(`on-`)?`--color-${r}-on-${e}-${i.slice(3)}`:i?`--color-${r}-${e}-${i}`:`--color-${r}-${e}`},n=(e,t)=>`--color-${e}-${t}`;var r=class extends HTMLElement{constructor(...e){super(...e),this.store={light:/* @__PURE__ */ new Map,dark:/* @__PURE__ */ new Map},this.roles=/* @__PURE__ */ new Map,this.roleVars=/* @__PURE__ */ new Set,this.syncers=/* @__PURE__ */ new Set,this.scheme=`light`,this.canSave=!1}static{this.observedAttributes=[`manifest`,`save-endpoint`]}connectedCallback(){this.boot()}async boot(){let e=this.getAttribute(`manifest`)??`/vitops/design-manifest.json`;try{let t=await fetch(e);if(!t.ok)throw Error(`manifest ${t.status}`);this.manifest=await t.json()}catch(t){console.warn(`[wc-theme-editor] manifest unavailable at ${e} — editor disabled`,t);return}this.styleEl=document.createElement(`style`),this.styleEl.id=`ds-overrides`,document.head.append(this.styleEl),this.hydrate(),this.render(),this.scheme=this.currentScheme(),new MutationObserver(()=>{let e=this.currentScheme();e!==this.scheme&&(this.scheme=e,this.syncers.forEach(e=>e()))}).observe(document.documentElement,{attributes:!0,attributeFilter:[`data-theme`]}),this.canSave=await this.probeSave(),this.build(this.manifest)}get saveUrl(){return this.getAttribute(`save-endpoint`)??`/__vitops/design-system`}async probeSave(){try{let e=await fetch(this.saveUrl);return e.ok?(await e.json()).writable===!0:!1}catch{return!1}}block(e,t){return t.size?`${e} {\n${[...t].map(([e,t])=>` ${e}: ${t};`).join(`
|
|
2
|
+
`)}\n}\n`:``}serializeCSS(){return this.block(`:root`,this.store.light)+this.block(`:root[data-brx-theme="dark"], :root[data-theme="dark"]`,this.store.dark)}render(){this.styleEl&&(this.styleEl.textContent=this.serializeCSS())}persist(){try{localStorage.setItem(e,JSON.stringify({light:[...this.store.light],dark:[...this.store.dark],roles:[...this.roles]}))}catch{}}hydrate(){try{let t=localStorage.getItem(e);if(!t)return;let n=JSON.parse(t);this.store.light=new Map(n.light??[]),this.store.dark=new Map(n.dark??[]),this.roles=new Map(n.roles??[]);for(let[e,t]of this.roles)this.markRoleVars(e,t)}catch{}}setOverride(e,t,n){let r=this.store[n];t==null||t===``?r.delete(e):r.set(e,t),this.render(),this.persist()}effective(e,t,n){return this.store[t].has(e)?this.store[t].get(e):t===`dark`&&this.store.light.has(e)?this.store.light.get(e):n}reset(){this.store.light.clear(),this.store.dark.clear(),this.roles.clear(),this.roleVars.clear(),this.render(),this.persist(),this.syncers.forEach(e=>e())}currentScheme(){let e=document.documentElement;return e.dataset.theme===`dark`||e.getAttribute(`data-brx-theme`)===`dark`?`dark`:`light`}setScheme(e){this.scheme=e,document.documentElement.dataset.theme=e,document.documentElement.style.colorScheme=`${e} only`;try{localStorage.setItem(`vitops-color-scheme`,e)}catch{}this.syncers.forEach(e=>e())}roleTokenNames(e,n,r){let i=e.colors.roles.find(e=>e.name===n)?.kind??`chromatic`,a=e.colors.roleTokens?.[r]?.[i];if(!a)return[];let o=[];for(let e of[`light`,`dark`])for(let[r,i]of Object.entries(a[e]))o.push([t(n,r),i,e]);return o}markRoleVars(e,t){if(this.manifest)for(let[n]of this.roleTokenNames(this.manifest,e,t))this.roleVars.add(n)}remapRole(e,t,n){let r=this.roleTokenNames(e,t,n);if(r.length){this.roles.set(t,n);for(let[e,t,n]of r)this.roleVars.add(e),this.store[n].set(e,t);this.render(),this.persist()}}deepSet(e,t,n){let r=t.split(`.`),i=e;for(let e=0;e<r.length-1;e++){let t=r[e];(typeof i[t]!=`object`||i[t]==null)&&(i[t]={}),i=i[t]}i[r[r.length-1]]=n}buildPatch(e){let t={},n=[];for(let[n,r]of this.roles){let i=e.colors.roles.find(e=>e.name===n)?.kind??`chromatic`;this.deepSet(t,`colors.roles.${n}`,{hue:r,kind:i})}for(let[r,i]of this.store.light){if(this.roleVars.has(r))continue;let a=e.reverseIndex[r];a?this.deepSet(t,a,i):n.push(r)}for(let e of this.store.dark.keys())this.roleVars.has(e)||n.push(`${e} (dark)`);return{patch:t,skipped:n}}el(e,t={},n=[]){let r=document.createElement(e),i=r;for(let[e,n]of Object.entries(t))n===!0?i.setAttribute(e,``):n!==!1&&i.setAttribute(e,String(n));for(let e of n)i.append(e);return r}row(e,t,n){return this.syncers.add(n),n(),this.el(`label`,{class:`ed-row`},[this.el(`span`,{class:`ed-label`,title:e},[e.replace(/^--/,``)]),t])}textInput(e,t,n=``){let r=this.el(`input`,{type:`text`,class:`ed-input`});return r.addEventListener(`input`,()=>this.setOverride(e,r.value.trim()||null,this.scheme)),this.row(e,r,()=>{r.value=this.store[this.scheme].get(e)??``,r.placeholder=n||t})}colorInput(e,t){let n=this.el(`input`,{type:`color`,class:`ed-color`}),r=this.el(`input`,{type:`text`,class:`ed-input ed-mono`}),i=t=>this.setOverride(e,t||null,`light`);n.addEventListener(`input`,()=>{r.value=n.value,i(n.value)}),r.addEventListener(`input`,()=>{/^#[0-9a-f]{6}$/i.test(r.value)&&(n.value=r.value),i(r.value.trim())});let a=this.el(`span`,{class:`ed-pair`},[n,r]);return this.row(e,a,()=>{let i=this.store.light.get(e)??t;r.value=i,/^#[0-9a-f]{6}$/i.test(i)&&(n.value=i)})}selectInput(e,t){let n=this.el(`select`,{class:`ed-input`});n.append(this.el(`option`,{value:``},[`— default —`]));for(let e of t)n.append(this.el(`option`,{value:e.value},[e.label]));return n.addEventListener(`change`,()=>this.setOverride(e,n.value||null,this.scheme)),this.row(e,n,()=>{n.value=this.store[this.scheme].get(e)??``})}remSlider(e,t){let n=parseFloat(t)||1,r=this.el(`input`,{type:`range`,class:`ed-range`,min:`0`,max:String(Math.max(4,Math.ceil(n*2))),step:`0.05`}),i=this.el(`output`,{class:`ed-out`});r.addEventListener(`input`,()=>{i.textContent=`${r.value}rem`,this.setOverride(e,`${r.value}rem`,this.scheme)});let a=this.el(`span`,{class:`ed-pair`},[r,i]);return this.row(e,a,()=>{r.value=String(parseFloat(this.effective(e,this.scheme,t))||n),i.textContent=this.store[this.scheme].get(e)??`auto`})}slider(e,t){let n=t.toValue??(e=>`${e}${t.unit??``}`),r=t.fromValue??(e=>Number.isNaN(parseFloat(e))?t.def:parseFloat(e)),i=this.el(`input`,{type:`range`,class:`ed-range`,min:String(t.min),max:String(t.max),step:String(t.step)}),a=this.el(`output`,{class:`ed-out`});i.addEventListener(`input`,()=>{let t=n(parseFloat(i.value));a.textContent=t,this.setOverride(e,t,this.scheme)});let o=this.el(`span`,{class:`ed-pair`},[i,a]);return this.row(e,o,()=>{let n=this.store[this.scheme].get(e);i.value=String(n==null?t.def:r(n)),a.textContent=n??`${t.def}${t.unit??``}`})}section(e,t=!1){let n=this.el(`div`,{class:`ed-body rhythm`});return{details:this.el(`details`,{class:`details details--marker-start ed-section`,...t?{open:!0}:{}},[this.el(`summary`,{class:`font-eyebrow`},[this.el(`span`,{class:`icon-mask details-icon ed-icon ed-icon--caret`,"aria-hidden":`true`}),e]),n]),body:n}}hint(e){return this.el(`p`,{class:`font-footnote text-surface-x-muted`},[e])}sub(e){let t=this.section(e);return t.details.classList.add(`ed-sub`),t}buildPalette(e){let{details:t,body:r}=this.section(`Palette`,!0);r.append(this.hint(`Hues feed the semantic roles below — edit a hue and every role mapped to it follows. Hue edits are scheme-global: dark mode re-points which step a token reads, it never redefines the hue itself.`));for(let t of e.colors.ramps){let i=this.sub(t);for(let r of e.colors.steps){let a=e.colors.palette[t]?.[r];a!=null&&i.body.append(this.colorInput(n(t,r),a))}r.append(i.details)}return r.append(this.buildRoles(e)),t}buildRoles(e){let{details:t,body:n}=this.sub(`Semantic roles`);n.append(this.hint(`Point a role at another hue — its whole functional token set (bg / border / solid / on-solid / text) is rewritten for both appearances.`));let r=e.colors.ramps;for(let{name:t,ramp:i}of e.colors.roles){let a=this.el(`select`,{class:`ed-input`});for(let e of r)a.append(this.el(`option`,{value:e},[e]));a.addEventListener(`change`,()=>this.remapRole(e,t,a.value)),n.append(this.row(`--${t}`,a,()=>{a.value=this.roles.get(t)??i}))}return t}buildTypography(e){let{details:t,body:n}=this.section(`Typography`),r=Object.keys(e.fonts).map(e=>({label:e,value:`var(--font-${e})`})),i=e.scales.text.map(e=>({label:e.name,value:`var(--text-${e.name})`})),a={tt:[`none`,`uppercase`,`lowercase`,`capitalize`],fst:[`normal`,`italic`,`oblique`],td:[`none`,`underline`,`line-through`],tw:[`wrap`,`balance`,`pretty`,`nowrap`]};for(let t of e.typography.roles){let o=e.typography.specs[t]??{},s=this.sub(t);for(let[n,{key:c}]of Object.entries(e.typography.hooks)){let e=`--${t}-${n}`,l=o[c]==null?``:String(o[c]);n===`ff`?s.body.append(this.selectInput(e,r)):n===`fs`?s.body.append(this.selectInput(e,i)):a[n]?s.body.append(this.selectInput(e,a[n].map(e=>({label:e,value:e})))):s.body.append(this.textInput(e,l,l||c))}n.append(s.details)}return t}buildSpacing(e){let{details:t,body:n}=this.section(`Spacing`);n.append(this.hint(`Each step overrides to a fixed rem (dropping the fluid clamp). Reset restores it.`));for(let t of e.scales.space)n.append(this.remSlider(`--space-${t.name}`,t.max));return t}buildLayout(){let{details:e,body:t}=this.section(`Layout`);return t.append(this.hint(`Vertical rhythm and the .centered track widths — global structure.`)),t.append(this.slider(`--rhythm-scale`,{min:.5,max:2.5,step:.05,def:1}),this.slider(`--width-measure`,{min:30,max:110,step:1,def:65,unit:`ch`}),this.slider(`--width-breakout`,{min:40,max:150,step:1,def:90,unit:`ch`}),this.slider(`--width-spotlight`,{min:60,max:200,step:1,def:120,unit:`ch`}),this.slider(`--gutter`,{min:0,max:6,step:.1,def:3,toValue:e=>`clamp(1rem, 4cqi, ${e}rem)`,fromValue:e=>{let t=/clamp\([^,]+,[^,]+,\s*([\d.]+)/.exec(e);return t?parseFloat(t[1]):3}})),e}buildPatterns(e){let t=this.section(`Pattern defaults`);for(let[n,r]of Object.entries(e.patterns.defaults))t.body.append(this.textInput(`--${n}-default`,r,r));let n=this.section(`Radii`);for(let[t,r]of Object.entries(e.patterns.radii))n.body.append(this.textInput(`--br-${t}`,r,r));let r=this.section(`Pattern groups`);for(let[t,n]of Object.entries(e.patterns.groups)){let e=this.sub(t);for(let[r,i]of Object.entries(n))e.body.append(this.textInput(`--${r}-${t}`,i,i));r.body.append(e.details)}let i=this.section(`Components`);i.body.append(this.hint(`Per-component geometry — overrides the group defaults for just this component.`));for(let t of e.patterns.items){if(!t.wrappable.length)continue;let e=this.sub(t.name);for(let{sfx:n,prop:r}of t.wrappable){let i=t.base[r]??``;e.body.append(this.textInput(`--${n}-${t.name}`,i,i))}i.body.append(e.details)}let a=this.section(`Shadows`);for(let[t,n]of Object.entries(e.shadows))a.body.append(this.textInput(`--shadow-${t}`,n,n));return[t.details,n.details,r.details,i.details,a.details]}async flash(e,t){let n=e.textContent;e.textContent=t,setTimeout(()=>e.textContent=n,1400)}async copy(e,t){try{await navigator.clipboard.writeText(e),this.flash(t,`Copied ✓`)}catch{this.flash(t,`Copy failed`)}}async save(e,t){let{patch:n}=this.buildPatch(e);if(!Object.keys(n).length)return void this.flash(t,`Nothing to save`);try{let e=await fetch(this.saveUrl,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({patch:n})});if(!e.ok){let n=await e.json().catch(()=>({}));console.error(`[wc-theme-editor] save rejected`,n.errors??e.statusText),this.flash(t,`Rejected ✗`);return}this.reset(),this.flash(t,`Saved ✓`)}catch(e){console.error(`[wc-theme-editor] save failed`,e),this.flash(t,`Failed ✗`)}}buildToolbar(e){let t=this.el(`div`,{class:`cluster-start ed-toolbar`}),n=this.el(`button`,{type:`button`,class:`btn ed-seg`},[`Light`]),r=this.el(`button`,{type:`button`,class:`btn ed-seg`},[`Dark`]),i=()=>{n.classList.toggle(`is-active`,this.scheme===`light`),r.classList.toggle(`is-active`,this.scheme===`dark`)};this.syncers.add(i),n.addEventListener(`click`,()=>this.setScheme(`light`)),r.addEventListener(`click`,()=>this.setScheme(`dark`)),i(),t.append(this.el(`span`,{class:`ed-segwrap`,role:`group`},[n,r]));let a=this.el(`button`,{type:`button`,class:`btn`},[`Copy CSS`]);a.addEventListener(`click`,()=>this.copy(this.serializeCSS()||`/* no overrides yet */`,a));let o=this.el(`button`,{type:`button`,class:`btn`},[`Copy JSON patch`]);if(o.addEventListener(`click`,()=>{let{patch:t,skipped:n}=this.buildPatch(e),r=n.length?`\n// runtime-only (no source mapping): ${n.join(`, `)}`:``;this.copy(JSON.stringify(t,null,2)+r,o)}),t.append(a,o),this.canSave){let n=this.el(`button`,{type:`button`,class:`cta`},[`Save to source`]);n.addEventListener(`click`,()=>void this.save(e,n)),t.append(n)}let s=this.el(`button`,{type:`button`,class:`btn ed-danger`},[`Reset all`]);return s.addEventListener(`click`,()=>this.reset()),t.append(s),t}build(e){let t=`ds-editor-panel`,n=this.el(`div`,{id:t,popover:`manual`,class:`drawer drawer--right drawer--modeless ed-panel bg-surface text-surface rhythm`,"aria-label":`Design-system editor`});n.append(this.el(`header`,{class:`cluster-between`},[this.el(`strong`,{class:`font-heading`},[`Theme editor`]),(()=>{let e=this.el(`button`,{type:`button`,class:`btn`,popovertarget:t,popovertargetaction:`hide`},[this.el(`span`,{class:`icon-mask ed-icon ed-icon--close`,"aria-hidden":`true`})]);return e.setAttribute(`aria-label`,`Close editor`),e})()]),this.buildToolbar(e),this.buildPalette(e),this.buildTypography(e),this.buildSpacing(e),this.buildLayout(),...this.buildPatterns(e));let r=this.el(`button`,{type:`button`,class:`cta ed-launch`,popovertarget:t},[this.el(`span`,{class:`icon-mask ed-icon ed-icon--theme`,"aria-hidden":`true`}),`Theme`]);document.addEventListener(`keydown`,e=>{e.key===`Escape`&&n.matches(`:popover-open`)&&n.hidePopover()}),this.append(r,n)}};customElements.get(`wc-theme-editor`)||customElements.define(`wc-theme-editor`,r);
|