@pithyjs/pidl-catalog 0.1.0-beta.1 → 0.1.0-beta.2
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 +11 -7
- package/dist/specs/atoms/data/bars.pidl.json +315 -0
- package/dist/specs/atoms/data/line-area.pidl.json +328 -0
- package/dist/specs/atoms/data/line.pidl.json +343 -0
- package/dist/specs/atoms/data/stacked-bar.pidl.json +341 -0
- package/dist/src/mutation-vocabulary.d.ts.map +1 -1
- package/dist/src/mutation-vocabulary.js +10 -3
- package/dist/src/mutation-vocabulary.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "component",
|
|
3
|
+
"id": "ui.stacked-bar",
|
|
4
|
+
"name": "StackedBar",
|
|
5
|
+
"displayName": "Stacked Bar Chart",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"level": "atom",
|
|
8
|
+
"domain": "data",
|
|
9
|
+
"bidi": true,
|
|
10
|
+
"description": "A single horizontal bar divided into proportional parts of one whole, with a colour key beneath. The same statement a Donut makes, drawn straight — better wherever the chart has to sit in a band rather than a square, and wherever the parts are lopsided, because a 2% slice of a ring is a wedge too narrow to see at its point while 2% of a bar is a stripe of constant height. Rendered in DOM as a flex row of segments, not SVG. The drawing and the key are aria-hidden; a visually-hidden data table carries every exact value and share and is the chart's accessible representation. Loading, empty (including an all-zero stack), and error states are first-class.",
|
|
11
|
+
"props": [
|
|
12
|
+
{
|
|
13
|
+
"name": "label",
|
|
14
|
+
"label": "Accessible name",
|
|
15
|
+
"type": "string",
|
|
16
|
+
"required": true,
|
|
17
|
+
"placeholder": "Budget by department",
|
|
18
|
+
"description": "Captions the visually-hidden data table screen readers read instead of the graphic. Required — it is the chart's accessible name."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "segments",
|
|
22
|
+
"label": "Segments",
|
|
23
|
+
"type": "StackedBarSegment[] | Signal<StackedBarSegment[]>",
|
|
24
|
+
"required": true,
|
|
25
|
+
"reactive": true,
|
|
26
|
+
"placeholder": [
|
|
27
|
+
{ "label": "Engineering", "value": 420 },
|
|
28
|
+
{ "label": "Marketing", "value": 260 },
|
|
29
|
+
{ "label": "Operations", "value": 130 }
|
|
30
|
+
],
|
|
31
|
+
"description": "Parts in stack order. A value must be a finite number >= 0; anything else is counted as zero, warned about in DEV, and still listed in the accessible table.",
|
|
32
|
+
"itemSchema": {
|
|
33
|
+
"label": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"required": true,
|
|
36
|
+
"description": "Category name. Appears in the colour key and as the row header in the accessible table."
|
|
37
|
+
},
|
|
38
|
+
"value": {
|
|
39
|
+
"type": "number",
|
|
40
|
+
"required": true,
|
|
41
|
+
"description": "The quantity. Finite and >= 0."
|
|
42
|
+
},
|
|
43
|
+
"color": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"editor": { "type": "token-color" },
|
|
46
|
+
"enum": [
|
|
47
|
+
"primary",
|
|
48
|
+
"secondary",
|
|
49
|
+
"tertiary",
|
|
50
|
+
"success",
|
|
51
|
+
"info",
|
|
52
|
+
"warning",
|
|
53
|
+
"danger",
|
|
54
|
+
"categorical-1",
|
|
55
|
+
"categorical-2",
|
|
56
|
+
"categorical-3",
|
|
57
|
+
"categorical-4",
|
|
58
|
+
"categorical-5",
|
|
59
|
+
"categorical-6"
|
|
60
|
+
],
|
|
61
|
+
"description": "Design-token name for this part. Omit to take the categorical palette slot for the segment's index — the correct default for identity encoding."
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "total",
|
|
67
|
+
"label": "Whole",
|
|
68
|
+
"type": "number | Signal<number>",
|
|
69
|
+
"reactive": true,
|
|
70
|
+
"placeholder": 1000,
|
|
71
|
+
"description": "The whole the parts are parts OF — a NUMBER, and the denominator every share is taken of. Note this is not DonutProps.total, which is a display string for the middle of the ring: a donut can only ever show the whole it was given, while a bar can show a whole the parts do not reach. Omit it and the parts fill the bar exactly. Pass one and any shortfall stays visible as unfilled track — '70 of a budget of 100' is a different statement from '70, which is all there is', and only this prop can tell them apart. Parts that overrun it are scaled to fill the bar exactly and DEV-warned; a value that is not a finite number greater than zero is DEV-warned and the sum is used instead."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "legend",
|
|
75
|
+
"label": "Colour key",
|
|
76
|
+
"editor": { "tab": "layout" },
|
|
77
|
+
"type": "enum",
|
|
78
|
+
"enum": ["end", "bottom", "none"],
|
|
79
|
+
"default": "bottom",
|
|
80
|
+
"description": "Where the colour key sits. 'bottom' (the default) puts it under the bar as a wrapping line, which is where a wide, short drawing wants it. 'end' places it beside the bar, wrapping underneath when the row cannot hold both — intrinsically, with no breakpoint deciding. 'none' removes it, and is only correct when the colour-to-category mapping is carried elsewhere: a one-segment bar, or a stack of bars under one shared key."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "size",
|
|
84
|
+
"label": "Size",
|
|
85
|
+
"editor": { "tab": "layout" },
|
|
86
|
+
"type": "enum",
|
|
87
|
+
"enum": ["sm", "md", "lg"],
|
|
88
|
+
"default": "md",
|
|
89
|
+
"description": "Sets the bar's thickness. It does NOT cap the chart's width, unlike the circular charts: a bar's inline extent is its SCALE rather than its size. The chart fills its column and stays intrinsically fluid, with no breakpoints."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "loading",
|
|
93
|
+
"label": "Loading",
|
|
94
|
+
"editor": { "tab": "settings" },
|
|
95
|
+
"type": "boolean | Signal<boolean>",
|
|
96
|
+
"default": false,
|
|
97
|
+
"reactive": true,
|
|
98
|
+
"description": "Hides the drawing and the table, shows a pulsing skeleton reserving exactly the bar's own height, and sets aria-busy on the drawing's wrapper. The colour key keeps its box and loses its text, so the chart does not resize when data arrives. Outranks every other state."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "error",
|
|
102
|
+
"label": "Error",
|
|
103
|
+
"editor": { "tab": "settings" },
|
|
104
|
+
"type": "boolean | Signal<boolean>",
|
|
105
|
+
"default": false,
|
|
106
|
+
"reactive": true,
|
|
107
|
+
"description": "Hides the drawing and the table and shows the error message. Outranks the empty state — a failed fetch usually leaves an empty array behind it."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "errorMessage",
|
|
111
|
+
"label": "Error message",
|
|
112
|
+
"editor": { "tab": "settings" },
|
|
113
|
+
"type": "string | Signal<string>",
|
|
114
|
+
"default": "Chart data unavailable",
|
|
115
|
+
"reactive": true,
|
|
116
|
+
"description": "Copy shown in the error state."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "emptyMessage",
|
|
120
|
+
"label": "Empty message",
|
|
121
|
+
"editor": { "tab": "settings" },
|
|
122
|
+
"type": "string",
|
|
123
|
+
"default": "No data to display",
|
|
124
|
+
"description": "Copy shown when there are no segments, or when every segment is zero."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "loadingLabel",
|
|
128
|
+
"label": "Loading message",
|
|
129
|
+
"editor": { "tab": "settings" },
|
|
130
|
+
"type": "string",
|
|
131
|
+
"default": "Loading chart…",
|
|
132
|
+
"description": "Copy shown in the loading state."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "id",
|
|
136
|
+
"label": "ID",
|
|
137
|
+
"editor": { "tab": "settings" },
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Explicit element ID on the <figure> root."
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"events": [],
|
|
143
|
+
"slots": [],
|
|
144
|
+
"tokens": {
|
|
145
|
+
"track.background": "--color-surface-variant",
|
|
146
|
+
"track.border": "--color-border at --border-width-thin",
|
|
147
|
+
"track.radius": "--radius-sm",
|
|
148
|
+
"segment.background": "--color-categorical-1 … --color-categorical-6, or any semantic colour",
|
|
149
|
+
"segment.background.fallback": "--color-text-muted (segments past the last categorical slot)",
|
|
150
|
+
"segment.emphasis.outline": "--color-text at --border-width-thick",
|
|
151
|
+
"segment.motion.duration": "--motion-duration-slow",
|
|
152
|
+
"segment.motion.easing": "--motion-easing-emphasized",
|
|
153
|
+
"legend.swatch.background": "--color-categorical-1 … --color-categorical-6 (the same token the swatch's own segment takes)",
|
|
154
|
+
"legend.name.color": "--color-text-muted",
|
|
155
|
+
"legend.value.color": "--color-text",
|
|
156
|
+
"legend.value.font": "--font-family-mono",
|
|
157
|
+
"legend.size": "--font-size-sm",
|
|
158
|
+
"status.color": "--color-text-muted",
|
|
159
|
+
"status.error.color": "--color-danger",
|
|
160
|
+
"placeholder.background": "--color-surface-variant",
|
|
161
|
+
"placeholder.radius": "--radius-sm",
|
|
162
|
+
"forced-colors.border": "--border-width-thin, --border-width-hairline"
|
|
163
|
+
},
|
|
164
|
+
"spacing": {
|
|
165
|
+
"internal": {
|
|
166
|
+
"bar-to-legend": {
|
|
167
|
+
"relationship": "friends",
|
|
168
|
+
"token": "md",
|
|
169
|
+
"note": "Stacked, the key reads as a caption of the drawing rather than as a separate block."
|
|
170
|
+
},
|
|
171
|
+
"legend-entry-to-entry": {
|
|
172
|
+
"relationship": "friends",
|
|
173
|
+
"token": "md",
|
|
174
|
+
"note": "Separate items scanned across one wrapping line, so they need more air between them than a swatch and its own name do."
|
|
175
|
+
},
|
|
176
|
+
"swatch-to-name": {
|
|
177
|
+
"relationship": "lovers",
|
|
178
|
+
"token": "xs",
|
|
179
|
+
"note": "A swatch means nothing apart from its name."
|
|
180
|
+
},
|
|
181
|
+
"graphic-to-status": { "relationship": "close_friends", "token": "sm" }
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"constraints": [
|
|
185
|
+
"Use logical properties only — no left/right, no width/height where inline-size/block-size applies",
|
|
186
|
+
"Colour is a design-token NAME, never a CSS value — there is no prop through which a hex code can reach a stylesheet",
|
|
187
|
+
"No function-valued props. Every prop has to be editable and serialisable through PIDL, so there is no formatValue callback; numbers are formatted by the kit's own formatter, which the key and the data table both go through so the two cannot disagree",
|
|
188
|
+
"Rendered in DOM, not SVG: a stacked bar is a row of rectangles, so the browser lays them out, no viewBox scaling stands between the reader's font size and the key beside it, and rounding both ends of the bar is one overflow: hidden rather than clipped paths",
|
|
189
|
+
"The graphic is aria-hidden — the visually-hidden data table is the accessible representation, so `label` is required",
|
|
190
|
+
"No title attribute on a segment. The design source used one; a title tooltip has no keyboard and no touch equivalent and is not reliably announced, so a mapping living there is a mapping some readers never get",
|
|
191
|
+
"The chart is width-agnostic: it fills its column and `size` sets thickness instead",
|
|
192
|
+
"Segments are flex: none — exactly their share, never grown to fill a remainder and never shrunk to make room. A shortfall under an explicit `total` is supposed to stay visible as unfilled track, and flex: 1 would quietly close it",
|
|
193
|
+
"A non-zero segment too small to see is widened to a minimum visible width, and that width is given back FIRST by any unfilled track and then by the leaders — levelled off the top of the stack, tallest down, rather than charged to any one segment. Segments are adjacent, so a floor that is not paid for pushes the last one off the end of the track. Levelling keeps the distortion where a given absolute width is the smallest relative error AND cannot reverse rank: charging the single largest could push it below the runner-up, so the bar said the opposite of its own table",
|
|
194
|
+
"If the floor cannot be satisfied at all — more real categories than the track has room to give each one its minimum — it is abandoned wholesale rather than applied to an arbitrary prefix of them, and the exact proportions stand",
|
|
195
|
+
"A value of zero is drawn at no width at all. Nothing is not a sliver, and a zero segment still appears in the table and the key",
|
|
196
|
+
"Parts that sum past an explicit total are scaled proportionally to fill the bar exactly, and DEV-warned. Relative sizes survive the rescale; the table still reports the true values",
|
|
197
|
+
"The drawn parts can never exceed the track: after the floor and its rebalance a final proportional rescale guarantees it, so the invariant is total rather than argued",
|
|
198
|
+
"An all-zero stack, a stack with no segments, and a sum that overflows to Infinity all render the empty state; the geometry never divides by zero",
|
|
199
|
+
"The drawing and the data table are hidden together in every non-ready state so the two cannot disagree about whether there is data. The KEY is not hidden with them — it keeps its box and loses its text to skeleton bars, so it states nothing while reserving the space it will need",
|
|
200
|
+
"The whole is a <tfoot> row in the data table. Under an explicit `total` it is NOT the sum of the body rows, and that gap is the shortfall the bar draws as unfilled track",
|
|
201
|
+
"An empty or whitespace-only segment label is a MISSING label and becomes 'Series N' — a blank row header is a category a reader can neither name nor look up",
|
|
202
|
+
"Segment colour defaults to the categorical palette slot for the segment index; past the sixth slot the segment is painted neutral rather than repeating slot 1",
|
|
203
|
+
"A multi-segment bar must carry a colour key — its own, or one shared on the page. Colour is the only thing identifying a segment (WCAG 1.4.1), and there is no room inside a 34px band for a label. legend: 'none' opts out of the built-in key, not out of the requirement",
|
|
204
|
+
"One colour resolution feeds both the segment and its swatch; a key painted by a second call is a key that can disagree with the graphic, and disagree quietly",
|
|
205
|
+
"Hover emphasis adds an inset ring rather than dimming the other segments — dimming a mark lowers its contrast against its surround, and WCAG 1.4.11 asks for 3:1 from every one of them",
|
|
206
|
+
"The segment width transition is inside a motion-safe block, and its width custom property defaults to 0% — an unset segment must take no room, never share it out"
|
|
207
|
+
],
|
|
208
|
+
"a11y": [
|
|
209
|
+
"The drawing carries aria-hidden='true' — it is a picture of the table beside it",
|
|
210
|
+
"A <table class='u-sr-only'> carries every exact value and share, captioned with the required `label` prop",
|
|
211
|
+
"Table headers are scoped: <th scope='col'> in the head, <th scope='row'> per segment",
|
|
212
|
+
"The shares in the table are the EXACT ones, never the drawn widths — the visibility floor and the overflow rescale have both had their hands on those, which is why the table and not the bar is the reading of record",
|
|
213
|
+
"Zero-value segments and segments widened by the floor both appear in the table at their true values",
|
|
214
|
+
"A share that is greater than zero but rounds to zero reads '<0.1%', never '0%'",
|
|
215
|
+
"The whole is a <tfoot> row, and under an explicit `total` it is the only place a screen reader can find the figure the shortfall is a shortfall against",
|
|
216
|
+
"The legend is aria-hidden: the table already carries every category, value and share, and exposing both would read the dataset twice, the second time in a flatter form. Nothing inside the legend is focusable, which is what makes that safe",
|
|
217
|
+
"No title tooltips on the segments — unreachable by keyboard and touch, and not reliably announced",
|
|
218
|
+
"Status messages live in a role='status' region present from first render — a live region inserted when it gains content is often not announced",
|
|
219
|
+
"aria-busy='true' while loading goes on the drawing's wrapper (.ui-chart-body), NEVER on the root. The role='status' region is a child of the root, and a live region inside an aria-busy subtree has its changes DEFERRED until busy clears — which happens in the same call that blanks the region, so the announcement an AT ends up processing is 'empty'",
|
|
220
|
+
"forced-color-adjust: none on the segments and the track, restated here because the shared partial's rule selects '.ui-chart-graphic > svg' and this chart has no SVG. In a chart the colour IS the data. Segments also gain a hairline CanvasText divider, so the divisions stay readable when a forced palette has collapsed the hues",
|
|
221
|
+
"Hover emphasis carries no information — every category and share is already in the key — so it is free to be pointer-only; no keyboard or touch reader loses anything by never triggering it",
|
|
222
|
+
"The segment growth animation is inside a prefers-reduced-motion: no-preference block"
|
|
223
|
+
],
|
|
224
|
+
"ai": {
|
|
225
|
+
"synonyms": [
|
|
226
|
+
"stacked bar",
|
|
227
|
+
"stacked bar chart",
|
|
228
|
+
"progress bar chart",
|
|
229
|
+
"composition bar",
|
|
230
|
+
"parts of a whole",
|
|
231
|
+
"share of total",
|
|
232
|
+
"breakdown bar",
|
|
233
|
+
"segmented bar",
|
|
234
|
+
"100% stacked bar",
|
|
235
|
+
"colour key",
|
|
236
|
+
"color key"
|
|
237
|
+
],
|
|
238
|
+
"avoid": [
|
|
239
|
+
"Passing a CSS colour — the color field takes a design-token name",
|
|
240
|
+
"Using it to compare independent categories — that is Bars. Everything here is a part of one whole",
|
|
241
|
+
"More than six uncoloured segments — fold the tail into an explicit 'Other' bucket",
|
|
242
|
+
"Encoding meaning with the semantic palette for arbitrary categories (danger for 'category 3' says category 3 is bad)",
|
|
243
|
+
"Setting legend='none' on a multi-segment bar with no other key on the page — that leaves colour as the sole carrier of meaning, and unlike Bars there is no room for a label inside the band",
|
|
244
|
+
"Asking for title tooltips on the segments — unreachable by touch and by keyboard, and the key is already on screen",
|
|
245
|
+
"Hand-rolling a key beside the component: the swatch has to resolve its token exactly as the segment does, and a separately built key can drift from the graphic",
|
|
246
|
+
"Passing a display string to `total` — it is a number, the denominator. DonutProps.total is the string one",
|
|
247
|
+
"Reading the drawn widths as exact: the visibility floor and the overflow rescale both bend them, and the table is the exact record"
|
|
248
|
+
],
|
|
249
|
+
"patterns": [
|
|
250
|
+
"Basic: <StackedBar label=\"'Budget by department'\" segments=\"departments\" />",
|
|
251
|
+
"With a visible shortfall: <StackedBar label=\"'Budget'\" segments=\"spend\" total=\"100\" />",
|
|
252
|
+
"Explicit colours: segments=[{ label: 'Passed', value: 900, color: 'success' }]",
|
|
253
|
+
"Live: <StackedBar label=\"'Mix'\" segments=\"() => mix()\" loading=\"isFetching\" />",
|
|
254
|
+
"Key beside the bar in a wide row: <StackedBar label=\"'Mix'\" segments=\"mix\" legend=\"'end'\" />",
|
|
255
|
+
"Key suppressed because one is shared across a stack of bars: <StackedBar label=\"'Mix'\" segments=\"mix\" legend=\"'none'\" />"
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
"examples": [
|
|
259
|
+
{
|
|
260
|
+
"id": "basic",
|
|
261
|
+
"title": "Three parts of one whole",
|
|
262
|
+
"pithy": "<StackedBar label=\"'Budget by department'\" segments=\"departments\" />",
|
|
263
|
+
"assertions": [
|
|
264
|
+
"figure.ui-chart.ui-stacked-bar with data-status='ready'",
|
|
265
|
+
"one .ui-stacked-bar-track holding one .ui-stacked-bar-segment per datum",
|
|
266
|
+
"the segments' widths sum to 100%",
|
|
267
|
+
"data-color runs categorical-1, categorical-2, categorical-3",
|
|
268
|
+
"no <svg> and no title attribute anywhere",
|
|
269
|
+
"no axe violations"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"id": "shortfall",
|
|
274
|
+
"title": "An explicit whole the parts do not reach",
|
|
275
|
+
"pithy": "<StackedBar label=\"'Budget'\" segments=\"[{label:'Spent',value:40},{label:'Committed',value:30}]\" total=\"100\" />",
|
|
276
|
+
"assertions": [
|
|
277
|
+
"segments render at 40% and 30%",
|
|
278
|
+
"30% of the track is left unfilled",
|
|
279
|
+
"the <tfoot> reads 'Total, 100, 100%' while the body rows sum to 70"
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"id": "overflow",
|
|
284
|
+
"title": "Parts that overrun the stated total",
|
|
285
|
+
"pithy": "<StackedBar label=\"'Budget'\" segments=\"[{label:'A',value:80},{label:'B',value:60}]\" total=\"100\" />",
|
|
286
|
+
"assertions": [
|
|
287
|
+
"the drawn widths sum to exactly 100%",
|
|
288
|
+
"their relative sizes survive the rescale",
|
|
289
|
+
"DEV warns that the segments exceed the total",
|
|
290
|
+
"the table still reports 80 and 60"
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"id": "long-tail",
|
|
295
|
+
"title": "A segment too small to see",
|
|
296
|
+
"pithy": "<StackedBar label=\"'Long tail'\" segments=\"[{label:'Bulk',value:100000},{label:'Speck',value:1}]\" />",
|
|
297
|
+
"assertions": [
|
|
298
|
+
"the tiny segment is widened to the minimum visible width",
|
|
299
|
+
"the largest segment gives that width back, so the parts still fit the track",
|
|
300
|
+
"the table reports the tiny segment's share as <0.1%"
|
|
301
|
+
]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"id": "zero-total",
|
|
305
|
+
"title": "Every segment is zero",
|
|
306
|
+
"pithy": "<StackedBar label=\"'Budget'\" segments=\"allZero\" />",
|
|
307
|
+
"assertions": [
|
|
308
|
+
"data-status='empty' — the parts are shares of a whole that is zero, so there is no segment to draw and the key would name nothing. This deliberately differs from Bars, whose rows label themselves and so stay 'ready'",
|
|
309
|
+
"no NaN or Infinity anywhere in the markup"
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"id": "legend-default",
|
|
314
|
+
"title": "The colour key is on by default, under the bar",
|
|
315
|
+
"pithy": "<StackedBar label=\"'Budget by department'\" segments=\"departments\" />",
|
|
316
|
+
"assertions": [
|
|
317
|
+
"the figure carries data-legend='bottom'",
|
|
318
|
+
"one .ui-chart-legend-item per segment, in mark order",
|
|
319
|
+
"each swatch's data-color equals its own segment's data-color",
|
|
320
|
+
"the legend is aria-hidden='true', and the table still carries the numbers once"
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"id": "loading",
|
|
325
|
+
"title": "Loading",
|
|
326
|
+
"pithy": "<StackedBar label=\"'Budget'\" segments=\"departments\" loading=\"true\" />",
|
|
327
|
+
"assertions": [
|
|
328
|
+
"aria-busy='true' on .ui-chart-body, and absent from the root",
|
|
329
|
+
"graphic and table both hidden, the key still present",
|
|
330
|
+
"role='status' region reads 'Loading chart…'"
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
"designGuidance": {
|
|
335
|
+
"surface": "No background of its own — the bar sits on whatever surface hosts it. The track uses --color-surface-variant with a --color-border hairline, so an unfilled remainder reads as a defined container rather than as empty page, which is what makes a shortfall legible as a shortfall.",
|
|
336
|
+
"depth": "Flat. No shadow: the segments already read as one object because the track clips them, and a drop shadow on a band this short muddies the boundary between neighbouring segments.",
|
|
337
|
+
"borders": "One hairline around the track, which is what gives an unfilled remainder an edge. Segments have no borders between them in normal rendering — adjacent categorical hues separate themselves. In forced-colors both the track and the segments opt out of colour substitution and the segments gain a hairline CanvasText divider, since a forced palette may have collapsed the hues that were doing the separating.",
|
|
338
|
+
"interactivity": "Hover pairs a key row with its segment, in both directions: the segment takes an inset ring, the row takes a surface-variant background. That is emphasis, not disclosure — it restates what the key already says, which is precisely why it may be pointer-only. It earns its place here more than on a donut, because adjacent stripes of similar colour are genuinely hard to match to a key by eye. An inset ring rather than dimming the neighbours: dimming is the one treatment that lowers a mark's contrast against its surround, and WCAG 1.4.11 asks 3:1 of every one of them.",
|
|
339
|
+
"bestPractices": "Use it for parts of one whole in a band — under a heading, along the top of a card, or wherever a square would waste the column. Prefer it over a Donut when the parts are lopsided: a 2% wedge tapers to nothing at its point, while a 2% stripe keeps its full height. Keep to six or fewer categories and fold the rest into 'Other' — the categorical palette guarantees separation for six. Order the segments so the meaning runs one way (best to worst, oldest to newest); a stacked bar in arbitrary order is harder to read than a list. Pass `total` whenever the shortfall is the point, and leave it off whenever it is not — an unfilled bar with no explanation reads as broken. Keep the key: it is the only thing that turns a colour into a category, and the palette's CVD work guarantees the slots are DISTINGUISHABLE from each other, never that any one of them is identifiable. Always give a specific `label`: it is the caption a screen-reader user hears."
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation-vocabulary.d.ts","sourceRoot":"","sources":["../../src/mutation-vocabulary.ts"],"names":[],"mappings":"AAAA,uHAAuH;AAEvH;;;;;;;;;GASG;AAEH,eAAO,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"mutation-vocabulary.d.ts","sourceRoot":"","sources":["../../src/mutation-vocabulary.ts"],"names":[],"mappings":"AAAA,uHAAuH;AAEvH;;;;;;;;;GASG;AAEH,eAAO,MAAM,mBAAmB,QA6DxB,CAAC"}
|
|
@@ -15,7 +15,7 @@ export const MUTATION_VOCABULARY = `
|
|
|
15
15
|
You can modify the site using these operations:
|
|
16
16
|
|
|
17
17
|
### Page Operations
|
|
18
|
-
- \`page.create\` — Create a new page. Include \`title\`, \`path\`, and
|
|
18
|
+
- \`page.create\` — Create a new page. Include \`title\`, \`path\`, and ALWAYS an explicit \`id\` (a short kebab-case slug, e.g. "about", "contact"). The \`id\` is not conditional: every page you create must be populated in the same response, and the blocks need the id to target.
|
|
19
19
|
- \`page.delete\` — Delete a page by ID
|
|
20
20
|
- \`page.reorder\` — Change the order of pages
|
|
21
21
|
|
|
@@ -30,6 +30,12 @@ Therefore, to build a NEW page with content:
|
|
|
30
30
|
|
|
31
31
|
NEVER reference a page you have not created (e.g. a \`block.add\` with \`"pageId": "about"\` when no page "about" exists and no \`page.create\` with \`"id": "about"\` precedes it). Do not invent page ids — only use ids you created or that already exist.
|
|
32
32
|
|
|
33
|
+
### Every Created Page Must Be Populated AND Linked
|
|
34
|
+
A \`page.create\` on its own is never a complete answer. In the SAME \`mutations\` array, also:
|
|
35
|
+
|
|
36
|
+
1. **Populate it** — one \`block.add\` per section, each using the pinned \`id\` as its \`pageId\`. A page with no blocks renders as a blank screen; creating one and stopping there gives the user nothing.
|
|
37
|
+
2. **Link it** — put the new page's \`path\` into the site's navigation. \`hasNavigation\` tells you which case you are in — it reports a \`ui.navbar\`, not a footer. When it is \`false\`, \`block.add\` a \`ui.navbar\` with \`"index": 0\` onto a page visitors already reach, listing the new page and every existing one except the not-found page (\`/404\`); \`label\` is a REQUIRED prop and the whole response is discarded without it. A \`ui.footer-section\` link group added the same way works too. When it is \`true\` you cannot add to the existing navbar here — \`block.update\` replaces props wholesale and you are not given its current \`items\` — so say in your \`summary\` that the page still needs linking. Nothing links to a page you only created, so no visitor can ever reach it.
|
|
38
|
+
|
|
33
39
|
### Block Operations
|
|
34
40
|
- \`block.add\` — Add a new block (component) to a page
|
|
35
41
|
- \`block.remove\` — Remove a block by ID
|
|
@@ -56,11 +62,12 @@ Single-mutation \`mutations\` value (valid, parseable JSON):
|
|
|
56
62
|
]
|
|
57
63
|
\`\`\`
|
|
58
64
|
|
|
59
|
-
Multi-page \`mutations\` value — note the explicit \`page.create.id\` reused as \`block.add.pageId
|
|
65
|
+
Multi-page \`mutations\` value — note the explicit \`page.create.id\` reused as \`block.add.pageId\`, and the third mutation adding a navbar that routes to the new path (valid, parseable JSON):
|
|
60
66
|
\`\`\`json
|
|
61
67
|
[
|
|
62
68
|
{ "op": "page.create", "id": "about", "title": "About", "path": "/about" },
|
|
63
|
-
{ "op": "block.add", "pageId": "about", "component": "ui.hero-section", "props": { "heading": "About Me" } }
|
|
69
|
+
{ "op": "block.add", "pageId": "about", "component": "ui.hero-section", "props": { "heading": "About Me" } },
|
|
70
|
+
{ "op": "block.add", "pageId": "home", "index": 0, "component": "ui.navbar", "props": { "label": "Primary", "brand": "Studio", "items": [{ "label": "Home", "href": "/" }, { "label": "About", "href": "/about" }] } }
|
|
64
71
|
]
|
|
65
72
|
\`\`\`
|
|
66
73
|
`.trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation-vocabulary.js","sourceRoot":"","sources":["../../src/mutation-vocabulary.ts"],"names":[],"mappings":"AAAA,uHAAuH;AAEvH;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG
|
|
1
|
+
{"version":3,"file":"mutation-vocabulary.js","sourceRoot":"","sources":["../../src/mutation-vocabulary.ts"],"names":[],"mappings":"AAAA,uHAAuH;AAEvH;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DlC,CAAC,IAAI,EAAE,CAAC"}
|
package/package.json
CHANGED