@getvitops/core 0.8.0 → 1.0.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,257 @@
1
1
  # @getvitops/core
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - bb92a14: **The colour system is rebuilt on a target-prefixed grammar and a shared lightness ladder.**
8
+ This is a breaking change to every colour token and utility class.
9
+
10
+ ## Why
11
+
12
+ Two axes shared one class namespace — functional _planes_ (`--<role>-bg-muted`) and
13
+ appearance-relative _stops_ (`--color-<role>-muted`) — arbitrated by a "plane wins" rule. The
14
+ result was not a scale. On the shipped palette, light mode:
15
+
16
+ | class | resolved to | step |
17
+ | ---------------------------------- | ------------ | --------------------------------------------- |
18
+ | `bg-ui-accent-x-muted` | stop | 100 |
19
+ | `bg-ui-accent-muted` | plane | 100 — identical, so `x-muted` was a dead rung |
20
+ | `bg-ui-accent` | plane | 50 — _lighter_ than both its "muted" rungs |
21
+ | `border-ui-accent-muted` / `-bold` | stop / plane | 300 / 300 — identical |
22
+ | `text-ui-accent-bold` | stop | 700 — _lighter_ than `text-…-muted` (800) |
23
+
24
+ Every family was non-monotonic, two had duplicate rungs, and `--color-<role>-muted` was
25
+ unreachable through any `bg-` class.
26
+
27
+ ## The new grammar
28
+
29
+ ```
30
+ --color-<target>-<role>[-<variant>] target ∈ bg | text | icon | border
31
+ ```
32
+
33
+ The target is **inside** the name, so `bg-danger-muted` and `text-danger-muted` are different
34
+ tokens and there is nothing left to arbitrate. **The class name is the token name minus
35
+ `--color-`** — one vocabulary instead of two.
36
+
37
+ Variants are ordinal (`xx-muted` < `x-muted` < `muted` < bare < `bold` < `x-bold`) and the
38
+ tables are sparse: only cells that hold their contrast target exist.
39
+
40
+ ## Role kinds
41
+
42
+ `colors.roles` values may now be `{ hue, kind }` as well as a bare hue string:
43
+
44
+ ```jsonc
45
+ "roles": {
46
+ "danger": "rust", // shorthand => chromatic
47
+ "surface": { "hue": "navy", "kind": "surface" }
48
+ }
49
+ ```
50
+
51
+ - **`surface`** — a page/panel colour: `bg-<role>` is the card, `bg-<role>-muted` the page
52
+ behind it, `bg-<role>-x-muted` a well, `bg-<role>-bold` the inverse surface. Full text scale.
53
+ - **`chromatic`** (default) — a signal colour: tints (`bg-<role>-x-muted`/`-muted`) and solids
54
+ (`bg-<role>-solid[-bold|-x-bold]`), with **no bare `bg-<role>`** — say how loud you mean.
55
+ `text-on-<role>` is the guaranteed foreground for the solid family.
56
+
57
+ ## Palette generation
58
+
59
+ Every ramp now sits on one **fixed lightness ladder** (50 → L 0.98 … 950 → L 0.21); only chroma
60
+ and hue vary. That is what makes a step mean the same lightness in every hue. Previously each
61
+ seed transposed the curve, so relative luminance at step 300 ranged from 0.253 to 0.384 across
62
+ the shipped palette — the same variant read differently depending on the role.
63
+
64
+ Authored colours are still reproduced **exactly**. A `seed`, an `anchors` entry or a `tones`
65
+ value is pinned verbatim at its nearest step; every other step takes the ladder. Snapping brand
66
+ colours to the ladder was measured and rejected: 11 of 18 real brand hexes moved beyond a
67
+ just-noticeable difference (worst ΔE-OK 0.050 — Facebook's `#1877F2` → `#0067e1`) and six left
68
+ sRGB gamut. Deviation is now bounded and local to pinned steps, and warns past ~0.03 L.
69
+
70
+ Two tones that claim the same step now **error** instead of one silently overwriting the other;
71
+ the record form (`tones: { "600": "…", "700": "…" }`) is how you resolve it.
72
+
73
+ ## New
74
+ - **`icon-<role>`** — a non-text tier, so a glyph may run more vivid than text. `icon` is now a
75
+ default utility family alongside `bg`/`text`/`border`.
76
+ - **`--color-border-focus`** — the focus-ring tone, taken from `ui-primary`'s solid.
77
+ - **Contrast is enforced at build time**, not only in tests: text ≥ APCA Lc 75 on its primary
78
+ background, ≥ 60 on secondary planes, icons and surface boundaries ≥ 45, both appearances.
79
+ A violation now fails `generate`. Chromatic text is checked against the _surface_ planes it
80
+ actually sits on, not only its own tints. `text-<role>-x-muted` (placeholder) and `-xx-muted`
81
+ (disabled) are explicitly exempt; nothing else is.
82
+
83
+ ## Migrating
84
+
85
+ Rename `--<role>-<suffix>` → `--color-<target>-<role>[-<variant>]`, and the same for classes:
86
+
87
+ | before | after |
88
+ | --------------------------------------- | ---------------------------------------------------------------- |
89
+ | `--<role>-bg` / `bg-<role>` (chromatic) | `--color-bg-<role>-x-muted` / `bg-<role>-x-muted` |
90
+ | `--<role>-bg-muted` | `--color-bg-<role>-muted` |
91
+ | `--<role>-solid` / `-solid-bold` | `--color-bg-<role>-solid` / `-solid-bold` |
92
+ | `--<role>-on-solid` / `text-on-<role>` | `--color-text-on-<role>` (class unchanged) |
93
+ | `--<role>-text` / `-text-muted` | `--color-text-<role>` / `-muted` |
94
+ | `--<role>-border` / `-border-bold` | `--color-border-<role>` / `-bold` |
95
+ | `--color-<role>-muted` (stop) | judge by use: `--color-bg-<role>-muted` or `--color-text-<role>` |
96
+
97
+ **`surface` background names rotate**, value-preserving: what was `--surface-bg` (the page) is
98
+ now `--color-bg-surface-muted`; what was `--surface-bg-bold` (raised) is now
99
+ `--color-bg-surface`. Elevation is expressed by which token you reach for — page `bg-muted`,
100
+ card `bg` — rather than a raised/sunken pair, which is what lets a future surfaces axis flatten
101
+ it without touching markup.
102
+
103
+ `vitops lint` reports role classes that no longer resolve, and now derives its suggestions from
104
+ what the generator actually emits rather than a hand-maintained list.
105
+
106
+ ### Patch Changes
107
+
108
+ - Updated dependencies [bb92a14]
109
+ - @getvitops/utils@1.0.0
110
+
111
+ ## 0.9.0
112
+
113
+ ### Minor Changes
114
+
115
+ - **The theme editor no longer dims the page it's editing.**
116
+
117
+ The panel used `.drawer`'s scrim and `popover="auto"`, which fought the whole purpose of a live
118
+ editor: the page you were tuning sat behind a 4px blur, and the first click on it dismissed the
119
+ panel. It is now modeless — no scrim, and clicking through to the page keeps it open. Escape still
120
+ closes it (wired explicitly, since `popover="manual"` doesn't provide it), as does the × button.
121
+
122
+ **New: `.drawer--modeless`.** The same treatment is available to any drawer — a side panel you work
123
+ _alongside_ rather than through (an inspector, a live editor, a filter rail). Pair it with
124
+ `popover="manual"`:
125
+
126
+ ```html
127
+ <div class="drawer drawer--right drawer--modeless" popover="manual">…</div>
128
+ ```
129
+
130
+ An `auto` popover light-dismisses on the first outside click, which is exactly the interaction a
131
+ modeless panel exists to permit — so `manual` is part of the pattern, not an afterthought. It also
132
+ drops Escape, so give the panel a visible close control.
133
+
134
+ **Added:** a drift guard tying `<wc-theme-editor>`'s dark-override selector to the generator's
135
+ `DARK_SEL`. The two must match or the editor's dark-mode edits land on a rule the page never
136
+ matches — a failure that is invisible in light mode, so nothing would have caught it.
137
+
138
+ - **Fixed: `Subgrid` and `Cards` rendered as unstyled lists, in every format.**
139
+
140
+ `Subgrid.astro` drew its geometry with Tailwind utilities — `grid-rows-subgrid` and
141
+ `row-span-(--row-span)`, the latter Tailwind v4's arbitrary-CSS-variable syntax. No framework CSS
142
+ layer defines those, so under `css.format: 'css'` or `'bricks'` the component had no layout at all.
143
+
144
+ It failed under `'tailwind'` too, which is the part worth internalising: **Tailwind v4 is JIT and
145
+ does not scan `node_modules`**, so a class that only a shipped component references is never
146
+ generated. A consumer had to add the package to Tailwind's `@source` by hand to get a subgrid;
147
+ without that, cards silently laid out at `grid-row: auto` — visually plausible, quietly wrong. The
148
+ same trap applied to `grid` in `Subgrid` and `sr-only` / `not-sr-only` in `Popover` / `Drawer`:
149
+ those are in the generator's `TW_CLASH` list, so the framework strips its own rules for them from
150
+ the tailwind bundle and defers to Tailwind — meaning they resolved only when the consumer's own
151
+ templates happened to use the same class.
152
+
153
+ The components now emit framework classes only, and the `subgrid` pattern owns the layout:
154
+ - `Subgrid` renders `<ul class="subgrid"><li>…` and ships no `<style>` block of its own.
155
+ - `Popover` / `Drawer` use a component-scoped `visually-hidden` instead of `sr-only`. The
156
+ `not-sr-only` on their icons was a no-op and is gone.
157
+ - The `subgrid` pattern absorbed the wrapped-row margin the component used to carry, so
158
+ hand-written `.subgrid` markup gets it too, and resets list markers on `ul`/`ol`.
159
+
160
+ **Also fixed: `Cards` discarded the `card` class and every class you put on a child.** It wrote
161
+ `card` onto the slotted element and then serialised that element's _inner_ HTML, dropping the
162
+ attribute it had just set. Child `class` and `style` now reach the rendered `<li>`.
163
+
164
+ **Breaking — the subgrid custom properties are renamed.** One `--subgrid-*` vocabulary now covers
165
+ both the pattern and the component; the old names are removed, not aliased:
166
+
167
+ | removed | use |
168
+ | ------------------------------- | -------------------- |
169
+ | `--items-per-row`, `--cols` | `--subgrid-cols` |
170
+ | `--rows-per-item`, `--row-span` | `--subgrid-row-span` |
171
+ | `--row-margin` | `--subgrid-row-gap` |
172
+
173
+ `--subgrid-gap` (the grid gap) is unchanged, as are the `.subgrid-cols-*` / `.subgrid-rows-*` /
174
+ `.subgrid-responsive` modifiers. Anything still setting an old name falls back to the pattern
175
+ defaults — 3 columns, span 2 — so grep for them when you upgrade.
176
+
177
+ ### Patch Changes
178
+
179
+ - **Fixed: `vitops init` and `vp create` scaffolded configs referencing tokens that no longer
180
+ exist.** `defaultConfig()` and the EmDash template still pointed at `--color-surface-xl` and
181
+ `--color-surface-xxl`, aliases from the named-step colour scale removed in 0.6. A scaffolded
182
+ project therefore got a `card` with no background and an invalid default border colour, with
183
+ nothing reporting it. Both now use `--surface-bg` / `--surface-bg-muted`. The EmDash template
184
+ also dropped its `patterns.radii.card` key, which collided with the `card` pattern on
185
+ `--br-card` — the collision `validate()` has been warning about (the `panel` group already
186
+ supplies the same radius, so nothing changes visually).
187
+
188
+ **Fixed: `.text-reveal` rendered invisible text.** Its gradient consumed
189
+ `--text-reveal-color-from`/`-to` with no defaults, so unless a consumer set both, the `var()`
190
+ substitution failed, `background` became invalid at computed-value time, and the accompanying
191
+ `color: transparent` left the text with no way to render. Both tones now default to the
192
+ functional text tokens.
193
+
194
+ **Fixed: `.bordered` fell back to `currentColor`** via a reference to `--color-surface-xl`,
195
+ which the generator never emitted. It now resolves `--surface-border`.
196
+
197
+ **New: `validate()` warns when a required role is missing.** `colors.roles` is an open map —
198
+ any name works and generates a full token set — but the shipped component CSS references
199
+ `brand-primary`, `danger`, `neutral`, `surface`, `ui-primary` and `warning` with no fallback,
200
+ so omitting one leaves those components uncoloured. This is a warning, not an error; the
201
+ required list is re-derived from the CSS partials by a test so it cannot drift.
202
+
203
+ `vitops docs` and `vitops agents` now surface config warnings (on stderr, so piping `docs` is
204
+ unaffected). They previously discarded them, unlike `generate` and `validate`.
205
+
206
+ Docs corrections, all in the generated bundle:
207
+ - **Raw scale classes are frozen and do not remap in dark mode** — stated explicitly for the
208
+ first time, with a migration table to the role equivalents. The dark-mode guarantee only ever
209
+ applied to functional role tokens, and nothing said so.
210
+ - **Roles are extensible over a required core** — the schema description and class reference
211
+ read as a closed enumeration, which is why a consumer forked their own colour layer rather
212
+ than adding a role.
213
+ - **The `md:` / `@md:` / `md-` distinction** in the tailwind format: `@md:` uses the framework's
214
+ breakpoints, `md:` works but uses Tailwind's (which differ — `sm:` is 40rem, `@sm:` is 30rem),
215
+ and `md-` is silently inert. Plus a note that registering `--container-*` also re-points
216
+ Tailwind's `max-w-*` scale.
217
+ - The css and bricks bundles now carry a `/*!` banner pointing at `npx vitops docs classes`
218
+ (the previous plain comment was stripped by the minifier, so it never reached the file).
219
+
220
+ Contrast checking now covers **every** background plane a role emits (`bg`, `bg-muted`, and
221
+ `bg-bold`), not just `bg` — body text on a `card` was previously unguaranteed.
222
+
223
+ - **Fixed: `<details>` disclosures never opened.**
224
+
225
+ Every `<details>` on a page using this framework was inert in Chrome 149 — clicking a summary
226
+ toggled the `open` attribute and nothing else happened. This affected the bare element, not just
227
+ `.details`, so it hit the docs table-of-contents, the mobile nav, and the theme editor's sections
228
+ alike.
229
+
230
+ The cause was the open/close animation on `::details-content`. It used the canonical recipe
231
+ (`block-size: 0 → auto` plus `content-visibility` under `allow-discrete`, backed by
232
+ `interpolate-size: allow-keywords`), and that recipe deadlocks: `content-visibility` stays `hidden`,
233
+ so the content has no layout, so `auto` resolves to `0`, so the transition has nothing to animate
234
+ and the state never advances. The failure is total rather than cosmetic — collapsed content simply
235
+ could not be reached.
236
+
237
+ Bisected against a bare `<details>` with only the framework stylesheet. `transition: none` is the
238
+ only thing that restores it; transitioning `block-size` alone, `content-visibility` alone,
239
+ `calc-size(auto, size)`, and the `grid-template-rows: 0fr → 1fr` technique all fail identically.
240
+
241
+ **So the transition is gone and disclosures now open instantly.** Correctness over motion. If you
242
+ reinstate it, verify a _first click actually expands_ in a real browser — the syntax is valid and
243
+ `@supports` reports it as supported, so a feature query proves nothing here; only the interaction
244
+ does.
245
+
246
+ **Also fixed: `.details--marker-start` right-aligned its label.** It mirrored the trailing marker's
247
+ `margin-inline-start: auto` as `margin-inline-end: auto`, which pushed everything after the icon to
248
+ the opposite edge instead of just placing the icon first.
249
+
250
+ **Also:** `<wc-theme-editor>`'s section headers now carry a disclosure marker. The `.details`
251
+ pattern removes the native one and expects a `.details-icon`; without it a panel of ~40 nested
252
+ sections gave no indication anything was collapsible.
253
+ - @getvitops/utils@0.9.0
254
+
3
255
  ## 0.8.0
4
256
 
5
257
  ### Minor Changes
package/css/animation.css CHANGED
@@ -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(--color-text-neutral-x-muted, #9099a8));
315
+ --_reveal-to: var(--text-reveal-color-to, var(--color-text-neutral, #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%;
@@ -64,7 +64,7 @@
64
64
  border: none;
65
65
  border-radius: 50%;
66
66
  color: var(--carousel-button-color, currentColor);
67
- background: var(--surface-bg-muted);
67
+ background: var(--color-bg-surface-muted);
68
68
  opacity: 0.9;
69
69
  transition:
70
70
  opacity 0.2s ease,
@@ -73,7 +73,7 @@
73
73
 
74
74
  &::scroll-button(*):hover {
75
75
  opacity: 1;
76
- background: var(--color-surface-muted);
76
+ background: var(--color-bg-surface-muted);
77
77
  }
78
78
 
79
79
  &::scroll-button(*):focus-visible {
@@ -211,7 +211,7 @@
211
211
  block-size: 2rem;
212
212
  border: none;
213
213
  border-radius: 50%;
214
- background: var(--surface-bg-muted);
214
+ background: var(--color-bg-surface-muted);
215
215
  color: var(--carousel-button-color, currentColor);
216
216
  opacity: 0.7;
217
217
  cursor: pointer;
@@ -10,8 +10,8 @@
10
10
  :where(:not(pre) > code) {
11
11
  font-family: var(--font-mono);
12
12
  font-size: 0.85em;
13
- background: var(--neutral-border);
14
- color: var(--neutral-text);
13
+ background: var(--color-border-neutral);
14
+ color: var(--color-text-neutral);
15
15
  padding: 0.1em 0.4em;
16
16
  border-radius: var(--br-default);
17
17
  }
@@ -20,7 +20,7 @@
20
20
  border-radius: var(--_br);
21
21
 
22
22
  &:focus {
23
- outline: 2px solid var(--combobox-focus-color, var(--color-brand-primary-muted));
23
+ outline: 2px solid var(--combobox-focus-color, var(--color-border-focus));
24
24
  outline-offset: 2px;
25
25
  }
26
26
  }
@@ -35,7 +35,7 @@
35
35
  border: var(--_b);
36
36
  border-radius: var(--_br);
37
37
  box-shadow: var(--_ds);
38
- background: var(--surface-bg);
38
+ background: var(--color-bg-surface);
39
39
  max-block-size: 15rem;
40
40
  overflow-y: auto;
41
41
  list-style: none;
@@ -55,7 +55,7 @@
55
55
 
56
56
  &:hover,
57
57
  &[aria-selected='true'] {
58
- background: var(--color-surface-muted);
58
+ background: var(--color-bg-surface-muted);
59
59
  }
60
60
 
61
61
  &[aria-disabled='true'] {
@@ -62,7 +62,7 @@
62
62
  grid-column: 1 / -1;
63
63
  margin-inline-start: var(--comment-indent, 2rem);
64
64
  padding-inline-start: var(--comment-indent, 2rem);
65
- border-inline-start: 2px solid var(--color-surface-muted);
65
+ border-inline-start: 2px solid var(--color-bg-surface-muted);
66
66
  display: flex;
67
67
  flex-direction: column;
68
68
  gap: 1rem;
@@ -38,7 +38,7 @@
38
38
  }
39
39
 
40
40
  &:focus-visible {
41
- outline: 2px solid var(--copy-focus-color, var(--color-brand-primary-muted));
41
+ outline: 2px solid var(--copy-focus-color, var(--color-border-focus));
42
42
  outline-offset: 2px;
43
43
  }
44
44
  }
@@ -66,8 +66,8 @@
66
66
  font-size: 0.75rem;
67
67
  border-radius: 0.25rem;
68
68
  white-space: nowrap;
69
- background: var(--surface-text);
70
- color: var(--surface-bg);
69
+ background: var(--color-text-surface);
70
+ color: var(--color-bg-surface);
71
71
  opacity: 0;
72
72
  visibility: hidden;
73
73
  transition:
@@ -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 */
@@ -25,8 +25,8 @@ dialog.dialog {
25
25
  border: var(--_b);
26
26
  border-radius: var(--_br);
27
27
  box-shadow: var(--_ds);
28
- background: var(--surface-bg);
29
- color: var(--surface-text);
28
+ background: var(--color-bg-surface);
29
+ color: var(--color-text-surface);
30
30
  max-block-size: calc(100vh - 2rem);
31
31
 
32
32
  &::backdrop {
@@ -15,8 +15,8 @@
15
15
  padding: var(--_p);
16
16
  border: var(--_b);
17
17
  box-shadow: var(--_ds);
18
- background: var(--surface-bg);
19
- color: var(--surface-text);
18
+ background: var(--color-bg-surface);
19
+ color: var(--color-text-surface);
20
20
  max-inline-size: none;
21
21
  max-block-size: none;
22
22
 
@@ -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;
@@ -32,7 +32,7 @@
32
32
 
33
33
  .form-group__error {
34
34
  font-size: var(--form-error-size, 0.75rem);
35
- color: var(--color-danger-bold);
35
+ color: var(--color-text-danger);
36
36
  display: flex;
37
37
  align-items: center;
38
38
  gap: 0.25rem;
@@ -87,7 +87,7 @@ fieldset.fieldset,
87
87
  border: var(--_b);
88
88
  border-radius: var(--_br);
89
89
  padding: var(--_p);
90
- background: var(--surface-bg);
90
+ background: var(--color-bg-surface);
91
91
  }
92
92
 
93
93
  /* ── Range + colour controls ───────────────────────────────────────────────── */
@@ -95,7 +95,7 @@ fieldset.fieldset,
95
95
  * natively, in both appearances, with no pseudo-element rewriting. The colour
96
96
  * swatch only needs its chrome removed so the well fills the control box. */
97
97
  input[type='range'] {
98
- accent-color: var(--range-accent, var(--ui-primary-solid));
98
+ accent-color: var(--range-accent, var(--color-bg-ui-primary-solid));
99
99
  inline-size: 100%;
100
100
  min-inline-size: 0;
101
101
  }
@@ -202,7 +202,7 @@ input[type='color'] {
202
202
  block-size: var(--_check-size);
203
203
  border: var(--_b);
204
204
  border-radius: var(--_br);
205
- background: var(--surface-bg);
205
+ background: var(--color-bg-surface);
206
206
  color: #fff;
207
207
  display: flex;
208
208
  align-items: center;
@@ -225,8 +225,8 @@ input[type='color'] {
225
225
  }
226
226
 
227
227
  input:checked + .custom-check__box {
228
- background: var(--brand-primary-solid);
229
- border-color: var(--brand-primary-solid);
228
+ background: var(--color-bg-brand-primary-solid);
229
+ border-color: var(--color-bg-brand-primary-solid);
230
230
 
231
231
  .custom-check__indicator {
232
232
  opacity: 1;
@@ -235,7 +235,7 @@ input[type='color'] {
235
235
  }
236
236
 
237
237
  input:focus-visible + .custom-check__box {
238
- outline: 2px solid var(--check-focus-color, var(--color-brand-primary-muted));
238
+ outline: 2px solid var(--check-focus-color, var(--color-border-focus));
239
239
  outline-offset: 2px;
240
240
  }
241
241
 
@@ -288,7 +288,7 @@ input[type='color'] {
288
288
  inline-size: var(--_switch-width);
289
289
  block-size: var(--_switch-height);
290
290
  border-radius: var(--br-switch-track, var(--br-pill, 999px));
291
- background: var(--color-surface-muted);
291
+ background: var(--color-bg-surface-muted);
292
292
  transition: background-color 0.2s ease;
293
293
  }
294
294
 
@@ -306,7 +306,7 @@ input[type='color'] {
306
306
  }
307
307
 
308
308
  input:checked + .switch__track {
309
- background: var(--brand-primary-solid);
309
+ background: var(--color-bg-brand-primary-solid);
310
310
 
311
311
  .switch__thumb {
312
312
  inset-inline-start: calc(100% - var(--_switch-thumb-size) - 2px);
@@ -314,7 +314,7 @@ input[type='color'] {
314
314
  }
315
315
 
316
316
  input:focus-visible + .switch__track {
317
- outline: 2px solid var(--switch-focus-color, var(--color-brand-primary-muted));
317
+ outline: 2px solid var(--switch-focus-color, var(--color-border-focus));
318
318
  outline-offset: 2px;
319
319
  }
320
320
 
@@ -360,7 +360,7 @@ select.custom-select {
360
360
  cursor: pointer;
361
361
 
362
362
  &:focus-visible {
363
- outline: 2px solid var(--select-focus-color, var(--color-brand-primary-muted));
363
+ outline: 2px solid var(--select-focus-color, var(--color-border-focus));
364
364
  outline-offset: 2px;
365
365
  }
366
366
  }
@@ -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(--color-border-surface, currentColor));
13
13
  }
14
14
 
15
15
  /* ── Group — collapse border-radius (+ shared borders) across direct children ─
@@ -57,11 +57,11 @@
57
57
  transition: background-color 0.15s ease;
58
58
 
59
59
  &:hover {
60
- background: var(--color-surface-muted);
60
+ background: var(--color-bg-surface-muted);
61
61
  }
62
62
 
63
63
  &:focus-visible {
64
- outline: 2px solid var(--icon-button-focus-color, var(--color-brand-primary-muted));
64
+ outline: 2px solid var(--icon-button-focus-color, var(--color-border-focus));
65
65
  outline-offset: 2px;
66
66
  }
67
67
 
@@ -62,7 +62,7 @@
62
62
  }
63
63
 
64
64
  &.list--divided .list__item + .list__item {
65
- border-block-start: 1px solid var(--list-divider-color, var(--color-surface-muted));
65
+ border-block-start: 1px solid var(--list-divider-color, var(--color-bg-surface-muted));
66
66
  }
67
67
 
68
68
  &.list--bordered {
@@ -110,7 +110,7 @@
110
110
  font-size: 0.75em;
111
111
  padding: 0.125rem 0.375rem;
112
112
  border-radius: var(--br-count, var(--br-pill, 999px));
113
- background: var(--color-surface-muted);
113
+ background: var(--color-bg-surface-muted);
114
114
  }
115
115
 
116
116
  .facet-list__item--active .facet-list__name {
@@ -140,7 +140,7 @@
140
140
  border-radius: var(--_br);
141
141
 
142
142
  &:focus {
143
- outline: 2px solid var(--filter-focus-color, var(--color-brand-primary-muted));
143
+ outline: 2px solid var(--filter-focus-color, var(--color-border-focus));
144
144
  outline-offset: 2px;
145
145
  }
146
146
  }
@@ -185,7 +185,7 @@
185
185
  .filtered-list__match {
186
186
  padding-inline: 0.125rem;
187
187
  border-radius: 0.125rem;
188
- background: var(--color-warning-muted);
188
+ background: var(--color-bg-warning-muted);
189
189
  }
190
190
  }
191
191
 
@@ -246,7 +246,7 @@
246
246
  }
247
247
 
248
248
  &:focus-within {
249
- outline: 2px solid var(--tag-list-focus-color, var(--color-brand-primary-muted));
249
+ outline: 2px solid var(--tag-list-focus-color, var(--color-border-focus));
250
250
  outline-offset: 2px;
251
251
  }
252
252
  }
@@ -43,7 +43,7 @@
43
43
  color: inherit;
44
44
 
45
45
  &:focus-visible {
46
- outline: 2px solid var(--tile-focus-color, var(--color-brand-primary-muted));
46
+ outline: 2px solid var(--tile-focus-color, var(--color-border-focus));
47
47
  outline-offset: 2px;
48
48
  }
49
49
 
@@ -13,7 +13,7 @@ nav:has(> .nav-items) {
13
13
 
14
14
  .nav-header {
15
15
  padding-bottom: 1rem;
16
- border-bottom: 1px solid var(--nav-border-color, var(--color-surface-muted));
16
+ border-bottom: 1px solid var(--nav-border-color, var(--color-bg-surface-muted));
17
17
  }
18
18
 
19
19
  .nav-items {
@@ -49,7 +49,7 @@ nav:has(> .nav-items) {
49
49
  flex-direction: column;
50
50
  gap: 1rem;
51
51
  padding-top: 1rem;
52
- border-top: 1px solid var(--nav-border-color, var(--color-surface-muted));
52
+ border-top: 1px solid var(--nav-border-color, var(--color-bg-surface-muted));
53
53
  }
54
54
 
55
55
  /* ── Dropdown submenu ─────────────────────────────────────────────────────── */
@@ -106,7 +106,7 @@ nav:has(> .nav-items) {
106
106
  border-radius: 0.375rem;
107
107
 
108
108
  &:hover {
109
- background-color: var(--color-surface-muted);
109
+ background-color: var(--color-bg-surface-muted);
110
110
  }
111
111
  }
112
112
  }
@@ -131,8 +131,8 @@ nav:has(> .nav-items) {
131
131
  inset: 0;
132
132
  height: 100dvh;
133
133
  width: 100%;
134
- background: var(--surface-bg);
135
- color: var(--surface-text);
134
+ background: var(--color-bg-surface);
135
+ color: var(--color-text-surface);
136
136
  opacity: 0;
137
137
  translate: -100%;
138
138
  transition-timing-function: var(--interact-easing, ease);
@@ -18,8 +18,8 @@
18
18
  border: var(--_b);
19
19
  border-radius: var(--_br);
20
20
  box-shadow: var(--_ds);
21
- background: var(--surface-bg);
22
- color: var(--surface-text);
21
+ background: var(--color-bg-surface);
22
+ color: var(--color-text-surface);
23
23
  animation: notification-enter 0.3s ease;
24
24
 
25
25
  &.notification--exiting {
@@ -77,7 +77,7 @@
77
77
  inset-block-end: 0;
78
78
  inset-inline: 0;
79
79
  block-size: 3px;
80
- background: var(--brand-primary-solid);
80
+ background: var(--color-bg-brand-primary-solid);
81
81
  border-radius: 0 0 var(--_br) var(--_br);
82
82
  transform-origin: left;
83
83
  animation: notification-progress var(--notification-duration, 5s) linear forwards;
@@ -123,7 +123,7 @@
123
123
 
124
124
  .scroll-progress__bar {
125
125
  block-size: 100%;
126
- background: var(--scroll-progress-color, var(--brand-primary-solid));
126
+ background: var(--scroll-progress-color, var(--color-bg-brand-primary-solid));
127
127
  transform-origin: left;
128
128
  animation: scroll-progress-grow linear;
129
129
  animation-timeline: scroll(root);
@@ -18,7 +18,7 @@
18
18
  (deferred.ts) on engines without `scroll-target-group` support. */
19
19
  .scroll-target-group a:target-current,
20
20
  .scroll-target-group a.is-current {
21
- color: var(--scroll-target-active-color, var(--brand-primary-solid));
21
+ color: var(--scroll-target-active-color, var(--color-bg-brand-primary-solid));
22
22
  }
23
23
 
24
24
  .scroll-target-group--indicator a {
@@ -47,7 +47,7 @@
47
47
  .scroll-target-group--bar a:target-current::before,
48
48
  .scroll-target-group--indicator a.is-current::before,
49
49
  .scroll-target-group--bar a.is-current::before {
50
- background-color: var(--scroll-target-active-color, var(--brand-primary-solid));
50
+ background-color: var(--scroll-target-active-color, var(--color-bg-brand-primary-solid));
51
51
  }
52
52
 
53
53
  .scroll-target-group--bar a {
@@ -107,7 +107,7 @@
107
107
 
108
108
  .toc-sidebar a:target-current,
109
109
  .toc-sidebar a.is-current {
110
- color: var(--scroll-target-active-color, var(--brand-primary-solid));
110
+ color: var(--scroll-target-active-color, var(--color-bg-brand-primary-solid));
111
111
  font-weight: 500;
112
112
  }
113
113
 
@@ -47,11 +47,11 @@
47
47
  border-radius: var(--br-control, 0.375rem);
48
48
 
49
49
  &:hover {
50
- background: var(--color-surface-muted);
50
+ background: var(--color-bg-surface-muted);
51
51
  }
52
52
 
53
53
  &:focus-visible {
54
- outline: 2px solid var(--sitenav-focus-color, var(--color-brand-primary-muted));
54
+ outline: 2px solid var(--sitenav-focus-color, var(--color-border-focus));
55
55
  outline-offset: 2px;
56
56
  }
57
57
  }
@@ -73,8 +73,8 @@
73
73
  max-inline-size: 100dvw;
74
74
  padding: 1.5rem;
75
75
  padding-block-start: 3.5rem; /* clearance for the close button */
76
- background: var(--surface-bg);
77
- color: var(--surface-text);
76
+ background: var(--color-bg-surface);
77
+ color: var(--color-text-surface);
78
78
  border: none;
79
79
  box-shadow: var(--ds-panel);
80
80
  overflow-y: auto;
@@ -149,11 +149,11 @@
149
149
  border-radius: var(--br-control, 0.375rem);
150
150
 
151
151
  &:hover {
152
- background: var(--color-surface-muted);
152
+ background: var(--color-bg-surface-muted);
153
153
  }
154
154
 
155
155
  &:focus-visible {
156
- outline: 2px solid var(--sitenav-focus-color, var(--color-brand-primary-muted));
156
+ outline: 2px solid var(--sitenav-focus-color, var(--color-border-focus));
157
157
  outline-offset: 2px;
158
158
  }
159
159
  }
@@ -280,8 +280,8 @@
280
280
  min-inline-size: 13rem;
281
281
  padding: 0.5rem;
282
282
  overflow: visible;
283
- background: var(--surface-bg);
284
- color: var(--surface-text);
283
+ background: var(--color-bg-surface);
284
+ color: var(--color-text-surface);
285
285
  border: var(--b-panel);
286
286
  border-radius: var(--br-panel);
287
287
  box-shadow: var(--ds-panel);
@@ -366,8 +366,8 @@
366
366
  min-inline-size: 13rem;
367
367
  padding: 0.5rem;
368
368
  overflow: visible;
369
- background: var(--surface-bg);
370
- color: var(--surface-text);
369
+ background: var(--color-bg-surface);
370
+ color: var(--color-text-surface);
371
371
  border: var(--b-panel);
372
372
  border-radius: var(--br-panel);
373
373
  box-shadow: var(--ds-panel);
@@ -453,8 +453,8 @@
453
453
  min-inline-size: 13rem;
454
454
  padding: 0.5rem;
455
455
  overflow: visible;
456
- background: var(--surface-bg);
457
- color: var(--surface-text);
456
+ background: var(--color-bg-surface);
457
+ color: var(--color-text-surface);
458
458
  border: var(--b-panel);
459
459
  border-radius: var(--br-panel);
460
460
  box-shadow: var(--ds-panel);
@@ -540,8 +540,8 @@
540
540
  min-inline-size: 13rem;
541
541
  padding: 0.5rem;
542
542
  overflow: visible;
543
- background: var(--surface-bg);
544
- color: var(--surface-text);
543
+ background: var(--color-bg-surface);
544
+ color: var(--color-text-surface);
545
545
  border: var(--b-panel);
546
546
  border-radius: var(--br-panel);
547
547
  box-shadow: var(--ds-panel);
@@ -38,11 +38,11 @@
38
38
  transition: background-color 0.15s ease;
39
39
 
40
40
  &:hover {
41
- background: var(--color-surface-muted);
41
+ background: var(--color-bg-surface-muted);
42
42
  }
43
43
 
44
44
  &:focus-visible {
45
- outline: 2px solid var(--split-link-focus-color, var(--color-brand-primary-muted));
45
+ outline: 2px solid var(--split-link-focus-color, var(--color-border-focus));
46
46
  outline-offset: 2px;
47
47
  }
48
48
 
@@ -71,8 +71,8 @@
71
71
  border: var(--_b);
72
72
  border-radius: var(--_br);
73
73
  box-shadow: var(--_ds);
74
- background: var(--surface-bg);
75
- color: var(--surface-text);
74
+ background: var(--color-bg-surface);
75
+ color: var(--color-text-surface);
76
76
  }
77
77
 
78
78
  /* Top-layer popover: anchored + placed via position-area. */
@@ -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
  }
@@ -41,7 +41,7 @@
41
41
  font-weight: 600;
42
42
  line-height: 1;
43
43
  border-radius: var(--br-badge-indicator, var(--br-pill, 999px));
44
- background: var(--color-danger-bold);
44
+ background: var(--color-text-danger);
45
45
  color: #fff;
46
46
 
47
47
  &.badge-indicator--dot {
@@ -225,7 +225,7 @@
225
225
 
226
226
  /* ── Highlight sweep ──────────────────────────────────────────────────────── */
227
227
  .text-highlight {
228
- --_highlight-color: var(--text-highlight-color, var(--color-warning-muted));
228
+ --_highlight-color: var(--text-highlight-color, var(--color-bg-warning-muted));
229
229
  --_highlight-duration: var(--text-highlight-duration, 0.5s);
230
230
  --_highlight-pad-block: var(--text-highlight-pad-block, 0.1em);
231
231
  --_highlight-pad-inline: var(--text-highlight-pad-inline, 0);
@@ -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);
@@ -56,7 +64,7 @@ wc-theme-editor {
56
64
  overflow: hidden;
57
65
  font-family: var(--font-mono, monospace);
58
66
  font-size: 0.9em;
59
- color: var(--surface-text-x-muted);
67
+ color: var(--color-text-surface-x-muted);
60
68
  text-overflow: ellipsis;
61
69
  white-space: nowrap;
62
70
  }
@@ -66,7 +74,7 @@ wc-theme-editor {
66
74
  padding: 0.3em 0.5em;
67
75
  border: var(--b-default);
68
76
  border-radius: var(--br-default);
69
- background: var(--surface-bg);
77
+ background: var(--color-bg-surface);
70
78
  color: inherit;
71
79
  font: inherit;
72
80
  }
@@ -99,16 +107,16 @@ wc-theme-editor {
99
107
  inset-block-start: 0;
100
108
  z-index: 1;
101
109
  padding-block: 0.5rem;
102
- background: var(--surface-bg);
110
+ background: var(--color-bg-surface);
103
111
  }
104
112
  .ed-segwrap {
105
113
  display: inline-flex;
106
114
  }
107
115
  .ed-seg.is-active {
108
- background: var(--ui-primary-solid);
109
- color: var(--ui-primary-on-solid);
116
+ background: var(--color-bg-ui-primary-solid);
117
+ color: var(--color-text-on-ui-primary);
110
118
  }
111
119
  .ed-danger {
112
- color: var(--danger-text);
120
+ color: var(--color-text-danger);
113
121
  }
114
122
  }
@@ -44,8 +44,8 @@
44
44
  padding: var(--_p);
45
45
  border-radius: var(--_br);
46
46
  box-shadow: var(--_ds);
47
- background: var(--surface-text);
48
- color: var(--surface-bg);
47
+ background: var(--color-text-surface);
48
+ color: var(--color-bg-surface);
49
49
  font-size: var(--_fs);
50
50
  white-space: nowrap;
51
51
  max-inline-size: 20rem;
@@ -18,7 +18,7 @@
18
18
  .tree__item > .tree {
19
19
  margin-inline-start: var(--tree-indent, 1.5rem);
20
20
  padding-inline-start: var(--tree-indent, 1.5rem);
21
- border-inline-start: 1px solid var(--color-surface-muted);
21
+ border-inline-start: 1px solid var(--color-bg-surface-muted);
22
22
  }
23
23
 
24
24
  .tree__content,
@@ -115,8 +115,8 @@
115
115
  inset-block-start: 0;
116
116
  block-size: 1rem;
117
117
  inline-size: calc(var(--tree-indent, 1.5rem) - 0.5rem);
118
- border-inline-start: 1px solid var(--color-surface-muted);
119
- border-block-end: 1px solid var(--color-surface-muted);
118
+ border-inline-start: 1px solid var(--color-bg-surface-muted);
119
+ border-block-end: 1px solid var(--color-bg-surface-muted);
120
120
  }
121
121
  }
122
122
 
package/css/utilities.css CHANGED
@@ -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;
package/dist/editor.js CHANGED
@@ -1,2 +1,2 @@
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);
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:`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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/core",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "description": "The Vitops design-system framework — CSS partials, Lit web components, and browser polyfills.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -31,6 +31,6 @@
31
31
  "lit": "^3.3.3",
32
32
  "scroll-timeline-polyfill": "^1.1.0",
33
33
  "temporal-polyfill": "^1.0.1",
34
- "@getvitops/utils": "0.8.0"
34
+ "@getvitops/utils": "1.0.0"
35
35
  }
36
36
  }