@ponchia/ui 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +552 -8
- package/MIGRATIONS.json +106 -0
- package/README.md +34 -8
- package/annotations/index.d.ts +402 -0
- package/annotations/index.d.ts.map +1 -0
- package/annotations/index.js +792 -0
- package/behaviors/carousel.js +198 -0
- package/behaviors/combobox.js +226 -0
- package/behaviors/command.js +190 -0
- package/behaviors/connectors.js +95 -0
- package/behaviors/crosshair.js +57 -0
- package/behaviors/dialog.js +74 -0
- package/behaviors/disclosure.js +26 -0
- package/behaviors/dismissible.js +25 -0
- package/behaviors/forms.js +186 -0
- package/behaviors/glyph.js +108 -0
- package/behaviors/index.d.ts +79 -0
- package/behaviors/index.js +18 -1409
- package/behaviors/internal.js +97 -0
- package/behaviors/legend.js +67 -0
- package/behaviors/menu.js +47 -0
- package/behaviors/popover.js +179 -0
- package/behaviors/spotlight.js +52 -0
- package/behaviors/table.js +136 -0
- package/behaviors/tabs.js +103 -0
- package/behaviors/theme.js +84 -0
- package/behaviors/toast.js +164 -0
- package/classes/classes.json +1857 -0
- package/classes/index.d.ts +306 -13
- package/classes/index.js +339 -12
- package/classes/vscode.css-custom-data.json +12 -0
- package/connectors/index.d.ts +191 -0
- package/connectors/index.d.ts.map +1 -0
- package/connectors/index.js +275 -0
- package/css/analytical.css +21 -0
- package/css/annotations.css +292 -0
- package/css/app.css +43 -13
- package/css/base.css +15 -10
- package/css/command.css +97 -0
- package/css/connectors.css +110 -0
- package/css/content.css +7 -1
- package/css/crosshair.css +100 -0
- package/css/dataviz.css +5 -1
- package/css/disclosure.css +38 -6
- package/css/dots.css +57 -0
- package/css/feedback.css +111 -2
- package/css/fonts.css +11 -7
- package/css/forms.css +42 -1
- package/css/generated.css +117 -0
- package/css/legend.css +272 -0
- package/css/marks.css +174 -0
- package/css/motion.css +24 -44
- package/css/navigation.css +7 -0
- package/css/overlay.css +31 -1
- package/css/primitives.css +109 -5
- package/css/report.css +39 -81
- package/css/selection.css +46 -0
- package/css/site.css +16 -2
- package/css/sources.css +221 -0
- package/css/spotlight.css +104 -0
- package/css/state.css +121 -0
- package/css/tokens.css +60 -37
- package/css/workbench.css +83 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -0
- package/dist/css/annotations.css +1 -0
- package/dist/css/app.css +1 -1
- package/dist/css/base.css +1 -1
- package/dist/css/command.css +1 -0
- package/dist/css/connectors.css +1 -0
- package/dist/css/content.css +1 -1
- package/dist/css/crosshair.css +1 -0
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/fonts.css +1 -1
- package/dist/css/forms.css +1 -1
- package/dist/css/generated.css +1 -0
- package/dist/css/legend.css +1 -0
- package/dist/css/marks.css +1 -0
- package/dist/css/motion.css +1 -1
- package/dist/css/navigation.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/selection.css +1 -0
- package/dist/css/site.css +1 -1
- package/dist/css/sources.css +1 -0
- package/dist/css/spotlight.css +1 -0
- package/dist/css/state.css +1 -0
- package/dist/css/tokens.css +1 -1
- package/dist/css/workbench.css +1 -0
- package/docs/adr/0003-theme-model.md +7 -4
- package/docs/annotations.md +425 -0
- package/docs/architecture.md +246 -0
- package/docs/command.md +95 -0
- package/docs/connectors.md +91 -0
- package/docs/contrast.md +116 -92
- package/docs/crosshair.md +63 -0
- package/docs/d2.md +195 -0
- package/docs/generated.md +91 -0
- package/docs/legends.md +184 -0
- package/docs/marks.md +93 -0
- package/docs/mermaid.md +152 -0
- package/docs/reference.md +385 -23
- package/docs/reporting.md +436 -63
- package/docs/selection.md +40 -0
- package/docs/sources.md +137 -0
- package/docs/spotlight.md +78 -0
- package/docs/stability.md +24 -2
- package/docs/state.md +85 -0
- package/docs/usage.md +123 -4
- package/docs/vega.md +225 -0
- package/docs/workbench.md +78 -0
- package/fonts/doto-400.woff2 +0 -0
- package/fonts/doto-500.woff2 +0 -0
- package/fonts/doto-600.woff2 +0 -0
- package/fonts/doto-700.woff2 +0 -0
- package/fonts/doto-800.woff2 +0 -0
- package/fonts/doto-900.woff2 +0 -0
- package/glyphs/glyphs.js +6 -4
- package/llms.txt +362 -14
- package/package.json +115 -12
- package/qwik/index.d.ts +42 -54
- package/qwik/index.d.ts.map +1 -0
- package/qwik/index.js +75 -3
- package/react/index.d.ts +39 -56
- package/react/index.d.ts.map +1 -0
- package/react/index.js +67 -3
- package/solid/index.d.ts +64 -56
- package/solid/index.d.ts.map +1 -0
- package/solid/index.js +70 -3
- package/tokens/d2.d.ts +38 -0
- package/tokens/d2.js +71 -0
- package/tokens/d2.json +43 -0
- package/tokens/index.d.ts +5 -5
- package/tokens/index.js +23 -5
- package/tokens/index.json +9 -0
- package/tokens/mermaid.d.ts +23 -0
- package/tokens/mermaid.js +181 -0
- package/tokens/mermaid.json +163 -0
- package/tokens/resolved.json +45 -1
- package/tokens/skins.js +3 -2
- package/tokens/tokens.dtcg.json +26 -0
- package/tokens/vega.d.ts +34 -0
- package/tokens/vega.js +155 -0
- package/tokens/vega.json +179 -0
- package/fonts/doto-400.ttf +0 -0
- package/fonts/doto-500.ttf +0 -0
- package/fonts/doto-600.ttf +0 -0
- package/fonts/doto-700.ttf +0 -0
- package/fonts/doto-800.ttf +0 -0
- package/fonts/doto-900.ttf +0 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# SVG annotations
|
|
2
|
+
|
|
3
|
+
`@ponchia/ui/css/annotations.css` is an opt-in SVG annotation layer for charts,
|
|
4
|
+
reports, and analytical figures. It follows the same grammar as
|
|
5
|
+
d3-annotation: a **subject** marks the thing being discussed, a **connector**
|
|
6
|
+
points away from it, and a **note** carries the visible explanation.
|
|
7
|
+
|
|
8
|
+
```css
|
|
9
|
+
@import '@ponchia/ui';
|
|
10
|
+
@import '@ponchia/ui/css/annotations.css';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use it with any SVG renderer. Bronto supplies classes and tiny geometry helpers;
|
|
14
|
+
it does not own chart scales, mutate the DOM, or provide draggable edit mode.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import {
|
|
18
|
+
annotationParts,
|
|
19
|
+
annotationTransform,
|
|
20
|
+
axisThresholdPath,
|
|
21
|
+
bracketSubjectPath,
|
|
22
|
+
circleSubjectPath,
|
|
23
|
+
connectorLine,
|
|
24
|
+
evidenceMarkerPath,
|
|
25
|
+
notePlacement,
|
|
26
|
+
noteTransform,
|
|
27
|
+
} from '@ponchia/ui/annotations';
|
|
28
|
+
|
|
29
|
+
const transform = annotationTransform({ x: 180, y: 72 });
|
|
30
|
+
const subject = circleSubjectPath({ radius: 18 });
|
|
31
|
+
const connector = connectorLine({
|
|
32
|
+
dx: 88,
|
|
33
|
+
dy: -42,
|
|
34
|
+
subject: { type: 'circle', radius: 18, radiusPadding: 4 },
|
|
35
|
+
});
|
|
36
|
+
const parts = annotationParts({
|
|
37
|
+
x: 180,
|
|
38
|
+
y: 72,
|
|
39
|
+
dx: 88,
|
|
40
|
+
dy: -42,
|
|
41
|
+
subject: { type: 'circle', radius: 18, radiusPadding: 4 },
|
|
42
|
+
});
|
|
43
|
+
const note = notePlacement({
|
|
44
|
+
x: 180,
|
|
45
|
+
y: 72,
|
|
46
|
+
width: 96,
|
|
47
|
+
height: 44,
|
|
48
|
+
bounds: { x: 0, y: 0, width: 360, height: 180 },
|
|
49
|
+
preferred: 'right',
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Markup model
|
|
54
|
+
|
|
55
|
+
Author annotation groups at the subject anchor. Use `dx` / `dy` as the note
|
|
56
|
+
offset, matching d3-annotation's mental model.
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<svg viewBox="0 0 360 180" role="img" aria-labelledby="chart-title chart-desc">
|
|
60
|
+
<title id="chart-title">Annotated delivery chart</title>
|
|
61
|
+
<desc id="chart-desc">A callout marks the delivery peak.</desc>
|
|
62
|
+
|
|
63
|
+
<g
|
|
64
|
+
class="ui-annotation ui-annotation--circle ui-annotation--accent"
|
|
65
|
+
transform="translate(180, 72)"
|
|
66
|
+
>
|
|
67
|
+
<path class="ui-annotation__subject" d="M0,-18A18,18 0 1 1 0,18A18,18 0 1 1 0,-18Z" />
|
|
68
|
+
<path class="ui-annotation__connector" d="M15.556,-7.424L88,-42" />
|
|
69
|
+
<g class="ui-annotation__note" transform="translate(88, -42)">
|
|
70
|
+
<path class="ui-annotation__note-line" d="M0,0H76" />
|
|
71
|
+
<text class="ui-annotation__title" y="-8">Peak</text>
|
|
72
|
+
<text class="ui-annotation__label" y="12">Delivery spike</text>
|
|
73
|
+
</g>
|
|
74
|
+
</g>
|
|
75
|
+
</svg>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The visible note text should also be represented in the figure caption,
|
|
79
|
+
`<desc>`, fallback table, or surrounding prose when the figure is complex.
|
|
80
|
+
|
|
81
|
+
Match the accessibility treatment to the annotation's job, in both directions:
|
|
82
|
+
a **data** annotation (a peak, a threshold, a watched region — it says something
|
|
83
|
+
the reader needs) must stay readable, so represent its text as above and do
|
|
84
|
+
**not** `aria-hidden` it; a purely **decorative** mark (a cover flourish, a
|
|
85
|
+
margin doodle that carries no data) should be `aria-hidden="true"` and
|
|
86
|
+
`focusable="false"` on the whole SVG so a screen reader skips the decoration.
|
|
87
|
+
The one thing to avoid is the middle: a meaningful callout hidden from assistive
|
|
88
|
+
tech, or decoration announced as if it were data.
|
|
89
|
+
|
|
90
|
+
## Variants and motion
|
|
91
|
+
|
|
92
|
+
Use one variant class per annotation group. Variants describe the visual
|
|
93
|
+
grammar, not data semantics:
|
|
94
|
+
|
|
95
|
+
| Variant | Use |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `ui-annotation--label` | Direct label with no connector. |
|
|
98
|
+
| `ui-annotation--callout` | Plain point-to-note callout. |
|
|
99
|
+
| `ui-annotation--elbow` | Dogleg connector around dense marks. |
|
|
100
|
+
| `ui-annotation--curve` | Softer connector for paths or flows. |
|
|
101
|
+
| `ui-annotation--circle` | Circular subject around a point or local cluster. |
|
|
102
|
+
| `ui-annotation--rect` | Rectangular subject around a bar, block, or region. |
|
|
103
|
+
| `ui-annotation--threshold` | Horizontal or vertical limit rule. |
|
|
104
|
+
| `ui-annotation--badge` | Compact numbered or categorical mark. |
|
|
105
|
+
| `ui-annotation--bracket` | Range span on one axis. |
|
|
106
|
+
| `ui-annotation--band` | Interval, confidence band, or risk window. |
|
|
107
|
+
| `ui-annotation--slope` | Trend or slope segment. |
|
|
108
|
+
| `ui-annotation--compare` | Before/after or A/B grouping. |
|
|
109
|
+
| `ui-annotation--cluster` | Several nearby outliers. |
|
|
110
|
+
| `ui-annotation--axis` | Axis milestone or reference tick. |
|
|
111
|
+
| `ui-annotation--timeline` | Event pin on a timeline. |
|
|
112
|
+
| `ui-annotation--evidence` | Proof, source, or evidence marker. |
|
|
113
|
+
|
|
114
|
+
Tones are `accent`, `muted`, `success`, `warning`, `danger`, and `info`.
|
|
115
|
+
Status tones (`success`, `warning`, `danger`, `info`) are only for annotations
|
|
116
|
+
that carry that status meaning.
|
|
117
|
+
|
|
118
|
+
Motion is opt-in and respects `prefers-reduced-motion`:
|
|
119
|
+
|
|
120
|
+
| Motion class | Effect |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `ui-annotation--draw` | Connectors draw in once; subjects reveal without losing dashed variant styling. |
|
|
123
|
+
| `ui-annotation--reveal` | Note fades into place. |
|
|
124
|
+
| `ui-annotation--pulse` | Subject or badge pulses gently. |
|
|
125
|
+
| `ui-annotation--focus` | Static emphasis with a stronger subject stroke. |
|
|
126
|
+
|
|
127
|
+
The class recipe mirrors this surface:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
ui.annotation({ variant: 'bracket', tone: 'info', motion: 'draw' });
|
|
131
|
+
// "ui-annotation ui-annotation--bracket ui-annotation--info ui-annotation--draw"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Geometry helpers
|
|
135
|
+
|
|
136
|
+
The helper module returns SVG strings only. It does not know about scales,
|
|
137
|
+
selections, DOM nodes, or frameworks.
|
|
138
|
+
|
|
139
|
+
| Helper | Returns |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| `annotationTransform({ x, y })` | Group transform for the subject anchor. |
|
|
142
|
+
| `noteTransform({ dx, dy, align, valign, width, height })` | Note transform from the subject anchor, with optional alignment. |
|
|
143
|
+
| `notePlacement({ x, y, width, height, bounds, preferred, inset })` | Bounded note offset, alignment and transform for one annotation. `inset` reserves an extra margin (e.g. the title stroke-halo, ~3) so a placement that "fits" doesn't clip. |
|
|
144
|
+
| `declutterLabels(items, { gap, min, max })` | Adjusted centres for `items` (`[{ pos, size }]`) so labels don't overlap along one axis (order-preserving). |
|
|
145
|
+
| `directLabels(items, { axis, cross, gap, min, max, shape })` | Decluttered label points **and** a leader path per item: `[{ x, y, anchor, key, d }]`. |
|
|
146
|
+
| `circleSubjectPath({ radius })` | Circle subject path. |
|
|
147
|
+
| `rectSubjectPath({ x, y, width, height, padding })` | Rect subject path. |
|
|
148
|
+
| `thresholdPath({ x1, y1, x2, y2 })` | Arbitrary threshold/rule path. |
|
|
149
|
+
| `axisThresholdPath({ orientation, value, start, end })` | Horizontal or vertical axis-aligned threshold. |
|
|
150
|
+
| `bracketSubjectPath({ x1, y1, x2, y2, depth })` | Dogleg bracket path. |
|
|
151
|
+
| `bandSubjectPath({ x, y, width, height, padding })` | Band or interval path. |
|
|
152
|
+
| `slopeSubjectPath({ x1, y1, x2, y2 })` | Trend segment path. |
|
|
153
|
+
| `comparisonBracePath({ x1, y1, x2, y2, depth })` | Comparison brace path. |
|
|
154
|
+
| `outlierClusterPath({ points, radius })` | Repeated circle subjects for a cluster. |
|
|
155
|
+
| `timelineEventPath({ size, direction })` | Event pin marker path. |
|
|
156
|
+
| `evidenceMarkerPath({ x, y, width, height, padding })` | Centered square/rect evidence marker path. |
|
|
157
|
+
| `connectorLine({ dx, dy, subject })` | Straight connector, trimmed against circle/rect subjects. |
|
|
158
|
+
| `connectorElbow({ dx, dy, subject, mid })` | Right-angle dogleg connector (H/V/H); `mid` (0..1, default 0.5) sets the turn position along the dominant axis. |
|
|
159
|
+
| `connectorCurve({ dx, dy, subject })` | Deterministic cubic connector. |
|
|
160
|
+
| `connectorEndDot({ x, y, radius })` | Dot marker path. |
|
|
161
|
+
| `connectorEndArrow({ x1, y1, x2, y2, size, spread })` | Arrow marker path. `x1,y1`→`x2,y2` sets the direction (the head sits at `x2,y2`); `spread` is the half-angle (default 0.32 ≈ a crisp 37° head). |
|
|
162
|
+
| `annotationParts(options)` | Convenience object with `transform`, `subject`, `connector`, and `note`. |
|
|
163
|
+
|
|
164
|
+
`declutterLabels` is a deliberately small, deterministic **1-D** declutter for
|
|
165
|
+
direct labels or axis ticks — sort, push overlaps apart by `size + gap`, slide
|
|
166
|
+
to fit under `max`. It is **not** a general 2-D collision solver: if more labels
|
|
167
|
+
are requested than the axis can hold, the overflow is yours to resolve (fewer
|
|
168
|
+
labels, a longer axis, or rotation). It returns numbers; you own the scale and
|
|
169
|
+
the DOM.
|
|
170
|
+
|
|
171
|
+
`directLabels` is the **direct-labeling** companion: it declutters labels along
|
|
172
|
+
one axis _and_ draws the leader from each true anchor to its placed label,
|
|
173
|
+
reusing the connector kernel. Each `items[i]` is `{ anchor: {x, y}, size, key? }`
|
|
174
|
+
in figure coordinates; labels declutter along `axis` (`'y'` = a vertical column,
|
|
175
|
+
the default) and sit at the fixed `cross` coordinate. It returns, in input
|
|
176
|
+
order, the placed label point `{ x, y }`, the echoed `anchor`/`key`, and the
|
|
177
|
+
leader path `d` (`shape`: `straight` · `elbow` · `curve`). Like everything here
|
|
178
|
+
it owns no scales, no DOM, and no 2-D placement — map data → figure coordinates
|
|
179
|
+
first, then drop each `d` into a `<path class="ui-annotation__connector">` and
|
|
180
|
+
position the label at `{ x, y }`:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
import { directLabels } from '@ponchia/ui/annotations';
|
|
184
|
+
|
|
185
|
+
// anchors are data points already projected into the figure's SVG coords
|
|
186
|
+
const labels = directLabels(
|
|
187
|
+
points.map((p) => ({ anchor: p, size: 18, key: p.id })),
|
|
188
|
+
{ axis: 'y', cross: width - 8, gap: 6, min: 12, max: height - 12 },
|
|
189
|
+
);
|
|
190
|
+
// labels[i] → { x, y, anchor, key, d }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
All numeric inputs must be finite. Negative radius, width, height, padding, and
|
|
194
|
+
marker size throw `RangeError`; non-finite values throw `TypeError`. Path
|
|
195
|
+
numbers are rounded to three decimals with trailing zeros removed so snapshots
|
|
196
|
+
and unit tests stay stable.
|
|
197
|
+
|
|
198
|
+
`notePlacement()` is intentionally small: it places one note inside explicit SVG
|
|
199
|
+
bounds using a preferred side (`right`, `left`, `top`, or `bottom`) and falls
|
|
200
|
+
back to another side or a clamped note transform. It is not a collision solver
|
|
201
|
+
for a whole chart. For dense annotation sets, pre-compute positions or author a
|
|
202
|
+
mobile-specific SVG.
|
|
203
|
+
|
|
204
|
+
### Using the helpers in a static, no-JS report
|
|
205
|
+
|
|
206
|
+
The [report layer](./reporting.md) is static and ships no behavior JS, but these
|
|
207
|
+
helpers are JS — so in a hand- or LLM-authored report you can't call them at
|
|
208
|
+
render time. Bridge the gap by running them **once, at author/build time**, and
|
|
209
|
+
pasting the returned strings straight into the SVG. The output is deterministic
|
|
210
|
+
(path numbers are rounded to three decimals), so the strings are stable and
|
|
211
|
+
diff-friendly.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
// author-time only — copy the logged strings into the static HTML
|
|
215
|
+
import { circleSubjectPath, connectorLine } from '@ponchia/ui/annotations';
|
|
216
|
+
|
|
217
|
+
circleSubjectPath({ radius: 15 });
|
|
218
|
+
// "M0,-15A15,15 0 1 1 0,15A15,15 0 1 1 0,-15Z"
|
|
219
|
+
connectorLine({ dx: 78, dy: -38, subject: { type: 'circle', radius: 15, radiusPadding: 0 } });
|
|
220
|
+
// "M13.485,-6.57L78,-38"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The static markup then carries only the resolved strings — no runtime, no
|
|
224
|
+
import:
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<g class="ui-annotation ui-annotation--circle ui-annotation--accent" transform="translate(34, 58)">
|
|
228
|
+
<path class="ui-annotation__subject" d="M0,-15A15,15 0 1 1 0,15A15,15 0 1 1 0,-15Z" />
|
|
229
|
+
<path class="ui-annotation__connector" d="M13.485,-6.57L78,-38" />
|
|
230
|
+
<g class="ui-annotation__note" transform="translate(78, -38)">…</g>
|
|
231
|
+
</g>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Using annotations off-chart
|
|
235
|
+
|
|
236
|
+
Annotations are not only for charts. Two report uses worth calling out:
|
|
237
|
+
|
|
238
|
+
- **A decorative margin mark.** A small `ui-annotation` group — a circled point
|
|
239
|
+
with a short note — adds a hand-annotated feel to a report cover or section
|
|
240
|
+
opener. It carries no data, so mark the whole SVG `aria-hidden="true"` and
|
|
241
|
+
`focusable="false"`: a screen reader should not read decoration.
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<svg width="440" height="92" viewBox="0 0 440 92" aria-hidden="true" focusable="false">
|
|
245
|
+
<g class="ui-annotation ui-annotation--circle ui-annotation--accent" transform="translate(34, 58)">
|
|
246
|
+
<path class="ui-annotation__subject" d="M0,-15A15,15 0 1 1 0,15A15,15 0 1 1 0,-15Z" />
|
|
247
|
+
<circle r="3.5" fill="var(--accent)" />
|
|
248
|
+
<path class="ui-annotation__connector" d="M13.485,-6.57L78,-38" />
|
|
249
|
+
<g class="ui-annotation__note" transform="translate(78, -38)">
|
|
250
|
+
<path class="ui-annotation__note-line" d="M0,0H188" />
|
|
251
|
+
<text class="ui-annotation__title" y="-8">You are here</text>
|
|
252
|
+
<text class="ui-annotation__label" y="12">a short, terse label</text>
|
|
253
|
+
</g>
|
|
254
|
+
</g>
|
|
255
|
+
</svg>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- **Bracketing a passage of prose belongs to marks, not here.** To bracket a
|
|
259
|
+
sentence or paragraph in running text, use `.ui-bracket-note` from the
|
|
260
|
+
[marks layer](./marks.md) — it is the prose analogue of
|
|
261
|
+
`ui-annotation--bracket`. SVG annotations are for SVG figures.
|
|
262
|
+
|
|
263
|
+
## Sizing: the user-unit trap
|
|
264
|
+
|
|
265
|
+
Annotation text (`__title`, `__label`) is sized in **SVG user units**, so it
|
|
266
|
+
scales with the figure. A 360-unit-wide chart stretched across a full report
|
|
267
|
+
column is scaled roughly 2.5–3×, and the callout text scales with it — long
|
|
268
|
+
notes turn huge and overflow the `viewBox` (SVG text is clipped, not wrapped).
|
|
269
|
+
Two rules keep callouts readable:
|
|
270
|
+
|
|
271
|
+
- **Keep note text terse** — a title and a few words, like the recipe examples
|
|
272
|
+
(`Peak`, `Limit`, `80 kB cap`). Push the full sentence into the figure caption,
|
|
273
|
+
the `<desc>`, or the fallback table.
|
|
274
|
+
- **Constrain the figure width** so the user-unit → pixel scale stays near
|
|
275
|
+
1–1.5×: set a `max-inline-size` on the SVG instead of letting it stretch to the
|
|
276
|
+
whole column, or author the `viewBox` at roughly the rendered pixel size.
|
|
277
|
+
|
|
278
|
+
## Density and responsive rules
|
|
279
|
+
|
|
280
|
+
Annotations are strongest when they explain the few things a reader would miss.
|
|
281
|
+
As a default, keep a single chart to three to five visible callouts. Use direct
|
|
282
|
+
labels for stable context, one accent callout for the main insight, and status
|
|
283
|
+
tones only for genuine status.
|
|
284
|
+
|
|
285
|
+
Dense SVGs should not shrink until the notes become unreadable. Use one of
|
|
286
|
+
these patterns:
|
|
287
|
+
|
|
288
|
+
- Keep the chart wide in a horizontally scrollable figure and provide fallback
|
|
289
|
+
table text.
|
|
290
|
+
- Author a simpler mobile SVG with fewer annotations.
|
|
291
|
+
- Move low-priority annotation text into the caption or fallback table on small
|
|
292
|
+
screens.
|
|
293
|
+
|
|
294
|
+
## Recipes
|
|
295
|
+
|
|
296
|
+
### Label
|
|
297
|
+
|
|
298
|
+
Use `ui-annotation--label` for direct labels when the subject is already clear.
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<g class="ui-annotation ui-annotation--label ui-annotation--muted" transform="translate(112, 48)">
|
|
302
|
+
<g class="ui-annotation__note">
|
|
303
|
+
<text class="ui-annotation__title">Baseline</text>
|
|
304
|
+
<text class="ui-annotation__label" y="16">Previous quarter</text>
|
|
305
|
+
</g>
|
|
306
|
+
</g>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Circle callout
|
|
310
|
+
|
|
311
|
+
Use a circle subject when the referenced point or local cluster matters.
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
<g class="ui-annotation ui-annotation--circle ui-annotation--accent" transform="translate(180, 72)">
|
|
315
|
+
<path class="ui-annotation__subject" d="M0,-18A18,18 0 1 1 0,18A18,18 0 1 1 0,-18Z" />
|
|
316
|
+
<path class="ui-annotation__connector" d="M15.556,-7.424L88,-42" />
|
|
317
|
+
<g class="ui-annotation__note" transform="translate(88, -42)">
|
|
318
|
+
<path class="ui-annotation__note-line" d="M0,0H84" />
|
|
319
|
+
<text class="ui-annotation__title" y="-8">Spike</text>
|
|
320
|
+
<text class="ui-annotation__label" y="12">Investigate change</text>
|
|
321
|
+
</g>
|
|
322
|
+
</g>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Rect callout
|
|
326
|
+
|
|
327
|
+
Use a rect subject for a band, bar, table region, or evidence block inside an
|
|
328
|
+
SVG figure.
|
|
329
|
+
|
|
330
|
+
```html
|
|
331
|
+
<g class="ui-annotation ui-annotation--rect ui-annotation--warning" transform="translate(206, 92)">
|
|
332
|
+
<path class="ui-annotation__subject" d="M-34,-16H34V16H-34Z" />
|
|
333
|
+
<path class="ui-annotation__connector" d="M34,-16L72,-46" />
|
|
334
|
+
<g class="ui-annotation__note" transform="translate(72, -46)">
|
|
335
|
+
<path class="ui-annotation__note-line" d="M0,0H96" />
|
|
336
|
+
<text class="ui-annotation__title" y="-8">Watch</text>
|
|
337
|
+
<text class="ui-annotation__label" y="12">Lower confidence</text>
|
|
338
|
+
</g>
|
|
339
|
+
</g>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Threshold
|
|
343
|
+
|
|
344
|
+
Use `ui-annotation--threshold` when a horizontal or vertical rule is the
|
|
345
|
+
subject.
|
|
346
|
+
|
|
347
|
+
```html
|
|
348
|
+
<g class="ui-annotation ui-annotation--threshold ui-annotation--danger" transform="translate(0, 96)">
|
|
349
|
+
<path class="ui-annotation__subject" d="M36,0L324,0" />
|
|
350
|
+
<path class="ui-annotation__connector" d="M240,0L282,-32" />
|
|
351
|
+
<g class="ui-annotation__note" transform="translate(282, -32)">
|
|
352
|
+
<text class="ui-annotation__title">Limit</text>
|
|
353
|
+
<text class="ui-annotation__label" y="16">Do not exceed</text>
|
|
354
|
+
</g>
|
|
355
|
+
</g>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Badge
|
|
359
|
+
|
|
360
|
+
Use badges for compact numbered or categorical markers. Do not rely on the badge
|
|
361
|
+
color alone; pair it with visible text, a caption, or a table row.
|
|
362
|
+
|
|
363
|
+
```html
|
|
364
|
+
<g class="ui-annotation ui-annotation--badge ui-annotation--info" transform="translate(72, 84)">
|
|
365
|
+
<circle class="ui-annotation__badge" r="12" />
|
|
366
|
+
<text class="ui-annotation__title" text-anchor="middle" dominant-baseline="central">1</text>
|
|
367
|
+
</g>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Chart figure recipe
|
|
371
|
+
|
|
372
|
+
Inside a report, keep the existing chart structure: caption, legend or direct
|
|
373
|
+
labels, annotated SVG, and fallback data. A useful annotated figure should show
|
|
374
|
+
more than one annotation family when the story needs it: direct labels for
|
|
375
|
+
stable references, threshold annotations for limits, circle/rect subjects for
|
|
376
|
+
specific data, and badge markers for compact index points.
|
|
377
|
+
|
|
378
|
+
```html
|
|
379
|
+
<figure class="ui-report__figure ui-print-exact" role="group" aria-labelledby="annotated-chart">
|
|
380
|
+
<figcaption id="annotated-chart" class="ui-report__caption">
|
|
381
|
+
Fig 2 - Weekly throughput, annotated at the peak
|
|
382
|
+
</figcaption>
|
|
383
|
+
<svg viewBox="0 0 360 160" role="img" aria-labelledby="throughput-title throughput-desc">
|
|
384
|
+
<title id="throughput-title">Weekly throughput with a peak callout</title>
|
|
385
|
+
<desc id="throughput-desc">Annotations mark the baseline, limit and highest research week.</desc>
|
|
386
|
+
<line x1="36" y1="112" x2="324" y2="112" stroke="var(--line)" />
|
|
387
|
+
<rect x="88" y="42" width="72" height="70" fill="var(--chart-1)" />
|
|
388
|
+
<rect x="188" y="70" width="72" height="42" fill="var(--chart-2)" />
|
|
389
|
+
<g class="ui-annotation ui-annotation--label ui-annotation--muted" transform="translate(36, 132)">
|
|
390
|
+
<g class="ui-annotation__note">
|
|
391
|
+
<text class="ui-annotation__title">Baseline</text>
|
|
392
|
+
<text class="ui-annotation__label" y="16">Previous quarter</text>
|
|
393
|
+
</g>
|
|
394
|
+
</g>
|
|
395
|
+
<g class="ui-annotation ui-annotation--threshold ui-annotation--danger" transform="translate(0, 66)">
|
|
396
|
+
<path class="ui-annotation__subject" d="M36,0L324,0" />
|
|
397
|
+
<path class="ui-annotation__connector" d="M272,0L304,-28" />
|
|
398
|
+
<g class="ui-annotation__note" transform="translate(234, -52)">
|
|
399
|
+
<text class="ui-annotation__title">Limit</text>
|
|
400
|
+
<text class="ui-annotation__label" y="16">Watch capacity</text>
|
|
401
|
+
</g>
|
|
402
|
+
</g>
|
|
403
|
+
<g class="ui-annotation ui-annotation--circle ui-annotation--accent" transform="translate(124, 42)">
|
|
404
|
+
<path class="ui-annotation__subject" d="M0,-18A18,18 0 1 1 0,18A18,18 0 1 1 0,-18Z" />
|
|
405
|
+
<path class="ui-annotation__connector" d="M16,-8L76,-36" />
|
|
406
|
+
<g class="ui-annotation__note" transform="translate(76, -36)">
|
|
407
|
+
<path class="ui-annotation__note-line" d="M0,0H80" />
|
|
408
|
+
<text class="ui-annotation__title" y="-8">Peak</text>
|
|
409
|
+
<text class="ui-annotation__label" y="12">Research high</text>
|
|
410
|
+
</g>
|
|
411
|
+
</g>
|
|
412
|
+
</svg>
|
|
413
|
+
<div class="ui-table-wrap">
|
|
414
|
+
<table class="ui-table ui-table--dense">
|
|
415
|
+
<caption>Annotated chart source data</caption>
|
|
416
|
+
<thead><tr><th>Week</th><th class="is-num">Hours</th></tr></thead>
|
|
417
|
+
<tbody><tr><td>Week 4</td><td class="is-num">18</td></tr></tbody>
|
|
418
|
+
</table>
|
|
419
|
+
</div>
|
|
420
|
+
</figure>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Status tones (`success`, `warning`, `danger`, `info`) are only for annotations
|
|
424
|
+
that carry that status meaning. Use `accent` for the primary insight and
|
|
425
|
+
`muted` for secondary callouts.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Architecture & Decisions
|
|
2
|
+
|
|
3
|
+
Status: accepted · 2026-05-15 · applies from v0.2.0
|
|
4
|
+
|
|
5
|
+
> **Separate ADRs.** Larger, self-contained decisions live under
|
|
6
|
+
> [`docs/adr/`](./adr/):
|
|
7
|
+
>
|
|
8
|
+
> - [ADR-0001 — Color system: governed evolution beyond monochrome](./adr/0001-color-system.md)
|
|
9
|
+
> (accepted; steps 1–8 implemented in 0.4.0) — the five-tier color
|
|
10
|
+
> constitution, the `check:color-policy`/`check:skins`/`check:charts`
|
|
11
|
+
> gates, opt-in colorways, data-viz, APCA advisory reporting, and the
|
|
12
|
+
> OKLCH core accent ramp.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
`@ponchia/ui` is the shared design layer for several projects on
|
|
17
|
+
different stacks: Astro, SvelteKit, and an
|
|
18
|
+
open-ended set of future apps (React, Solid, Qwik, plain HTML, server-rendered
|
|
19
|
+
templates). The question driving this document: is plain CSS the right
|
|
20
|
+
universal substrate, or should the framework ship per-framework components?
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
**Plain, class-based CSS is the canonical and only universal layer.** It is
|
|
25
|
+
the single artifact every target consumes natively with zero adapter. A
|
|
26
|
+
per-framework component library would make every non-chosen framework a
|
|
27
|
+
second-class citizen and multiply the maintenance surface for the same button.
|
|
28
|
+
|
|
29
|
+
The known gaps of a pure-CSS framework — contract visibility, a home for
|
|
30
|
+
unavoidable JS, and distribution — are addressed as **thin, optional layers
|
|
31
|
+
on top of the CSS, none of which require a framework commitment**:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
@ponchia/ui
|
|
35
|
+
├── css/ canonical universal layer (the framework) [required]
|
|
36
|
+
├── tokens/ design tokens as JS/JSON, for JS/canvas/tooling [optional]
|
|
37
|
+
├── classes/ typed class-name contract + recipe builders [optional]
|
|
38
|
+
├── behaviors/ vanilla, SSR-safe JS for stateful widgets [optional]
|
|
39
|
+
├── connectors/ pure SVG leader-line geometry kernel (no DOM) [optional]
|
|
40
|
+
├── annotations/ pure SVG callout geometry (builds on connectors) [optional]
|
|
41
|
+
├── glyphs/ dot-matrix glyph registry/renderers [optional]
|
|
42
|
+
├── react/ thin React hooks over behaviors [optional peer]
|
|
43
|
+
├── solid/ thin Solid primitives over behaviors [optional peer]
|
|
44
|
+
└── qwik/ thin Qwik hooks over behaviors (useVisibleTask$) [optional peer]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Consequences of each layer
|
|
48
|
+
|
|
49
|
+
- **css/** — wrapped in a single `@layer bronto`. Any un-layered CSS in a
|
|
50
|
+
consumer wins the cascade without specificity wars or `!important`. This is
|
|
51
|
+
a deliberate behavioural change vs. unlayered v0.1.0; consumers pin a tag
|
|
52
|
+
so it ships only on the next version bump.
|
|
53
|
+
- **Fonts** — `@font-face` moved out of `tokens.css` into `css/fonts.css`
|
|
54
|
+
with URLs relative to the package (`../fonts/*`), so font hosting is
|
|
55
|
+
decoupled from the token layer and resolves through bundlers or static
|
|
56
|
+
serving without an absolute `/fonts` assumption.
|
|
57
|
+
- **tokens/** — `index.js` (`cssVars`) is the single source of truth for token
|
|
58
|
+
values. The four `:root` palette blocks of `css/tokens.css` are **generated**
|
|
59
|
+
from it (`scripts/gen-tokens-css.mjs`), as are the JSON artifacts (`index.json`,
|
|
60
|
+
`tokens.dtcg.json`, `resolved.json`). So the dark palette is authored once,
|
|
61
|
+
not in three places (the two CSS dark blocks are now identical by
|
|
62
|
+
construction), resolving the duplication ADR-0003 flagged. The CSS-only
|
|
63
|
+
presets (density / contrast / OLED) stay hand-authored below a marker and are
|
|
64
|
+
preserved across regeneration. `scripts/check-fresh.mjs` fails CI if
|
|
65
|
+
`css/tokens.css` drifts from the model.
|
|
66
|
+
- **classes/** — `cls` is the flat registry; recipes only emit from it;
|
|
67
|
+
`scripts/check-classes.mjs` enforces a bidirectional match with the
|
|
68
|
+
stylesheet's `.ui-*` selectors. The class contract cannot silently rot.
|
|
69
|
+
- **behaviors/** — vanilla, dependency-free, side-effect-free on import,
|
|
70
|
+
SSR-safe. Chosen over Web Components (SSR/hydration friction with Astro
|
|
71
|
+
islands and SvelteKit) and over per-framework packages (maintenance
|
|
72
|
+
multiplier). Revisit Web Components only if stateful widgets accumulate.
|
|
73
|
+
`index.js` is a barrel; each behavior lives in its own module
|
|
74
|
+
(`dialog.js`, `combobox.js`, …) over a shared `internal.js` of DOM helpers,
|
|
75
|
+
so the public import surface is unchanged.
|
|
76
|
+
- **glyphs/** — static bitmap data and SSR-safe render helpers. The
|
|
77
|
+
256-cell DOM renderers are for display and solid inline icons; the `.ui-icon`
|
|
78
|
+
mask renderer is for dense icon-at-scale use.
|
|
79
|
+
- **react/** / **solid/** / **qwik/** — optional lifecycle adapters over `behaviors/`.
|
|
80
|
+
They do not define markup, own state, or fork behavior logic; they only run
|
|
81
|
+
the vanilla initializers on mount and cleanup on unmount/dispose.
|
|
82
|
+
- **`css/analytical.css` — the analytical roll-up.** This convenience file
|
|
83
|
+
`@import`s exactly **seven** analytical-figure leaves: `annotations`,
|
|
84
|
+
`legend`, `marks`, `connectors`, `spotlight`, `crosshair`, and `selection`.
|
|
85
|
+
The adjacent opt-in leaves — `sources`, `state`, `generated`, `workbench`,
|
|
86
|
+
and `command` — are report/tooling/trust surfaces that are intentionally
|
|
87
|
+
**not** part of the analytical roll-up and must be imported individually.
|
|
88
|
+
Importing `analytical.css` does not pull in any of those five.
|
|
89
|
+
- **Root export (`.`) is CSS-only.** `exports["."]` resolves to the CSS
|
|
90
|
+
bundle (`dist/bronto.css`). It is a CSS side-effect import for CSS-aware
|
|
91
|
+
bundlers (`@import '@ponchia/ui'` in CSS, or a side-effect
|
|
92
|
+
`import '@ponchia/ui'` in Vite/Astro/SvelteKit). There is no runtime JS at
|
|
93
|
+
the package root — Node/runtime JS imports of `.` are not supported. All JS
|
|
94
|
+
entrypoints are explicit subpaths (`/behaviors`, `/classes`, `/tokens`,
|
|
95
|
+
`/glyphs`, `/react`, `/solid`, `/qwik`, `/skins`, `/charts`). This is a
|
|
96
|
+
permanent, intentional contract.
|
|
97
|
+
|
|
98
|
+
## Repository layout
|
|
99
|
+
|
|
100
|
+
The repo root mixes five kinds of directory that look alike but follow very
|
|
101
|
+
different rules. Two distinctions matter most: several are **path-frozen
|
|
102
|
+
published subpaths** — the directory name _is_ the public import specifier
|
|
103
|
+
(`@ponchia/ui/react` resolves to `./react/`), so they cannot be moved or
|
|
104
|
+
renamed — and several are **generated** and must never be hand-edited (a
|
|
105
|
+
generator overwrites them and a drift gate fails CI).
|
|
106
|
+
|
|
107
|
+
| Path | Kind | Edit here? | Notes |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| `css/` | source | yes | The framework. Hand-authored `@layer bronto` CSS. (`css/tokens.css` palette blocks and `css/generated.css` are generated — see below.) |
|
|
110
|
+
| `tokens/index.js` | source | yes | The single source of truth for token **values** (`cssVars`). |
|
|
111
|
+
| `classes/index.js`, `behaviors/`, `annotations/`, `connectors/`, `react/`, `solid/`, `qwik/`, `glyphs/`, `shiki/` | source · published-subpath (path-frozen) | yes — but **do not move** | Authored ESM shipped as-is; the dir name is the public import path. The `.d.ts` beside them are generated/drift-checked: `connectors`/`annotations`/`react`/`solid`/`qwik` are emitted from JSDoc by `tsc` (`npm run dts:emit`), `classes`/`tokens`/`glyphs` from the runtime; only `behaviors/index.d.ts` is still hand-maintained (its barrel + destructured-param shape emit poorly), guarded by `check-behaviors`. |
|
|
112
|
+
| `dist/` | generated | no | Build of `css/` (`npm run dist:build`); byte-checked by `check:dist`. |
|
|
113
|
+
| `tokens/index.json`, `tokens/resolved.json`, `tokens/tokens.dtcg.json`, `tokens/charts.json`, `classes/index.d.ts`, `tokens/index.d.ts`, `tokens/{skins,charts}.d.ts`, `glyphs/glyphs.d.ts`, `classes/vscode.css-custom-data.json`, `docs/reference.md` | generated | no | Committed build artifacts; regenerate with `npm run prepack`, never hand-edit. Drift-checked in `npm run check`. |
|
|
114
|
+
| `fonts/` | vendored | — | The Doto webfont (woff2) + its OFL license. |
|
|
115
|
+
| `scripts/` | tooling | yes | `gen-*` regenerate artifacts, `check-*` are the drift/contract gates wired into `npm run check`, plus `build-dist`, `serve`, `size-report`. |
|
|
116
|
+
| `docs/` | source (mostly) | yes | Hand-authored docs + ADRs; the curated subset in `package.json` `files` ships in the tarball. `docs/reference.md` is generated. |
|
|
117
|
+
| `demo/`, `test/`, `examples/` | fixtures | yes | The self-driving demo/showcase, the unit + Playwright e2e suite, and consumer example apps built against the packed tarball. |
|
|
118
|
+
| `.github/`, `*.config.mjs`, `.prettierrc`, `.stylelintrc.json`, `tsconfig.json`, `.editorconfig` | config | yes | CI workflows and tool config. |
|
|
119
|
+
| `package.json`, `llms.txt`, `CHANGELOG.md`, `MIGRATIONS.json`, `README.md`, `CONTRIBUTING.md`, `ROADMAP.md`, `LICENSE` | meta | yes | Manifest, the agent entrypoint, the curated changelog, the rename map, and project docs. |
|
|
120
|
+
|
|
121
|
+
The **path-frozen** dirs are the cost of zero-build, path-stable publishing:
|
|
122
|
+
`files` map 1:1 to published paths and the consumer's own bundler tree-shakes
|
|
123
|
+
the ESM, so there is no `src/` indirection (and no JS bundler — see the
|
|
124
|
+
distribution decision below). **Generated** files are regenerated from their
|
|
125
|
+
source and policed by a drift gate — edit the source, run the generator, commit
|
|
126
|
+
the result.
|
|
127
|
+
|
|
128
|
+
## Drift control
|
|
129
|
+
|
|
130
|
+
Every data mirror is backed by a check wired into `npm run check`, run by CI
|
|
131
|
+
on every push/PR and again by `release.yml` before publish (see "Release
|
|
132
|
+
gating" below), so a version that fails any invariant never reaches npm.
|
|
133
|
+
|
|
134
|
+
| Invariant | Enforced by |
|
|
135
|
+
| ----------------------------------------------- | ------------------- |
|
|
136
|
+
| exports / import graph / `files` consistent | `check-exports.mjs` |
|
|
137
|
+
| pure generated mirrors fresh — `tokens.css`/`index.json`, `dtcg.json`, `resolved.json`, `classes`/`tokens` `.d.ts`, `reference.md`, vscode data — each byte-equal to its generator (registry: `scripts/lib/artifacts.mjs`) | `check-fresh.mjs` |
|
|
138
|
+
| `classes` `cls` ⇄ `.ui-*` selectors | `check-classes.mjs` |
|
|
139
|
+
| `connectors`/`annotations`/`react`/`solid`/`qwik` `.d.ts` (+ maps) == fresh `tsc` emit of their JSDoc | `check-dts-emit.mjs` |
|
|
140
|
+
| `behaviors/index.d.ts` ⇄ `behaviors/*` exports (the one hand-maintained leaf `.d.ts`) | `check-behaviors.mjs` |
|
|
141
|
+
| legend swatch colours ⊆ `charts.js` · opt-in | `check-legend.mjs` |
|
|
142
|
+
| color tokens tiered · no raw chromatic color in components | `check-color-policy.mjs` |
|
|
143
|
+
| `css/skins.css` ⇄ `tokens/skins.js` · colorways opt-in | `check-skins.mjs` |
|
|
144
|
+
| every shipped colorway accent meets its WCAG floor | `check-contrast.mjs` |
|
|
145
|
+
| `dataviz.css`/`charts.json`/`charts.d.ts` ⇄ `tokens/charts.js` · CVD-distinguishable · opt-in | `check-charts.mjs` |
|
|
146
|
+
| `shiki/nothing.json` valid + on rationed palette | `check-shiki.mjs` |
|
|
147
|
+
| `dist/*.css` == fresh build of `css/` + budget | `check-dist.mjs` |
|
|
148
|
+
| published tarball == intended `files` only | `check-pack.mjs` |
|
|
149
|
+
| published `.d.ts` compile + reject typos | `tsc` (`check:types`) |
|
|
150
|
+
| CSS style/correctness | Stylelint |
|
|
151
|
+
| non-CSS source style | Prettier (`check:format`) |
|
|
152
|
+
|
|
153
|
+
`check-dist` is the most supply-chain-critical row: `dist/bronto.css` is
|
|
154
|
+
the default `exports["."]` consumers actually load, so its byte-equality
|
|
155
|
+
to a fresh build of `css/` is what makes the committed bundle trustworthy.
|
|
156
|
+
The `check-dist` size ceiling (`BUDGET` in `build-dist.mjs`) is calibrated
|
|
157
|
+
to the current bundle with deliberate headroom — it is the consumer-facing
|
|
158
|
+
payload contract, raised only intentionally with a CHANGELOG note.
|
|
159
|
+
`check:types` compiles the published declarations against
|
|
160
|
+
`test/types.test-d.ts`, whose `@ts-expect-error`s would fail to compile
|
|
161
|
+
if the generated literal `cls`/token types stopped rejecting typos —
|
|
162
|
+
so the *value* of the generated `.d.ts` is itself gated, not just their
|
|
163
|
+
freshness (`check-fresh`).
|
|
164
|
+
|
|
165
|
+
## Release gating
|
|
166
|
+
|
|
167
|
+
`release.yml` (on a pushed `v*` tag) is a five-job DAG, serialized by a
|
|
168
|
+
`concurrency: release-publish` group so two tags can't race the dist-tag
|
|
169
|
+
pointer:
|
|
170
|
+
|
|
171
|
+
- `validate` — read-only: `npm run check` + tag↔version match. `check`
|
|
172
|
+
includes `check:release`; for a prerelease tag the base version's
|
|
173
|
+
CHANGELOG section need only exist (`## Unreleased — x.y.z` is fine) —
|
|
174
|
+
only a stable release must carry a dated heading.
|
|
175
|
+
- `e2e` — Playwright (visual + axe a11y, both themes, cross-engine) in
|
|
176
|
+
the pinned `mcr.microsoft.com/playwright` container.
|
|
177
|
+
- `examples` — `needs: validate`: builds the downstream example
|
|
178
|
+
apps against the **packed tarball**, mirroring CI. Catches a broken
|
|
179
|
+
published surface (exports map / missing file / unresolved subpath)
|
|
180
|
+
that `check:pack`'s file-allowlist inspection cannot — so the release
|
|
181
|
+
path runs the same consumer smoke as merge-to-main.
|
|
182
|
+
- `publish-npm` — `needs: [validate, e2e, examples]`: `npm publish` with
|
|
183
|
+
provenance. Runs in the `npm-publish` **Environment** (required-reviewer
|
|
184
|
+
protection), so after the gates pass the run pauses for a manual approval
|
|
185
|
+
in the Actions UI before anything reaches npm — a guard against an
|
|
186
|
+
accidental tag push publishing. Dist-tag is derived from the tag: stable
|
|
187
|
+
(`v0.4.0`) → `latest`; SemVer prerelease (`v0.4.0-rc.1`, any hyphenated
|
|
188
|
+
identifier) → `next`, so the default `npm i @ponchia/ui` never moves onto
|
|
189
|
+
an unstable build (opt in with `@ponchia/ui@next`).
|
|
190
|
+
- `release-notes` — `needs: publish-npm`: a GitHub Release for visibility
|
|
191
|
+
(transitively gated on a successful publish, hence on the gates above);
|
|
192
|
+
prerelease tags are flagged so they aren't surfaced as "Latest". The Release
|
|
193
|
+
**body is the curated `CHANGELOG.md` section** for the tag
|
|
194
|
+
(`scripts/changelog-section.mjs`), not GitHub's auto-generated PR list — one
|
|
195
|
+
source of truth, surfaced where readers look.
|
|
196
|
+
|
|
197
|
+
Because the documented install path is the npm package, **the npm publish
|
|
198
|
+
is a real gate**: if `validate`, `e2e`, *or* `examples` fails,
|
|
199
|
+
`publish-npm` never runs, the version never reaches the registry, and
|
|
200
|
+
consumers never resolve it.
|
|
201
|
+
(Corollary: a flaky `e2e` blocks releases — that is deliberate; fix the
|
|
202
|
+
flake, don't bypass the gate.) Permissions are least-privilege per job
|
|
203
|
+
(only `release-notes` gets `contents: write`; only `publish-npm` gets
|
|
204
|
+
`id-token: write` for provenance).
|
|
205
|
+
|
|
206
|
+
GitHub still serves the raw tag tarball `archive/refs/tags/vX.Y.Z.tar.gz`
|
|
207
|
+
for any tag, ungated — that path is legacy/fallback, deliberately *not* the
|
|
208
|
+
documented install, so it is no longer the safeguard-critical surface.
|
|
209
|
+
Process still applies: bump `package.json`, land on `main`, go green, tag.
|
|
210
|
+
|
|
211
|
+
## Decision — distribution: npm public `@ponchia/ui`
|
|
212
|
+
|
|
213
|
+
Decided 2026-05-15. The framework is consumed by a growing set of
|
|
214
|
+
heterogeneous web frontends (Astro, SvelteKit, React, Solid, Qwik, vanilla),
|
|
215
|
+
several deploying via third-party CI. The only option where onboarding a new
|
|
216
|
+
frontend is `npm i @ponchia/ui` with zero per-consumer config is **npm
|
|
217
|
+
public**, and it uniquely also closes the release-gating gap (publish *is*
|
|
218
|
+
the gate). GitHub Packages was rejected: it requires auth to install even
|
|
219
|
+
public packages, i.e. an `.npmrc` + token on every frontend and CI runner —
|
|
220
|
+
the exact friction to avoid. The raw tag tarball is kept as an ungated
|
|
221
|
+
legacy/fallback only.
|
|
222
|
+
|
|
223
|
+
The npm scope `@bronto` is not ownable, so the package name is
|
|
224
|
+
**`@ponchia/ui`**. Naming layers, intentionally distinct:
|
|
225
|
+
|
|
226
|
+
- **npm package**: `@ponchia/ui` (registry identity).
|
|
227
|
+
- **CSS cascade layer**: `@layer bronto` and `data-bronto-*` behavior
|
|
228
|
+
attributes (the design-system namespace — unchanged; renaming gains
|
|
229
|
+
nothing and risks consumer overrides).
|
|
230
|
+
- **Workspace / brand**: "Bronto" (repo `Ponchia/bronto-ui`) — unchanged.
|
|
231
|
+
|
|
232
|
+
This split is deliberate; the README states it so the apparent mismatch is
|
|
233
|
+
explained, not surprising.
|
|
234
|
+
|
|
235
|
+
### Post-publish checklist
|
|
236
|
+
|
|
237
|
+
- Confirm npm `latest` points at the tagged version and the package page shows
|
|
238
|
+
provenance.
|
|
239
|
+
- Run `npm pack --dry-run --json` locally or from CI logs and confirm the
|
|
240
|
+
intended file count/payload.
|
|
241
|
+
- Build the packed examples matrix (vanilla, Astro, SvelteKit, React, Solid, Qwik)
|
|
242
|
+
from the tarball, not a workspace link.
|
|
243
|
+
- Confirm the GitHub Release body matches the curated changelog section.
|
|
244
|
+
- If a bad package is published, deprecate that exact version on npm, publish a
|
|
245
|
+
patched version, and link the deprecation note to the changelog/security
|
|
246
|
+
advisory as appropriate.
|