@canonical/anatomy-dsl 0.2.2 → 0.5.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/README.md CHANGED
@@ -9,12 +9,11 @@ A YAML-based DSL for representing design system component anatomies — platform
9
9
  node:
10
10
  uri: global.component.button
11
11
  styles:
12
- layout.type: flow
13
- layout.direction: horizontal
12
+ layout.type: inline-flex
14
13
  layout.align: center
15
- spacing.internal: spacing/medium
16
- appearance.background: color/surface/button
17
- appearance.radius: radius/button
14
+ spacing.internal.inline.start: spacing.inset.action.inline
15
+ appearance.background: [modifier.color.foreground.primary, color.foreground.primary]
16
+ appearance.radius: dimension.radius.medium
18
17
  edges:
19
18
  - node:
20
19
  uri: global.subcomponent.button-icon
@@ -28,7 +27,201 @@ node:
28
27
  slotName: default
29
28
  ```
30
29
 
31
- Style values are **design token paths** (`spacing/medium`, `color/surface/button`) — forward-slash delimited references resolved at runtime against the active theme. Primitives like `flow` and `center` are used for layout semantics that don't vary across themes.
30
+ Style values are **token symbols**, written in the symbol's own dotted
31
+ spelling: `dimension.radius.medium`, `color.foreground.primary`. A list is
32
+ the **fallback order** — a one-to-one transcription of the implementation's
33
+ own `var()` chain, so `[modifier.color.foreground.primary,
34
+ color.foreground.primary]` says "read the primary-foreground channel, and
35
+ fall back to the primary foreground token", which is what the reference CSS
36
+ does. A **channel** (`modifier.*`, `surface.*`) is a symbol like any other:
37
+ the anatomy consumes it by name where the implementation reads the channel,
38
+ and the semantic token by name where it reads the token — nothing is
39
+ inferred. Primitives like `inline-flex` and `center` are used for layout
40
+ semantics that do not vary across themes, and one may end a list where the
41
+ implementation's chain ends in a literal
42
+ (`[modifier.color.icon, modifier.color.text, currentColor]`).
43
+
44
+ Keys come from a **closed roster** — `definitions/style-keys.yaml`, from
45
+ which `definitions/registry.ttl` is generated. The roster is measured, not
46
+ designed: its 111 keys are the right-hand column of a table whose left-hand
47
+ column was every CSS property bound on a component selector across 89
48
+ published component stylesheets, so a key exists because an implementation
49
+ binds the property. That measurement lives in `canonical/design-system`,
50
+ where the reference stylesheets are an input; this package reads no CSS and
51
+ holds the roster, with its provenance stated in both files. The
52
+ slash-delimited path and the trailing `?` marker of earlier versions are
53
+ retired — see `docs/api-reference.md` §5, §11 and §12.
54
+
55
+ ## Projections
56
+
57
+ Projections bind the anatomy tree directly to graph data, after the Relay
58
+ fragment-colocation pattern: the tree carries its data requirements the way a
59
+ lens component carries its fragment.
60
+
61
+ ```yaml
62
+ ---
63
+ node:
64
+ uri: global.component.entity-card
65
+ projection:
66
+ on: Component # type condition — the tree is a view over one Component
67
+ edges:
68
+ - node:
69
+ uri: global.subcomponent.entity-card-header
70
+ projection:
71
+ field: _meta.title # this node renders the entity's title
72
+ relation:
73
+ cardinality: "1"
74
+ slotName: header
75
+ - node:
76
+ uri: global.component.chip
77
+ projection:
78
+ field: _meta.title # relative to the traversed Tag
79
+ relation:
80
+ cardinality: "0..*"
81
+ slotName: tags
82
+ projection:
83
+ field: documentationStages # traversal populating the slot
84
+ ```
85
+
86
+ Semantics:
87
+
88
+ - The **root node's** `projection.on` establishes the data context — the anatomy
89
+ is a parameterized view over one entity of that GraphQL type, like
90
+ `fragment EntityCard on Component`. Children inherit the context.
91
+ - `projection.field` on a **relation** is a traversal: the slot is populated
92
+ from that field of the current context. The DSL names the field only —
93
+ never `edges.node`. Anatomy cardinality ↔ graph multiplicity is the Relay
94
+ pattern taken one level further.
95
+ - **Cardinality decomposes against the schema.** The upper bound claims
96
+ multiplicity: `..1` maps to an object or scalar field, `..*` to a
97
+ connection or list. The lower bound claims nullability: `0..` tolerates
98
+ `null`, `1..` requires the provider to always have the value. So
99
+ `field: _meta.title` may sit under `cardinality: "1"` (title is total in
100
+ the contract), but a nullable field like `summary` must sit under `0..1`.
101
+ `1..*` asserts a non-empty list — deliberately stronger than GraphQL can
102
+ express, and checkable only at runtime.
103
+ - **Mechanism-blindness.** Whether a plural field is a Relay connection
104
+ (`subcomponents`) or a plain list (`properties`) is a provider mechanism,
105
+ not an anatomy fact. Consumers discover the shape from the SDL and unwrap
106
+ `edges { node }` when needed; an anatomy survives a provider promoting a
107
+ list to a connection unchanged.
108
+ - `projection.on` on a **child node** narrows the traversed entity's type — the
109
+ analog of an inline fragment. Switch cases carrying different `on` values
110
+ mirror an interface resolved through inline fragments.
111
+ - `projection.field` on a **node** means the node renders that field's value,
112
+ as a dot-delimited path relative to the enclosing context (`_meta.title`).
113
+ - A node projection needs at least one of `on` / `field`; a relation projection
114
+ requires `field` and admits no type condition (narrowing belongs on the
115
+ child node).
116
+
117
+ Field names are anchored to the provider schema the docsite runs against
118
+ (compiled with `prefixing: "none"`); the committed SDL is the naming authority.
119
+
120
+ A fully projected anatomy **derives a GraphQL fragment** mechanically — the
121
+ example above reads as:
122
+
123
+ ```graphql
124
+ fragment EntityCardAnatomy on Component {
125
+ _meta { title } # header node
126
+ documentationStages { # tags relation (connection per SDL)
127
+ edges { node { _meta { title } } } # each chip
128
+ }
129
+ }
130
+ ```
131
+
132
+ The derivation rules (type conditions, dot-path expansion, connection
133
+ unwrapping, inline fragments) are specified in the API reference §3.10
134
+ *Derived fragment*; a worked gallery of every projection form is in §13, and
135
+ `examples/yaml/entity-card.anatomy.yaml` is the golden example.
136
+
137
+ ## Props (pinned values)
138
+
139
+ A named node can **pin** props of the component it references — fixing a prop
140
+ value at one tree position:
141
+
142
+ ```yaml
143
+ node:
144
+ uri: global.component.icon
145
+ props:
146
+ icon: chevron-down
147
+ ```
148
+
149
+ This is how icons become idiomatic with zero icon-specific machinery. The
150
+ design system models the icon as a component whose glyph is a required prop
151
+ (`ds:global.component.icon` › `ds:hasProperty [ ds:name "icon" ]`), so icon
152
+ usage in anatomies splits into exactly two cases:
153
+
154
+ - **Consumer-filled icon slot** — an icon-component edge with a slot and no
155
+ pin. The consumer chooses the glyph; the anatomy correctly says nothing.
156
+ - **Component-intrinsic icon** — the accordion chevron, the modal close ×, a
157
+ status glyph: the component's own spec fixes the glyph, and the anatomy
158
+ pins it.
159
+
160
+ Semantics:
161
+
162
+ - Pins live on **named nodes only** — anonymous nodes have no prop surface.
163
+ This is enforced in the types, the parser, and SHACL.
164
+ - The DSL **never defines a prop surface** (names, types, optionality live in
165
+ the design system ontology); it only asserts values. Whether a pinned prop
166
+ exists on the component, and whether a value is admissible (e.g. a glyph
167
+ name in the icon set), are consumer-side checks against the DS graph — the
168
+ same posture as projection checking against the provider SDL.
169
+ - Values are scalars, coerced to strings — no symbols and no fallback lists: a pin is a value, not a style.
170
+ Pins are *values with meaning*, not styles: a theme may reskin what
171
+ `chevron-down` looks like (asset layer), but never remap which glyph an
172
+ anatomy means.
173
+ - A **data-driven** value is a projection (`projection: { field: … }`), and a
174
+ **state-driven** one is a `switch` — pinned props are static by design.
175
+
176
+ In TTL, pins reify like styles do: `hasProp [ a :Prop ; propName "…" ;
177
+ propValue "…" ]`. See `examples/yaml/status-header.anatomy.yaml` for
178
+ intrinsic icons, a status-glyph switch, and an unpinned consumer slot in one
179
+ anatomy.
180
+
181
+ ## Interaction states
182
+
183
+ Interaction states re-value style channels — they never add structure. A
184
+ style key takes an `@state` suffix scoping its value to a state; the unmarked
185
+ key is the default state:
186
+
187
+ ```yaml
188
+ styles:
189
+ interaction.cursor: pointer
190
+ interaction.cursor@disabled: not-allowed
191
+ appearance.background: [modifier.color.foreground.primary, color.foreground.primary]
192
+ appearance.background@hover: color.foreground.primary.hover
193
+ appearance.background@disabled: color.foreground.primary.disabled
194
+ # A slot may read a channel only in one state.
195
+ appearance.outline.color@focus: [modifier.color.focusRing, color.focusRing, currentColor]
196
+ ```
197
+
198
+ The state vocabulary is **closed and registry-governed**: `hover`, `active`,
199
+ `focus`, `disabled`, `selected`. Naming follows the industry consensus where
200
+ systems diverge — `active` subsumes Material's *pressed* and Spectrum's
201
+ *down* (and matches Canonical's own token tree); `focus` maps to CSS
202
+ `:focus-visible` (Spectrum's *key-focus*). `@default` is invalid — absence is
203
+ the default. Candidate additions (`checked`, `visited`, `dragged`, `pending`,
204
+ `error`, `read-only`) go through the registry, never by loosening the schema.
205
+
206
+ The boundaries that keep "state machines out of scope" true:
207
+
208
+ - **States hold style values only.** A state that changes the tree is not a
209
+ state — it is a `switch on: internal` case (async-button's
210
+ idle/loading/success/error). The DSL declares appearance *per* state, never
211
+ transitions, triggers, or logic.
212
+ - **Gate vs appearance**: `props: { disabled: true }` (or the consumer) puts
213
+ a node in the disabled state; `…@disabled` styles say how it looks there.
214
+ - A state-scoped value that differs from its base state's is a **lint, not a
215
+ constraint**: it is reported with the ranks at which they differ and never
216
+ rejected, because the reference does it — Button's `:disabled` reads
217
+ `color.text.disabled`, a different symbol from its resting `color.text`.
218
+ - The grammar reserves repeatable markers for compound states
219
+ (`@selected@hover`, canonical order: value/control state before user-action
220
+ state); v1 permits a single `@`.
221
+
222
+ In TTL, the Style tuple gains one optional dimension:
223
+ `[ a :Style ; :styleKey "appearance.background" ; :styleState "hover" ; :styleValue "…" ]`.
224
+ See `examples/yaml/stateful-button.anatomy.yaml`.
32
225
 
33
226
  ## Install
34
227
 
@@ -50,19 +243,19 @@ const ttl = anatomyToTTL(spec);
50
243
  The button example above produces:
51
244
 
52
245
  ```turtle
53
- @prefix : <http://anatomy-dsl.example.org/ontology#> .
246
+ @prefix : <https://anatomy.canonical.com/> .
247
+ @prefix dt: <https://dt.canonical.com/> .
54
248
 
55
249
  [] a :Specification ;
56
250
  :rootNode [
57
251
  a :NamedNode ;
58
252
  :uri "global.component.button" ;
59
253
  :hasStyle
60
- [ :styleKey "layout.type" ; :styleValue "flow" ] ,
61
- [ :styleKey "layout.direction" ; :styleValue "horizontal" ] ,
62
- [ :styleKey "layout.align" ; :styleValue "center" ] ,
63
- [ :styleKey "spacing.internal" ; :styleValue "spacing/medium" ] ,
64
- [ :styleKey "appearance.background" ; :styleValue "color/surface/button" ] ,
65
- [ :styleKey "appearance.radius" ; :styleValue "radius/button" ] ;
254
+ [ a :Style ; :styleKey "layout.type" ; :styleValue "inline-flex" ] ,
255
+ [ a :Style ; :styleKey "layout.align" ; :styleValue "center" ] ,
256
+ [ a :Style ; :styleKey "spacing.internal.inline.start" ; :styleValue "spacing.inset.action.inline" ; :consumes ( dt:spacing.inset.action.inline ) ] ,
257
+ [ a :Style ; :styleKey "appearance.background" ; :styleValue "[modifier.color.foreground.primary, color.foreground.primary]" ; :consumes ( dt:modifier.color.foreground.primary dt:color.foreground.primary ) ] ,
258
+ [ a :Style ; :styleKey "appearance.radius" ; :styleValue "dimension.radius.medium" ; :consumes ( dt:dimension.radius.medium ) ] ;
66
259
  :hasEdge [
67
260
  a :Edge ;
68
261
  :edgeTarget [
@@ -111,31 +304,53 @@ All types mirror the [OWL ontology](definitions/ontology.ttl) exactly:
111
304
  | `Node` | `NamedNode \| AnonymousNode` (discriminated on `type`) |
112
305
  | `Edge` | Reified parent→child relationship |
113
306
  | `Relation` | Cardinality and optional slot name |
114
- | `Style` | Reified key-value tuple |
307
+ | `Style` | Reified key-value tuple, with an optional interaction `state` dimension |
115
308
  | `Switch` | Polymorphic position (discriminator: `props \| internal \| override`) |
116
309
  | `SwitchCase` | One alternative within a switch |
310
+ | `Projection` | Fragment-style graph binding on a node (`on` type condition and/or `field` path) |
311
+ | `RelationProjection` | Traversal populating a slot (`field` required) |
312
+ | `Prop` | Pinned prop value on a named node (reified name-value tuple) |
117
313
 
118
314
  ## Repository Structure
119
315
 
120
316
  ```
121
- definitions/ Turtle ontology (OWL) + SHACL shapes
122
- schemas/ JSON Schema for validating .anatomy.yaml files
123
- docs/ API reference (merged WD404 + WD404.1)
124
- examples/ Example anatomy files (YAML + Turtle pairs)
125
- src/ TypeScript types, parser, and transform
317
+ definitions/ Turtle ontology (OWL) + SHACL shapes, the style-key roster and
318
+ the registry generated from it, and the lift fixture — all of
319
+ it public API, since exports lists it
320
+ docs/ API reference (WD404 + WD404.1 + WD404.2 + WD404.3)
321
+ examples/ Example anatomy files (YAML + Turtle pairs), the corpus the
322
+ round-trip and SHACL tests read
323
+ src/ TypeScript types, parser, value grammar, transform and the
324
+ generators
126
325
  ```
127
326
 
128
327
  ## Scope
129
328
 
130
- The Anatomy DSL describes **structure only**. It does not handle:
329
+ The Anatomy DSL describes **structure**, **graph-data bindings** (projections
330
+ — what data each position renders), **pinned prop values** (props — fixed
331
+ component configuration at a position), and **state-scoped styles**
332
+ (interaction states — how channels re-value per state). It does not handle:
131
333
 
132
- - **Prop mapping** — which props a component accepts and how they map to behaviour
133
- - **State or state machines** component states, transitions, or interaction logic
334
+ - **Prop surface definition** — which props a component accepts, their types
335
+ and optionality live in the design system ontology; the DSL only pins values
336
+ - **State machines** — transitions, triggers, and interaction logic; the DSL
337
+ declares appearance per state only, and structural state variation is the
338
+ switch construct's job
134
339
  - **Modifier descriptions** — only design token references are supported, not semantic modifier definitions
135
340
 
136
341
  ## Design Notes
137
342
 
138
- Styles are modelled as reified key-value tuples (`hasStyle [ styleKey "…" ; styleValue "…" ]`). This keeps the ontology open-ended while remaining lossless. Frequently used style keys may be promoted to first-class datatype properties in a future version.
343
+ Styles are modelled as reified key-value tuples
344
+ (`hasStyle [ a :Style ; :styleKey "…" ; :styleValue "…" ]`), which stays
345
+ lossless whatever the key roster becomes. Since 0.4.0 a tuple whose key takes
346
+ a token also carries `:consumes`, an ordered `rdf:List` of the `dt:` symbols
347
+ it reads: `:styleValue` is the authored spelling kept verbatim as evidence —
348
+ it is where a terminal literal lives, since a literal is not a symbol — and
349
+ `:consumes` is the form a query can walk. The key vocabulary is no longer
350
+ open: `anatomy:styleKey`'s `sh:in` is projected from the registry, so a key
351
+ outside the roster is a SHACL violation rather than a silent addition.
352
+
353
+ Projections and pinned props reuse the same reification idiom (`hasProjection [ projectionType "…" ; projectionField "…" ]`, `hasProp [ propName "…" ; propValue "…" ]`). Projections attach to both nodes and relations — the reified `Relation` is precisely what makes slot-level traversal annotations possible without changing the `Edge` class; pins attach to named nodes only.
139
354
 
140
355
  ## Specification Status
141
356
 
@@ -143,3 +358,5 @@ Styles are modelled as reified key-value tuples (`hasStyle [ styleKey "…" ; st
143
358
  |---------|--------------------------|----------------|
144
359
  | [WD404](https://docs.google.com/document/d/1eFr-SNsAZyidnZzpWp1Jeegiat_SSM7mOW_G8p3nXo8/edit?tab=t.pndvuecem8cf) | Anatomy DSL | Approved |
145
360
  | [WD404.1](https://docs.google.com/document/d/1eFr-SNsAZyidnZzpWp1Jeegiat_SSM7mOW_G8p3nXo8/edit?tab=t.pndvuecem8cf) | Anatomy DSL — Addendum 1 | Pending Review |
361
+ | WD404.2 | Anatomy DSL — Projections | Draft (this repository) |
362
+ | WD404.3 | Anatomy DSL — Prop pinning | Draft (this repository) |
@@ -0,0 +1,216 @@
1
+ {
2
+ "$comment": [
3
+ "The committed lift fixture (ADR J §4.1, §8.2). Both TypeScript repositories assert against it, so anatomy-dsl and design-system lift identically and cannot drift apart without a failing test.",
4
+ "`values` is the value lift this package implements: `liftSymbols(value)` over what the YAML parser holds for a style value — a scalar or a sequence of scalars — returning the symbols in fallback order. A primitive is not a symbol: it resolves against nothing by design, so it is never lifted.",
5
+ "`rejections` is the retired notation, with the rule each breaks. Every one throws an AnatomyValueError naming the value and the rule.",
6
+ "`names` is the CSS-name lift of §5.1 step 5d, which design-system implements because it needs S4 to run: a custom-property name to the dotted symbol it lifts to, or null where it names no symbol in any stratum and is kept as consumed with a register row (AT.09). It is DATA here, not an implementation — this package ships it so the two repositories agree on the expected pairs."
7
+ ],
8
+ "values": [
9
+ { "case": "a symbol", "value": "color.text", "symbols": ["color.text"] },
10
+ {
11
+ "case": "a camelCase segment is the symbol's own spelling, not renamed",
12
+ "value": "color.focusRing",
13
+ "symbols": ["color.focusRing"]
14
+ },
15
+ {
16
+ "case": "a numeric segment",
17
+ "value": "dimension.100",
18
+ "symbols": ["dimension.100"]
19
+ },
20
+ {
21
+ "case": "a deep camelCase symbol",
22
+ "value": "typography.weight.semiBold",
23
+ "symbols": ["typography.weight.semiBold"]
24
+ },
25
+ {
26
+ "case": "a channel symbol, consumed by name where the implementation reads the channel",
27
+ "value": "modifier.color.text",
28
+ "symbols": ["modifier.color.text"]
29
+ },
30
+ {
31
+ "case": "the surface channel spelling",
32
+ "value": "surface.color.background",
33
+ "symbols": ["surface.color.background"]
34
+ },
35
+ {
36
+ "case": "the programme's central binding: a channel then the semantic token",
37
+ "value": ["modifier.color.text", "color.text"],
38
+ "symbols": ["modifier.color.text", "color.text"]
39
+ },
40
+ {
41
+ "case": "three ranks, in the reference's own order",
42
+ "value": ["modifier.color.border", "color.border.highlighted", "color.border"],
43
+ "symbols": [
44
+ "modifier.color.border",
45
+ "color.border.highlighted",
46
+ "color.border"
47
+ ]
48
+ },
49
+ {
50
+ "case": "a terminal literal is kept in the value and lifted from none of it",
51
+ "value": ["modifier.color.icon", "modifier.color.text", "currentColor"],
52
+ "symbols": ["modifier.color.icon", "modifier.color.text"]
53
+ },
54
+ {
55
+ "case": "a dimension tail",
56
+ "value": ["dimension.stroke.thickness.large", "2px"],
57
+ "symbols": ["dimension.stroke.thickness.large"]
58
+ },
59
+ {
60
+ "case": "a bare keyword, for a key the registry says takes a primitive",
61
+ "value": "flow",
62
+ "symbols": []
63
+ },
64
+ {
65
+ "case": "a hyphenated CSS keyword, which the reference uses throughout",
66
+ "value": "not-allowed",
67
+ "symbols": []
68
+ },
69
+ { "case": "a number", "value": "1.6", "symbols": [] },
70
+ { "case": "a percentage", "value": "0%", "symbols": [] },
71
+ { "case": "a colour", "value": "#ccc", "symbols": [] },
72
+ {
73
+ "case": "a literal holding a slash between spaces: the slash ban is on symbol spellings, never on literals",
74
+ "value": "1 / -1",
75
+ "symbols": []
76
+ },
77
+ {
78
+ "case": "a literal a bare YAML would read as an alias",
79
+ "value": "*",
80
+ "symbols": []
81
+ },
82
+ {
83
+ "case": "an undeclared name is still a symbol by grammar: whether it resolves is the register's question, not the parser's",
84
+ "value": ["modifier.surface", "color.foreground.primary"],
85
+ "symbols": ["modifier.surface", "color.foreground.primary"]
86
+ },
87
+ {
88
+ "case": "a computed state variable, kept as consumed under the hover.X spelling",
89
+ "value": ["hover.color.foreground.secondary", "color.foreground.secondary.hover"],
90
+ "symbols": [
91
+ "hover.color.foreground.secondary",
92
+ "color.foreground.secondary.hover"
93
+ ]
94
+ }
95
+ ],
96
+ "rejections": [
97
+ { "case": "a slash path", "value": "spacing/medium", "rule": "slashPath" },
98
+ {
99
+ "case": "a slash path with a camelCase-bearing segment",
100
+ "value": "color/focus-ring",
101
+ "rule": "slashPath"
102
+ },
103
+ {
104
+ "case": "a slash path inside a sequence",
105
+ "value": ["modifier/color/foreground", "color/foreground/primary"],
106
+ "rule": "slashPath"
107
+ },
108
+ {
109
+ "case": "the retired optional marker on a slash path",
110
+ "value": "color/surface/button?",
111
+ "rule": "slashPath"
112
+ },
113
+ {
114
+ "case": "the retired optional marker on a dotted symbol",
115
+ "value": "color.surface.button?",
116
+ "rule": "marker"
117
+ },
118
+ { "case": "the reserved root segment", "value": "$root", "rule": "root" },
119
+ {
120
+ "case": "the reserved root segment inside a dotted name",
121
+ "value": "color.$root.text",
122
+ "rule": "root"
123
+ },
124
+ {
125
+ "case": "a bare root segment",
126
+ "value": "color.root.text",
127
+ "rule": "root"
128
+ },
129
+ {
130
+ "case": "a primitive before the end: the sequence is the fallback order and a literal is what the chain ends in",
131
+ "value": ["2px", "dimension.stroke.thickness.large"],
132
+ "rule": "primitiveNotLast"
133
+ },
134
+ {
135
+ "case": "a primitive in the middle",
136
+ "value": ["modifier.color.text", "currentColor", "color.text"],
137
+ "rule": "primitiveNotLast"
138
+ },
139
+ {
140
+ "case": "a one-element sequence: a fallback order needs two",
141
+ "value": ["color.text"],
142
+ "rule": "singleton"
143
+ },
144
+ { "case": "an empty sequence", "value": [], "rule": "empty" },
145
+ { "case": "an empty scalar", "value": "", "rule": "empty" }
146
+ ],
147
+ "names": [
148
+ {
149
+ "case": "a plain semantic token",
150
+ "variable": "--color-text",
151
+ "symbol": "color.text"
152
+ },
153
+ {
154
+ "case": "camelCase is restored from the kebab spelling S4 emits",
155
+ "variable": "--color-focus-ring",
156
+ "symbol": "color.focusRing"
157
+ },
158
+ {
159
+ "case": "the drift spelling S4 also emits for the same symbol",
160
+ "variable": "--color-focusRing",
161
+ "symbol": "color.focusRing"
162
+ },
163
+ {
164
+ "case": "a modifier channel",
165
+ "variable": "--modifier-color-text",
166
+ "symbol": "modifier.color.text"
167
+ },
168
+ {
169
+ "case": "a surface channel",
170
+ "variable": "--surface-color-background",
171
+ "symbol": "surface.color.background"
172
+ },
173
+ {
174
+ "case": "a channel with no base symbol: kept as consumed, register row X7",
175
+ "variable": "--modifier-surface",
176
+ "symbol": "modifier.surface",
177
+ "resolves": false
178
+ },
179
+ {
180
+ "case": "a state variant of a channel: register row X5",
181
+ "variable": "--modifier-surface-hover",
182
+ "symbol": "modifier.surface.hover",
183
+ "resolves": false
184
+ },
185
+ {
186
+ "case": "a computed state variable: the double dash lifts to a dot, register row X15",
187
+ "variable": "--hover--color-foreground-secondary",
188
+ "symbol": "hover.color.foreground.secondary",
189
+ "resolves": false
190
+ },
191
+ {
192
+ "case": "a component-local variable is followed through its :root definition and replaced by what it consumes",
193
+ "variable": "--button-color-text",
194
+ "symbol": "color.text",
195
+ "via": "--button-color-text: var(--color-text)"
196
+ },
197
+ {
198
+ "case": "a component-local variable whose definition is itself a chain",
199
+ "variable": "--button-color-background",
200
+ "symbol": "modifier.color.foreground.primary",
201
+ "via": "--button-color-background: var(--modifier-color-foreground-primary, var(--color-foreground-primary))"
202
+ },
203
+ {
204
+ "case": "an undeclared name: kept as consumed with a comment, register row X1",
205
+ "variable": "--motion-duration-fast",
206
+ "symbol": "motion.duration.fast",
207
+ "resolves": false
208
+ },
209
+ {
210
+ "case": "a composite typography slot no stratum declares",
211
+ "variable": "--typography-text-secondary-font-size",
212
+ "symbol": "typography.text.secondary.fontSize",
213
+ "resolves": false
214
+ }
215
+ ]
216
+ }