@adobe/design-data-spec 4.12.1 → 4.13.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.
@@ -0,0 +1,74 @@
1
+ {
2
+ "tokens": [],
3
+ "components": [
4
+ {
5
+ "$id": "https://opensource.adobe.com/spectrum-design-data/schemas/v0/components/checkbox.json",
6
+ "name": "checkbox",
7
+ "displayName": "Checkbox",
8
+ "meta": {
9
+ "category": "inputs",
10
+ "documentationUrl": "https://spectrum.adobe.com/page/checkbox/"
11
+ },
12
+ "anatomy": [
13
+ {
14
+ "name": "box",
15
+ "description": "Square control.",
16
+ "required": true,
17
+ "contains": ["checkmark"]
18
+ },
19
+ {
20
+ "name": "checkmark",
21
+ "description": "Mark shown when selected."
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "visuals": [
27
+ {
28
+ "component": "checkbox",
29
+ "root": "box",
30
+ "sizeScale": {
31
+ "m": 100
32
+ },
33
+ "parts": {
34
+ "box": {
35
+ "kind": "box",
36
+ "children": ["checkmark"]
37
+ },
38
+ "checkmark": {
39
+ "kind": "icon",
40
+ "icon": {
41
+ "set": "ui",
42
+ "name": "tick"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ],
48
+ "icons": [
49
+ {
50
+ "set": "ui",
51
+ "source": {
52
+ "repository": "https://github.com/adobe/react-spectrum",
53
+ "commit": "0000000000000000000000000000000000000000",
54
+ "path": "ui-icons",
55
+ "license": "Apache-2.0",
56
+ "copyright": "Copyright 2024 Adobe. All rights reserved."
57
+ },
58
+ "icons": {
59
+ "checkmark": {
60
+ "sizes": {
61
+ "100": {
62
+ "viewBox": "0 0 10 10",
63
+ "paths": [
64
+ {
65
+ "d": "M1 5l3 3 5-6"
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "layer": 2,
3
+ "errors": [
4
+ {
5
+ "rule_id": "SPEC-085",
6
+ "severity": "error",
7
+ "message_pattern": ".*checkmark.*checkbox.*ui:tick.*set 'ui' has no icon 'tick'.*"
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "tokens": [],
3
+ "components": [
4
+ {
5
+ "$id": "https://opensource.adobe.com/spectrum-design-data/schemas/v0/components/checkbox.json",
6
+ "name": "checkbox",
7
+ "displayName": "Checkbox",
8
+ "meta": {
9
+ "category": "inputs",
10
+ "documentationUrl": "https://spectrum.adobe.com/page/checkbox/"
11
+ },
12
+ "anatomy": [
13
+ {
14
+ "name": "box",
15
+ "description": "Square control.",
16
+ "required": true,
17
+ "contains": ["checkmark"]
18
+ },
19
+ {
20
+ "name": "checkmark",
21
+ "description": "Mark shown when selected."
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "visuals": [
27
+ {
28
+ "component": "checkbox",
29
+ "root": "box",
30
+ "sizeScale": {
31
+ "m": 100
32
+ },
33
+ "parts": {
34
+ "box": {
35
+ "kind": "box",
36
+ "children": ["checkmark"]
37
+ },
38
+ "checkmark": {
39
+ "kind": "icon",
40
+ "icon": {
41
+ "set": "ui",
42
+ "name": "checkmark"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ],
48
+ "icons": [
49
+ {
50
+ "set": "ui",
51
+ "source": {
52
+ "repository": "https://github.com/adobe/react-spectrum",
53
+ "commit": "0000000000000000000000000000000000000000",
54
+ "path": "ui-icons",
55
+ "license": "Apache-2.0",
56
+ "copyright": "Copyright 2024 Adobe. All rights reserved."
57
+ },
58
+ "icons": {
59
+ "checkmark": {
60
+ "sizes": {
61
+ "75": {
62
+ "viewBox": "0 0 10 10",
63
+ "paths": [
64
+ {
65
+ "d": "M1 5l3 3 5-6"
66
+ }
67
+ ]
68
+ },
69
+ "200": {
70
+ "viewBox": "0 0 10 10",
71
+ "paths": [
72
+ {
73
+ "d": "M1 5l3 3 5-6"
74
+ }
75
+ ]
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ]
82
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "layer": 2,
3
+ "errors": [
4
+ {
5
+ "rule_id": "SPEC-086",
6
+ "severity": "warning",
7
+ "message_pattern": ".*checkmark.*checkbox.*ui:checkmark at scale 100 \\(sizeScale for size 'm'\\).*no drawing.*75, 200.*"
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "tokens": [],
3
+ "components": [
4
+ {
5
+ "$id": "https://opensource.adobe.com/spectrum-design-data/schemas/v0/components/checkbox.json",
6
+ "name": "checkbox",
7
+ "displayName": "Checkbox",
8
+ "meta": {
9
+ "category": "inputs",
10
+ "documentationUrl": "https://spectrum.adobe.com/page/checkbox/"
11
+ },
12
+ "anatomy": [
13
+ {
14
+ "name": "box",
15
+ "description": "Square control.",
16
+ "required": true,
17
+ "contains": ["checkmark"]
18
+ },
19
+ {
20
+ "name": "checkmark",
21
+ "description": "Mark shown when selected."
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "visuals": [
27
+ {
28
+ "component": "checkbox",
29
+ "root": "box",
30
+ "sizeScale": {
31
+ "m": 100
32
+ },
33
+ "parts": {
34
+ "box": {
35
+ "kind": "box",
36
+ "children": ["checkmark"]
37
+ },
38
+ "checkmark": {
39
+ "kind": "icon",
40
+ "icon": {
41
+ "set": "ui",
42
+ "name": "checkmark"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ],
48
+ "icons": [
49
+ {
50
+ "set": "ui",
51
+ "source": {
52
+ "repository": "https://github.com/adobe/react-spectrum",
53
+ "commit": "0000000000000000000000000000000000000000",
54
+ "path": "ui-icons",
55
+ "license": "Apache-2.0",
56
+ "copyright": "Copyright 2024 Adobe. All rights reserved."
57
+ },
58
+ "icons": {
59
+ "checkmark": {
60
+ "sizes": {
61
+ "100": {
62
+ "viewBox": "0 0 10 10",
63
+ "paths": [
64
+ {
65
+ "d": "M1 5l3 3 5-6"
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ]
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/design-data-spec",
3
- "version": "4.12.1",
3
+ "version": "4.13.0",
4
4
  "description": "Design Data Specification — prose, JSON Schemas, rule catalog, and conformance fixtures for the design data format",
5
5
  "type": "module",
6
6
  "repository": {
package/rules/rules.yaml CHANGED
@@ -1097,3 +1097,59 @@ rules:
1097
1097
  message: "Relationship #{index} has no uuid (component '{component}', property '{property}')"
1098
1098
  spec_ref: spec/relationship-format.md#identity
1099
1099
  introduced_in: "1.0.0-draft"
1100
+
1101
+ - id: SPEC-085
1102
+ name: visual-icon-exists
1103
+ severity: error
1104
+ layer: 2
1105
+ category: component
1106
+ note: >
1107
+ Runs only when the `icons/` catalog is loaded (`validate --icons-path`, the probed
1108
+ `packages/design-data/icons`, or `validate-dataset`). The recipe schema requires the
1109
+ reference on every `icon` part; this rule checks it against the catalog.
1110
+ assert: >
1111
+ The `icon` an icon part references (`set` and `name`) MUST exist in the `icons/` catalog.
1112
+ message: "Part '{part}' of '{component}' references icon {set}:{name}, but {reason}"
1113
+ spec_ref: spec/component-visuals.md#icons
1114
+ introduced_in: "1.0.0-draft"
1115
+
1116
+ - id: SPEC-086
1117
+ name: visual-icon-scale
1118
+ severity: warning
1119
+ layer: 2
1120
+ category: component
1121
+ note: >
1122
+ UI icons have one drawing per scale index. The renderer picks the scale from the part's
1123
+ size binding and falls back to the nearest drawing, which is usually a different design
1124
+ (stroke weight and proportions change between sizes). Checked over the request grid,
1125
+ like SPEC-079. Workflow icons have one drawing and are not checked. Runs only when the
1126
+ `icons/` catalog is loaded.
1127
+ assert: >
1128
+ Every `ui` icon a recipe draws SHOULD have a drawing at the scale its size binding gives:
1129
+ the bound size token's `scaleIndex` or `size` key, else the recipe's `sizeScale` entry
1130
+ for the request's size.
1131
+ message: "Part '{part}' of '{component}' draws ui:{name} at scale {scale} ({source}), which has no drawing; the nearest of {available} is drawn instead"
1132
+ spec_ref: spec/component-visuals.md#icons
1133
+ introduced_in: "1.0.0-draft"
1134
+
1135
+ - id: SPEC-087
1136
+ name: visual-derived-context-matches-token
1137
+ severity: warning
1138
+ layer: 2
1139
+ category: component
1140
+ note: >
1141
+ A recipe's `derived` entry declares a relationship context whose value follows from the
1142
+ solved layout (`(height - label.lineHeight) / 2`) instead of being bound. The context's
1143
+ token is still the documented value, so it must agree with the formula. The check binds
1144
+ the context to the root's padding in a scratch copy of the recipe, solves each size at
1145
+ each scale (`desktop`, `mobile`) and compares. A context whose relationship resolves no
1146
+ token is reported once per entry. A token with a desktop value only is not missing: it
1147
+ resolves for mobile too, so it shows as a mobile mismatch. A disagreement is one warning per
1148
+ scale, so a desktop one is told apart from mobile. Mobile values that the tokens do not
1149
+ carry yet show up here until the token data is fixed.
1150
+ assert: >
1151
+ For every size and scale, the token a `derived` context's relationship gives SHOULD equal
1152
+ the value of its formula over the solved scene.
1153
+ message: "Derived context '{context}' of '{component}' ({from}): the token value differs from the formula at {scale} for {sizes}"
1154
+ spec_ref: spec/component-visuals.md#derived-contexts
1155
+ introduced_in: "1.0.0-draft"
@@ -166,7 +166,11 @@
166
166
  "description": "The group's relationship property with its size part dropped (the recipe's `{sizeIndex}`/`{size}` template without the placeholder). Distinct within a block.",
167
167
  "type": "string"
168
168
  },
169
- "drawn": { "type": "boolean" }
169
+ "drawn": { "type": "boolean" },
170
+ "derived": {
171
+ "description": "True when the group is a context the recipe derives from the layout (`derived` in the recipe) instead of binding.",
172
+ "type": "boolean"
173
+ }
170
174
  },
171
175
  "additionalProperties": false
172
176
  }
@@ -182,7 +186,11 @@
182
186
  "prop": { "type": "string" },
183
187
  "kind": { "type": "string" },
184
188
  "label": { "type": "string" },
185
- "relationship": { "type": ["string", "null"] }
189
+ "relationship": { "type": ["string", "null"] },
190
+ "derived": {
191
+ "description": "True for a context the recipe derives from the layout. The span is appended to scene.measurements (kind `padding`, token `derived from height`), and `label` is `derived from height` instead of a token name.",
192
+ "type": "boolean"
193
+ }
186
194
  },
187
195
  "additionalProperties": false
188
196
  }
@@ -40,6 +40,40 @@
40
40
  "minimum": 0
41
41
  }
42
42
  },
43
+ "derived": {
44
+ "type": "array",
45
+ "description": "Relationship contexts the recipe does not bind because their value follows from the solved layout (spec/component-visuals.md, Derived contexts). SPEC-087 checks each against its token.",
46
+ "items": {
47
+ "type": "object",
48
+ "required": ["context", "property", "part", "from"],
49
+ "properties": {
50
+ "context": {
51
+ "type": "string",
52
+ "description": "The relationship `context` this entry derives."
53
+ },
54
+ "property": {
55
+ "type": "string",
56
+ "description": "The relationship `scope.property`, as in a `relationship` selector; may use {sizeIndex}, {size}."
57
+ },
58
+ "part": {
59
+ "type": "string",
60
+ "pattern": "^[a-z][a-z0-9-]*$",
61
+ "description": "The part the spacing leads to. Spec sheets put the marker there."
62
+ },
63
+ "edge": {
64
+ "enum": ["top", "bottom"],
65
+ "default": "top",
66
+ "description": "The container edge the spacing starts from."
67
+ },
68
+ "from": {
69
+ "type": "string",
70
+ "pattern": "^\\(height - [a-z][a-z0-9-]*\\.(lineHeight|height)\\) / 2$",
71
+ "description": "The only formula: `(height - <part>.lineHeight) / 2` or `(height - <part>.height) / 2`. `height` is the container's solved height."
72
+ }
73
+ },
74
+ "additionalProperties": false
75
+ }
76
+ },
43
77
  "parts": {
44
78
  "type": "object",
45
79
  "minProperties": 1,
@@ -53,9 +87,41 @@
53
87
  },
54
88
  "additionalProperties": false,
55
89
  "$defs": {
90
+ "iconRef": {
91
+ "type": "object",
92
+ "description": "The icon an icon part draws, from the icons/ catalog (icon-set.schema.json). SPEC-085 checks it exists.",
93
+ "required": ["set", "name"],
94
+ "properties": {
95
+ "set": { "enum": ["ui", "workflow"] },
96
+ "name": {
97
+ "type": "string",
98
+ "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
99
+ },
100
+ "content": {
101
+ "type": "boolean",
102
+ "description": "The icon is content, like a label: this is the recipe's default and a render request may replace it. Default false."
103
+ },
104
+ "optional": {
105
+ "type": "boolean",
106
+ "description": "A render request may remove this content icon (icon none). Requires content. Default false."
107
+ }
108
+ },
109
+ "if": {
110
+ "properties": { "optional": { "const": true } },
111
+ "required": ["optional"]
112
+ },
113
+ "then": {
114
+ "properties": { "content": { "const": true } },
115
+ "required": ["content"]
116
+ },
117
+ "additionalProperties": false
118
+ },
56
119
  "part": {
57
120
  "type": "object",
58
121
  "required": ["kind"],
122
+ "if": { "properties": { "kind": { "const": "icon" } } },
123
+ "then": { "required": ["icon"] },
124
+ "else": { "not": { "required": ["icon"] } },
59
125
  "properties": {
60
126
  "kind": {
61
127
  "enum": ["box", "text", "icon", "ring", "progress"]
@@ -76,9 +142,8 @@
76
142
  "pattern": "^[a-z][a-z0-9-]*$",
77
143
  "description": "For a ring: the part it outlines. Defaults to the root."
78
144
  },
79
- "glyph": {
80
- "enum": ["checkmark", "dash", "chevron"],
81
- "description": "For an icon nested in a box: the built-in glyph to draw. Unset draws a placeholder."
145
+ "icon": {
146
+ "$ref": "#/$defs/iconRef"
82
147
  },
83
148
  "pressTarget": {
84
149
  "type": "string",
@@ -19,7 +19,8 @@
19
19
  "fields": { "const": true },
20
20
  "modeSets": { "const": true },
21
21
  "registry": { "const": true },
22
- "guidelines": { "const": true }
22
+ "guidelines": { "const": true },
23
+ "icons": { "const": true }
23
24
  },
24
25
  "additionalProperties": false
25
26
  },
@@ -32,7 +33,8 @@
32
33
  "fields": { "type": "string", "minLength": 1 },
33
34
  "modeSets": { "type": "string", "minLength": 1 },
34
35
  "registry": { "type": "string", "minLength": 1 },
35
- "guidelines": { "type": "string", "minLength": 1 }
36
+ "guidelines": { "type": "string", "minLength": 1 },
37
+ "icons": { "type": "string", "minLength": 1 }
36
38
  },
37
39
  "additionalProperties": false
38
40
  }
@@ -0,0 +1,110 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://opensource.adobe.com/spectrum-design-data/schemas/v0/icon-set.schema.json",
4
+ "title": "Icon set",
5
+ "description": "Layer 1 structural schema for one icon set in the icons/ catalog (spec/dataset-layout.md): path geometry keyed by icon id, with the source it was imported from. Paths carry no colour; a renderer fills them with the part's colour. A UI icon has one drawing per size, keyed by the scale index of the icon size tokens.",
6
+ "type": "object",
7
+ "required": ["set", "source", "icons"],
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "format": "uri"
12
+ },
13
+ "set": {
14
+ "type": "string",
15
+ "pattern": "^[a-z][a-z0-9-]*$",
16
+ "description": "Set id, also the file name (ui.json is set ui)."
17
+ },
18
+ "description": {
19
+ "type": "string"
20
+ },
21
+ "source": {
22
+ "type": "object",
23
+ "description": "Provenance and licence of the imported geometry.",
24
+ "required": ["repository", "commit", "path", "license", "copyright"],
25
+ "properties": {
26
+ "repository": { "type": "string", "format": "uri" },
27
+ "commit": {
28
+ "type": "string",
29
+ "pattern": "^[0-9a-f]{40}$",
30
+ "description": "Full commit SHA the files were read at."
31
+ },
32
+ "path": {
33
+ "type": "string",
34
+ "description": "Directory of the source files in the repository."
35
+ },
36
+ "license": {
37
+ "type": "string",
38
+ "description": "SPDX licence id."
39
+ },
40
+ "copyright": { "type": "string" },
41
+ "notice": { "type": "string" }
42
+ },
43
+ "additionalProperties": false
44
+ },
45
+ "icons": {
46
+ "type": "object",
47
+ "description": "Icons keyed by id (kebab-case; the icon-terms registry id where one exists).",
48
+ "propertyNames": { "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$" },
49
+ "minProperties": 1,
50
+ "additionalProperties": {
51
+ "oneOf": [
52
+ { "$ref": "#/$defs/geometry" },
53
+ {
54
+ "type": "object",
55
+ "required": ["sizes"],
56
+ "properties": {
57
+ "sizes": {
58
+ "type": "object",
59
+ "description": "One drawing per size, keyed by scale index (50, 75, 100, …).",
60
+ "propertyNames": { "pattern": "^[0-9]+$" },
61
+ "minProperties": 1,
62
+ "additionalProperties": { "$ref": "#/$defs/geometry" }
63
+ }
64
+ },
65
+ "additionalProperties": false
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ },
71
+ "additionalProperties": false,
72
+ "$defs": {
73
+ "geometry": {
74
+ "type": "object",
75
+ "required": ["viewBox", "paths"],
76
+ "properties": {
77
+ "viewBox": {
78
+ "type": "string",
79
+ "pattern": "^-?[0-9.]+ -?[0-9.]+ [0-9.]+ [0-9.]+$",
80
+ "description": "SVG viewBox: min-x min-y width height."
81
+ },
82
+ "paths": {
83
+ "type": "array",
84
+ "minItems": 1,
85
+ "items": {
86
+ "type": "object",
87
+ "required": ["d"],
88
+ "properties": {
89
+ "d": {
90
+ "type": "string",
91
+ "minLength": 1,
92
+ "description": "SVG path data in viewBox units."
93
+ },
94
+ "fillRule": {
95
+ "enum": ["nonzero", "evenodd"],
96
+ "description": "SVG fill-rule; nonzero when absent."
97
+ }
98
+ },
99
+ "additionalProperties": false
100
+ }
101
+ },
102
+ "source": {
103
+ "type": "string",
104
+ "description": "Source file name the drawing was read from."
105
+ }
106
+ },
107
+ "additionalProperties": false
108
+ }
109
+ }
110
+ }
@@ -41,6 +41,7 @@ Status: **draft / RFC**. The schema is [`component-visual.schema.json`](../schem
41
41
  | `component` | string | REQUIRED | Component `name`. |
42
42
  | `root` | string | REQUIRED | Key of the root part in `parts`. |
43
43
  | `sizeScale` | object | OPTIONAL | Maps each `size` option value to a numeric scale index (e.g. `m` → `100`). Enables `{sizeIndex}` in selectors. A component with no `size` option declares one entry, the size renderers draw. |
44
+ | `derived` | array | OPTIONAL | Relationship contexts the recipe derives from the layout instead of binding. See [Derived contexts](#derived-contexts). SPEC-087. |
44
45
  | `notVisual` | array | OPTIONAL | Component options and states that deliberately have no visual in this recipe (for example `["isReadOnly"]`). SPEC-066. |
45
46
  | `parts` | object | REQUIRED | Map of anatomy part name → part definition. |
46
47
 
@@ -53,7 +54,7 @@ Status: **draft / RFC**. The schema is [`component-visual.schema.json`](../schem
53
54
  | `visibleWhen` | condition | The part is drawn only when the condition holds. Absent means always drawn. |
54
55
  | `placement` | object | For a child that is positioned rather than flowed: `anchor` (`top-start`, `top-end`, `bottom-start`, `bottom-end`, `center`) and optional `inset` value. `start`/`end` are logical and flip under RTL. |
55
56
  | `around` | string | For a `ring`: the part it outlines. Defaults to the root. |
56
- | `glyph` | enum | For an `icon` nested in a box: the built-in glyph to draw (`checkmark`, `dash`, `chevron`). Unset draws a placeholder. A `chevron` points to the end; `rotation` turns it. |
57
+ | `icon` | object | For an `icon` part (REQUIRED there, not allowed on other kinds): the icon to draw, `{"set": "ui" \| "workflow", "name": "<id>"}`, plus optional `content` and `optional` flags. See [Icons](#icons). |
57
58
  | `pressTarget` | string | For the root: the part the pressed scale applies to (for example a switch handle). Parts nested inside it scale with it (a checkbox `box` scales with its mark). Defaults to the whole scene. |
58
59
  | `props` | object | Map of **visual property** → value source or conditional list. |
59
60
 
@@ -86,7 +87,38 @@ Status: **draft / RFC**. The schema is [`component-visual.schema.json`](../schem
86
87
 
87
88
  **Column stretch.** In a column root, a flowed `box` with no `width` and no row content fills the column's inner width (a divider or a content region). A flowed `box` with no `height` and no children is as tall as its block padding.
88
89
 
89
- **Nested boxes.** A flowed `box` child paints itself (`fill`, `stroke`, `strokeWidth`, `cornerRadius`) and may list its own `children`, which are boxes laid out inside it. A nested child is centered across the parent. Along the inline axis its `justify` prop (`start`, `center`, `end`) places it, with an inset equal to the cross-axis gap. A nested child with no `width` or `height` fills the parent. A `cornerRadius` of `0.5` with `relativeTo: "height"` draws a pill, or a circle for a square box. The switch uses this for its track and handle. A nested `icon` is centered on both axes and draws its `glyph`; the checkbox uses this for its checkmark and dash.
90
+ **Nested boxes.** A flowed `box` child paints itself (`fill`, `stroke`, `strokeWidth`, `cornerRadius`) and may list its own `children`, which are boxes laid out inside it. A nested child is centered across the parent. Along the inline axis its `justify` prop (`start`, `center`, `end`) places it, with an inset equal to the cross-axis gap. A nested child with no `width` or `height` fills the parent. A `cornerRadius` of `0.5` with `relativeTo: "height"` draws a pill, or a circle for a square box. The switch uses this for its track and handle. A nested `icon` is centered on both axes; the checkbox uses this for its checkmark and dash.
91
+
92
+ ### Icons
93
+
94
+ An `icon` part names the icon it draws from the dataset's `icons/` catalog ([`icon-set.schema.json`](../schemas/icon-set.schema.json)):
95
+
96
+ ```json
97
+ "disclosure-icon": { "kind": "icon", "icon": { "set": "ui", "name": "chevron" }, "props": { ... } }
98
+ ```
99
+
100
+ | Field | Type | Description |
101
+ | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
102
+ | `set` | enum | `ui` (component glyphs such as `chevron`, `checkmark`, `dash`, one drawing per size) or `workflow` (content icons such as `add`, one 20 px drawing). |
103
+ | `name` | string | Icon id in that set. SPEC-085 checks it exists. |
104
+ | `content` | boolean | The icon is content, like a label: the recipe's icon is a default that a render request may replace. Default `false` (the icon is part of the design). |
105
+ | `optional` | boolean | With `content`, a render request may also remove the icon. Default `false`. |
106
+
107
+ A `chevron` points to the end; the `rotation` prop turns it (90 when an accordion item is open).
108
+
109
+ **Content icons.** A render request **MAY** carry `icon`, either `{"set", "name"}` or `"none"`. It applies to every part whose `icon` has `content: true`, the way `label` replaces the text of the recipe's label. `{"set", "name"}` replaces the part's icon. `"none"` removes the part from the layout as if its `visibleWhen` did not hold; it is allowed only on a part with `optional: true`, and on any other part the renderer **MUST** keep the recipe's icon and emit a diagnostic. A request icon that is not in the catalog draws the placeholder with a diagnostic. Parts without `content` ignore the request's `icon`. The button's icon is `content` and `optional` (a label-only button), the badge's is `content` only, because its `icon` option already hides it. Command-line and API spellings of the override (`--icon workflow:edit`, `--icon none`) belong to the tools, not to this format.
110
+
111
+ **Choosing a drawing.** A `workflow` icon has one drawing, scaled to the part box. A `ui` icon has one drawing per scale index, and the renderer **MUST** choose its scale like this:
112
+
113
+ 1. The token bound to the part's `width` (else `height`) gives it: the token name's `scaleIndex`, else its `size` key (`xs` 50, `s` 75, `m` 100, `l` 200, `xl` 300, `xxl` 400, the scale of `workflow-icon-medium` and its siblings).
114
+ 2. Otherwise (a literal size, an inline relationship value or a token whose name has neither field), the recipe's `sizeScale` entry for the request's `size`.
115
+ 3. Otherwise 100.
116
+
117
+ The drawing at that scale is used; when the icon has none, the renderer **MUST** draw the nearest available scale (the larger one on a tie). SPEC-086 warns when a recipe falls back, since another size's drawing has different stroke weights and proportions. The scale is chosen once per size and mode, from the bindings the part has with no states and default options, so a state or option never changes the drawing. A renderer **MAY** make this choice when it compiles the recipe.
118
+
119
+ **Drawing.** The renderer scales the icon's `viewBox` uniformly to fit the part box, centered, applies `rotation` about the box center, and fills every path, honoring its `fillRule`, with the part's resolved `color`. Icon data carries no color. A part box that is not square leaves the icon at the smaller side, so an icon part's `width` and `height` are the icon's size, not the size of the shape it draws.
120
+
121
+ An icon with no drawing (no such set or icon in the catalog, or no catalog) **MUST** draw a placeholder plus in the part box and emit a diagnostic naming the set, the icon and the request's size.
90
122
 
91
123
  ### Value sources
92
124
 
@@ -110,6 +142,31 @@ An entry on `cornerRadius` **MAY** set `relativeTo: "height"`, making its numeri
110
142
 
111
143
  **NORMATIVE:** A renderer **MUST NOT** substitute a literal for a value source it cannot resolve. It **MUST** emit a diagnostic naming the part, property and selector, and omit that property.
112
144
 
145
+ ### Derived contexts
146
+
147
+ Some relationship contexts describe a value the layout already implies. The vertical spacing from a container's edge to its label is `(height - line height) / 2` for a content-centred container, so a recipe that binds the container's height and centres its content gets that spacing for free. Binding the spacing token as well would fix the value where the design source derives it (and break it when the label wraps or the scale changes). A recipe lists such a context under `derived` rather than binding it:
148
+
149
+ ```json
150
+ "derived": [
151
+ {
152
+ "context": "Spacing (top/bottom edge to label)",
153
+ "property": "component-top-to-text-{sizeIndex}",
154
+ "part": "label",
155
+ "from": "(height - label.lineHeight) / 2"
156
+ }
157
+ ]
158
+ ```
159
+
160
+ | Field | Description |
161
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | `context` | The relationship `context` that is derived. |
163
+ | `property` | The relationship `scope.property` of the records it covers, as in a `relationship` selector. |
164
+ | `part` | The part the spacing leads to. Spec sheets put the marker there. |
165
+ | `edge` | `top` (default) or `bottom`: the container edge the spacing starts from. |
166
+ | `from` | The formula. Only `(height - <part>.lineHeight) / 2` and `(height - <part>.height) / 2` exist; `height` is the container's solved height. |
167
+
168
+ **NORMATIVE:** A renderer **MUST NOT** read a `derived` entry; it changes no scene. `<part>.lineHeight` is the part's solved height, which is its line height for a single-line label; tooling **MUST** solve with a label that does not wrap. Tooling reads a `derived` entry for two things. A spec sheet shows the context with a highlight on `part`, a marker and a measurement of the formula's value over the solved scene, labelled as derived from height. SPEC-087 compares, for every size and scale, the token the context's relationship gives with the formula's value over the solved scene. The comparison is a warning: a context whose relationship resolves no token is reported once, and a disagreement means the token data or the recipe's layout is wrong, never that the recipe binds the token. A control that is centred on the first label line (a checkbox or switch control) declares the label's formula, since the shared token is the space above the label line.
169
+
113
170
  ### Interim: relationship `context` strings
114
171
 
115
172
  Relationship `context` is free text today (for example `Background (selected, emphasized)`). Recipes bind to those strings so the pilot can ship without changing relationship data. This is brittle: a reworded context breaks a recipe, and SPEC-062 reports a `relationship` selector that matches no relationship. **RECOMMENDED:** a follow-up adds structured `options` to relationships so recipes can select by option and state instead of by label.
@@ -135,6 +192,9 @@ The recipe schema checks one file's structure (Layer 1). A JSON Schema cannot re
135
192
  | SPEC-079 | Warning. A platform-owned recipe renders over the request grid with no scene diagnostic. Planned to become an error in a later minor release |
136
193
  | SPEC-080 | Info. A platform recipe replaces the foundation recipe of the same component name |
137
194
  | SPEC-083 | Warning. A part binds only props the renderer reads for it (the root, its `kind`, or a `placement`); the schema accepts more, and the renderer ignores the rest |
195
+ | SPEC-085 | Error. An `icon` part's `set` and `name` exist in the `icons/` catalog. Runs when the catalog is loaded (`--icons-path`, or the probed `packages/design-data/icons`) |
196
+ | SPEC-086 | Warning. Over the request grid, every `ui` icon has a drawing at the scale its size binding gives (see [Icons](#icons)), so the renderer does not fall back to the nearest size |
197
+ | SPEC-087 | Warning. For every size and scale, the token a `derived` context gives equals the value of its formula over the solved scene (see [Derived contexts](#derived-contexts)) |
138
198
 
139
199
  SPEC-066 to SPEC-070 are warnings, so `validate` stays green while recipes are migrated. A ratchet keeps the debt from growing: `sdk/core/tests/recipe_lint_baseline.rs` compares each (rule, recipe) warning count with `sdk/core/tests/fixtures/recipe-lint-baseline.json`. A new warning fails the test, an improvement must lower the baseline, and a recipe that is not in the baseline must have none.
140
200
 
@@ -154,7 +214,7 @@ These are the conventions the lints above enforce, plus the decisions made while
154
214
  1. **Bind through relationships.** A recipe takes a value from the component's relationships by default (SPEC-067). Use a `token` selector only when no relationship covers the value, and a literal only for layout keywords, `transparent`, zero, ratio radii or geometry no token describes (SPEC-068). If the token you need is missing, file the gap; do not hide it behind a literal. Put option-dependent choices in the relationships, not in the recipe: pin `scope.options` on the relationship records (a default with no options, overrides that pin them) and bind the role once, so a variant added later is a data change. **Exception: platform-owned components.** A recipe for a component that a platform manifest declares (for example `button-ios`) MAY bind platform tokens directly with `token` selectors, because the platform's own code binds tokens by name and has no relationship layer. This applies to platform-owned components only; foundation components stay relationship-first. SPEC-062 and, for platform recipes, SPEC-079 keep checking that every selected token resolves. Watch the recipe size: the mapping from option to token then lives in the recipe, so name the platform's tokens so that selectors can template the option values, and measure the recipe's line count (see [Proposal 014](../../../docs/proposals/014-platform-owned-visual-recipes.md)).
155
215
  2. **Bind current tokens.** Do not bind a deprecated token (SPEC-069; SPEC-058 covers relationships). When the only token is deprecated, bind it and record the gap, so the lint keeps showing it.
156
216
  3. **Selection is an option, not a state.** Model "selected" as the `isSelected` option and scope relationships with `options.isSelected: true`. The renderer treats `isSelected` as the `selected` state and `isDisabled` as `disabled` when choosing between tokens that carry a state.
157
- 4. **Cover the component.** Every option and state the component declares either changes the recipe or is listed in `notVisual` (SPEC-066). `notVisual` records a deliberate absence, for example `isReadOnly` (it only suppresses hover and press) or `icon` until a shared icon registry exists.
217
+ 4. **Cover the component.** Every option and state the component declares either changes the recipe or is listed in `notVisual` (SPEC-066). `notVisual` records a deliberate absence, for example `isReadOnly` (it only suppresses hover and press), or the button's `icon` option, which names a workflow icon but does not yet choose the content icon a request draws (p6ga.17.4 decides how the two relate).
158
218
  5. **Press scales the part the design scales.** Set `pressTarget` to the part the design source scales when pressed: the checkbox scales its `box` (with its mark), the switch scales its `handle`. Parts nested inside the target scale with it, and the label does not.
159
219
  6. **Priorities.** Disabled overrides interaction states with `priority: 10` (11 where a more specific disabled case must win). Error uses `priority: 5`.
160
220
  7. **Relationship contexts are the design token group label,** with no `S1` or `S2` prefix, and the same label names the same concept in every component (for example `Control (disabled)`).
@@ -165,7 +225,7 @@ New recipes **MUST** be free of SPEC-066 to SPEC-070 warnings. Existing recipes
165
225
 
166
226
  ## Render request and scene graph
167
227
 
168
- A render request is tool input, not dataset content: `{component, options, states, modes, overlays}`. A **matrix** is a list of requests plus a layout (rows × columns by option or state).
228
+ A render request is tool input, not dataset content: `{component, options, states, modes, label, icon, overlays}`. `label` replaces the label text and `icon` the content icon (see [Icons](#icons)). A **matrix** is a list of requests plus a layout (rows × columns by option or state).
169
229
 
170
230
  The scene graph is the contract between resolution and output:
171
231
 
@@ -189,7 +249,7 @@ These record the research behind this draft and are not normative.
189
249
 
190
250
  **R3 — Layout: minimal flex first.** The pilot box model needs row direction, centering, gap, padding and one absolutely placed child. A purpose-built layout of that size avoids a dependency and keeps WASM small. Revisit `taffy` when text-field (wrapping, stacked label and help text) is piloted.
191
251
 
192
- **R4 — Icons.** Glyph geometry is sourced as SVG path data keyed by icon name (`workflow-icon`) and by the pilot's fixed glyphs (hold-icon corner triangle, checkmark). Literal paths are the one allowed use of `{"value": ...}`. A shared icon registry is follow-up work.
252
+ **R4 — Icons.** Icon geometry is SVG path data in the dataset's `icons/` catalog, imported from React Spectrum S2 (UI icons at every size, a few workflow icons). Recipes reference it by set and name ([Icons](#icons)); they hold no path data.
193
253
 
194
254
  **R5 — Figma.** The Figma backend consumes the scene graph and emits node JSON for a plugin to create frames bound to Figma Variables by token id, reusing the Variable mapping in `sdk/plugins/figma`. It requires no Figma-specific data in recipes.
195
255
 
@@ -18,6 +18,7 @@ A dataset is rooted at a single **dataset root** directory. The following table
18
18
  | `mode-sets/` | Optional (registered) | Mode set declarations, one per `*.json` file. | [`mode-set.schema.json`](../schemas/mode-set.schema.json) |
19
19
  | `registry/` | Optional (registered) | Registry value collections (vocabulary), one or more `*.json` files. | [`registry-value.json`](../schemas/registry-value.json) |
20
20
  | `guidelines/` | Optional (registered) | One guideline document per `*.json` file. | [`guideline.schema.json`](../schemas/guideline.schema.json) |
21
+ | `icons/` | Optional (registered) | One icon set (path geometry keyed by icon id) per `*.json` file. | [`icon-set.schema.json`](../schemas/icon-set.schema.json) |
21
22
 
22
23
  **NORMATIVE:** A conformant dataset **MUST** contain a `tokens/` directory holding at least one `*.tokens.json` file. A dataset with no `tokens/` directory, or a `tokens/` directory containing no `*.tokens.json` file, is **not** conformant.
23
24
 
@@ -37,6 +38,7 @@ A dataset is rooted at a single **dataset root** directory. The following table
37
38
  | `mode-sets/` | `*.json` | One declaration per file. |
38
39
  | `registry/` | `*.json` | One value collection per file. |
39
40
  | `guidelines/` | `*.json` | One declaration per file. |
41
+ | `icons/` | `*.json` | One set per file; by convention named after its `set` (`ui.json`). |
40
42
 
41
43
  ## Dataset discovery
42
44
 
@@ -47,7 +49,7 @@ Tooling that operates on "the current dataset" (rather than an explicit path) **
47
49
  3. **In-repo probing** — when running inside a monorepo checkout, probe for the standard layout relative to the working directory (`packages/design-data/tokens` and one level up). This preserves the in-repo workflow with zero configuration.
48
50
  4. **Embedded snapshot** — when no dataset is found on disk, tooling MAY fall back to a snapshot of the foundation dataset embedded in the binary, materialized to a local cache directory.
49
51
 
50
- **NORMATIVE:** The required directory (`tokens/`) and registered optional directories are resolved **relative to the dataset root** determined above. A discovery tier that resolves a dataset root **MUST** look for `tokens/`, `components/`, `fields/`, `mode-sets/`, `registry/`, and `guidelines/` directly under that root.
52
+ **NORMATIVE:** The required directory (`tokens/`) and registered optional directories are resolved **relative to the dataset root** determined above. A discovery tier that resolves a dataset root **MUST** look for `tokens/`, `components/`, `fields/`, `mode-sets/`, `registry/`, `guidelines/`, and `icons/` directly under that root.
51
53
 
52
54
  The reference implementation of this algorithm lives in the SDK at `sdk/core/src/data_source/mod.rs` (`resolve`, `from_root`, and `probe_cwd`).
53
55