@ponchia/ui 0.3.5 → 0.4.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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Ponchia/bronto-ui/badge)](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE)
10
10
 
11
- **A CSS-first, framework-agnostic UI framework with a "Nothing"-inspired look — monochrome surfaces, one rationed red accent, dot-matrix display type, hairline borders, restrained motion. Zero runtime dependencies. Re-brand the whole thing with one CSS variable.**
11
+ **A CSS-first, framework-agnostic UI framework built on token-driven restraint — monochrome by default, one rationed accent, dot-matrix display type, hairline borders. Color is _tiered_: we don't decorate with it, we signal with it. The "Nothing"-inspired look is the default skin, not the architecture — opt-in colorways (amber CRT · phosphor green · e-ink), a dot-matrix icon set, and a colourblind-safe data-viz palette prove the one knob is real. Zero runtime dependencies; re-brand the whole thing with one CSS variable.**
12
12
 
13
13
  ### [Live demo →](https://ponchia.github.io/bronto-ui/)  ·  [Theme playground →](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)
14
14
 
@@ -79,6 +79,18 @@ toast('Saved', { tone: 'success' }); // body-anchored stack, no markup needed
79
79
 
80
80
  Behaviors cover theme persistence, disclosure, dropdown menus, native-`<dialog>` modals/drawers, tabs, combobox, form validation, table sort, carousel and toasts — wired by `data-bronto-*` attributes.
81
81
 
82
+ **5. (Optional) display glyphs — a 43-glyph dot-matrix icon set:**
83
+
84
+ ```js
85
+ import { renderGlyph } from '@ponchia/ui/glyphs';
86
+
87
+ el.innerHTML = renderGlyph('search', { label: 'Search' }); // role="img"
88
+ // or drop a placeholder and expand it: <span data-bronto-glyph="check"></span>
89
+ // import { initDotGlyph } from '@ponchia/ui/behaviors'; initDotGlyph();
90
+ ```
91
+
92
+ Arrows, chevrons, check/close/plus/minus, search/menu/gear, info/warning/bell/lock, home/user/heart/star/spark — rendered on the same `.ui-dotmatrix` primitive and re-skinned by the `--field-dot*` tokens (no SVG, no font). Default dot look is for **display** sizes; pass **`{ solid: true }`** to fuse the cells into a crisp pixel glyph that works as an **inline icon down to ~16px**.
93
+
82
94
  ## What's in the box
83
95
 
84
96
  - **Primitives** — buttons, cards, chips, badges, links, key/value, `ui-num`, avatars.
@@ -89,6 +101,9 @@ Behaviors cover theme persistence, disclosure, dropdown menus, native-`<dialog>`
89
101
  - **Shells** — an admin dashboard shell (`ui-app-*`) and a content/marketing site shell (`ui-site*`, `ui-container`).
90
102
  - **Prose** — `.ui-prose` styles raw, unclassed semantic HTML (Markdown / CMS / LLM output) with zero classes.
91
103
  - **Motion & dots** — the dot-matrix motif kit: dot grid, status dots, dot loaders, the orbital spinner, matrix reveal — all reduced-motion aware.
104
+ - **Glyphs** — `@ponchia/ui/glyphs`, a 43-glyph dot-matrix icon set on the `.ui-dotmatrix` primitive (display marks + crisp `solid` inline icons), no SVG/font.
105
+ - **Colorways** _(opt-in)_ — `data-bronto-skin="amber-crt | phosphor-green | e-ink"`: a root-level recolour of the one accent (OKLCH, per-theme, contrast-gated), never in the default bundle.
106
+ - **Data-viz** _(opt-in)_ — a colourblind-safe chart palette (`--chart-*` + dot-matrix pattern fills, resolved hex in `charts.json`), gated under simulated protan/deutan/tritan vision. Charts only, never UI chrome.
92
107
 
93
108
  Full generated catalog of every class: **[docs/reference.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/reference.md)**. The decision guide (which primitive when): **[docs/usage.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/usage.md)**.
94
109
 
@@ -103,11 +118,17 @@ Everything accent-colored derives from a single `--accent` variable via `color-m
103
118
 
104
119
  Buttons, focus rings, dot motifs, accent borders and soft fills all follow automatically. Light/dark is `data-theme="light"` / `"dark"` on `<html>` (defaults to `prefers-color-scheme`); `data-density` and `data-contrast` give density and contrast presets. A full re-skin (radius, display face, dot density, surfaces) is a handful more token overrides — the "Nothing" look is the **default skin, not the architecture**.
105
120
 
121
+ **One system, many skins.** That the knob is real isn't a claim — it ships: drop in `@ponchia/ui/css/skins.css` and set `data-bronto-skin="amber-crt | phosphor-green | e-ink"` on `<html>` for a complete, contrast-gated recolour (the derived accent family, focus ring, dot-matrix and glyphs all follow). Colour is governed in **tiers** — neutral canvas · one accent · locked status · display expression · opt-in data-viz — so it always earns its place; the full constitution is **[ADR-0001](https://github.com/Ponchia/bronto-ui/blob/main/docs/adr/0001-color-system.md)**.
122
+
106
123
  > When you change `--accent`, contrast becomes yours: verify your hue in the **[theme playground](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)** (it shows the derived family and computed WCAG ratios). Full contract: **[docs/theming.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/theming.md)**.
107
124
 
125
+ ## Accessibility
126
+
127
+ Not an afterthought — a gate. Every contractual token pairing has a declared WCAG 2.1 conformance level the build **fails** below (`docs/contrast.md`, regenerated + CI-checked), and the shipped colorways are held to the same floors. The data-viz palette is gated for distinctness under simulated protanopia/deuteranopia/tritanopia, and never relies on colour alone (dot-matrix pattern fills). APCA `Lc` is published advisory alongside WCAG. Components ship ARIA-correct markup and SSR-safe keyboard behaviors; `prefers-reduced-motion`, `prefers-contrast` and `forced-colors` are all honored.
128
+
108
129
  ## Works with anything
109
130
 
110
- The CSS is the framework, so it works with React, Svelte/SvelteKit, Astro, Vue, Solid or plain HTML — there's no component runtime to adopt. The optional `classes` and `behaviors` entrypoints pull in **no** UI framework and are SSR-safe.
131
+ The CSS is the framework, so it works with React, Svelte/SvelteKit, Astro, Vue, Solid or plain HTML — there's no component runtime to adopt. The optional `classes` and `behaviors` entrypoints pull in **no** UI framework and are SSR-safe. For React and Solid there are also **optional thin bindings** — `@ponchia/ui/react` and `@ponchia/ui/solid` wrap the behaviors as hooks (`useDialog`, `useToast`, …); `react`/`solid-js` are optional peer deps, so the core stays zero-dependency.
111
132
 
112
133
  Per-framework getting-started guides + runnable example apps live in the repo:
113
134
 
@@ -121,14 +142,15 @@ Per-framework getting-started guides + runnable example apps live in the repo:
121
142
 
122
143
  ## Extras
123
144
 
124
- - **Tokens as data** — `import tokens, { themeColor, cssVars } from '@ponchia/ui/tokens'` (plus `tokens.json`, W3C DTCG `tokens.dtcg.json`, and `tokens/resolved.json` for charts/data-viz).
145
+ - **Tokens as data** — `import tokens, { themeColor, cssVars } from '@ponchia/ui/tokens'` (plus `tokens.json`, W3C DTCG `tokens.dtcg.json`, and `tokens/resolved.json` — concrete hex per theme for canvas/SVG/MapLibre).
146
+ - **Chart colours for dashboards** — `import charts from '@ponchia/ui/charts.json'` (resolved hex per theme; series 1 = your accent) and the opt-in `@ponchia/ui/css/dataviz.css`.
125
147
  - **Editor IntelliSense** — point VS Code at the shipped custom-data file so every token autocompletes in `var(--…)`:
126
148
  ```json
127
149
  { "css.customData": ["node_modules/@ponchia/ui/classes/vscode.css-custom-data.json"] }
128
150
  ```
129
- - **For AI coding agents** — the package ships `llms.txt` at its root plus `docs/reference.md`, `docs/usage.md` and `docs/theming.md` inside the tarball, so an offline agent has the full API without guessing.
151
+ - **For AI coding agents** — the package ships `llms.txt` at its root plus `docs/reference.md`, `docs/usage.md`, `docs/theming.md`, `docs/contrast.md`, the color constitution `docs/adr/0001-color-system.md` and the `CHANGELOG` inside the tarball, so an offline agent has the full API and rationale without guessing.
130
152
 
131
- > The package root is **CSS-only**: `@import '@ponchia/ui'` in CSS, never `import '@ponchia/ui'` in JS. The JS entrypoints are the explicit subpaths `/tokens`, `/classes`, `/behaviors`.
153
+ > The package root is **CSS-only**: `@import '@ponchia/ui'` in CSS, never `import '@ponchia/ui'` in JS. The JS entrypoints are the explicit subpaths `/tokens`, `/classes`, `/behaviors`, `/glyphs`.
132
154
 
133
155
  ## Browser support
134
156
 
@@ -136,14 +158,16 @@ Evergreen only. The framework relies on cascade layers, `:has()`, `color-mix()`,
136
158
 
137
159
  ## Versioning
138
160
 
139
- Pre-1.0 and deliberately so. **Until `1.0.0`, breaking changes ship in the _minor_** (`0.x.0`); patches (`0.x.y`) are always non-breaking. Pin with the patch range — at `0.x`, `~0.3.0` (and equivalently `^0.3.0`) resolves to `>=0.3.0 <0.4.0`, giving you safe patches while holding back the next breaking minor. Every breaking change is called out under a **BREAKING** heading in the **[CHANGELOG](https://github.com/Ponchia/bronto-ui/blob/main/CHANGELOG.md)** with a migration note.
161
+ Pre-1.0 and deliberately so. **Until `1.0.0`, breaking changes ship in the _minor_** (`0.x.0`); patches (`0.x.y`) are always non-breaking. Pin with the patch range — at `0.x`, `~0.4.0` (and equivalently `^0.4.0`) resolves to `>=0.4.0 <0.5.0`, giving you safe patches while holding back the next breaking minor. Every breaking change is called out under a **BREAKING** heading in the **[CHANGELOG](https://github.com/Ponchia/bronto-ui/blob/main/CHANGELOG.md)** with a migration note.
140
162
 
141
163
  Contractual (changes are breaking): the `--accent` derivation and token **names**, the `.ui-*` class and recipe names, the `data-bronto-*` attributes, and each behavior's cleanup contract. Not contractual (may change any release): token **values** (visual tuning) and internal leaf-file / `@layer` boundaries.
142
164
 
165
+ Release candidates publish to the `next` dist-tag, never to `latest` — opt in with `npm i @ponchia/ui@next` to try an upcoming version early. A plain `npm i @ponchia/ui` only ever resolves a stable release.
166
+
143
167
  ## Links
144
168
 
145
169
  - **[Live demo](https://ponchia.github.io/bronto-ui/)** · **[Theme playground](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)**
146
- - **[Class reference](https://github.com/Ponchia/bronto-ui/blob/main/docs/reference.md)** · **[Usage guide](https://github.com/Ponchia/bronto-ui/blob/main/docs/usage.md)** · **[Theming](https://github.com/Ponchia/bronto-ui/blob/main/docs/theming.md)** · **[Contrast](https://github.com/Ponchia/bronto-ui/blob/main/docs/contrast.md)**
170
+ - **[Class reference](https://github.com/Ponchia/bronto-ui/blob/main/docs/reference.md)** · **[Usage guide](https://github.com/Ponchia/bronto-ui/blob/main/docs/usage.md)** · **[Theming](https://github.com/Ponchia/bronto-ui/blob/main/docs/theming.md)** · **[Contrast](https://github.com/Ponchia/bronto-ui/blob/main/docs/contrast.md)** · **[Color system (ADR-0001)](https://github.com/Ponchia/bronto-ui/blob/main/docs/adr/0001-color-system.md)**
147
171
  - **[CHANGELOG](https://github.com/Ponchia/bronto-ui/blob/main/CHANGELOG.md)** · **[Roadmap](https://github.com/Ponchia/bronto-ui/blob/main/ROADMAP.md)** · **[Contributing](https://github.com/Ponchia/bronto-ui/blob/main/CONTRIBUTING.md)**
148
172
 
149
173
  ## License
@@ -143,3 +143,14 @@ export interface ToastOpts {
143
143
  * function that dismisses it early. No-op (returns noop) without a DOM.
144
144
  */
145
145
  export declare function toast(message: string, opts?: ToastOpts): Cleanup;
146
+
147
+ /**
148
+ * Expand `[data-bronto-glyph="name"]` placeholders into a `.ui-dotmatrix`
149
+ * grid of cells — the DOM counterpart to `renderGlyph` from
150
+ * `@ponchia/ui/glyphs`. Decorative by default (`aria-hidden`); add
151
+ * `data-bronto-glyph-label` to expose it as `role="img"`. Unknown glyph names
152
+ * are left untouched. SSR-safe, idempotent (skips an already-expanded host).
153
+ * Returns a cleanup that removes the cells and restores the original
154
+ * attributes.
155
+ */
156
+ export declare function initDotGlyph(opts?: DelegateOpts): Cleanup;
@@ -14,6 +14,8 @@
14
14
  * const stop = initThemeToggle(); // wire [data-bronto-theme-toggle]
15
15
  */
16
16
 
17
+ import { GLYPH_SIZE, glyphCells } from '../glyphs/glyphs.js';
18
+
17
19
  const THEMES = ['light', 'dark'];
18
20
  const noop = () => {};
19
21
  const hasDom = () => typeof document !== 'undefined';
@@ -1240,3 +1242,110 @@ export function initCarousel({ root } = {}) {
1240
1242
 
1241
1243
  return () => cleanups.forEach((fn) => fn());
1242
1244
  }
1245
+
1246
+ function restoreAttr(el, name, prev) {
1247
+ if (prev === null) el.removeAttribute(name);
1248
+ else el.setAttribute(name, prev);
1249
+ }
1250
+
1251
+ /**
1252
+ * Expand `[data-bronto-glyph="name"]` placeholders into a `.ui-dotmatrix`
1253
+ * grid of GLYPH_SIZE² cells — the DOM counterpart to renderGlyph() from
1254
+ * `@ponchia/ui/glyphs`, for when you'd rather drop a placeholder than inline
1255
+ * the markup. Decorative by default (`aria-hidden`); add
1256
+ * `data-bronto-glyph-label` to expose it as `role="img"`. An unknown glyph
1257
+ * name is left untouched. Idempotent (skips an already-expanded host); the
1258
+ * returned cleanup removes the cells and restores the original attributes.
1259
+ */
1260
+ export function initDotGlyph({ root } = {}) {
1261
+ if (!hasDom()) return noop;
1262
+ const host = root || document;
1263
+ const els = [];
1264
+ if (host !== document && host.matches?.('[data-bronto-glyph]')) els.push(host);
1265
+ els.push(...(host.querySelectorAll?.('[data-bronto-glyph]') ?? []));
1266
+ const cleanups = [];
1267
+
1268
+ for (const el of els) {
1269
+ // Scope to DIRECT-child cells (the ones we append) — so a placeholder that
1270
+ // legitimately nests its own .ui-dotmatrix is neither mis-read as already
1271
+ // expanded here nor have its inner cells removed by cleanup below.
1272
+ if (el.querySelector(':scope > .ui-dotmatrix__cell')) continue; // already expanded
1273
+ const cells = glyphCells(el.getAttribute('data-bronto-glyph'));
1274
+ if (!cells.length) continue; // unknown glyph — leave the placeholder as-is
1275
+
1276
+ const label = el.getAttribute('data-bronto-glyph-label');
1277
+ // `data-bronto-glyph-solid` → square, gapless pixel glyph (legible small),
1278
+ // the DOM counterpart to renderGlyph's `solid` option. Implies glyph-only.
1279
+ const solid = el.hasAttribute('data-bronto-glyph-solid');
1280
+ // `data-bronto-glyph-anim="reveal|pulse"` → decorative animation (the DOM
1281
+ // counterpart to renderGlyph's `anim`; reduced-motion-safe via CSS).
1282
+ const animAttr = el.getAttribute('data-bronto-glyph-anim');
1283
+ const animClass =
1284
+ animAttr === 'reveal'
1285
+ ? 'ui-dotmatrix--reveal'
1286
+ : animAttr === 'pulse'
1287
+ ? 'ui-dotmatrix--pulse'
1288
+ : null;
1289
+ const hadAnimClass = animClass ? el.classList.contains(animClass) : false;
1290
+ const hadMatrix = el.classList.contains('ui-dotmatrix');
1291
+ const hadCols = el.style.getPropertyValue('--dotmatrix-cols');
1292
+ const hadRadius = el.style.getPropertyValue('--dotmatrix-dot-radius');
1293
+ const hadGap = el.style.getPropertyValue('--dotmatrix-gap');
1294
+ const hadAriaHidden = el.getAttribute('aria-hidden');
1295
+ const hadRole = el.getAttribute('role');
1296
+ const hadAriaLabel = el.getAttribute('aria-label');
1297
+
1298
+ el.classList.add('ui-dotmatrix');
1299
+ if (animClass) el.classList.add(animClass);
1300
+ el.style.setProperty('--dotmatrix-cols', String(GLYPH_SIZE));
1301
+ if (solid) {
1302
+ el.style.setProperty('--dotmatrix-dot-radius', '0');
1303
+ el.style.setProperty('--dotmatrix-gap', '0');
1304
+ }
1305
+ if (label) {
1306
+ el.setAttribute('role', 'img');
1307
+ el.setAttribute('aria-label', label);
1308
+ el.removeAttribute('aria-hidden'); // a labelled img must not also be hidden
1309
+ } else {
1310
+ el.setAttribute('aria-hidden', 'true');
1311
+ }
1312
+
1313
+ const frag = document.createDocumentFragment();
1314
+ cells.forEach((c, i) => {
1315
+ const span = document.createElement('span');
1316
+ span.className = !c.on
1317
+ ? 'ui-dotmatrix__cell'
1318
+ : c.tone === 'hot'
1319
+ ? 'ui-dotmatrix__cell ui-dotmatrix__cell--hot'
1320
+ : c.tone === 'accent'
1321
+ ? 'ui-dotmatrix__cell ui-dotmatrix__cell--accent'
1322
+ : 'ui-dotmatrix__cell';
1323
+ if (!c.on && solid) span.style.background = 'transparent'; // glyph-only
1324
+ if (animAttr === 'reveal') span.style.setProperty('--i', String(i)); // scan stagger
1325
+ frag.appendChild(span);
1326
+ });
1327
+ el.appendChild(frag);
1328
+
1329
+ cleanups.push(() => {
1330
+ el.querySelectorAll(':scope > .ui-dotmatrix__cell').forEach((n) => n.remove());
1331
+ if (!hadMatrix) el.classList.remove('ui-dotmatrix');
1332
+ if (animClass && !hadAnimClass) el.classList.remove(animClass);
1333
+ if (solid) {
1334
+ if (hadRadius) el.style.setProperty('--dotmatrix-dot-radius', hadRadius);
1335
+ else el.style.removeProperty('--dotmatrix-dot-radius');
1336
+ if (hadGap) el.style.setProperty('--dotmatrix-gap', hadGap);
1337
+ else el.style.removeProperty('--dotmatrix-gap');
1338
+ }
1339
+ if (hadCols) el.style.setProperty('--dotmatrix-cols', hadCols);
1340
+ else el.style.removeProperty('--dotmatrix-cols');
1341
+ restoreAttr(el, 'aria-hidden', hadAriaHidden);
1342
+ restoreAttr(el, 'role', hadRole);
1343
+ restoreAttr(el, 'aria-label', hadAriaLabel);
1344
+ // Don't leave behind empty class=""/style="" we ourselves created.
1345
+ if (el.getAttribute('class') === '') el.removeAttribute('class');
1346
+ if (el.getAttribute('style') === '') el.removeAttribute('style');
1347
+ });
1348
+ }
1349
+
1350
+ return () => cleanups.forEach((fn) => fn());
1351
+ }
@@ -63,6 +63,9 @@ export declare const cls: {
63
63
  readonly dotmatrixCell: 'ui-dotmatrix__cell';
64
64
  readonly dotmatrixCellHot: 'ui-dotmatrix__cell--hot';
65
65
  readonly dotmatrixCellAccent: 'ui-dotmatrix__cell--accent';
66
+ readonly dotmatrixReveal: 'ui-dotmatrix--reveal';
67
+ readonly dotmatrixPulse: 'ui-dotmatrix--pulse';
68
+ readonly icon: 'ui-icon';
66
69
  readonly dotfield: 'ui-dotfield';
67
70
  readonly dotrule: 'ui-dotrule';
68
71
  readonly dotbar: 'ui-dotbar';
package/classes/index.js CHANGED
@@ -68,6 +68,9 @@ export const cls = Object.freeze({
68
68
  dotmatrixCell: 'ui-dotmatrix__cell',
69
69
  dotmatrixCellHot: 'ui-dotmatrix__cell--hot',
70
70
  dotmatrixCellAccent: 'ui-dotmatrix__cell--accent',
71
+ dotmatrixReveal: 'ui-dotmatrix--reveal',
72
+ dotmatrixPulse: 'ui-dotmatrix--pulse',
73
+ icon: 'ui-icon',
71
74
  dotfield: 'ui-dotfield',
72
75
  dotrule: 'ui-dotrule',
73
76
  dotbar: 'ui-dotbar',
@@ -7,19 +7,19 @@
7
7
  },
8
8
  {
9
9
  "name": "--accent-1",
10
- "description": "Global scale token. Value: `color-mix(in srgb, var(--accent) 8%, var(--bg))`"
10
+ "description": "Global scale token. Value: `color-mix(in oklch, var(--accent) 8%, var(--bg))`"
11
11
  },
12
12
  {
13
13
  "name": "--accent-2",
14
- "description": "Global scale token. Value: `color-mix(in srgb, var(--accent) 16%, var(--bg))`"
14
+ "description": "Global scale token. Value: `color-mix(in oklch, var(--accent) 16%, var(--bg))`"
15
15
  },
16
16
  {
17
17
  "name": "--accent-3",
18
- "description": "Global scale token. Value: `color-mix(in srgb, var(--accent) 32%, var(--bg))`"
18
+ "description": "Global scale token. Value: `color-mix(in oklch, var(--accent) 32%, var(--bg))`"
19
19
  },
20
20
  {
21
21
  "name": "--accent-4",
22
- "description": "Global scale token. Value: `color-mix(in srgb, var(--accent) 60%, var(--bg))`"
22
+ "description": "Global scale token. Value: `color-mix(in oklch, var(--accent) 60%, var(--bg))`"
23
23
  },
24
24
  {
25
25
  "name": "--accent-5",
@@ -209,14 +209,6 @@
209
209
  "name": "--mono",
210
210
  "description": "Global scale token. Value: `'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', ui-monospace, monospace`"
211
211
  },
212
- {
213
- "name": "--orange",
214
- "description": "Theme token. Light: `#a85f32` · Dark: `#d08c5b`"
215
- },
216
- {
217
- "name": "--orange-soft",
218
- "description": "Theme token. Light: `rgb(168, 95, 50, 0.13)` · Dark: `rgb(208, 140, 91, 0.15)`"
219
- },
220
212
  {
221
213
  "name": "--panel",
222
214
  "description": "Theme token. Light: `#ffffff` · Dark: `#0c0c0c`"
@@ -0,0 +1,96 @@
1
+ /* @ponchia/ui — GENERATED from tokens/charts.js by scripts/gen-charts.mjs.
2
+ * Do not edit by hand; run `npm run charts:build`. Drift-checked in CI.
3
+ *
4
+ * Tier-4 data-viz palette (ADR-0001). OPT-IN: import `@ponchia/ui/css/dataviz.css`
5
+ * on demand; never in the default bundle, never UI chrome. --chart-1 is the live
6
+ * accent (series 1 = brand). Pair colour N with pattern N (a 2nd, non-colour
7
+ * channel): background: var(--chart-3); background-image: var(--chart-pattern-3);
8
+ * background-size: var(--chart-pattern-size); --chart-pattern-ink: <contrast>. */
9
+
10
+ :root {
11
+ --chart-1: var(--accent);
12
+ --chart-2: #e69f00;
13
+ --chart-3: #56b4e9;
14
+ --chart-4: #009e73;
15
+ --chart-5: #f0e442;
16
+ --chart-6: #0072b2;
17
+ --chart-7: #cc79a7;
18
+ --chart-8: #4d5358;
19
+ --chart-seq-1: oklch(94% 0.03 25deg);
20
+ --chart-seq-2: oklch(85% 0.07 25deg);
21
+ --chart-seq-3: oklch(74% 0.12 25deg);
22
+ --chart-seq-4: oklch(62% 0.16 25deg);
23
+ --chart-seq-5: oklch(50% 0.16 25deg);
24
+ --chart-seq-6: oklch(38% 0.13 25deg);
25
+ --chart-div-1: oklch(45% 0.14 255deg);
26
+ --chart-div-2: oklch(62% 0.1 250deg);
27
+ --chart-div-3: oklch(82% 0.05 245deg);
28
+ --chart-div-4: oklch(90% 0.01 250deg);
29
+ --chart-div-5: oklch(80% 0.07 60deg);
30
+ --chart-div-6: oklch(66% 0.13 55deg);
31
+ --chart-div-7: oklch(56% 0.15 45deg);
32
+
33
+ /* Dot-matrix pattern fills — pair with the matching colour (WCAG 1.4.1). */
34
+ --chart-pattern-size: 8px;
35
+ --chart-pattern-ink: rgb(0 0 0 / 0.34);
36
+ --chart-pattern-1: none;
37
+ --chart-pattern-2: radial-gradient(circle at 50% 50%, var(--chart-pattern-ink) 1.4px, transparent 1.6px);
38
+ --chart-pattern-3: radial-gradient(circle at 0 0, var(--chart-pattern-ink) 1.4px, transparent 1.6px);
39
+ --chart-pattern-4: radial-gradient(circle at 25% 25%, var(--chart-pattern-ink) 1.2px, transparent 1.4px), radial-gradient(circle at 75% 75%, var(--chart-pattern-ink) 1.2px, transparent 1.4px);
40
+ --chart-pattern-5: radial-gradient(circle at 50% 25%, var(--chart-pattern-ink) 1.2px, transparent 1.4px), radial-gradient(circle at 50% 75%, var(--chart-pattern-ink) 1.2px, transparent 1.4px);
41
+ --chart-pattern-6: radial-gradient(circle at 25% 50%, var(--chart-pattern-ink) 1.2px, transparent 1.4px), radial-gradient(circle at 75% 50%, var(--chart-pattern-ink) 1.2px, transparent 1.4px);
42
+ --chart-pattern-7: radial-gradient(circle at 25% 25%, var(--chart-pattern-ink) 1px, transparent 1.2px), radial-gradient(circle at 75% 25%, var(--chart-pattern-ink) 1px, transparent 1.2px), radial-gradient(circle at 25% 75%, var(--chart-pattern-ink) 1px, transparent 1.2px), radial-gradient(circle at 75% 75%, var(--chart-pattern-ink) 1px, transparent 1.2px);
43
+ --chart-pattern-8: radial-gradient(circle at 50% 50%, transparent 1.2px, var(--chart-pattern-ink) 1.4px, transparent 2.2px);
44
+ }
45
+
46
+ @media (prefers-color-scheme: dark) {
47
+ :root:not([data-theme='light']) {
48
+ --chart-1: var(--accent);
49
+ --chart-2: #e69f00;
50
+ --chart-3: #56b4e9;
51
+ --chart-4: #009e73;
52
+ --chart-5: #f0e442;
53
+ --chart-6: #0072b2;
54
+ --chart-7: #cc79a7;
55
+ --chart-8: #4d5358;
56
+ --chart-seq-1: oklch(30% 0.1 25deg);
57
+ --chart-seq-2: oklch(42% 0.15 25deg);
58
+ --chart-seq-3: oklch(55% 0.17 25deg);
59
+ --chart-seq-4: oklch(68% 0.15 25deg);
60
+ --chart-seq-5: oklch(80% 0.1 25deg);
61
+ --chart-seq-6: oklch(90% 0.05 25deg);
62
+ --chart-div-1: oklch(70% 0.13 250deg);
63
+ --chart-div-2: oklch(60% 0.12 252deg);
64
+ --chart-div-3: oklch(48% 0.08 255deg);
65
+ --chart-div-4: oklch(40% 0.01 250deg);
66
+ --chart-div-5: oklch(58% 0.1 60deg);
67
+ --chart-div-6: oklch(72% 0.13 58deg);
68
+ --chart-div-7: oklch(80% 0.12 55deg);
69
+ --chart-pattern-ink: rgb(255 255 255 / 0.42);
70
+ }
71
+ }
72
+
73
+ :root[data-theme='dark'] {
74
+ --chart-1: var(--accent);
75
+ --chart-2: #e69f00;
76
+ --chart-3: #56b4e9;
77
+ --chart-4: #009e73;
78
+ --chart-5: #f0e442;
79
+ --chart-6: #0072b2;
80
+ --chart-7: #cc79a7;
81
+ --chart-8: #4d5358;
82
+ --chart-seq-1: oklch(30% 0.1 25deg);
83
+ --chart-seq-2: oklch(42% 0.15 25deg);
84
+ --chart-seq-3: oklch(55% 0.17 25deg);
85
+ --chart-seq-4: oklch(68% 0.15 25deg);
86
+ --chart-seq-5: oklch(80% 0.1 25deg);
87
+ --chart-seq-6: oklch(90% 0.05 25deg);
88
+ --chart-div-1: oklch(70% 0.13 250deg);
89
+ --chart-div-2: oklch(60% 0.12 252deg);
90
+ --chart-div-3: oklch(48% 0.08 255deg);
91
+ --chart-div-4: oklch(40% 0.01 250deg);
92
+ --chart-div-5: oklch(58% 0.1 60deg);
93
+ --chart-div-6: oklch(72% 0.13 58deg);
94
+ --chart-div-7: oklch(80% 0.12 55deg);
95
+ --chart-pattern-ink: rgb(255 255 255 / 0.42);
96
+ }
package/css/dots.css CHANGED
@@ -47,25 +47,89 @@
47
47
  /* Data-bound dot matrix — a grid of state cells (activity / heatmap),
48
48
  the on-brand counterpart to the decorative .ui-dotgrid background.
49
49
  This is the grid + cell tones only; the data→cell mapping is the
50
- consumer's. Tune density via --dotmatrix-cols / --dotmatrix-gap. */
50
+ consumer's. Tune density via --dotmatrix-cols / --dotmatrix-gap; set
51
+ --dotmatrix-dot to a fixed dot length (intrinsic sizing, e.g. for the
52
+ display glyphs in @ponchia/ui/glyphs) instead of stretching to fit;
53
+ set --dotmatrix-dot-radius: 0 (with --dotmatrix-gap: 0) to fuse the dots
54
+ into a crisp pixel glyph that stays legible at small/inline sizes.
55
+
56
+ Tier-3 "display expression" knobs (see docs/adr/0001-color-system.md) — the
57
+ on-brand substitute for decorative colour, since a dot-matrix display speaks
58
+ in brightness + time, not hue. All default to a no-op, so the default render
59
+ is unchanged; the optional colorways (css/skins.css) set them:
60
+ --dotmatrix-glow phosphor bloom around lit cells (default 0 = off)
61
+ --dotmatrix-pulse-min the floor the --pulse animation dips to (default 0.55)
62
+ --dotmatrix-reveal-step per-cell scan cadence for --reveal (default 3ms)
63
+ Density stays on --dotmatrix-cols / --dotmatrix-gap / --dotmatrix-dot. */
51
64
  .ui-dotmatrix {
52
65
  display: grid;
53
66
  gap: var(--dotmatrix-gap, 0.5rem);
54
- grid-template-columns: repeat(var(--dotmatrix-cols, 12), minmax(0, 1fr));
67
+ grid-template-columns: repeat(var(--dotmatrix-cols, 12), var(--dotmatrix-dot, minmax(0, 1fr)));
55
68
  }
56
69
 
57
70
  .ui-dotmatrix__cell {
58
71
  aspect-ratio: 1;
59
72
  background: var(--field-dot);
60
- border-radius: 50%;
73
+ border-radius: var(--dotmatrix-dot-radius, 50%);
61
74
  }
62
75
 
76
+ /* Lit cells optionally bloom — a phosphor/LED glow in the cell's own colour.
77
+ 0 blur by default → no shadow painted, so display use is unaffected. */
63
78
  .ui-dotmatrix__cell--hot {
64
79
  background: var(--field-dot-hot);
80
+ box-shadow: 0 0 var(--dotmatrix-glow, 0) var(--field-dot-hot);
65
81
  }
66
82
 
67
83
  .ui-dotmatrix__cell--accent {
68
84
  background: var(--field-dot-accent);
85
+ box-shadow: 0 0 var(--dotmatrix-glow, 0) var(--field-dot-accent);
86
+ }
87
+
88
+ /* Animation (opt-in, decorative): a dot-matrix that "plays". Meaning stays
89
+ in the static cells + the element's label — motion is enhancement only,
90
+ and the reduced-motion block below switches it off. */
91
+
92
+ /* --reveal: cells power on in a left→right, top→bottom scan. The per-cell
93
+ delay reads `--i` (the row-major index) which the renderer sets. */
94
+ .ui-dotmatrix--reveal .ui-dotmatrix__cell {
95
+ animation: dotmatrixCellOn var(--duration-base) var(--ease-out) both;
96
+ animation-delay: calc(var(--i, 0) * var(--dotmatrix-reveal-step, 3ms));
97
+ }
98
+
99
+ /* `from`-only on purpose: when reduced-motion kills the animation the cells
100
+ stay at their natural opacity:1 — i.e. the settled end-state still shows. */
101
+ @keyframes dotmatrixCellOn {
102
+ from {
103
+ opacity: 0;
104
+ transform: scale(0.35);
105
+ }
106
+ }
107
+
108
+ /* --pulse: the whole glyph breathes, for a "live"/attention state. */
109
+ .ui-dotmatrix--pulse {
110
+ animation: dotmatrixPulse 1.8s var(--ease-standard) infinite;
111
+ }
112
+
113
+ @keyframes dotmatrixPulse {
114
+ 50% {
115
+ opacity: var(--dotmatrix-pulse-min, 0.55);
116
+ transform: scale(0.94);
117
+ }
118
+ }
119
+
120
+ /* Inline icon wrapper — the one-node glyph render path (renderGlyph(name,
121
+ { render: 'mask' })). A single element masked by the glyph bitmap, so it
122
+ scales with the text and inherits currentColor — for icon-at-scale (e.g.
123
+ one in every table row) where the 256-cell .ui-dotmatrix is too heavy.
124
+ --icon-size (default 1em) sizes it; --icon-mask carries the bitmap. */
125
+ .ui-icon {
126
+ display: inline-block;
127
+ flex: 0 0 auto;
128
+ inline-size: var(--icon-size, 1em);
129
+ block-size: var(--icon-size, 1em);
130
+ background: currentcolor;
131
+ vertical-align: -0.125em;
132
+ mask: var(--icon-mask) center / contain no-repeat;
69
133
  }
70
134
 
71
135
  /* Status dot — the glyph-style state indicator. */
@@ -289,7 +353,9 @@
289
353
  .ui-dot--live::after,
290
354
  .ui-dotloader span,
291
355
  .ui-dotspinner,
292
- .ui-dotbar--indeterminate i {
356
+ .ui-dotbar--indeterminate i,
357
+ .ui-dotmatrix--reveal .ui-dotmatrix__cell,
358
+ .ui-dotmatrix--pulse {
293
359
  animation: none;
294
360
  }
295
361
 
package/css/skins.css ADDED
@@ -0,0 +1,54 @@
1
+ /* @ponchia/ui — GENERATED from tokens/skins.js by scripts/gen-skins.mjs.
2
+ * Do not edit by hand; run `npm run skins:build`. Drift-checked in CI.
3
+ *
4
+ * Optional display colorways (ADR-0001). OPT-IN: imported on demand via
5
+ * `@ponchia/ui/css/skins.css`, never part of the default bundle. Apply with
6
+ * `data-bronto-skin="amber-crt | e-ink | phosphor-green"` on :root / <html> (a
7
+ * root-level choice like data-theme), re-pointing the one accent. The accent's
8
+ * derived family recomputes from the live var(--accent); status colours + the
9
+ * neutral canvas are untouched. Every accent below is gated by check-contrast.mjs. */
10
+
11
+ /* Amber CRT */
12
+ :root[data-bronto-skin='amber-crt'] {
13
+ --accent: oklch(52% 0.11 67deg);
14
+ }
15
+
16
+ :root[data-theme='dark'][data-bronto-skin='amber-crt'] {
17
+ --accent: oklch(82% 0.15 82deg);
18
+ --dotmatrix-glow: 0.4em;
19
+ }
20
+
21
+ /* E-ink */
22
+ :root[data-bronto-skin='e-ink'] {
23
+ --accent: oklch(34% 0.012 250deg);
24
+ }
25
+
26
+ :root[data-theme='dark'][data-bronto-skin='e-ink'] {
27
+ --accent: oklch(84% 0.008 250deg);
28
+ }
29
+
30
+ /* Phosphor Green */
31
+ :root[data-bronto-skin='phosphor-green'] {
32
+ --accent: oklch(52% 0.13 150deg);
33
+ }
34
+
35
+ :root[data-theme='dark'][data-bronto-skin='phosphor-green'] {
36
+ --accent: oklch(84% 0.19 150deg);
37
+ --dotmatrix-glow: 0.4em;
38
+ }
39
+
40
+ @media (prefers-color-scheme: dark) {
41
+ :root:not([data-theme='light'])[data-bronto-skin='amber-crt'] {
42
+ --accent: oklch(82% 0.15 82deg);
43
+ --dotmatrix-glow: 0.4em;
44
+ }
45
+
46
+ :root:not([data-theme='light'])[data-bronto-skin='e-ink'] {
47
+ --accent: oklch(84% 0.008 250deg);
48
+ }
49
+
50
+ :root:not([data-theme='light'])[data-bronto-skin='phosphor-green'] {
51
+ --accent: oklch(84% 0.19 150deg);
52
+ --dotmatrix-glow: 0.4em;
53
+ }
54
+ }
package/css/tokens.css CHANGED
@@ -67,10 +67,10 @@
67
67
  surfaces (the use case tokens/index.js advertises). Derived from the
68
68
  single --accent knob via color-mix against the theme bg, so it
69
69
  re-brands and theme-adapts automatically; no per-theme duplication. */
70
- --accent-1: color-mix(in srgb, var(--accent) 8%, var(--bg));
71
- --accent-2: color-mix(in srgb, var(--accent) 16%, var(--bg));
72
- --accent-3: color-mix(in srgb, var(--accent) 32%, var(--bg));
73
- --accent-4: color-mix(in srgb, var(--accent) 60%, var(--bg));
70
+ --accent-1: color-mix(in oklch, var(--accent) 8%, var(--bg));
71
+ --accent-2: color-mix(in oklch, var(--accent) 16%, var(--bg));
72
+ --accent-3: color-mix(in oklch, var(--accent) 32%, var(--bg));
73
+ --accent-4: color-mix(in oklch, var(--accent) 60%, var(--bg));
74
74
  --accent-5: var(--accent);
75
75
  --accent-6: var(--accent-strong);
76
76
 
@@ -135,8 +135,6 @@
135
135
  --success-soft: rgb(47, 125, 79, 0.12);
136
136
  --warning: #806414;
137
137
  --warning-soft: rgb(128, 100, 20, 0.13);
138
- --orange: #a85f32;
139
- --orange-soft: rgb(168, 95, 50, 0.13);
140
138
  --danger: #c01622;
141
139
  --danger-soft: rgb(192, 22, 34, 0.1);
142
140
  --info: #1f63c4;
@@ -188,8 +186,6 @@
188
186
  --success-soft: rgb(78, 194, 126, 0.14);
189
187
  --warning: #d8bd72;
190
188
  --warning-soft: rgb(216, 189, 114, 0.14);
191
- --orange: #d08c5b;
192
- --orange-soft: rgb(208, 140, 91, 0.15);
193
189
  --danger: #ff4d54;
194
190
  --danger-soft: rgb(255, 77, 84, 0.15);
195
191
  --info: #6fb0e6;
@@ -227,8 +223,6 @@
227
223
  --success-soft: rgb(78, 194, 126, 0.14);
228
224
  --warning: #d8bd72;
229
225
  --warning-soft: rgb(216, 189, 114, 0.14);
230
- --orange: #d08c5b;
231
- --orange-soft: rgb(208, 140, 91, 0.15);
232
226
  --danger: #ff4d54;
233
227
  --danger-soft: rgb(255, 77, 84, 0.15);
234
228
  --info: #6fb0e6;