@ponchia/ui 0.5.0 → 0.6.3
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 +386 -4
- package/MIGRATIONS.json +14 -0
- package/README.md +29 -6
- package/annotations/index.d.ts +398 -276
- package/annotations/index.d.ts.map +1 -0
- package/annotations/index.js +350 -77
- package/behaviors/carousel.d.ts +28 -0
- package/behaviors/carousel.d.ts.map +1 -0
- package/behaviors/carousel.js +20 -16
- package/behaviors/combobox.d.ts +40 -0
- package/behaviors/combobox.d.ts.map +1 -0
- package/behaviors/combobox.js +111 -29
- package/behaviors/command.d.ts +41 -0
- package/behaviors/command.d.ts.map +1 -0
- package/behaviors/command.js +27 -15
- package/behaviors/connectors.d.ts +17 -0
- package/behaviors/connectors.d.ts.map +1 -0
- package/behaviors/connectors.js +7 -5
- package/behaviors/crosshair.d.ts +42 -0
- package/behaviors/crosshair.d.ts.map +1 -0
- package/behaviors/crosshair.js +23 -6
- package/behaviors/dialog.d.ts +20 -0
- package/behaviors/dialog.d.ts.map +1 -0
- package/behaviors/dialog.js +6 -2
- package/behaviors/disclosure.d.ts +10 -0
- package/behaviors/disclosure.d.ts.map +1 -0
- package/behaviors/disclosure.js +6 -2
- package/behaviors/dismissible.d.ts +10 -0
- package/behaviors/dismissible.d.ts.map +1 -0
- package/behaviors/dismissible.js +6 -2
- package/behaviors/forms.d.ts +27 -0
- package/behaviors/forms.d.ts.map +1 -0
- package/behaviors/forms.js +54 -13
- package/behaviors/glyph.d.ts +14 -0
- package/behaviors/glyph.d.ts.map +1 -0
- package/behaviors/glyph.js +28 -5
- package/behaviors/index.d.ts +31 -237
- package/behaviors/index.d.ts.map +1 -0
- package/behaviors/index.js +17 -0
- package/behaviors/inert.d.ts +20 -0
- package/behaviors/inert.d.ts.map +1 -0
- package/behaviors/inert.js +46 -0
- package/behaviors/internal.d.ts +25 -0
- package/behaviors/internal.d.ts.map +1 -0
- package/behaviors/internal.js +77 -1
- package/behaviors/legend.d.ts +35 -0
- package/behaviors/legend.d.ts.map +1 -0
- package/behaviors/legend.js +32 -2
- package/behaviors/menu.d.ts +16 -0
- package/behaviors/menu.d.ts.map +1 -0
- package/behaviors/menu.js +6 -2
- package/behaviors/modal.d.ts +41 -0
- package/behaviors/modal.d.ts.map +1 -0
- package/behaviors/modal.js +124 -0
- package/behaviors/popover.d.ts +28 -0
- package/behaviors/popover.d.ts.map +1 -0
- package/behaviors/popover.js +78 -7
- package/behaviors/spotlight.d.ts +17 -0
- package/behaviors/spotlight.d.ts.map +1 -0
- package/behaviors/spotlight.js +7 -5
- package/behaviors/table.d.ts +36 -0
- package/behaviors/table.d.ts.map +1 -0
- package/behaviors/table.js +84 -17
- package/behaviors/tabs.d.ts +20 -0
- package/behaviors/tabs.d.ts.map +1 -0
- package/behaviors/tabs.js +17 -14
- package/behaviors/theme.d.ts +54 -0
- package/behaviors/theme.d.ts.map +1 -0
- package/behaviors/theme.js +22 -3
- package/behaviors/toast.d.ts +49 -0
- package/behaviors/toast.d.ts.map +1 -0
- package/behaviors/toast.js +47 -3
- package/classes/classes.json +2527 -0
- package/classes/index.d.ts +134 -15
- package/classes/index.js +280 -80
- package/classes/vscode.css-custom-data.json +12 -0
- package/connectors/index.d.ts +201 -69
- package/connectors/index.d.ts.map +1 -0
- package/connectors/index.js +142 -25
- package/css/app.css +69 -13
- package/css/base.css +15 -10
- package/css/bullet.css +108 -0
- package/css/code.css +98 -0
- package/css/connectors.css +17 -0
- package/css/content.css +22 -3
- package/css/crosshair.css +7 -7
- package/css/dataviz.css +5 -1
- package/css/diff.css +153 -0
- package/css/disclosure.css +53 -7
- package/css/dots.css +94 -7
- package/css/feedback.css +97 -7
- package/css/forms.css +113 -4
- package/css/legend.css +16 -9
- package/css/marks.css +38 -8
- package/css/motion.css +98 -53
- package/css/navigation.css +7 -0
- package/css/overlay.css +90 -3
- package/css/primitives.css +158 -13
- package/css/report.css +73 -56
- package/css/sidenote.css +67 -0
- package/css/site.css +16 -2
- package/css/sources.css +43 -1
- package/css/spark.css +62 -0
- package/css/spotlight.css +1 -1
- package/css/table.css +9 -2
- package/css/term.css +110 -0
- package/css/textref.css +63 -0
- package/css/toc.css +91 -0
- package/css/tokens.css +49 -1
- package/css/tree.css +134 -0
- package/css/workbench.css +1 -1
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/app.css +1 -1
- package/dist/css/base.css +1 -1
- package/dist/css/bullet.css +1 -0
- package/dist/css/code.css +1 -0
- package/dist/css/connectors.css +1 -1
- package/dist/css/content.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/diff.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/forms.css +1 -1
- package/dist/css/legend.css +1 -1
- package/dist/css/marks.css +1 -1
- 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/sidenote.css +1 -0
- package/dist/css/site.css +1 -1
- package/dist/css/sources.css +1 -1
- package/dist/css/spark.css +1 -0
- package/dist/css/spotlight.css +1 -1
- package/dist/css/table.css +1 -1
- package/dist/css/term.css +1 -0
- package/dist/css/textref.css +1 -0
- package/dist/css/toc.css +1 -0
- package/dist/css/tokens.css +1 -1
- package/dist/css/tree.css +1 -0
- package/dist/css/workbench.css +1 -1
- package/docs/adr/0003-theme-model.md +1 -1
- package/docs/annotations.md +133 -14
- package/docs/architecture.md +49 -6
- package/docs/bullet.md +78 -0
- package/docs/code.md +76 -0
- package/docs/contrast.md +116 -92
- package/docs/d2.md +196 -0
- package/docs/diff.md +146 -0
- package/docs/legends.md +23 -3
- package/docs/marks.md +9 -2
- package/docs/mermaid.md +169 -0
- package/docs/reference.md +201 -26
- package/docs/reporting.md +416 -57
- package/docs/sidenote.md +64 -0
- package/docs/sources.md +27 -0
- package/docs/spark.md +78 -0
- package/docs/stability.md +10 -2
- package/docs/term.md +81 -0
- package/docs/textref.md +78 -0
- package/docs/theming.md +44 -5
- package/docs/toc.md +83 -0
- package/docs/tree.md +74 -0
- package/docs/usage.md +354 -16
- package/docs/vega.md +244 -0
- package/docs/workbench.md +7 -1
- package/glyphs/glyphs.js +13 -5
- package/llms.txt +285 -14
- package/package.json +95 -17
- package/qwik/index.d.ts +44 -59
- package/qwik/index.d.ts.map +1 -0
- package/qwik/index.js +65 -3
- package/react/index.d.ts +41 -61
- package/react/index.d.ts.map +1 -0
- package/react/index.js +63 -3
- package/solid/index.d.ts +68 -61
- package/solid/index.d.ts.map +1 -0
- package/solid/index.js +66 -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 +15 -1
- 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/docs/usage.md
CHANGED
|
@@ -26,6 +26,14 @@ into a preset on `<html>` or any subtree:
|
|
|
26
26
|
Scope it, don't globalize blindly: a dashboard with one marketing-style
|
|
27
27
|
hero can set `compact` on `<html>` and `comfortable` on the hero section.
|
|
28
28
|
|
|
29
|
+
> **`data-density` is the global preset; per-component density verbs differ by
|
|
30
|
+
> family.** Some components carry their own local density modifier, and the verb
|
|
31
|
+
> is **not** uniform: it's `--dense` on `ui-table`/`ui-dotgrid` but `--compact`
|
|
32
|
+
> on `ui-prose`/`ui-legend`/`ui-report`. So `ui-prose--dense` and
|
|
33
|
+
> `ui-table--compact` both no-op. When in doubt, reach for the global
|
|
34
|
+
> `data-density` preset; use the local modifier only where it exists (check the
|
|
35
|
+
> base's `modifiers` in classes.json).
|
|
36
|
+
|
|
29
37
|
## Badge vs chip vs status dot
|
|
30
38
|
|
|
31
39
|
All three are small. They are **not** interchangeable:
|
|
@@ -35,9 +43,38 @@ All three are small. They are **not** interchangeable:
|
|
|
35
43
|
| **status dot** | a single piece of state on something else (row online, build ok). Smallest possible signal; pair with text for a11y, never color-only. |
|
|
36
44
|
| **badge** | a label *classifying* the thing it sits on (count, tone, "BETA"). Static, not actionable. `ui.badge({ tone })`. |
|
|
37
45
|
| **chip** | a discrete, often removable/selectable token the user manipulated (a filter, a tag input value). Interactive affordance implied. |
|
|
46
|
+
| **tag** | a static keyword/category label (`ui.tag`), like a badge but reads as a non-interactive tag. Use `chip` for anything the user selects/removes; `tag` for a fixed label. |
|
|
38
47
|
|
|
39
48
|
Rule of thumb: state → dot, classification → badge, user-controlled value
|
|
40
|
-
→ chip.
|
|
49
|
+
→ chip, fixed keyword → tag. (`ui-property` is a **workbench** primitive — a
|
|
50
|
+
key/value spec row scoped to `@ponchia/ui/css/workbench.css`, not a general
|
|
51
|
+
content label; don't reach for it to tag prose.)
|
|
52
|
+
|
|
53
|
+
**Tone vocabulary varies by family — by design.** Colour is rationed, so not
|
|
54
|
+
every component carries every tone. The status tones
|
|
55
|
+
(`--success`/`--warning`/`--danger`) plus `--info` ride the status families —
|
|
56
|
+
`ui-alert`, `ui-toast`, `ui-meter`, `ui-dot`, and `ui-badge` all carry `--info`.
|
|
57
|
+
The neutral `--muted` is **not** a status tone: it rides several *neutral*
|
|
58
|
+
bases (`ui-badge`, `ui-num`, `ui-eyebrow`, `ui-mark`, `ui-annotation`,
|
|
59
|
+
`ui-connector`, `ui-crosshair`) but is deliberately absent from the status
|
|
60
|
+
families `ui-dot`/`ui-alert`/`ui-toast`/`ui-meter`. Because the CSS is a
|
|
61
|
+
plain class list, a hand-written unknown modifier (e.g. `ui-dot--muted`,
|
|
62
|
+
`ui-meter--muted`) **silently no-ops** — CSS can't warn. The `ui.*` builders are
|
|
63
|
+
safer on both ends: TypeScript rejects an out-of-set tone at author time, and at
|
|
64
|
+
runtime they `console.warn` (then drop the tone) so a JS caller sees the mistake
|
|
65
|
+
rather than shipping a bare, untoned element. The authoritative
|
|
66
|
+
per-component tone list is each base's `modifiers` array in
|
|
67
|
+
[`@ponchia/ui/classes.json`](../classes/classes.json) — read it rather than
|
|
68
|
+
extrapolating a tone you saw on one component onto another.
|
|
69
|
+
|
|
70
|
+
**Tone is a colour channel, so it collapses under Windows High-Contrast Mode.**
|
|
71
|
+
A `ui-badge--success` and a `ui-badge--danger` render identically once HCM
|
|
72
|
+
remaps colours (the tinted background + tone border are flattened to one system
|
|
73
|
+
colour). Status dots and the meter fill re-assert a distinct system colour, but
|
|
74
|
+
the badge/chip/tag tints cannot carry five differentiable tones — so treat badge
|
|
75
|
+
tone as **decorative emphasis** and make sure the badge's text label carries the
|
|
76
|
+
status word ("Failed", not a bare red pill). Same WCAG 1.4.1 reasoning the dots
|
|
77
|
+
follow.
|
|
41
78
|
|
|
42
79
|
## Numbers: `ui-num` vs the table state classes
|
|
43
80
|
|
|
@@ -49,6 +86,21 @@ Rule of thumb: state → dot, classification → badge, user-controlled value
|
|
|
49
86
|
freed from the table. Do not hand-roll right-align + `text-green`;
|
|
50
87
|
that's the duplication `ui-num` exists to kill.
|
|
51
88
|
|
|
89
|
+
**The positive/negative vocabulary is spelled three ways — match the mechanism
|
|
90
|
+
to the host, they are not interchangeable:**
|
|
91
|
+
|
|
92
|
+
| Where | Positive / negative | Kind |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `ui-num` primitive (anywhere) | `ui-num--pos` / `ui-num--neg` (`ui.num({ tone: 'pos' })`) | **modifier** |
|
|
95
|
+
| Inside `.ui-table` | `is-pos` / `is-neg` | **state hook** (table-scoped) |
|
|
96
|
+
| `ui-delta` trend | `ui-delta--up` / `ui-delta--down` (`ui.delta({ dir })`) | **direction** |
|
|
97
|
+
|
|
98
|
+
They share the same tone tokens but **don't cross over**: `ui-num is-pos`
|
|
99
|
+
no-ops (the `is-pos` rule is scoped to table cells), and `ui-delta--pos` doesn't
|
|
100
|
+
exist. Use the modifier on `ui-num`, the `is-*` state inside a table, and
|
|
101
|
+
`--up`/`--down` on `ui-delta` (which also flips with `ui.delta({ invert })` when
|
|
102
|
+
up is the *bad* direction — latency, error rate, cost).
|
|
103
|
+
|
|
52
104
|
## Prose vs primitives, and prose inside a card
|
|
53
105
|
|
|
54
106
|
- `ui-prose` styles **raw, unclassed semantic HTML** (MDX / CMS / LLM
|
|
@@ -60,6 +112,42 @@ Rule of thumb: state → dot, classification → badge, user-controlled value
|
|
|
60
112
|
`.ui-card` itself, so card padding/border stays the card's and prose
|
|
61
113
|
rhythm stays the content's. One responsibility per element.
|
|
62
114
|
|
|
115
|
+
## Centred width: `ui-center` vs `ui-container`
|
|
116
|
+
|
|
117
|
+
Both cap a centred column, but they are **different primitives with different
|
|
118
|
+
box models** — pick by intent:
|
|
119
|
+
|
|
120
|
+
- **`ui-center`** — a *reading measure*. `--center-max` is the **inner** content
|
|
121
|
+
width (content-box); the `--center-gutter` padding adds *outside* it. Use it to
|
|
122
|
+
hold prose/body to a comfortable line length.
|
|
123
|
+
- **`ui-container`** — a *page frame*. `--container` (/`--container-narrow`
|
|
124
|
+
/`--container-wide`) is the **total** max width (border-box). Use it as the
|
|
125
|
+
outer wrapper that aligns a page's sections to a shared edge.
|
|
126
|
+
|
|
127
|
+
Rule of thumb: measure of text → `ui-center`; page-level frame → `ui-container`.
|
|
128
|
+
Don't nest one inside the other expecting the caps to compose — they measure
|
|
129
|
+
different boxes.
|
|
130
|
+
|
|
131
|
+
## Container queries: `ui-cq`
|
|
132
|
+
|
|
133
|
+
`ui-cq` is the one primitive that changes how *other* primitives respond. Add it
|
|
134
|
+
to a wrapper and its descendants adapt to **that box's** inline size, not the
|
|
135
|
+
viewport — so the same `ui-grid` / `ui-statgrid` / `ui-app-metrics` collapses to
|
|
136
|
+
one column inside a slim panel even when the window is wide (island-safe; it
|
|
137
|
+
nests). Two thresholds are built in: `ui-grid` drops to a single column at
|
|
138
|
+
**34rem** and `ui-statgrid`/`ui-app-metrics` at **30rem**, measured on the `ui-cq`
|
|
139
|
+
box. Note `rem` in a container query resolves against the **root** font size, not
|
|
140
|
+
16px — at Bronto's 15px root that's ≈**510px** and ≈**450px**, ~6% tighter than a
|
|
141
|
+
16px mental model. And be aware `ui-grid` already collapses on its own via an
|
|
142
|
+
intrinsic `auto-fit` minmax, so `ui-cq` barely changes it — the primitive that
|
|
143
|
+
genuinely *needs* `ui-cq` to collapse by container (not viewport) is
|
|
144
|
+
`ui-statgrid`/`ui-app-metrics`. The container is named `bronto` (hardcoded — there
|
|
145
|
+
is no `--cq-name` knob; an author-set one is ignored), so an outer query never
|
|
146
|
+
accidentally matches an inner grid. `ui-cq` is inert until applied, so adding it
|
|
147
|
+
never shifts an existing layout. Reach for it whenever a layout must respond to
|
|
148
|
+
its container (a resizable pane, a sidebar widget, an embedded card) rather than
|
|
149
|
+
the page.
|
|
150
|
+
|
|
63
151
|
## Static reports
|
|
64
152
|
|
|
65
153
|
Use the opt-in `@ponchia/ui/css/report.css` layer for static, PDF-first
|
|
@@ -70,10 +158,11 @@ for narrative body content you do not fully control.
|
|
|
70
158
|
|
|
71
159
|
Do not turn every report block into a card. Use `ui-report__summary`,
|
|
72
160
|
`ui-report__finding`, and `ui-report__evidence` for document structure; use
|
|
73
|
-
`ui-card` only when the block is genuinely a repeated card item.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
161
|
+
`ui-card` only when the block is genuinely a repeated card item. bronto ships
|
|
162
|
+
**no chart component**: for a chart, theme Vega-Lite (`@ponchia/ui/vega`, see
|
|
163
|
+
[vega.md](vega.md)) or hand-author a token-themed inline SVG painted from the
|
|
164
|
+
data-viz palette tokens. Always wrap it in a `ui-report__figure` with a caption,
|
|
165
|
+
a `.ui-legend` key, and fallback data. Full LLM/static report cookbook:
|
|
77
166
|
[reporting.md](reporting.md).
|
|
78
167
|
|
|
79
168
|
## Buttons: variant and size
|
|
@@ -102,6 +191,13 @@ destructive action).
|
|
|
102
191
|
| toast | transient, out-of-flow, system-initiated. Danger toasts route to an assertive live region; everything else polite. |
|
|
103
192
|
| tooltip | supplemental, hover/focus, never essential info (it's not announced reliably; don't hide required content in it). |
|
|
104
193
|
|
|
194
|
+
The CSS `ui-tooltip` is hover/focus-only and CSS can't wire it to assistive
|
|
195
|
+
tech for you — associate the bubble with its trigger yourself, or it conveys
|
|
196
|
+
nothing to a screen reader: give `.ui-tooltip__bubble` an `id`, point the
|
|
197
|
+
trigger's `aria-describedby` at it, and keep the bubble `role="tooltip"`. For a
|
|
198
|
+
tooltip that must stay visible near a viewport edge or inside a scroll
|
|
199
|
+
container, use `initPopover` (a real focus-managed panel) instead.
|
|
200
|
+
|
|
105
201
|
## Meter vs progress
|
|
106
202
|
|
|
107
203
|
Both are a thin horizontal bar; they mean different things.
|
|
@@ -110,10 +206,22 @@ Both are a thin horizontal bar; they mean different things.
|
|
|
110
206
|
indeterminate (`ui-progress--indeterminate`). The fill is always accent.
|
|
111
207
|
- **`ui-meter`** — a *measured static value*: coverage, disk, capacity, a
|
|
112
208
|
KPI against a target. Never indeterminate. Tone the fill by threshold
|
|
113
|
-
(`ui.meter({ tone })` → accent/success/warning/danger); the unset
|
|
114
|
-
default is neutral. Drive the width with the shared `--value` knob
|
|
115
|
-
(`style="--value: 72"`,
|
|
116
|
-
|
|
209
|
+
(`ui.meter({ tone })` → accent/success/warning/danger/info); the unset
|
|
210
|
+
default is neutral. Drive the width with the shared `--value` knob — a
|
|
211
|
+
**unitless number 0–100** (`style="--value: 72"`, *not* `72%`: a `%` is
|
|
212
|
+
invalid against the registered `<number>` type and the fill drops to empty).
|
|
213
|
+
The class string paints a 0-width, unannounced bar on its own, so set the
|
|
214
|
+
value **and** its ARIA together with `attrs.meter(72)` (or `attrs.progress`)
|
|
215
|
+
from `@ponchia/ui/classes` — it returns `role="meter"` +
|
|
216
|
+
`aria-valuenow/min/max` + the `--value` style, normalized to your
|
|
217
|
+
`{ min, max }`. Spread it: `<div class={ui.meter({ tone })} {...attrs.meter(72)}>`.
|
|
218
|
+
|
|
219
|
+
**Indeterminate progress** is the one exception: call `attrs.progress()` with
|
|
220
|
+
**no argument**. ARIA requires `aria-valuenow` be *omitted* for an indeterminate
|
|
221
|
+
bar — emitting `0` would announce "0%", indistinguishable from a real stalled-at-
|
|
222
|
+
zero bar — so the helper returns just `role="progressbar"` + `aria-busy="true"`
|
|
223
|
+
(no `aria-valuenow`, no `--value`). Pair it with the class:
|
|
224
|
+
`<div class={ui.progress({ indeterminate: true })} {...attrs.progress()}>`.
|
|
117
225
|
|
|
118
226
|
Rule of thumb: *something is happening* → progress; *something measures
|
|
119
227
|
this much* → meter.
|
|
@@ -123,7 +231,10 @@ this much* → meter.
|
|
|
123
231
|
- **`ui-steps`** — a stepper for a multi-step flow. Use an `<ol>`. State is
|
|
124
232
|
ARIA-driven (the framework rule): the active step is `aria-current="step"`
|
|
125
233
|
(no class); completed steps take `ui-steps__item--done`. Markers are
|
|
126
|
-
auto-numbered by CSS counter.
|
|
234
|
+
auto-numbered by CSS counter. `--done` is a **visual** state only — it isn't
|
|
235
|
+
announced, so if "completed" must reach AT, add visually-hidden text
|
|
236
|
+
(e.g. `<span class="ui-visually-hidden">completed</span>`) or an `aria-label`
|
|
237
|
+
on the step.
|
|
127
238
|
- **`ui-timeline`** — a vertical event list on a hairline spine (`<ol>` of
|
|
128
239
|
`ui-timeline__item`, optional `ui-timeline__time`). `aria-current` on an
|
|
129
240
|
item marks the live/most-recent event.
|
|
@@ -135,14 +246,116 @@ this much* → meter.
|
|
|
135
246
|
(`aria-hidden`) and the input keeps its full width. Don't hand-roll an
|
|
136
247
|
absolute overlay.
|
|
137
248
|
|
|
249
|
+
## Navigation: the landmarks and names the classes don't carry
|
|
250
|
+
|
|
251
|
+
The navigation classes are styling only — the ARIA scaffolding is yours, and
|
|
252
|
+
without it these widgets are unlabelled or unannounced:
|
|
253
|
+
|
|
254
|
+
- **`ui-breadcrumb`** — wrap it in `<nav aria-label="Breadcrumb">` and mark the
|
|
255
|
+
last (current) crumb with `aria-current="page"`.
|
|
256
|
+
- **`ui-pagination`** — wrap it in `<nav aria-label="Pagination">`; give the
|
|
257
|
+
current page `aria-current="page"`; label icon-only prev/next controls
|
|
258
|
+
(`aria-label="Previous page"`). Disable a control with native `disabled`
|
|
259
|
+
(a `<button>`) for full inertness, **or** `aria-disabled="true"` for a control
|
|
260
|
+
that stays focusable/announced (e.g. a disabled `<a>`). CSS dims both and makes
|
|
261
|
+
`aria-disabled` pointer-inert, but only native `disabled` is keyboard-inert on
|
|
262
|
+
its own — to stop an `aria-disabled` control from activating on Enter/Space,
|
|
263
|
+
wire `initDisabledGuard()` once near your root (see Behaviors).
|
|
264
|
+
- **`ui-tabs`** — `initTabs` adds the full APG wiring (roles, roving tabindex,
|
|
265
|
+
`aria-selected`, panel `hidden`, focusable panel). If you wire tabs yourself,
|
|
266
|
+
name the `ui-tabs__list` (`role="tablist"` + an `aria-label`) and pair each
|
|
267
|
+
tab with its panel via `aria-controls`/`aria-labelledby`. Every tab needs a
|
|
268
|
+
matching panel — a tab with no `aria-controls` target is an orphan that
|
|
269
|
+
announces as selected but reveals nothing.
|
|
270
|
+
- **Icon-only buttons** (`ui-button--icon` and any glyph-only control) carry no
|
|
271
|
+
text node, so they're nameless to AT — give them an `aria-label`
|
|
272
|
+
(`<button class="ui-button ui-button--icon" aria-label="Delete">`).
|
|
273
|
+
- **`ui-sitenav` / `ui-app-nav`** — signal the current link with
|
|
274
|
+
`aria-current="page"` (both honour it; `ui-app-nav` also accepts the
|
|
275
|
+
visual-only `.is-active`, but prefer `aria-current`).
|
|
276
|
+
- **`ui-skiplink`** — keep it the first focusable element and point its `href`
|
|
277
|
+
at the `id` of your main landmark.
|
|
278
|
+
|
|
279
|
+
## App shell: the admin dashboard frame
|
|
280
|
+
|
|
281
|
+
`ui-app-shell` is a CSS-only two-column admin frame (sidebar rail + main
|
|
282
|
+
column) that collapses to a single column with a horizontal rail below 880px —
|
|
283
|
+
no behavior required. The nesting matters; the rail is `ui-app-rail` and the
|
|
284
|
+
content side is `ui-app-main`:
|
|
285
|
+
|
|
286
|
+
```html
|
|
287
|
+
<div class="ui-app-shell">
|
|
288
|
+
<aside class="ui-app-rail">
|
|
289
|
+
<span class="ui-app-rail__brand">Acme</span>
|
|
290
|
+
<nav class="ui-app-nav" aria-label="Primary">
|
|
291
|
+
<span class="ui-app-nav__section">Main</span>
|
|
292
|
+
<a href="/overview" aria-current="page">Overview</a>
|
|
293
|
+
<a href="/reports">Reports</a>
|
|
294
|
+
</nav>
|
|
295
|
+
<div class="ui-app-rail__account">…</div>
|
|
296
|
+
</aside>
|
|
297
|
+
<main class="ui-app-main">
|
|
298
|
+
<header class="ui-app-topbar"><h1 class="ui-app-topbar__title">Overview</h1></header>
|
|
299
|
+
<div class="ui-app-content">
|
|
300
|
+
<section class="ui-app-panel">
|
|
301
|
+
<div class="ui-app-panel__head"><h2 class="ui-app-panel__title">KPIs</h2></div>
|
|
302
|
+
<div class="ui-app-metrics">…<div class="ui-app-metric">…</div></div>
|
|
303
|
+
</section>
|
|
304
|
+
</div>
|
|
305
|
+
</main>
|
|
306
|
+
</div>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Knobs: `--app-rail` sets the rail width (default 14rem); `ui-app-shell--full`
|
|
310
|
+
drops the rail for a single-column app. `ui-app-nav` honours `aria-current="page"`
|
|
311
|
+
(preferred) and the visual-only `.is-active`.
|
|
312
|
+
|
|
313
|
+
## Menus: `data-bronto-menu` + `initMenu`
|
|
314
|
+
|
|
315
|
+
A dropdown menu is a native `<details data-bronto-menu>` styled as
|
|
316
|
+
`ui-menu-host` → `ui-menu` (with `ui-menu__item` / `ui-menu__sep` /
|
|
317
|
+
`ui-menu__label`). It opens/closes natively, but `initMenu()` adds the close
|
|
318
|
+
affordances a menu needs: outside-click close, Escape, and closing on item
|
|
319
|
+
activation. Without the behavior the menu opens but never dismisses itself.
|
|
320
|
+
|
|
321
|
+
**Clipping limitation.** The menu panel is positioned in normal flow (not the
|
|
322
|
+
top layer), so an ancestor with `overflow: hidden`/`auto` or a transform
|
|
323
|
+
**clips** it — the same edge case the tooltip has. If your menu lives inside a
|
|
324
|
+
scroll container or a clipped card and the panel gets cut off, either lift the
|
|
325
|
+
`ui-menu-host` out of the clipping ancestor, or reach for `initPopover` (which
|
|
326
|
+
escapes to the browser top layer via the native `popover` attribute) for that
|
|
327
|
+
control instead.
|
|
328
|
+
|
|
329
|
+
## Avatar: it's an unlabelled blob until you name it
|
|
330
|
+
|
|
331
|
+
`ui-avatar` is a presentation box. Give it an accessible name yourself: an
|
|
332
|
+
image avatar needs real `alt` text (`alt=""` only if it's purely decorative
|
|
333
|
+
beside a visible name); an initials avatar needs an accessible name on the
|
|
334
|
+
element (e.g. `aria-label="Ada Lovelace"`) because the initials alone don't
|
|
335
|
+
convey identity to AT. Keep initials to ~2 characters — the box is
|
|
336
|
+
`overflow: hidden` and silently clips a third.
|
|
337
|
+
|
|
138
338
|
## Modal: native `<dialog>` vs `is-open`
|
|
139
339
|
|
|
140
340
|
Prefer the **native `<dialog>`** path — you get top-layer, backdrop and
|
|
141
|
-
focus-trap free
|
|
142
|
-
|
|
143
|
-
|
|
341
|
+
focus-trap free (wire it with `initDialog` for open-triggers + focus-return).
|
|
342
|
+
Only use `ui-modal.is-open` (`ui.modal({ open: true })`) when a portal/React
|
|
343
|
+
modal genuinely can't be a `<dialog>`. The **backdrop and top-layer stacking
|
|
344
|
+
stay yours**, but you no longer have to hand-roll the focus trap: mark the
|
|
345
|
+
overlay `data-bronto-modal` and call `initModal()`. While `is-open` it traps
|
|
346
|
+
focus with `inert` (the rest of the page goes non-interactive), returns focus to
|
|
347
|
+
the opener on close, and dispatches a cancelable `bronto:modal:close` on Escape —
|
|
348
|
+
you still own the `is-open` class, so drop it in response. On bind `initModal`
|
|
349
|
+
also gives the overlay `role="dialog"` + `aria-modal="true"` and dev-warns if it
|
|
350
|
+
has no accessible name (add `aria-label`/`aria-labelledby`). A drawer is a modal
|
|
144
351
|
that enters from an edge — same rule.
|
|
145
352
|
|
|
353
|
+
**Scroll-lock is not automatic on either path.** Neither the native `<dialog>`
|
|
354
|
+
nor the `is-open` path freezes the background — the page behind an open modal can
|
|
355
|
+
still scroll. If that matters, toggle a lock yourself while the modal is open
|
|
356
|
+
(`document.documentElement.style.overflow = 'hidden'`, restored on close), or add
|
|
357
|
+
`html:has(dialog[open]) { overflow: hidden }` for the native path.
|
|
358
|
+
|
|
146
359
|
## Carousel & lightbox: one primitive, two skins
|
|
147
360
|
|
|
148
361
|
`ui-carousel` is a scroll-snap track of `__slide`s wired by `initCarousel`
|
|
@@ -181,6 +394,13 @@ that fuses the cells into a square, gapless pixel glyph that stays crisp and
|
|
|
181
394
|
legible down to **~16px** — so the same set doubles as real inline UI icons,
|
|
182
395
|
not just decoration. (Below the dot fragments into dot-soup; solid does not.)
|
|
183
396
|
|
|
397
|
+
One caveat on ink: `solid` cells inherit the dot palette (`--field-dot-hot`,
|
|
398
|
+
~40% alpha), so at small sizes a solid glyph reads as a soft grey, not full
|
|
399
|
+
ink. When you want a crisp, full-strength small icon (toolbar, button affordance),
|
|
400
|
+
use the one-node mask renderer instead — `renderGlyph(name, { render: 'mask' })`
|
|
401
|
+
paints the glyph in `currentColor` on a `.ui-icon`, so it tracks text colour at
|
|
402
|
+
any size.
|
|
403
|
+
|
|
184
404
|
`renderGlyph(name, { label })` returns an SSR-safe string: decorative
|
|
185
405
|
(`aria-hidden`) by default, or `role="img"` + `aria-label` when you pass a
|
|
186
406
|
`label` — which is how it conveys meaning to assistive tech. Prefer the
|
|
@@ -285,13 +505,131 @@ authoring engine.
|
|
|
285
505
|
- Annotation text must be visible or represented in the figure caption, SVG
|
|
286
506
|
`<desc>`, or fallback table. Full detail in [annotations.md](annotations.md).
|
|
287
507
|
|
|
508
|
+
## Forms: the contracts the markup alone won't tell you
|
|
509
|
+
|
|
510
|
+
- **Disabled — pick one mechanism.** Use the native `disabled` attribute for a
|
|
511
|
+
genuinely inert control (`ui-input`, `ui-select`, `ui-textarea`, `ui-switch`
|
|
512
|
+
/`ui-check`/`ui-segmented` wrapping a native input, `ui-range`, `ui-file`,
|
|
513
|
+
`ui-button`): the browser greys it, blocks activation, and skips it in tab
|
|
514
|
+
order, and bronto styles the disabled cue. Use `aria-disabled="true"` **only**
|
|
515
|
+
when the control must stay focusable/announced — bronto then adds
|
|
516
|
+
`pointer-events: none` to `ui-button`/`ui-link` so the pointer can't activate
|
|
517
|
+
it. That is **pointer-inert, not keyboard-inert**: CSS can't stop Enter/Space,
|
|
518
|
+
so wire `initDisabledGuard()` (Behaviors) to block keyboard activation across
|
|
519
|
+
every `aria-disabled` control. Either way you still own removing it from the
|
|
520
|
+
submit logic.
|
|
521
|
+
- **Read-only ≠ disabled.** A `readonly` input keeps its value in form submission
|
|
522
|
+
and stays focusable/selectable; `disabled` does neither. Bronto gives a
|
|
523
|
+
read-only field a quiet muted fill so it doesn't read as a live editable field —
|
|
524
|
+
reach for `readonly` when the value matters but mustn't be edited, `disabled`
|
|
525
|
+
when it should be inert and skipped.
|
|
526
|
+
- **Combobox** (`data-bronto-combobox`) reads its options from the DOM at
|
|
527
|
+
`initCombobox()` time — re-run it after you replace the option list (or add
|
|
528
|
+
`data-bronto-combobox-live`). The selected **option's text label** is shown in
|
|
529
|
+
the input while the `bronto:change` event carries the option's `data-value`
|
|
530
|
+
code — so put the human label in the `<li>` text and the code in `data-value`.
|
|
531
|
+
The `.ui-combobox__empty` ("No matches") is hidden until a filter empties the
|
|
532
|
+
list. Two intentional single-select APG deviations: ArrowDown on a closed list
|
|
533
|
+
filters rather than pre-selecting the first option, and Tab closes without
|
|
534
|
+
committing a merely-highlighted option (Enter/click commits).
|
|
535
|
+
- **Validation** is opt-in via `data-bronto-validate` on the form plus
|
|
536
|
+
`initFormValidation()`; it surfaces messages into a `ui-error-summary` you
|
|
537
|
+
provide. The summary's title is the legible sans, not the display face — it's
|
|
538
|
+
meant to be read.
|
|
539
|
+
|
|
540
|
+
## Reveal: `ui-reveal` needs JS, `ui-scroll-reveal` doesn't
|
|
541
|
+
|
|
542
|
+
`ui-scroll-reveal` is scroll-driven and **zero-JS** — reach for it in a static
|
|
543
|
+
or LLM-authored report. `ui-reveal` is the JS variant: it starts hidden and you
|
|
544
|
+
toggle `is-visible` (e.g. from an `IntersectionObserver` you own) to play it in.
|
|
545
|
+
With scripting disabled it degrades to fully visible, but if scripting is *on*
|
|
546
|
+
and nothing toggles `is-visible`, the content stays hidden — so only use
|
|
547
|
+
`ui-reveal` when you are wiring that toggle.
|
|
548
|
+
|
|
549
|
+
**View transitions (`ui-vt`).** `ui-vt` names an element via `--ui-vt-name` so it
|
|
550
|
+
morphs across a `document.startViewTransition()` or a cross-document navigation.
|
|
551
|
+
The name must be **unique per document** at the moment a transition runs: applying
|
|
552
|
+
`ui-vt` with one shared `--ui-vt-name` across every card in a list (the obvious
|
|
553
|
+
loop) makes the browser skip the transition — and it is **not** silent: it logs a
|
|
554
|
+
console error and **rejects `vt.ready`** (with an `InvalidStateError`). If you
|
|
555
|
+
don't `vt.ready.catch(…)`, that surfaces as an unhandled promise rejection. Give
|
|
556
|
+
each element its own name (`--ui-vt-name: card-7`) or only mark the single element
|
|
557
|
+
that actually morphs.
|
|
558
|
+
|
|
559
|
+
## Loading affordances need a role you supply
|
|
560
|
+
|
|
561
|
+
`ui-spinner`, `ui-dotspinner`, `ui-skeleton`, and an indeterminate `ui-progress`
|
|
562
|
+
are decorative animations — bronto can't know their semantics. Give the busy
|
|
563
|
+
region `aria-busy="true"` (or `role="status"` with an `aria-live` text label like
|
|
564
|
+
"Loading…"), and mark a purely decorative spinner `aria-hidden="true"`. Without
|
|
565
|
+
one of these a screen reader announces nothing while the user waits.
|
|
566
|
+
|
|
567
|
+
## Popover: prefer the native top layer
|
|
568
|
+
|
|
569
|
+
`initPopover()` shows a `.ui-popover` in the browser **top layer** when the panel
|
|
570
|
+
carries the native `popover` attribute (never clipped by `overflow`/stacking);
|
|
571
|
+
without it, it falls back to an `is-open` class that a clipping ancestor can cut
|
|
572
|
+
off. Add `popover` to the panel for the robust path — the `is-open` form is a
|
|
573
|
+
fallback, not the default to copy.
|
|
574
|
+
|
|
575
|
+
It is a **non-modal** dialog by design: the panel gets `role="dialog"` and focus
|
|
576
|
+
moves into it, but there is **no focus trap** and the rest of the page stays
|
|
577
|
+
interactive — Tab moves *out* of the panel (it does not cycle), and it closes on
|
|
578
|
+
Escape or outside-click. Don't assume `<dialog>`-modal semantics; if you need a
|
|
579
|
+
trap and an inert backdrop, use a real modal (`<dialog>` + `initDialog`, or
|
|
580
|
+
`initModal`). And the `is-open` fallback is a plain stacked element, so it sits
|
|
581
|
+
*under* any open native `<dialog>`'s top layer — another reason to prefer the
|
|
582
|
+
native `popover` attribute when a popover and a dialog can be open together.
|
|
583
|
+
|
|
584
|
+
## Two tiers: CSS-native vs behavior-required
|
|
585
|
+
|
|
586
|
+
Not every component works with JavaScript off — know which tier you are shipping
|
|
587
|
+
before you rely on the no-JS path.
|
|
588
|
+
|
|
589
|
+
**CSS-native — fully operable with JS off.** Safe in static or LLM-authored
|
|
590
|
+
HTML, print/PDF, and before any hydration:
|
|
591
|
+
|
|
592
|
+
| Component | How it works without JS |
|
|
593
|
+
| --- | --- |
|
|
594
|
+
| Tooltip (`ui-tooltip`) | `:hover` / `:focus-within` (+ anchor positioning where supported) |
|
|
595
|
+
| Accordion | native `<details>` / `<summary>` |
|
|
596
|
+
| Segmented control (`ui-segmented`) | `:has(input:checked)` over a radio group |
|
|
597
|
+
| Scroll-reveal (`ui-scroll-reveal`) | scroll-driven animation, zero JS |
|
|
598
|
+
| Modal via native `<dialog>` | the element brings focus-trap + Escape; `initDialog` only adds open-triggers + focus-return |
|
|
599
|
+
|
|
600
|
+
**Behavior-required — a CSS skin that needs its `init*` to be interactive.**
|
|
601
|
+
These are JS widgets wearing the Bronto look; without the behavior they are inert
|
|
602
|
+
(and a couple are *worse* than inert — see the tabs row):
|
|
603
|
+
|
|
604
|
+
| Component | Behavior | With the behavior absent |
|
|
605
|
+
| --- | --- | --- |
|
|
606
|
+
| Tabs (`ui-tabs`) | `initTabs` | **author panels visible** — ship `hidden` panels and if `initTabs` never runs the content is unreachable |
|
|
607
|
+
| Combobox (`ui-combobox`) | `initCombobox` | a plain text input beside an unfiltered list |
|
|
608
|
+
| Command palette (`ui-command`) | `initCommand` | a static, unfiltered list |
|
|
609
|
+
| Table sort/select (`[data-bronto-sortable]`) | `initTableSort` | a static table (still readable) |
|
|
610
|
+
| Popover (`ui-popover`) | `initPopover` | no placement/ARIA — prefer the native `popover` attribute |
|
|
611
|
+
| Carousel (`ui-carousel`) | `initCarousel` | a native scroll-snap track (usable, no controls) |
|
|
612
|
+
| Controlled modal (`ui-modal.is-open`) | `initModal` | no focus trap — provide one or use native `<dialog>` |
|
|
613
|
+
| Menu (`data-bronto-menu`) | `initMenu` | a button next to a list with no open/close, outside-click, or Escape |
|
|
614
|
+
| Toast | `toast()` | nothing — it is imperative-only |
|
|
615
|
+
|
|
616
|
+
One cross-cutting guard, not tied to a single component:
|
|
617
|
+
|
|
618
|
+
| Concern | Behavior | With the behavior absent |
|
|
619
|
+
| --- | --- | --- |
|
|
620
|
+
| `aria-disabled="true"` controls | `initDisabledGuard` | dimmed + pointer-inert via CSS, but still **keyboard**-activatable on Enter/Space (native `disabled` is already fully inert) |
|
|
621
|
+
|
|
622
|
+
Rule of thumb: if a component needs ARIA-state sync, focus management, a keyboard
|
|
623
|
+
model, or persisted/dynamic state, it is behavior-required — that is the exact
|
|
624
|
+
boundary of what CSS alone cannot do.
|
|
625
|
+
|
|
288
626
|
## When to add a behavior
|
|
289
627
|
|
|
290
628
|
The CSS is the framework; `@ponchia/ui/behaviors` is the *sanctioned*
|
|
291
629
|
home for the little JS that genuinely needs scripting (theme persistence,
|
|
292
|
-
disclosure, dialog glue, toast, combobox, form-validation,
|
|
293
|
-
Reach for it instead of reimplementing — every initializer is
|
|
294
|
-
idempotent, and returns a cleanup. If you find yourself writing focus
|
|
630
|
+
disclosure, dialog glue, modal focus-trap, toast, combobox, form-validation,
|
|
631
|
+
table-sort). Reach for it instead of reimplementing — every initializer is
|
|
632
|
+
SSR-safe, idempotent, and returns a cleanup. If you find yourself writing focus
|
|
295
633
|
management or `aria-expanded` toggling by hand, there is probably already
|
|
296
634
|
a behavior for it.
|
|
297
635
|
|