@getvitops/generator 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,176 @@
1
1
  # @getvitops/generator
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **The `tailwind` format now emits the full role colour vocabulary.** It previously emitted only
8
+ the functional role utilities, so 87 classes — `bg-<role>-x-muted`, `bg-<role>-bold`,
9
+ `bg-<role>-x-bold`, `text-<role>-bold`, `text-<role>-x-bold`,
10
+ `border-<role>-{muted,x-muted,x-bold}`, for every role — existed in the `css` and `bricks`
11
+ outputs and silently did nothing in `tailwind`. Nothing in the test suite built the tailwind
12
+ format, so the divergence was invisible; it was found by a consumer who hand-forked four
13
+ background planes into their own stylesheet to work around the absence.
14
+
15
+ All three formats now render from one emitter (`roleColorUtilities()`), which resolves the
16
+ plane-vs-stop namespace collision explicitly instead of relying on the CSS minifier dropping a
17
+ shadowed rule. No class changes meaning, and the `css`/`bricks` output is unchanged.
18
+
19
+ Also fixed in the tailwind format:
20
+ - `colors.utilities` is now honoured. It was hardcoded to `bg`/`text`/`border`, so enabling
21
+ `outline`/`fill`/`stroke` worked in `css`/`bricks` and was ignored here. (For raw hue scales
22
+ it remains a floor rather than a ceiling — those are `@theme` colours, and Tailwind derives
23
+ every colour family from them on demand.)
24
+ - **Component container queries are no longer stripped.** The pass that drops the framework's
25
+ pre-expanded `md-*` breakpoint utilities (Tailwind regenerates those as `@md:`) matched every
26
+ `@container (min-width: …)` block, including component behaviour — most visibly the
27
+ sitenav's, so `.sitenav--bp-{sm,md,lg,xl}` were removed and the nav stayed in its mobile
28
+ layout at every width.
29
+
30
+ New: **`vitops lint`** reports framework classes in your source that resolve to nothing — the
31
+ failure mode where an unknown utility looks exactly like a working one. It is format-aware
32
+ (`md-flex-row` is a real class in `css`/`bricks` and inert in `tailwind`) and only judges
33
+ classes anchored to your own config, so it stays quiet on Tailwind's utilities and your own.
34
+
35
+ ```
36
+ vitops lint --format tailwind --src src
37
+ ```
38
+
39
+ - **The `css` and `bricks` bundles now ship cascade layers, so a utility can override a pattern.**
40
+
41
+ `class="card bg-danger-muted"` previously tinted the card in the `tailwind` format and silently
42
+ did nothing in `css`/`bricks`: every colour utility was emitted before every pattern and both sat
43
+ at `0-1-0`, so the pattern won on source order. The bundle now emits
44
+
45
+ ```css
46
+ @layer vitops.base, vitops.components, vitops.utilities;
47
+ ```
48
+
49
+ - `vitops.base` — the UA reset and the pure `:root` token blocks
50
+ - `vitops.components` — the animation engine, structural layout, and every pattern
51
+ - `vitops.utilities` — `bg-*`, `text-*`, `border-*`, `drop-shadow-*`, `font-*`, animation
52
+ effects, and the display/`sr-only` families
53
+
54
+ No rule changed — 1482 rules before, 1482 after — only precedence.
55
+
56
+ **What this changes for you.** Unlayered CSS beats every cascade layer regardless of specificity,
57
+ so your own stylesheet, an Astro scoped `<style>`, or a Bricks-authored class now overrides the
58
+ framework with no `!important` and no specificity escalation. That is the intended override story
59
+ and the reason to layer at all.
60
+
61
+ **Migration — only if you ship a reset.** An unlayered reset will now beat the framework
62
+ component rules it used to lose to; a bare `p { margin: 0 }` defeats `.rhythm`. Put it in a layer
63
+ and declare the order **before** the stylesheet loads:
64
+
65
+ ```html
66
+ <style>
67
+ @layer my.reset, vitops.base, vitops.components, vitops.utilities;
68
+ </style>
69
+ <link rel="stylesheet" href="/styles.css" />
70
+ ```
71
+
72
+ Declaring it _after_ the link makes `my.reset` a name introduced later, which sorts **last** —
73
+ highest priority — and the reset wins anyway. That ordering rule is the one non-obvious step.
74
+
75
+ The `tailwind` format is byte-identical. Its utilities are `@utility` definitions, which Tailwind
76
+ already places in a layer above `components` and which are what make `hover:`/`@md:` variants
77
+ work; wrapping them ourselves would win the cascade and lose the variants.
78
+
79
+ Known gap: `layout.css` mixes structural rules (`.rhythm`, `.centered`) with utilities (`.m-*`,
80
+ `.flex`, `.split-*`) in one partial, so it sits in `vitops.components` whole and its utility half
81
+ cannot yet override a pattern. Splitting it is tracked separately.
82
+
83
+ ### Patch Changes
84
+
85
+ - **Fixed: `vitops init` and `vp create` scaffolded configs referencing tokens that no longer
86
+ exist.** `defaultConfig()` and the EmDash template still pointed at `--color-surface-xl` and
87
+ `--color-surface-xxl`, aliases from the named-step colour scale removed in 0.6. A scaffolded
88
+ project therefore got a `card` with no background and an invalid default border colour, with
89
+ nothing reporting it. Both now use `--surface-bg` / `--surface-bg-muted`. The EmDash template
90
+ also dropped its `patterns.radii.card` key, which collided with the `card` pattern on
91
+ `--br-card` — the collision `validate()` has been warning about (the `panel` group already
92
+ supplies the same radius, so nothing changes visually).
93
+
94
+ **Fixed: `.text-reveal` rendered invisible text.** Its gradient consumed
95
+ `--text-reveal-color-from`/`-to` with no defaults, so unless a consumer set both, the `var()`
96
+ substitution failed, `background` became invalid at computed-value time, and the accompanying
97
+ `color: transparent` left the text with no way to render. Both tones now default to the
98
+ functional text tokens.
99
+
100
+ **Fixed: `.bordered` fell back to `currentColor`** via a reference to `--color-surface-xl`,
101
+ which the generator never emitted. It now resolves `--surface-border`.
102
+
103
+ **New: `validate()` warns when a required role is missing.** `colors.roles` is an open map —
104
+ any name works and generates a full token set — but the shipped component CSS references
105
+ `brand-primary`, `danger`, `neutral`, `surface`, `ui-primary` and `warning` with no fallback,
106
+ so omitting one leaves those components uncoloured. This is a warning, not an error; the
107
+ required list is re-derived from the CSS partials by a test so it cannot drift.
108
+
109
+ `vitops docs` and `vitops agents` now surface config warnings (on stderr, so piping `docs` is
110
+ unaffected). They previously discarded them, unlike `generate` and `validate`.
111
+
112
+ Docs corrections, all in the generated bundle:
113
+ - **Raw scale classes are frozen and do not remap in dark mode** — stated explicitly for the
114
+ first time, with a migration table to the role equivalents. The dark-mode guarantee only ever
115
+ applied to functional role tokens, and nothing said so.
116
+ - **Roles are extensible over a required core** — the schema description and class reference
117
+ read as a closed enumeration, which is why a consumer forked their own colour layer rather
118
+ than adding a role.
119
+ - **The `md:` / `@md:` / `md-` distinction** in the tailwind format: `@md:` uses the framework's
120
+ breakpoints, `md:` works but uses Tailwind's (which differ — `sm:` is 40rem, `@sm:` is 30rem),
121
+ and `md-` is silently inert. Plus a note that registering `--container-*` also re-points
122
+ Tailwind's `max-w-*` scale.
123
+ - The css and bricks bundles now carry a `/*!` banner pointing at `npx vitops docs classes`
124
+ (the previous plain comment was stripped by the minifier, so it never reached the file).
125
+
126
+ Contrast checking now covers **every** background plane a role emits (`bg`, `bg-muted`, and
127
+ `bg-bold`), not just `bg` — body text on a `card` was previously unguaranteed.
128
+
129
+ - Docs now name both dark-mode attributes. The generated colour docs still described the
130
+ dark flip as hanging off `:root[data-brx-theme="dark"]` alone, from before the selector
131
+ also matched `:root[data-theme="dark"]` (the attribute `<color-scheme-toggle>` writes) —
132
+ so a non-Bricks consumer reading `vitops docs color` would wire up an attribute that
133
+ nothing matches. The selector moved into `shared.ts` as `DARK_SEL` (also exported from
134
+ the package index) and the docs interpolate it, so the two can't drift again.
135
+ - c949cae: `vitops init` no longer scaffolds a config that warns on its own output.
136
+
137
+ `defaultConfig()` declared `patterns.radii.card`, a key named after the `card` pattern — the exact
138
+ collision `validate()` started reporting in 0.8.0. The primitive won `--br-card`, shadowing the
139
+ pattern's override hook and leaving its `--br-card-group` alias unreachable, so a scaffolded project
140
+ was warned about a config it had just been given.
141
+
142
+ The key is dropped. This is **value-preserving**: `card` belongs to the `panel` group, which already
143
+ carries the same `0.5rem`, so `--br-card-group` → `--br-panel` → `0.5rem` renders identically.
144
+
145
+ **Migration:** none for existing configs — your `design-system.json` is not touched. If you copied
146
+ the scaffold and reference `var(--br-card)` directly in hand-written CSS, note it is now an
147
+ _override hook_ (undefined until you set it) rather than a defined primitive; read `--br-card-group`
148
+ instead, or keep your own `radii` key under a name no pattern uses.
149
+
150
+ - **The theme editor no longer dims the page it's editing.**
151
+
152
+ The panel used `.drawer`'s scrim and `popover="auto"`, which fought the whole purpose of a live
153
+ editor: the page you were tuning sat behind a 4px blur, and the first click on it dismissed the
154
+ panel. It is now modeless — no scrim, and clicking through to the page keeps it open. Escape still
155
+ closes it (wired explicitly, since `popover="manual"` doesn't provide it), as does the × button.
156
+
157
+ **New: `.drawer--modeless`.** The same treatment is available to any drawer — a side panel you work
158
+ _alongside_ rather than through (an inspector, a live editor, a filter rail). Pair it with
159
+ `popover="manual"`:
160
+
161
+ ```html
162
+ <div class="drawer drawer--right drawer--modeless" popover="manual">…</div>
163
+ ```
164
+
165
+ An `auto` popover light-dismisses on the first outside click, which is exactly the interaction a
166
+ modeless panel exists to permit — so `manual` is part of the pattern, not an afterthought. It also
167
+ drops Escape, so give the panel a visible close control.
168
+
169
+ **Added:** a drift guard tying `<wc-theme-editor>`'s dark-override selector to the generator's
170
+ `DARK_SEL`. The two must match or the editor's dark-mode edits land on a rule the page never
171
+ matches — a failure that is invisible in light mode, so nothing would have caught it.
172
+ - @getvitops/utils@0.9.0
173
+
3
174
  ## 0.8.0
4
175
 
5
176
  ### Minor Changes
@@ -303,6 +303,16 @@
303
303
  failure leaves readable text rather than blank space. Scroll-timeline is also
304
304
  guarded — without it the text simply shows the final (to) color, still legible. */
305
305
  .text-reveal {
306
+ /* Both tones MUST default, and NOT to currentColor. They are consumed by the
307
+ gradient below, where a var() substitution failure makes `background`
308
+ invalid at computed-value time — which, with the `color: transparent` in
309
+ that same block, renders the text completely invisible rather than
310
+ degrading. currentColor is unusable there for the same reason: inside the
311
+ @supports block it resolves to that transparent value. So default to the
312
+ functional text tokens (they flip with the appearance) with a literal
313
+ backstop, since this is precisely the declaration that must never fail. */
314
+ --_reveal-from: var(--text-reveal-color-from, var(--neutral-text-x-muted, #9099a8));
315
+ --_reveal-to: var(--text-reveal-color-to, var(--neutral-text, #05070d));
306
316
  /* Fallback baseline: ordinary visible text in the start color. */
307
317
  color: var(--text-reveal-color-from, currentColor);
308
318
  }
@@ -313,8 +323,8 @@
313
323
  --text-reveal-background,
314
324
  linear-gradient(
315
325
  var(--text-reveal-slant, 90deg),
316
- var(--text-reveal-color-to) 50%,
317
- var(--text-reveal-color-from) 50%
326
+ var(--_reveal-to) 50%,
327
+ var(--_reveal-from) 50%
318
328
  )
319
329
  );
320
330
  background-size: 200% 100%;
@@ -37,15 +37,31 @@ details,
37
37
  rotate: var(--_details-icon-rotate, 90deg);
38
38
  }
39
39
 
40
+ /**
41
+ * Collapse without a transition — deliberately.
42
+ *
43
+ * This carried the canonical open/close animation (`block-size: 0 → auto` with
44
+ * `content-visibility` under `allow-discrete`, backed by the
45
+ * `interpolate-size: allow-keywords` in utilities.css). In Chrome 149 that
46
+ * deadlocks: `content-visibility` stays `hidden`, so the content has no layout,
47
+ * so `auto` resolves to `0`, so the transition has nothing to run — and the
48
+ * disclosure never opens *at all*. Not "opens without animating": every
49
+ * `<details>` on the page becomes inert, which is a total content-loss bug.
50
+ *
51
+ * Verified against a bare `<details>` with only this stylesheet, and bisected:
52
+ * `transition: none` is the only thing that restores it. Transitioning only
53
+ * `block-size`, only `content-visibility`, `calc-size(auto, size)`, and the
54
+ * `grid-template-rows: 0fr → 1fr` technique all fail the same way.
55
+ *
56
+ * So: correctness over motion. Re-add the transition only with a live
57
+ * cross-browser check that a first click actually expands — a passing
58
+ * `@supports` query is not evidence, since the syntax is valid and supported;
59
+ * it's the interaction that breaks.
60
+ */
40
61
  &::details-content {
41
62
  display: block;
42
63
  overflow: hidden;
43
64
  block-size: 0;
44
- opacity: 0;
45
- transition:
46
- block-size 0.3s ease,
47
- opacity 0.3s ease,
48
- content-visibility 0.3s allow-discrete;
49
65
  }
50
66
 
51
67
  > summary + * {
@@ -62,13 +78,16 @@ details,
62
78
 
63
79
  &[open]::details-content {
64
80
  block-size: auto;
65
- opacity: 1;
66
81
  }
67
82
 
83
+ /* Leading marker. The default (trailing) icon uses `margin-inline-start: auto`
84
+ to push itself to the end; the mirrored `margin-inline-end: auto` here did
85
+ the same thing to everything *after* it, so the label got shoved to the
86
+ opposite edge and the variant read as right-aligned text. A leading marker
87
+ just wants both auto margins gone — `gap` on the summary does the spacing. */
68
88
  &.details--marker-start > summary > .details-icon {
69
89
  order: -1;
70
- margin-inline-start: 0;
71
- margin-inline-end: auto;
90
+ margin-inline: 0;
72
91
  }
73
92
 
74
93
  /* Plus/minus variant — toggle two sibling icon elements */
@@ -24,6 +24,21 @@
24
24
  backdrop-filter: blur(4px);
25
25
  }
26
26
 
27
+ /**
28
+ * Modeless — a panel you work *alongside* rather than through. Drops the
29
+ * scrim so the page behind stays legible, which is the whole point for a
30
+ * live editor or an inspector: you're watching what your edits do.
31
+ *
32
+ * Pair with `popover="manual"`. An `auto` popover light-dismisses on the
33
+ * first click outside — i.e. exactly the interaction a modeless panel exists
34
+ * to allow — and `manual` also drops Esc, so give it an explicit close
35
+ * control (and wire Esc yourself if the panel warrants it).
36
+ */
37
+ &.drawer--modeless::backdrop {
38
+ background: none;
39
+ backdrop-filter: none;
40
+ }
41
+
27
42
  &,
28
43
  &.drawer--left {
29
44
  inset-block: 0;
@@ -9,7 +9,7 @@
9
9
  --_bs: var(--bs, solid);
10
10
  border-width: var(--_bw);
11
11
  border-style: var(--_bs);
12
- border-color: var(--bc, var(--color-surface-xl, currentColor));
12
+ border-color: var(--bc, var(--surface-border, currentColor));
13
13
  }
14
14
 
15
15
  /* ── Group — collapse border-radius (+ shared borders) across direct children ─
@@ -3,51 +3,97 @@
3
3
  * Consumes the generated token cascade (src/css/generated/tokens.css).
4
4
  */
5
5
 
6
- /* ── Subgrid — content-aligned cards/tables via CSS subgrid ────────────────── */
6
+ /* ── Subgrid — content-aligned cards/tables via CSS subgrid ────────────────────
7
+ *
8
+ * Each item spans `--subgrid-row-span` row tracks of the parent and re-declares
9
+ * them with `grid-template-rows: subgrid`, so the tranches inside every item —
10
+ * head, body, footer — land on the same row lines no matter how much content
11
+ * each one holds.
12
+ *
13
+ * <ul class="subgrid" style="--subgrid-cols: 3; --subgrid-row-span: 3">
14
+ *
15
+ * Public custom properties:
16
+ * --subgrid-cols columns (default 3)
17
+ * --subgrid-row-span row tracks each item spans, i.e. tranches per item (2)
18
+ * --subgrid-gap the grid gap — between columns, and between tranches (1.5rem)
19
+ * --subgrid-row-gap EXTRA space between wrapped rows of items (0); see below
20
+ */
7
21
  .subgrid {
8
- --_items-per-row: var(--items-per-row, 3);
9
- --_rows-per-item: var(--rows-per-item, 2);
10
- --_subgrid-gap: var(--subgrid-gap, 1.5rem);
22
+ --_cols: var(--subgrid-cols, 3);
23
+ --_row-span: var(--subgrid-row-span, 2);
24
+ --_gap: var(--subgrid-gap, 1.5rem);
25
+ --_row-gap: var(--subgrid-row-gap, 0px);
11
26
 
12
27
  display: grid;
13
- grid-template-columns: repeat(var(--_items-per-row), 1fr);
14
- gap: var(--_subgrid-gap);
28
+ grid-template-columns: repeat(var(--_cols), 1fr);
29
+ gap: var(--_gap);
15
30
 
16
31
  > * {
17
- grid-row: span var(--_rows-per-item);
32
+ grid-row: span var(--_row-span);
18
33
  display: grid;
19
34
  grid-template-rows: subgrid;
20
35
  }
21
36
 
37
+ /* Spacing between items is the grid's job, not the flow's — neutralise the
38
+ `.rhythm :where(ul, ol) > li + li` margin, which reaches into any list
39
+ inside a prose container and outweighs `> *` on specificity. */
40
+ &:is(ul, ol) > li + li {
41
+ margin-block-start: 0;
42
+ }
43
+
44
+ /* The grid's own row gap is the gap between an item's *tranches*, so it can't
45
+ also be the gap between *rows of items* — items past the first row take an
46
+ extra margin instead. Computed in CSS rather than from a server-rendered
47
+ index because --_cols can change in a media query. Guarded the same way as
48
+ .stagger (animation.css): sibling-index() lacks only Firefox as of 2026,
49
+ and where it's missing the margin stays 0 and rows fall back to the grid
50
+ gap, rather than producing an invalid margin. The second selector repeats
51
+ the rule at the specificity rhythm forces above. */
52
+ @supports (--x: sibling-index()) {
53
+ > *,
54
+ &:is(ul, ol) > li + li {
55
+ --_row-index: round(down, calc((sibling-index() - 1) / var(--_cols)), 1);
56
+ margin-block-start: calc(clamp(0, var(--_row-index), 1) * var(--_row-gap));
57
+ }
58
+ }
59
+
60
+ /* Markerless when it's a list — a subgrid of cards is a list of things, and
61
+ the markers would sit in the gutter. */
62
+ &:is(ul, ol) {
63
+ list-style: none;
64
+ margin: 0;
65
+ padding: 0;
66
+ }
67
+
22
68
  &.subgrid-cols-1 {
23
- --_items-per-row: 1;
69
+ --_cols: 1;
24
70
  }
25
71
  &.subgrid-cols-2 {
26
- --_items-per-row: 2;
72
+ --_cols: 2;
27
73
  }
28
74
  &.subgrid-cols-3 {
29
- --_items-per-row: 3;
75
+ --_cols: 3;
30
76
  }
31
77
  &.subgrid-cols-4 {
32
- --_items-per-row: 4;
78
+ --_cols: 4;
33
79
  }
34
80
 
35
81
  &.subgrid-rows-2 {
36
- --_rows-per-item: 2;
82
+ --_row-span: 2;
37
83
  }
38
84
  &.subgrid-rows-3 {
39
- --_rows-per-item: 3;
85
+ --_row-span: 3;
40
86
  }
41
87
  &.subgrid-rows-4 {
42
- --_rows-per-item: 4;
88
+ --_row-span: 4;
43
89
  }
44
90
 
45
91
  &.subgrid-responsive {
46
92
  @media (width < 60rem) {
47
- --_items-per-row: min(var(--items-per-row, 3), 2);
93
+ --_cols: min(var(--subgrid-cols, 3), 2);
48
94
  }
49
95
  @media (width < 40rem) {
50
- --_items-per-row: 1;
96
+ --_cols: 1;
51
97
  }
52
98
  }
53
99
  }
@@ -38,6 +38,14 @@ wc-theme-editor {
38
38
  }
39
39
  .ed-section > summary {
40
40
  padding-block: 0.35rem;
41
+ gap: 0.4rem;
42
+ }
43
+ /* The pattern's icon slot is sized for body text; at panel density it needs to
44
+ be small enough not to outweigh the label it sits next to. */
45
+ .details-icon {
46
+ font-size: 0.75em;
47
+ line-height: 1;
48
+ opacity: 0.7;
41
49
  }
42
50
  .ed-sub {
43
51
  border: var(--b-default);
@@ -311,13 +311,15 @@
311
311
  Animate the UA-generated content box directly; interpolate-size (Layer 2)
312
312
  lets block-size reach `auto`. Guarded — engines without ::details-content just
313
313
  snap open (fully accessible). */
314
+ /* NOTE: the transition that used to live here is gone, for the same reason as in
315
+ patterns/details.css — in Chrome 149 *any* transition on ::details-content
316
+ deadlocks (content-visibility stays hidden → `auto` resolves to 0 → nothing
317
+ runs) and the disclosure never opens. Collapsing still works; only the motion
318
+ is dropped. Don't reinstate it without checking that a first click expands. */
314
319
  @supports selector(::details-content) {
315
320
  details::details-content {
316
321
  overflow: clip;
317
322
  block-size: 0;
318
- transition:
319
- block-size var(--reveal-duration) var(--interact-easing, ease),
320
- content-visibility var(--reveal-duration) allow-discrete;
321
323
  }
322
324
  details[open]::details-content {
323
325
  block-size: auto;
@@ -1,2 +1,2 @@
1
1
  const e=`vitops:ds-overrides`,t=`/__vitops/design-system`,n=(e,t)=>t.startsWith(`stop-`)?`--color-${e}-${t.slice(5)}`:`--${e}-${t}`,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=t===`surface`?`surface`:`default`,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[e,n]of this.roles)this.deepSet(t,`colors.roles.${e}`,n);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 ed-section`,...t?{open:!0}:{}},[this.el(`summary`,{class:`font-eyebrow`},[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:`auto`,class:`drawer drawer--right 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`},[`×`]);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},[`🎨 Theme`]);this.append(r,n)}};customElements.get(`wc-theme-editor`)||customElements.define(`wc-theme-editor`,i);
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=t===`surface`?`surface`:`default`,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[e,n]of this.roles)this.deepSet(t,`colors.roles.${e}`,n);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:`details-icon`,"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`},[`×`]);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},[`🎨 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);