@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.
Files changed (153) hide show
  1. package/CHANGELOG.md +552 -8
  2. package/MIGRATIONS.json +106 -0
  3. package/README.md +34 -8
  4. package/annotations/index.d.ts +402 -0
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +792 -0
  7. package/behaviors/carousel.js +198 -0
  8. package/behaviors/combobox.js +226 -0
  9. package/behaviors/command.js +190 -0
  10. package/behaviors/connectors.js +95 -0
  11. package/behaviors/crosshair.js +57 -0
  12. package/behaviors/dialog.js +74 -0
  13. package/behaviors/disclosure.js +26 -0
  14. package/behaviors/dismissible.js +25 -0
  15. package/behaviors/forms.js +186 -0
  16. package/behaviors/glyph.js +108 -0
  17. package/behaviors/index.d.ts +79 -0
  18. package/behaviors/index.js +18 -1409
  19. package/behaviors/internal.js +97 -0
  20. package/behaviors/legend.js +67 -0
  21. package/behaviors/menu.js +47 -0
  22. package/behaviors/popover.js +179 -0
  23. package/behaviors/spotlight.js +52 -0
  24. package/behaviors/table.js +136 -0
  25. package/behaviors/tabs.js +103 -0
  26. package/behaviors/theme.js +84 -0
  27. package/behaviors/toast.js +164 -0
  28. package/classes/classes.json +1857 -0
  29. package/classes/index.d.ts +306 -13
  30. package/classes/index.js +339 -12
  31. package/classes/vscode.css-custom-data.json +12 -0
  32. package/connectors/index.d.ts +191 -0
  33. package/connectors/index.d.ts.map +1 -0
  34. package/connectors/index.js +275 -0
  35. package/css/analytical.css +21 -0
  36. package/css/annotations.css +292 -0
  37. package/css/app.css +43 -13
  38. package/css/base.css +15 -10
  39. package/css/command.css +97 -0
  40. package/css/connectors.css +110 -0
  41. package/css/content.css +7 -1
  42. package/css/crosshair.css +100 -0
  43. package/css/dataviz.css +5 -1
  44. package/css/disclosure.css +38 -6
  45. package/css/dots.css +57 -0
  46. package/css/feedback.css +111 -2
  47. package/css/fonts.css +11 -7
  48. package/css/forms.css +42 -1
  49. package/css/generated.css +117 -0
  50. package/css/legend.css +272 -0
  51. package/css/marks.css +174 -0
  52. package/css/motion.css +24 -44
  53. package/css/navigation.css +7 -0
  54. package/css/overlay.css +31 -1
  55. package/css/primitives.css +109 -5
  56. package/css/report.css +39 -81
  57. package/css/selection.css +46 -0
  58. package/css/site.css +16 -2
  59. package/css/sources.css +221 -0
  60. package/css/spotlight.css +104 -0
  61. package/css/state.css +121 -0
  62. package/css/tokens.css +60 -37
  63. package/css/workbench.css +83 -0
  64. package/dist/bronto.css +1 -1
  65. package/dist/css/analytical.css +1 -0
  66. package/dist/css/annotations.css +1 -0
  67. package/dist/css/app.css +1 -1
  68. package/dist/css/base.css +1 -1
  69. package/dist/css/command.css +1 -0
  70. package/dist/css/connectors.css +1 -0
  71. package/dist/css/content.css +1 -1
  72. package/dist/css/crosshair.css +1 -0
  73. package/dist/css/disclosure.css +1 -1
  74. package/dist/css/dots.css +1 -1
  75. package/dist/css/feedback.css +1 -1
  76. package/dist/css/fonts.css +1 -1
  77. package/dist/css/forms.css +1 -1
  78. package/dist/css/generated.css +1 -0
  79. package/dist/css/legend.css +1 -0
  80. package/dist/css/marks.css +1 -0
  81. package/dist/css/motion.css +1 -1
  82. package/dist/css/navigation.css +1 -1
  83. package/dist/css/overlay.css +1 -1
  84. package/dist/css/primitives.css +1 -1
  85. package/dist/css/report.css +1 -1
  86. package/dist/css/selection.css +1 -0
  87. package/dist/css/site.css +1 -1
  88. package/dist/css/sources.css +1 -0
  89. package/dist/css/spotlight.css +1 -0
  90. package/dist/css/state.css +1 -0
  91. package/dist/css/tokens.css +1 -1
  92. package/dist/css/workbench.css +1 -0
  93. package/docs/adr/0003-theme-model.md +7 -4
  94. package/docs/annotations.md +425 -0
  95. package/docs/architecture.md +246 -0
  96. package/docs/command.md +95 -0
  97. package/docs/connectors.md +91 -0
  98. package/docs/contrast.md +116 -92
  99. package/docs/crosshair.md +63 -0
  100. package/docs/d2.md +195 -0
  101. package/docs/generated.md +91 -0
  102. package/docs/legends.md +184 -0
  103. package/docs/marks.md +93 -0
  104. package/docs/mermaid.md +152 -0
  105. package/docs/reference.md +385 -23
  106. package/docs/reporting.md +436 -63
  107. package/docs/selection.md +40 -0
  108. package/docs/sources.md +137 -0
  109. package/docs/spotlight.md +78 -0
  110. package/docs/stability.md +24 -2
  111. package/docs/state.md +85 -0
  112. package/docs/usage.md +123 -4
  113. package/docs/vega.md +225 -0
  114. package/docs/workbench.md +78 -0
  115. package/fonts/doto-400.woff2 +0 -0
  116. package/fonts/doto-500.woff2 +0 -0
  117. package/fonts/doto-600.woff2 +0 -0
  118. package/fonts/doto-700.woff2 +0 -0
  119. package/fonts/doto-800.woff2 +0 -0
  120. package/fonts/doto-900.woff2 +0 -0
  121. package/glyphs/glyphs.js +6 -4
  122. package/llms.txt +362 -14
  123. package/package.json +115 -12
  124. package/qwik/index.d.ts +42 -54
  125. package/qwik/index.d.ts.map +1 -0
  126. package/qwik/index.js +75 -3
  127. package/react/index.d.ts +39 -56
  128. package/react/index.d.ts.map +1 -0
  129. package/react/index.js +67 -3
  130. package/solid/index.d.ts +64 -56
  131. package/solid/index.d.ts.map +1 -0
  132. package/solid/index.js +70 -3
  133. package/tokens/d2.d.ts +38 -0
  134. package/tokens/d2.js +71 -0
  135. package/tokens/d2.json +43 -0
  136. package/tokens/index.d.ts +5 -5
  137. package/tokens/index.js +23 -5
  138. package/tokens/index.json +9 -0
  139. package/tokens/mermaid.d.ts +23 -0
  140. package/tokens/mermaid.js +181 -0
  141. package/tokens/mermaid.json +163 -0
  142. package/tokens/resolved.json +45 -1
  143. package/tokens/skins.js +3 -2
  144. package/tokens/tokens.dtcg.json +26 -0
  145. package/tokens/vega.d.ts +34 -0
  146. package/tokens/vega.js +155 -0
  147. package/tokens/vega.json +179 -0
  148. package/fonts/doto-400.ttf +0 -0
  149. package/fonts/doto-500.ttf +0 -0
  150. package/fonts/doto-600.ttf +0 -0
  151. package/fonts/doto-700.ttf +0 -0
  152. package/fonts/doto-800.ttf +0 -0
  153. package/fonts/doto-900.ttf +0 -0
package/docs/d2.md ADDED
@@ -0,0 +1,195 @@
1
+ # D2
2
+
3
+ [D2](https://d2lang.com) compiles a diagram script to **SVG** (Go CLI or the
4
+ `@terrastruct/d2` WASM build). Like the [Mermaid integration](./mermaid.md),
5
+ `@ponchia/ui` doesn't render diagrams — it **themes** them from your tokens and
6
+ lets you **annotate** the result. Two things ship:
7
+
8
+ - `@ponchia/ui/d2` — helpers that produce on-brand D2 theme overrides.
9
+ - `@ponchia/ui/d2.json` — the resolved slot → hex maps, for any consumer.
10
+
11
+ D2 stays the consumer's renderer; this is config only, and D2 is **not** a
12
+ dependency.
13
+
14
+ ## Theme a diagram
15
+
16
+ D2's theme is a compact set of named colour slots. Override them with the bronto
17
+ palette by prepending a `vars` block to your diagram source — `brontoD2Vars()`
18
+ returns exactly that block (both light and dark):
19
+
20
+ ```js
21
+ import { brontoD2Vars } from '@ponchia/ui/d2';
22
+
23
+ const source = brontoD2Vars() + `
24
+ api -> db: query
25
+ db: Store { shape: cylinder }
26
+ `;
27
+ // → render `source` with the D2 CLI or @terrastruct/d2
28
+ ```
29
+
30
+ If you render through D2's Go or WASM API instead of source, pass the slot map to
31
+ its `themeOverrides` / `darkThemeOverrides`:
32
+
33
+ ```js
34
+ import { brontoD2Overrides } from '@ponchia/ui/d2';
35
+ brontoD2Overrides('dark'); // { N1: '#e6e6e6', B1: '#555555', … }
36
+ ```
37
+
38
+ For a build step or non-JS host, read `@ponchia/ui/d2.json` directly.
39
+
40
+ > **file:// portability.** A report opened from disk (`file://`) cannot
41
+ > `import` the `@ponchia/ui/d2` module **nor** `fetch('…/d2.json')` — the browser
42
+ > blocks both across the `null`/file origin (CORS), exactly as with
43
+ > [Vega](./vega.md#from-a-cdn-no-bundler) and [Mermaid](./mermaid.md#theme-a-diagram).
44
+ > This is rarely an issue for D2, whose native path is **build-time
45
+ > pre-rendering** to a frozen SVG (no client runtime — see below), but if you do
46
+ > theme D2 in the browser over `file://`, **inline** the slot map (paste the
47
+ > object from `d2.json`) rather than importing it. Over `http(s)` the
48
+ > `import`/`fetch` forms both work.
49
+
50
+ ### Why resolved colours, not `var(--x)`
51
+
52
+ D2 compiles to a **frozen SVG** in Go/WASM — there is no client CSS cascade, so a
53
+ `var(--accent)` could never resolve. The maps therefore ship **resolved hex per
54
+ theme**, projected from the same token source as
55
+ [`tokens/resolved.json`](./architecture.md) / `charts.json`. Each slot is set
56
+ explicitly (D2 does not derive a ramp from one seed). Because the output is a
57
+ static SVG, **build-time pre-rendering is D2's native path** — ideal for the
58
+ [report layer](./reporting.md).
59
+
60
+ ### What the slots paint
61
+
62
+ The mapping keeps a diagram **monochrome by default** — the rationed accent is
63
+ not spent on borders, edges, or shapes the author never marked:
64
+
65
+ | Slots | Paint | bronto mapping |
66
+ | --- | --- | --- |
67
+ | `N1`–`N3` | Text · muted · subtle | `--text` · `--text-soft` · `--text-dim` |
68
+ | `N4`–`N5` | Strong / regular lines | `--line-strong` · `--line` |
69
+ | `N6`–`N7` | Subtle background · canvas | `--panel-soft` · `--bg` |
70
+ | `B1` | Shape borders **and** connections (edges) | `--line-strong` |
71
+ | `B2`–`B3` | Muted borders | `--line` |
72
+ | `B4`–`B6` | Container fills (outer → leaf) | `--panel-soft` · `--bg-elevated` · `--panel` |
73
+ | `AA*` / `AB*` | Alternative-accent ramps (special-shape fills, class/sql headers) | kept **neutral** |
74
+
75
+ The alt-accent ramps are deliberately neutral: D2 spends them on special shapes
76
+ (cylinders, class/sql-table headers) by default, so mapping them to the accent
77
+ would colour shapes you never marked. Keeping them neutral is what holds the
78
+ monochrome default.
79
+
80
+ ### Spending the accent
81
+
82
+ To emphasise a node, opt it into a class that sets the accent explicitly — the
83
+ accent appears only where you ask for it:
84
+
85
+ ```d2
86
+ classes: {
87
+ accent: {
88
+ style: { fill: "#d71921"; font-color: "#ffffff"; stroke: "#b2151b" }
89
+ }
90
+ }
91
+
92
+ alert: Alert { class: accent }
93
+ ```
94
+
95
+ Use the resolved `--accent` / `--accent-strong` hex (from
96
+ [`tokens/resolved.json`](./architecture.md), per theme) for the `fill` / `stroke`
97
+ so the emphasis matches the rest of the surface. Reserve it for the one thing a
98
+ reader must not miss.
99
+
100
+ > **The label on an accent fill uses `--on-accent`, not `--accent-text`.** A
101
+ > filled-accent node needs **on-accent ink** for its `font-color` — the resolved
102
+ > `--on-accent` token (white on the light accent, black on the dark accent;
103
+ > ≥ 4.5:1, gated in [contrast.md](./contrast.md)). Do **not** reach for
104
+ > `--accent-text` by name: that is the inverse token — _accent-coloured text for
105
+ > a neutral background_ (it resolves to `--accent-strong`, ~1.3:1 on the accent
106
+ > fill, an unreadable label). The literal `#ffffff` above is `--on-accent` for
107
+ > the light theme; pull the per-theme hex from `tokens/resolved.json`.
108
+
109
+ ### Frozen inline SVG (no D2 runtime)
110
+
111
+ A static, PDF-first report often has no D2 binary in its pipeline. When you only
112
+ need a few boxes and arrows, **hand-author a token-themed inline `<svg>`** instead
113
+ of running D2 — the same frozen-figure route the [report chart
114
+ recipe](./reporting.md#chart-figure-recipe) uses. Paint each element from the
115
+ token that the live theme map would have resolved, so the frozen diagram still
116
+ re-skins (drive fills from `var(--token)` in the inline `style`/attributes, or
117
+ paste the resolved hex from [`tokens/resolved.json`](./architecture.md) for a
118
+ `file://` PDF):
119
+
120
+ | Diagram element | bronto token | (live D2 slot it mirrors) |
121
+ | --- | --- | --- |
122
+ | Node fill | `--panel` | `B4`–`B6` container fills |
123
+ | Node border | `--line-strong` | `B1` |
124
+ | Node label | `--text` | `N1` |
125
+ | Edge / connector + arrowhead | `--line-strong` | `B1` |
126
+ | Edge label | `--text-soft` | `N2` |
127
+ | Cluster / container fill | `--panel-soft` | `B4` |
128
+ | Accent (emphasised) node fill · its label | `--accent` · `--on-accent` | the `accent` class above |
129
+
130
+ ```html
131
+ <svg viewBox="0 0 320 120" role="img" aria-labelledby="flow-title">
132
+ <title id="flow-title">Ingest → Queue → Worker</title>
133
+ <!-- edge -->
134
+ <line x1="92" y1="40" x2="128" y2="40" stroke="var(--line-strong)" marker-end="url(#arrow)" />
135
+ <line x1="220" y1="40" x2="256" y2="40" stroke="var(--line-strong)" marker-end="url(#arrow)" />
136
+ <defs>
137
+ <marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto">
138
+ <path d="M0,0 L8,4 L0,8 z" fill="var(--line-strong)" />
139
+ </marker>
140
+ </defs>
141
+ <!-- neutral node -->
142
+ <rect x="16" y="22" width="76" height="36" rx="6" fill="var(--panel)" stroke="var(--line-strong)" />
143
+ <text x="54" y="44" text-anchor="middle" fill="var(--text)" font-size="12">Ingest</text>
144
+ <!-- accent (emphasised) node: on-accent ink on the accent fill -->
145
+ <rect x="128" y="22" width="92" height="36" rx="6" fill="var(--accent)" stroke="var(--accent-strong)" />
146
+ <text x="174" y="44" text-anchor="middle" fill="var(--on-accent)" font-size="12">Queue</text>
147
+ <rect x="256" y="22" width="64" height="36" rx="6" fill="var(--panel)" stroke="var(--line-strong)" />
148
+ <text x="288" y="44" text-anchor="middle" fill="var(--text)" font-size="12">Worker</text>
149
+ </svg>
150
+ ```
151
+
152
+ For anything larger or graph-laid-out, run D2 with the theme map and freeze its
153
+ output — don't hand-lay a complex graph.
154
+
155
+ ### Fit to small screens
156
+
157
+ D2 emits an SVG with explicit `width`/`height` from its layout, so on a narrow
158
+ screen it overflows unless you make it fluid. Two build-time options:
159
+
160
+ - **Scale to fit** — drop the fixed `width`/`height` attributes (keep the
161
+ `viewBox`) so the SVG shrinks to its container. Best for diagrams that stay
162
+ legible at smaller sizes.
163
+ - **Scroll a wide diagram** — wrap it in `overflow-x: auto` so a large diagram
164
+ scrolls inside its box instead of pushing the page wide, the same pattern the
165
+ report layer uses for wide figures.
166
+
167
+ ```css
168
+ .diagram-scroll {
169
+ overflow-x: auto;
170
+ }
171
+ .diagram-scroll svg {
172
+ max-inline-size: 100%;
173
+ block-size: auto;
174
+ }
175
+ ```
176
+
177
+ Render **non-sketch** at a fixed `scale` for predictable boxes.
178
+
179
+ ## Annotate a diagram
180
+
181
+ D2 output is SVG, so the [annotation layer](./annotations.md) composes onto it
182
+ exactly as in the [Mermaid recipe](./mermaid.md#annotate-a-diagram): render to a
183
+ frozen SVG (the D2 CLI `d2 in.d2 out.svg`, or `@terrastruct/d2` in a build
184
+ script), read the target shape's box, and paste a `<g class="ui-annotation">`
185
+ computed with `@ponchia/ui/annotations`. The same caveats apply — D2's internal
186
+ SVG (element ids, the root transform) is not a public contract, so pin your D2
187
+ version, render **non-sketch**, account for `pad` / `scale`, and avoid
188
+ `animate-interval` (a multi-board animated SVG would not track a single overlay).
189
+
190
+ ## Scope
191
+
192
+ bronto owns the theme map (gated: every slot resolves to a colour, both themes,
193
+ no `var()` leaks) and the annotation geometry. It does not own D2's rendering,
194
+ its internal SVG, or its CLI — those stay D2's, and the overlay is a documented
195
+ composition, not a shipped runtime binding.
@@ -0,0 +1,91 @@
1
+ # Generated content & AI trust
2
+
3
+ `@ponchia/ui/css/generated.css` is an opt-in layer of **trust surfaces for AI /
4
+ system-generated content**. Bronto is not a chat framework — it does not ship
5
+ chat bubbles. It ships the surfaces that make generated content legible and
6
+ accountable: a marked region, an origin label, and quiet collapsible reasoning
7
+ and tool-call logs. It pairs with the [source & provenance layer](./sources.md).
8
+
9
+ ```css
10
+ @import '@ponchia/ui';
11
+ @import '@ponchia/ui/css/generated.css';
12
+ ```
13
+
14
+ Bronto styles the disclosure, origin, and trace surfaces. The host owns model
15
+ metadata, reasoning-visibility policy, tool execution, redaction, and safety.
16
+ Prefer evidence and source links over decorative "AI sparkle" styling. Not in
17
+ the core bundle.
18
+
19
+ ## Generated region — `.ui-generated`
20
+
21
+ Wraps a block of machine-authored content with a quiet accent edge and an
22
+ optional origin `__label`.
23
+
24
+ ```html
25
+ <section class="ui-generated">
26
+ <p class="ui-generated__label">Model output</p>
27
+ <p>The migration cut p95 latency by 38% in the first hour.</p>
28
+ </section>
29
+ ```
30
+
31
+ ## Origin label — `.ui-origin-label`
32
+
33
+ A small tag for "AI assisted", "model output", "tool output", or "human
34
+ reviewed". `--ai` accent-tints it for model-generated origin; the default is a
35
+ neutral tag. The words carry the meaning — the tint is reinforcement.
36
+
37
+ ```html
38
+ <span class="ui-origin-label ui-origin-label--ai">AI generated</span>
39
+ <span class="ui-origin-label">Human reviewed</span>
40
+ ```
41
+
42
+ ## Reasoning — `.ui-reasoning`
43
+
44
+ A quiet, collapsible "how this was produced" block on a native `<details>` (zero
45
+ JS). Keep it closed by default; the host decides what reasoning is safe to show.
46
+
47
+ ```html
48
+ <details class="ui-reasoning">
49
+ <summary>Reasoning</summary>
50
+ <p class="ui-reasoning__body">Compared the Q2 and Q3 latency tables, then…</p>
51
+ </details>
52
+ ```
53
+
54
+ ## Tool log — `.ui-tool-log` / `.ui-tool-call`
55
+
56
+ A document-grade record of tool invocations. Each `.ui-tool-call` is a
57
+ collapsible `<details>`: a `__name`, an optional `__status` (put a `.ui-dot` or
58
+ text), and a `__body` with the arguments or output.
59
+
60
+ ```html
61
+ <div class="ui-tool-log">
62
+ <details class="ui-tool-call">
63
+ <summary>
64
+ <span class="ui-tool-call__name">search_docs(query)</span>
65
+ <span class="ui-tool-call__status"><span class="ui-dot ui-dot--live"></span> ok</span>
66
+ </summary>
67
+ <pre class="ui-tool-call__body">{ "query": "p95 latency", "hits": 3 }</pre>
68
+ </details>
69
+ </div>
70
+ ```
71
+
72
+ ## Recipe
73
+
74
+ ```js
75
+ import { ui } from '@ponchia/ui/classes';
76
+
77
+ ui.originLabel({ ai: true }); // "ui-origin-label ui-origin-label--ai"
78
+ ```
79
+
80
+ The other parts are class-only (`cls.generated`, `cls.toolCall`, …) — they have
81
+ no options, so apply the class directly.
82
+
83
+ ## Accessibility
84
+
85
+ - The reasoning and tool-call disclosures are native `<details>` — keyboard- and
86
+ screen-reader-accessible with no JS.
87
+ - An origin label must read as text ("AI generated"); the accent tint is not the
88
+ only signal.
89
+ - A confidence / verdict widget is intentionally **not** shipped: don't
90
+ fabricate precision. Add a confidence surface only when the product has a real
91
+ signal to show.
@@ -0,0 +1,184 @@
1
+ # Legends & data keys
2
+
3
+ `@ponchia/ui/css/legend.css` is an opt-in layer of **data keys** for charts,
4
+ reports, and analytical figures. A legend maps a visual encoding (colour, a
5
+ pattern, a shape, a gradient) to its meaning. It reads the same Tier-4
6
+ `--chart-*` tokens the [data-viz palette](./reference.md) ships, so a key never
7
+ drifts from the series it describes.
8
+
9
+ ```css
10
+ @import '@ponchia/ui';
11
+ @import '@ponchia/ui/css/dataviz.css';
12
+ @import '@ponchia/ui/css/legend.css';
13
+ ```
14
+
15
+ Bronto **paints and positions** the key; it owns no scales, no data→pixel
16
+ mapping, and no series state. Pair it with any chart — a token-themed inline
17
+ SVG, or an external engine like [Vega-Lite](./vega.md) — the host owns the
18
+ chart.
19
+
20
+ ## What it is not
21
+
22
+ - Not a chart engine. It does not compute tick positions, "nice" numbers,
23
+ bin thresholds, or bubble radii — you supply the tick/range **text**; the
24
+ CSS positions the slots you give it.
25
+ - Not a colour source. Swatch colour always comes from a `--chart-*` token
26
+ (enforced by `check:legend`), never a hand-rolled hex.
27
+
28
+ ## Accessibility: colour is never the only channel
29
+
30
+ A legend that distinguishes series by **colour alone** fails
31
+ [WCAG 1.4.1 Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html).
32
+ In a legend, the **text label** is the required non-colour channel — every
33
+ entry carries its name, so the meaning survives even if colour is lost
34
+ (`forced-colors`, monochrome print, colour-vision deficiency). Where the chart
35
+ *mark itself* is colour-only (a bare area or line), also pair the swatch with
36
+ its `--chart-pattern-*` so the figure — not just the legend — stays readable.
37
+
38
+ Recommended structure: wrap the figure and its key in a `<figure>` with a
39
+ `<figcaption>`, and give the legend its own group label.
40
+
41
+ ```html
42
+ <figure role="group" aria-labelledby="fig-1-title">
43
+ <figcaption id="fig-1-title">Fig 1 — Weekly focus split</figcaption>
44
+
45
+ <!-- … the chart … -->
46
+
47
+ <ul class="ui-legend" aria-label="Series">
48
+ <li class="ui-legend__item">
49
+ <span
50
+ class="ui-legend__swatch"
51
+ style="--chart-color: var(--chart-1); --chart-pattern: var(--chart-pattern-1)"
52
+ aria-hidden="true"
53
+ ></span>
54
+ <span class="ui-legend__label">Research</span>
55
+ </li>
56
+ <li class="ui-legend__item">
57
+ <span class="ui-legend__swatch ui-legend__swatch--2" aria-hidden="true"></span>
58
+ <span class="ui-legend__label">Delivery</span>
59
+ </li>
60
+ </ul>
61
+ </figure>
62
+ ```
63
+
64
+ The swatch is decorative (`aria-hidden="true"`) — the meaning is the label
65
+ text. Set its colour either inline (`--chart-color`) to mirror exactly what the
66
+ chart mark uses, or with a `--N` index helper for the categorical palette.
67
+
68
+ ## Parts
69
+
70
+ | Class | Role |
71
+ | --- | --- |
72
+ | `ui-legend` | The container (a wrapping inline row by default). |
73
+ | `ui-legend__title` | Optional heading for the key. |
74
+ | `ui-legend__item` | One entry — swatch + label (+ value). |
75
+ | `ui-legend__swatch` | The colour/pattern chip. |
76
+ | `ui-legend__symbol` | A glyph/shape chip (fill an `.ui-icon` mask). |
77
+ | `ui-legend__label` | The series name (the non-colour channel). |
78
+ | `ui-legend__value` | Optional trailing value/range. |
79
+ | `ui-legend__caption` | Optional footnote (units, source). |
80
+ | `ui-legend__track` | The gradient bar (continuous keys). |
81
+ | `ui-legend__ticks` / `ui-legend__tick` | Tick labels under the track. |
82
+
83
+ ### Swatch colour
84
+
85
+ | Approach | Use |
86
+ | --- | --- |
87
+ | `style="--chart-color: var(--chart-3)"` | Mirror any token, any order; add `--chart-pattern` to match a patterned mark. |
88
+ | `class="… ui-legend__swatch--3"` | Categorical palette series 1–8 — sets `--chart-3` for you. |
89
+
90
+ `ui-legend__swatch--circle` and `ui-legend__swatch--line` change the chip shape
91
+ (dot series, line series).
92
+
93
+ A `ui-legend__symbol` chip is an `.ui-icon` mask — it needs a `--icon-mask`
94
+ (e.g. `style="--icon-mask: var(--glyph-dot)"`) or it paints a solid square, like
95
+ any [icon](./reference.md). And an **interactive** legend entry must be a real
96
+ `<button>` (as in the example below) — a non-button `ui-legend__item` carrying
97
+ `data-series` is not keyboard-reachable.
98
+
99
+ **Keying the de-emphasised series.** In an accent-rationed chart — one mark
100
+ painted with [`brontoVegaAccent`](./vega.md#spending-the-accent), the rest left
101
+ quiet with `brontoVegaNeutral` — the quiet neutral **is** the last categorical
102
+ series, `--chart-8` (`#4d5358`). So a legend that honestly mirrors that chart
103
+ uses `ui-legend__swatch--1` for the highlighted entry and
104
+ `ui-legend__swatch--8` for the "everything else" entry — both are real palette
105
+ tokens, so the key never drifts from the marks and `check:legend` stays happy.
106
+ Don't hand-roll a grey: `--chart-8` is the neutral by construction.
107
+
108
+ ## Variants
109
+
110
+ | Modifier | Effect |
111
+ | --- | --- |
112
+ | `ui-legend--vertical` | Stack entries instead of the wrapping row. |
113
+ | `ui-legend--compact` | Denser type and gaps. |
114
+ | `ui-legend--with-values` | Align a trailing `__value` column across rows. |
115
+ | `ui-legend--gradient` | Continuous colour ramp (`__track` + `__ticks`). |
116
+ | `ui-legend--diverging` | Use the 7-stop diverging ramp (with `--gradient`). |
117
+ | `ui-legend--threshold` | Binned `swatch │ range-label` grid. |
118
+ | `ui-legend--interactive` | Entries are toggle controls (see below). |
119
+
120
+ The recipe mirrors this surface:
121
+
122
+ ```js
123
+ import { ui } from '@ponchia/ui/classes';
124
+
125
+ ui.legend({ type: 'gradient', diverging: true });
126
+ // "ui-legend ui-legend--gradient ui-legend--diverging"
127
+ ui.legendSwatch({ series: 3, shape: 'circle' });
128
+ // "ui-legend__swatch ui-legend__swatch--3 ui-legend__swatch--circle"
129
+ ```
130
+
131
+ ### Continuous ramp
132
+
133
+ Supply the min/mid/max tick **text**; the track interpolates the sequential
134
+ ramp in OKLCH (`--diverging` swaps in the diverging ramp around its neutral
135
+ centre).
136
+
137
+ ```html
138
+ <div class="ui-legend ui-legend--gradient" role="group" aria-label="Density">
139
+ <span class="ui-legend__track" aria-hidden="true"></span>
140
+ <span class="ui-legend__ticks">
141
+ <span class="ui-legend__tick">0</span>
142
+ <span class="ui-legend__tick">50</span>
143
+ <span class="ui-legend__tick">100</span>
144
+ </span>
145
+ </div>
146
+ ```
147
+
148
+ ## Interactive legends (optional)
149
+
150
+ An interactive legend toggles a series on or off. Bronto ships the **control
151
+ surface** only; the **host owns the data**. The split is deliberate (it keeps
152
+ the legend out of chart-engine territory):
153
+
154
+ - **Bronto:** each entry is a `<button aria-pressed>`. The optional
155
+ `initLegend` behavior flips `aria-pressed`, toggles `.is-inactive`, and
156
+ dispatches `bronto:legend:toggle` with `{ detail: { series, active } }`.
157
+ - **You:** listen for the event, hide/show your own series, and announce the
158
+ change through an `aria-live` region you own.
159
+
160
+ ```html
161
+ <ul class="ui-legend ui-legend--interactive" data-bronto-legend aria-label="Series">
162
+ <li>
163
+ <button type="button" class="ui-legend__item" aria-pressed="true" data-series="research">
164
+ <span class="ui-legend__swatch ui-legend__swatch--1" aria-hidden="true"></span>
165
+ <span class="ui-legend__label">Research</span>
166
+ </button>
167
+ </li>
168
+ </ul>
169
+ ```
170
+
171
+ ```js
172
+ import { initLegend } from '@ponchia/ui/behaviors';
173
+ const stop = initLegend(); // returns a cleanup fn
174
+ document.addEventListener('bronto:legend:toggle', (e) => {
175
+ const { series, active } = e.detail;
176
+ // hide/show your series, then announce it in your own aria-live region
177
+ });
178
+ ```
179
+
180
+ Convention: `aria-pressed="true"` means the series is **shown** (the default).
181
+ The entry's label never changes on toggle — only `aria-pressed` and
182
+ `.is-inactive` flip, so a screen reader reads a stable name with a clear
183
+ pressed state. React/Solid/Qwik consumers can use `useLegend()` instead of
184
+ calling `initLegend` directly.
package/docs/marks.md ADDED
@@ -0,0 +1,93 @@
1
+ # Text marks & evidence
2
+
3
+ `@ponchia/ui/css/marks.css` is an opt-in layer of **evidence and emphasis marks
4
+ for running text** — the prose counterpart to the [SVG annotations](./annotations.md)
5
+ layer. Annotations call out a figure; marks call out a sentence. The look is
6
+ sober and report-grade (good for docs, audits, and generated/LLM reports), not
7
+ a hand-drawn highlighter.
8
+
9
+ ```css
10
+ @import '@ponchia/ui';
11
+ @import '@ponchia/ui/css/marks.css';
12
+ ```
13
+
14
+ Monochrome by default — the rationed accent is opt-in via `--accent`, and the
15
+ status tones are only for status-bearing emphasis.
16
+
17
+ ## Inline marks — `.ui-mark`
18
+
19
+ Put `.ui-mark` on a `<mark>` so the emphasis is semantic, not just visual.
20
+
21
+ ```html
22
+ <p>
23
+ The migration <mark class="ui-mark ui-mark--accent">cut p95 latency by 38%</mark>,
24
+ but <mark class="ui-mark ui-mark--danger ui-mark--underline">error rate doubled</mark>
25
+ in the first hour.
26
+ </p>
27
+ ```
28
+
29
+ | Style | Effect |
30
+ | --- | --- |
31
+ | _(default)_ | Highlighter fill. |
32
+ | `ui-mark--underline` | Coloured underline (no fill). |
33
+ | `ui-mark--box` | Outlined box. |
34
+ | `ui-mark--strike` | Strikethrough (removed/superseded text). |
35
+
36
+ | Tone | Use |
37
+ | --- | --- |
38
+ | _(default)_ | Neutral ink emphasis (monochrome). |
39
+ | `ui-mark--accent` | The rationed accent — "this is the proof". |
40
+ | `ui-mark--success` / `--warning` / `--danger` / `--info` | Status-bearing emphasis only. |
41
+ | `ui-mark--muted` | De-emphasis. |
42
+
43
+ `ui-mark--draw` sweeps the highlight in once on load; it respects
44
+ `prefers-reduced-motion` (reduced motion shows the resting full highlight). It
45
+ applies to the highlight fill, so pair it with the default (no style modifier).
46
+
47
+ > **A mark is a behind-text highlight, not a filled chip.** The fill is a
48
+ > low-alpha gradient *behind* the running text, so the text keeps its normal ink
49
+ > and the contrast stays text-on-background — even `ui-mark--accent` is
50
+ > contrast-safe by construction. Don't reach for `--on-accent` here (that ink is
51
+ > for a *solid* accent fill, like a button or a D2 node); a `<mark>` never needs
52
+ > it.
53
+
54
+ ## Passage bracket — `.ui-bracket-note`
55
+
56
+ Brackets a whole block and optionally labels it — the prose analogue of
57
+ `ui-annotation--bracket`. Useful for "this paragraph is the evidence/caveat".
58
+
59
+ ```html
60
+ <blockquote class="ui-bracket-note ui-bracket-note--accent">
61
+ <span class="ui-bracket-note__label">Source</span>
62
+ Q3 incident review, §4 — sustained for 47 minutes before rollback.
63
+ </blockquote>
64
+ ```
65
+
66
+ Tones: `--accent` (the rationed accent), `--success`, `--warning`, `--danger`,
67
+ `--info`. The default is a neutral bracket.
68
+
69
+ ## Recipes
70
+
71
+ ```js
72
+ import { ui } from '@ponchia/ui/classes';
73
+
74
+ ui.mark({ tone: 'accent', motion: 'draw' });
75
+ // "ui-mark ui-mark--accent ui-mark--draw"
76
+ ui.mark({ style: 'underline', tone: 'danger' });
77
+ // "ui-mark ui-mark--underline ui-mark--danger"
78
+ ui.bracketNote({ tone: 'warning' });
79
+ // "ui-bracket-note ui-bracket-note--warning"
80
+ ```
81
+
82
+ ## Accessibility
83
+
84
+ - A `.ui-mark` is **visual emphasis**; it does not announce itself to screen
85
+ readers. When the emphasis carries meaning that the surrounding words don't,
86
+ state it in the text (a screen-reader user can't see the highlight) — the same
87
+ rule as colour (WCAG 1.4.1). Use a native `<mark>` so the relationship is at
88
+ least semantic.
89
+ - In `forced-colors` mode a highlight `background` is dropped, so marks add an
90
+ underline to keep the emphasis visible; the `--box`/`--underline`/`--strike`
91
+ styles already survive as a system colour.
92
+ - `.ui-bracket-note` is a plain block; wrap a quotation in `<blockquote>` (or a
93
+ region with its own heading/label) so its role is conveyed without the border.