@getvitops/core 8.0.1 → 8.2.0-alpha.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 CHANGED
@@ -1,5 +1,164 @@
1
1
  # @getvitops/core
2
2
 
3
+ ## 8.2.0-alpha.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
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
34
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
35
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
36
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
37
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
38
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
39
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
40
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
41
+ `site.deployment.repo`/`branch` config fields.
42
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
43
+ wrapper over GitHub's Contents API.
44
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
45
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
46
+ route instead of the dev-only Vite endpoint.
47
+
48
+ ### Patch Changes
49
+
50
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
51
+ both projects' GA releases.
52
+
53
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
54
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
55
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
56
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
57
+ code here needed to change, only the declared range.
58
+
59
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
60
+ the version bump:
61
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
62
+ `css.format: 'tailwind'` without installing either, which threw on build.
63
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
64
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
65
+ the build never generated.
66
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
67
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
68
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
69
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
70
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
71
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
72
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
73
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
74
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
75
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
76
+ Uncomment it once scaffolded into its own repo.
77
+
78
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
79
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
80
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
81
+ now names the bad key instead of reporting a bare "Invalid input").
82
+
83
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
84
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
85
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
86
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
87
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
88
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
89
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
90
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
91
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
92
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
93
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
94
+ evolving default) and making the template a real pnpm workspace member (would change how its
95
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
96
+
97
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
98
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
99
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
100
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
101
+ compose with vite-plus's own `defineConfig`). A type-level shim
102
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
103
+ runtime `vp test` failure for this one file is still open.
104
+
105
+ ## 8.1.0
106
+
107
+ ### Minor Changes
108
+
109
+ - **Added — `.no-rhythm` utility (all formats).**
110
+
111
+ A `<ul>`/`<ol>` inside a `.rhythm` container picks up a top margin on every item but the first
112
+ (`.rhythm :where(ul, ol) > li + li`), which is correct for prose but wrong for a list whose item
113
+ spacing is already a `gap`-based grid or flex wrap the framework has no pattern for. `.gallery`,
114
+ `.tag-list`, `.cluster`, `.masonry`, `.comment-thread`, `.subgrid` and `.grid-auto` already
115
+ neutralise this automatically; `.no-rhythm` is the same escape hatch for a consumer's own custom
116
+ list — add it alongside your own class (`<ul class="my-cards no-rhythm">`) to opt out. It wins by
117
+ cascade layer (`vitops.utilities`, later than every pattern's `vitops.components`), the same
118
+ guarantee a colour utility uses to beat a pattern's own background — no selector tricks needed on
119
+ your end.
120
+
121
+ ### Patch Changes
122
+
123
+ - **Fixed — `.banner[hidden]` now actually hides (all formats).**
124
+
125
+ `.banner`'s own `display: flex` and the UA `[hidden] { display: none }` rule are both 0-1-0
126
+ specificity, and the author stylesheet wins that tie by source order — so `<p class="banner" hidden>`
127
+ rendered visibly instead of hiding. Added an explicit `.banner[hidden] { display: none }` override.
128
+ No markup change required.
129
+
130
+ - **Added — `vitops init --site` and `vitops setup`, the scripted client-onboarding path.**
131
+
132
+ `vitops init --site [--name] [--url]` scaffolds a starter `site.json` (the umbrella `Config`,
133
+ embedding a starter design system at `designSystem.themes.default`) instead of a bare
134
+ `design-system.json` — exported as `defaultSiteConfig()` from `@getvitops/generator`, alongside a
135
+ new `CONFIG_SCHEMA_LOCAL_PATH` and an optional `$schema` field on `ConfigSchema` so a scaffolded
136
+ config gets editor autocomplete the same way `design-system.json` already does.
137
+
138
+ `vitops setup <repo|hosting|domain|search|ads|all>` (new `@getvitops/utils/hosting` subpath: a
139
+ pure planner + `wrangler`/`gh`-shelling executors, following the same idempotent
140
+ plan/observe/apply split as `onboarding`/`ads`) automates GitHub repo + branch policy and
141
+ Cloudflare dev D1/R2 + repo secrets, and plans/checks the workers.dev-vs-custom-domain
142
+ consistency without ever applying the cutover itself — see AGENTS.md's new "Client onboarding"
143
+ section for the full design rationale. `search`/`ads` forward to the existing `search setup`/
144
+ `ads setup` subcommands rather than reimplementing them.
145
+
146
+ No consumer-facing change in `@getvitops/vite` or `@getvitops/astro` in this release — bumped
147
+ only because they are in the `fixed` release group with `generator`/`utils`/`cli`.
148
+ `@getvitops/core` does have a real fix in this release; see the separate `banner-hidden-fix`
149
+ changeset.
150
+
151
+ - **Fixed — five patterns rendered as `<ul>`/`<ol>` kept the UA list styles (all formats).**
152
+
153
+ `.gallery`, `.tag-list`, `.cluster`, `.masonry` and `.comment-thread` never neutralised the
154
+ browser's default list padding/margin/markers, so `<Gallery />`'s generated `<ul role="list">`
155
+ (and any hand-authored `<ul>` carrying one of the other four classes) rendered with a
156
+ 40px-ish inline padding and a bullet on every item. Each now resets `margin`/`padding`/
157
+ `list-style` behind a `:is(ul, ol)` gate — conditional so the class stays usable on a `div` —
158
+ and neutralises `.rhythm :where(ul, ol) > li + li`'s sibling margin the same way `.subgrid` and
159
+ `.grid-auto` already do, so an item inside a prose container doesn't pick up a stray top
160
+ margin that offsets the first grid cell alone. No markup change required.
161
+
3
162
  ## 8.0.1
4
163
 
5
164
  ## 8.0.0
@@ -11,7 +170,6 @@
11
170
 
12
171
  **Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
13
172
  contract changed):**
14
-
15
173
  - `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
16
174
  wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
17
175
  is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
@@ -52,7 +210,6 @@
52
210
  ### Minor Changes
53
211
 
54
212
  - Add Google Business Profile reviews: fetch, cache, render.
55
-
56
213
  - **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
57
214
  one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
58
215
  never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
@@ -87,7 +244,6 @@
87
244
  `--site` for now.)
88
245
 
89
246
  **Fixed:**
90
-
91
247
  - `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
92
248
  `@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
93
249
  being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
@@ -822,20 +978,28 @@ hidden` would pull it out of the accessible-name computation along with the visu
822
978
  {
823
979
  "designSystem": {
824
980
  "themes": {
825
- "default": {/* … */},
981
+ "default": {
982
+ /* … */
983
+ },
826
984
  },
827
985
  },
828
986
  "organization": {
829
987
  "name": "Acme",
830
988
  "contact": "hq",
831
989
  "locations": {
832
- "hq": {/* … */},
990
+ "hq": {
991
+ /* … */
992
+ },
833
993
  },
834
994
  },
835
995
  "site": {
836
996
  "defaultLocale": "en",
837
- "domains": {/* … */},
838
- "analytics": {/* … */},
997
+ "domains": {
998
+ /* … */
999
+ },
1000
+ "analytics": {
1001
+ /* … */
1002
+ },
839
1003
  },
840
1004
  }
841
1005
  ```
@@ -1073,8 +1237,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
1073
1237
 
1074
1238
  ```js
1075
1239
  vitops({
1076
- analytics: { googleAnalytics: "G-XXXXXXXXXX", plausible: "acme.com" },
1077
- consent: { policyUrl: "/legal/cookies" },
1240
+ analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
1241
+ consent: { policyUrl: '/legal/cookies' },
1078
1242
  });
1079
1243
  ```
1080
1244
 
@@ -1698,7 +1862,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1698
1862
  ```js
1699
1863
  // astro.config.mjs
1700
1864
  getvitops({
1701
- css: { input: "design-system.json", format: "css", out: "src/styles" },
1865
+ css: { input: 'design-system.json', format: 'css', out: 'src/styles' },
1702
1866
  editor: true,
1703
1867
  });
1704
1868
  ```
@@ -270,6 +270,19 @@
270
270
  }
271
271
  }
272
272
 
273
+ /* Opt out of `.rhythm`'s ambient `li + li` sibling margin (layout.css) for a list
274
+ whose item spacing is already the layout's job — a custom `gap`-based grid or
275
+ flex wrap the framework has no pattern for. Wins by LAYER (this file is
276
+ `vitops.utilities`, later than `.rhythm`'s `vitops.components`), the same
277
+ guarantee `bg-danger-muted` uses to beat `.card` — no specificity trick
278
+ needed, unlike every pattern-local `:is(ul, ol) > li + li` neutraliser above
279
+ and in patterns/*.css. The five framework list patterns (gallery, tag-list,
280
+ cluster, masonry, comment-thread) already do this automatically and don't
281
+ need this class; reach for it on markup the framework doesn't own. */
282
+ .no-rhythm > li + li {
283
+ margin-top: 0;
284
+ }
285
+
273
286
  /* ── Flex direction (display + direction; tiered classes set both) ── */
274
287
  .flex {
275
288
  display: flex;
package/css/layout.css CHANGED
@@ -133,6 +133,37 @@
133
133
  .rhythm > :where(ul, ol) + p {
134
134
  margin-top: calc(var(--rhythm-base) * var(--rhythm-list-p));
135
135
  }
136
+ /* The only DESCENDANT rule in this block (every other rule above is `>`-scoped to
137
+ direct children) — it reaches into a list at any depth, including one nested
138
+ inside a pattern several levels down. That is deliberate: it is the correct
139
+ default for prose, and prose lists can be arbitrarily nested.
140
+
141
+ It is also 0-1-2, not 0-1-0 like the rest of this file's `:where()`-guarded
142
+ rules — `li + li` sits OUTSIDE the `:where()`, so it carries real weight. A
143
+ pattern whose item spacing is already its own job (a `gap`-based grid or flex
144
+ wrap: `.gallery`, `.tag-list`, `.cluster`, `.masonry`, `.comment-thread`,
145
+ `.subgrid`, `.grid-auto`) neutralises this LOCALLY, in its own partial, with
146
+ `&:is(ul, ol) > li + li { margin-block-start: 0; }` — `:is(ul, ol)` pushes it
147
+ to 0-1-3, safely past this rule's 0-1-2 regardless of source order. Consumer
148
+ markup gets the same escape hatch as a public utility: `.no-rhythm`
149
+ (layout-utilities.css), which wins by LAYER instead, since it lives in
150
+ `vitops.utilities`.
151
+
152
+ Two alternatives were considered and rejected, worth recording so neither is
153
+ re-proposed:
154
+ - Centralising the exclusion HERE, as `:where(ul, ol):not(:where(.gallery,
155
+ .subgrid, …))`. Rejected: it re-opens exactly the registry the heading rules
156
+ above were rewritten to delete (see the comment there) — an enumeration that
157
+ is always one pattern short, unlike a closed set like "what a heading is".
158
+ It also fails SILENTLY (a missing name renders a stray margin, nothing
159
+ errors) where the local form fails a build (`bundle-layers.test.ts`'s
160
+ per-pattern drift guard), and this file's `.rhythm` block is hand-mirrored
161
+ into a tailwind template literal (`generate.ts`) that has already drifted
162
+ once — a second selector list to keep in step, not a cleanup.
163
+ - Zeroing `--rhythm-li-li` locally instead of overriding `margin-top`. It
164
+ inherits (declared on `:root`), so it would also zero the rhythm of any
165
+ PROSE list nested inside the opted-out pattern — losing the exact behaviour
166
+ the descendant combinator above exists to provide. */
136
167
  .rhythm :where(ul, ol) > li + li {
137
168
  margin-top: calc(var(--rhythm-base) * var(--rhythm-li-li));
138
169
  }
@@ -62,3 +62,10 @@
62
62
  flex-shrink: 0;
63
63
  }
64
64
  }
65
+
66
+ /* `.banner`'s own `display: flex` and the UA `[hidden] { display: none }` are
67
+ both 0-1-0 — a tie the author sheet wins on source order, so a hidden
68
+ banner would render anyway. Name `[hidden]` explicitly so it always wins. */
69
+ .banner[hidden] {
70
+ display: none;
71
+ }
@@ -13,6 +13,25 @@
13
13
  gap: var(--cluster-gap, 0.5rem);
14
14
  }
15
15
 
16
+ /* Markerless — so ADD `role="list"` to the element. Safari + VoiceOver stop
17
+ announcing a marker-less <ul> as a list, so this reset silently costs the
18
+ semantics the <ul> was chosen for. Nothing in the framework emits this markup,
19
+ so only the author can restore it. Conditional so `.cluster` on a `div` is
20
+ untouched; scoped to `.cluster` alone — the `cluster-*` attribute selectors above
21
+ are alignment modifiers, not the list itself. */
22
+ .cluster:is(ul, ol) {
23
+ margin: 0;
24
+ padding: 0;
25
+ list-style: none;
26
+ }
27
+
28
+ /* Item spacing is the wrap's job, not the flow's — neutralise `.rhythm`'s
29
+ `li + li` margin (see layout.css for why this is local rather than
30
+ centralised). */
31
+ .cluster:is(ul, ol) > li + li {
32
+ margin-block-start: 0;
33
+ }
34
+
16
35
  .cluster-start-start {
17
36
  place-content: start start;
18
37
  }
@@ -85,4 +85,22 @@
85
85
  display: flex;
86
86
  flex-direction: column;
87
87
  gap: 1.5rem;
88
+
89
+ /* Markerless — so ADD `role="list"` to the element. Safari + VoiceOver stop
90
+ announcing a marker-less <ul> as a list, so this reset silently costs the
91
+ semantics the <ul> was chosen for. Nothing in the framework emits this markup,
92
+ so only the author can restore it. Conditional so `.comment-thread` on a `div`
93
+ is untouched. */
94
+ &:is(ul, ol) {
95
+ margin: 0;
96
+ padding: 0;
97
+ list-style: none;
98
+ }
99
+
100
+ /* Item spacing is the flex `gap` above's job, not the flow's — neutralise
101
+ `.rhythm`'s `li + li` margin (see layout.css for why this is local rather
102
+ than centralised). */
103
+ &:is(ul, ol) > li + li {
104
+ margin-block-start: 0;
105
+ }
88
106
  }
@@ -27,6 +27,22 @@ wc-gallery {
27
27
  display: grid;
28
28
  grid-template-columns: repeat(auto-fill, minmax(var(--gallery-thumb-size, 150px), 1fr));
29
29
  gap: var(--gallery-gap, 0.5rem);
30
+
31
+ /* Gallery.astro emits this as `<ul role="list">`, so the UA's list padding, block
32
+ margin and markers all apply. The pattern owns no outer spacing — the context it
33
+ sits in does. Conditional so `.gallery` on a `div` is untouched. */
34
+ &:is(ul, ol) {
35
+ margin: 0;
36
+ padding: 0;
37
+ list-style: none;
38
+ }
39
+
40
+ /* Item spacing is the grid's job, not the flow's — neutralise `.rhythm`'s
41
+ `li + li` margin (see layout.css for why this is local rather than
42
+ centralised). */
43
+ &:is(ul, ol) > li + li {
44
+ margin-block-start: 0;
45
+ }
30
46
  }
31
47
 
32
48
  .gallery__item {
@@ -244,6 +244,23 @@
244
244
  align-items: center;
245
245
  font-size: var(--_fs);
246
246
 
247
+ /* Markerless — so ADD `role="list"` to the element. Safari + VoiceOver stop
248
+ announcing a marker-less <ul> as a list, so this reset silently costs the
249
+ semantics the <ul> was chosen for. Nothing in the framework emits this markup,
250
+ so only the author can restore it. Conditional so `.tag-list` on a `div` is
251
+ untouched — `--_p` already overrides the UA padding either way. */
252
+ &:is(ul, ol) {
253
+ margin: 0;
254
+ list-style: none;
255
+ }
256
+
257
+ /* Item spacing is the wrap's job, not the flow's — neutralise `.rhythm`'s
258
+ `li + li` margin (see layout.css for why this is local rather than
259
+ centralised). */
260
+ &:is(ul, ol) > li + li {
261
+ margin-block-start: 0;
262
+ }
263
+
247
264
  .tag-list__input {
248
265
  flex: 1;
249
266
  min-inline-size: 6rem;
@@ -17,6 +17,12 @@
17
17
  * `100% + gap`, because the seam between copies is one more gap that has to be
18
18
  * consumed for the loop to be seamless — translate by a bare `-100%` and the
19
19
  * items visibly bunch up once per revolution.
20
+ *
21
+ * `Marquee.astro` authors THREE `.marquee__content` copies (the slotted one
22
+ * plus two `aria-hidden`), not two — a very wide track under the 100% floor
23
+ * needs more than one duplicate to stay covered. `<wc-marquee>` treats the
24
+ * first as its source and discards the rest, replacing them with however many
25
+ * its own measurement calls for.
20
26
  */
21
27
 
22
28
  .marquee {
@@ -7,6 +7,24 @@
7
7
  .masonry {
8
8
  columns: var(--masonry-columns, 3);
9
9
 
10
+ /* Markerless — so ADD `role="list"` to the element. Safari + VoiceOver stop
11
+ announcing a marker-less <ul> as a list, so this reset silently costs the
12
+ semantics the <ul> was chosen for. Nothing in the framework emits this markup,
13
+ so only the author can restore it. Conditional so `.masonry` on a `div` is
14
+ untouched. */
15
+ &:is(ul, ol) {
16
+ margin: 0;
17
+ padding: 0;
18
+ list-style: none;
19
+ }
20
+
21
+ /* Item spacing is the columns/masonry layout's job, not the flow's —
22
+ neutralise `.rhythm`'s `li + li` margin (see layout.css for why this is
23
+ local rather than centralised). */
24
+ &:is(ul, ol) > li + li {
25
+ margin-block-start: 0;
26
+ }
27
+
10
28
  > * {
11
29
  break-inside: avoid;
12
30
  }
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`?!0:e.choices[t]===!0}function d(e,t){return t===`necessary`?!0: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=new Set,b=new WeakSet,x=new Set,S=new Set,C=new Map,w=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=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=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.#p),this.addEventListener(`submit`,this.#o),document.addEventListener(n,this.#a),this.#e=e.subscribe(()=>{this.#n&&this.#t===`settings`?this.#f():e.needed()?this.#u(`prompt`):this.#d()})}disconnectedCallback(){this.removeEventListener(`click`,this.#p),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.#f(),this.hidden=!1,this.#n=!0;let t=this.#i;t?.isConnected&&!t.matches(`:popover-open`)&&t.showPopover()}#d(){this.#n=!1,this.#i?.hidePopover?.(),this.hidden=!0}#f(){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)}#p=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.#m(r,()=>!0)):n.hasAttribute(`data-consent-reject`)?t.set(this.#m(r,()=>!1)):t.set(this.#m(r,e=>this.#c(e)?.checked===!0)),this.#d()};#m(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};
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.#p),this.addEventListener(`submit`,this.#o),document.addEventListener(n,this.#a),this.#e=e.subscribe(()=>{this.#n&&this.#t===`settings`?this.#f():e.needed()?this.#u(`prompt`):this.#d()})}disconnectedCallback(){this.removeEventListener(`click`,this.#p),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.#f(),this.hidden=!1,this.#n=!0;let t=this.#i;t?.isConnected&&!t.matches(`:popover-open`)&&t.showPopover()}#d(){this.#n=!1,this.#i?.hidePopover?.(),this.hidden=!0}#f(){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)}#p=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.#m(r,()=>!0)):n.hasAttribute(`data-consent-reject`)?t.set(this.#m(r,()=>!1)):t.set(this.#m(r,e=>this.#c(e)?.checked===!0)),this.#d()};#m(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=`/__vitops/design-system`,n=(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}`},r=(e,t)=>`--color-${e}-${t}`;var i=class extends HTMLElement{constructor(...e){super(...e),this.store={light:new Map,dark:new Map},this.roles=new Map,this.roleVars=new Set,this.syncers=new Set,this.scheme=`light`,this.canSave=!1}static{this.observedAttributes=[`manifest`]}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)}async probeSave(){try{let e=await fetch(t);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,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);