@apliteni/apliteni-ui 0.23.2 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.23.2",
3
+ "version": "0.23.3",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -1,19 +1,12 @@
1
- // Overlay primitives — the page state the kit's modal surfaces share.
2
- //
3
- // Drawer and Confirm are the same problem twice: content over a scrim that owns
4
- // the keyboard until it is answered. Three of the questions they raise are
5
- // properties of the *page*, not of either component what is inert right now,
6
- // which overlay Escape talks to, and where Tab may go — and an overlay that
7
- // answers them from its own storage gets them wrong as soon as a second one is
8
- // open. So they are answered here, once, from one stack per document.
9
- //
10
- // Internal — not re-exported from src/index.js.
11
-
12
- // What each overlay paints on, as its stylesheet resolves it today: a drawer at
13
- // `--z-overlay` (src/styles/drawer.css) and a confirm one above it
14
- // (src/styles/confirm.css). Absolute values, not ranks, so a sheet that moves and
15
- // a table that did not is a failed test rather than a keyboard on the wrong layer
16
- // — stories/overlay-css.test.js holds these two numbers to those two rules.
1
+ // Overlay primitives — the page state the kit's modal surfaces share. Drawer and Confirm
2
+ // are the same problem twice: content over a scrim that owns the keyboard until it is
3
+ // answered. What is inert, which overlay Escape talks to and where Tab may go are
4
+ // properties of the *page*, so they are answered here from one stack per document rather
5
+ // than from either component's own storage. Internalnot re-exported from src/index.js.
6
+
7
+ // What each overlay paints on today: a drawer at `--z-overlay` (styles/drawer.css) and a
8
+ // confirm above it (styles/confirm.css). Absolute values, not ranks, so a sheet that moves
9
+ // and a table that did not is a failed test — stories/overlay-css.test.js holds both.
17
10
  export const OVERLAY_LAYER = { drawer: 100, confirm: 101 };
18
11
 
19
12
  const FOCUSABLE = [
@@ -33,10 +26,9 @@ function pageOf(doc) {
33
26
  return page;
34
27
  }
35
28
 
36
- // Focusable to the *browser*, not merely matching the selector. A control in a
37
- // closed overlay, an inert subtree or a hidden ancestor is skipped by the real
38
- // tab order, so a trap that counts it wraps at an element focus never reaches
39
- // and Tab walks straight out of the modal.
29
+ // Focusable to the *browser*, not merely matching the selector. A control in a closed
30
+ // overlay, an inert subtree or a hidden ancestor is skipped by the real tab order, so a
31
+ // trap that counts it wraps at an element focus never reaches.
40
32
  function reachable(el) {
41
33
  for (let n = el; n && n.nodeType === 1; n = n.parentElement) {
42
34
  if (n.inert || n.hasAttribute('inert') || n.hasAttribute('hidden')) return false;
@@ -53,10 +45,9 @@ export function focusablesIn(panel) {
53
45
  return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(reachable);
54
46
  }
55
47
 
56
- // Hide everything *outside* `root` from AT + the tab order: walk root→body and
57
- // mark each ancestor's other children. The scrim and panel live inside the root
58
- // so they stay interactive; an overlay one layer down is outside it, so it does
59
- // not — a drawer under an aria-modal alertdialog must not be tabbable.
48
+ // Hide everything *outside* `root` from AT + the tab order: walk root→body and mark each
49
+ // ancestor's other children. Scrim and panel live inside the root so they stay
50
+ // interactive; a drawer under an aria-modal alertdialog is outside it and must not be.
60
51
  function mark(page, doc, root) {
61
52
  let node = root;
62
53
  while (node && node.parentElement && node !== doc.body) {
@@ -81,10 +72,9 @@ function unmark(page) {
81
72
  page.marked = [];
82
73
  }
83
74
 
84
- // Recompute the page from the stack — never replay a snapshot taken when an
85
- // overlay opened, because by the time it closes that snapshot describes a page
86
- // that has moved on. Roots torn down while open drop out here, so the record of
87
- // what to give back outlives the node that hid it.
75
+ // Recompute the page from the stack — never replay a snapshot taken when an overlay
76
+ // opened, because by the time it closes that snapshot describes a page that has moved on.
77
+ // Roots torn down while open drop out here.
88
78
  function sync(doc) {
89
79
  const page = pageOf(doc);
90
80
  for (let i = page.stack.length - 1; i >= 0; i--) {
@@ -128,12 +118,11 @@ function place(root, panel, dismiss, layer, where) {
128
118
  sync(doc);
129
119
  }
130
120
 
131
- // The layer this root actually paints on. In a browser the live z-index is the
132
- // truth, so a consumer who moves either overlay in their own stylesheet gets the
133
- // keyboard on the layer they can see. JSDOM has no cascade to resolve: it hands
134
- // back the declared text — `calc(var(--z-overlay) + 1)` with the kit's sheets,
135
- // `auto` with none — and neither is a number, so under test the passed constant
136
- // stands. `auto` must not read as 0; anything unparseable falls through.
121
+ // The layer this root actually paints on. In a browser the live z-index is the truth, so
122
+ // a consumer who moves an overlay in their own stylesheet gets the keyboard on the layer
123
+ // they can see. JSDOM hands back declared text rather than a number, so under test the
124
+ // passed constant stands — `auto` must not read as 0, and anything unparseable falls
125
+ // through.
137
126
  function paintedLayer(root, layer) {
138
127
  const painted = root.ownerDocument.defaultView?.getComputedStyle(root)?.zIndex;
139
128
  const live = painted ? Number(painted) : NaN;
@@ -152,16 +141,11 @@ export function pushOverlay(root, panel, dismiss, layer) {
152
141
  }
153
142
 
154
143
  /**
155
- * Take on a root that arrived already open — markup rendered with `open: true`,
156
- * which nobody called open…() for. Without this its aria-modal is a claim the
157
- * page contradicts.
158
- *
159
- * It goes in by PAINT ORDER, because wiring has no history to order by. Document
160
- * position only separates two overlays on the same layer, and there it is the
161
- * right answer rather than a fallback: at equal stack levels the later root
162
- * paints on top (CSS 2.2 Appendix E, steps 8 and 9).
163
- *
164
- * A root that renders closed is left alone — wiring is not an opening.
144
+ * Take on a root that arrived already open — markup rendered with `open: true`, which
145
+ * nobody called open…() for. Without this its aria-modal is a claim the page contradicts.
146
+ * It goes in by PAINT ORDER, because wiring has no history to order by; document position
147
+ * separates two overlays on the same layer, where the later root paints on top (CSS 2.2
148
+ * Appendix E, steps 8 and 9). A root that renders closed is left alone.
165
149
  */
166
150
  export function adoptOverlay(root, panel, dismiss, layer) {
167
151
  if (!root.classList.contains('is-open')) return;
@@ -10,9 +10,8 @@ import { sidebarNav, breadcrumbs } from './nav.js';
10
10
  import { prism } from '../assets/brand.js';
11
11
  import { ACCOUNT_NAV, toMenuTuple, initials } from './account-nav.js';
12
12
 
13
- // The one account navigation definition lives in account-nav.js, because
14
- // topbar.js needs it too and this file already imports topbar.js. Re-exported
15
- // here so the published name docs/library.md documents keeps working.
13
+ // The one account navigation definition lives in account-nav.js because topbar.js needs
14
+ // it too; re-exported here so the name docs/library.md publishes keeps working.
16
15
  export { ACCOUNT_NAV };
17
16
 
18
17
  const str = (v) => (v == null ? '' : String(v));
@@ -20,32 +19,24 @@ const isRecord = (v) => typeof v === 'object' && v !== null;
20
19
 
21
20
  // ---- the options bag, settled once --------------------------------------
22
21
  //
23
- // Every option below carries a shape, and a default parameter covers
24
- // `undefined` and nothing else so `nav: null` from an /auth/me, a `maxWidth`
25
- // out of tenant config, a `crumbs` string written by somebody reading the
26
- // migration note all arrived as they were. A shell that throws mid-render takes
27
- // the page with it, so each is settled here, before the first sink sees it, and
28
- // a parameter is protected by being declared rather than by what it crashes
29
- // into. Adding one to appShell() means adding it to SHAPES or deciding in the
30
- // open that it needs nothing.
31
-
32
- // accountShell() has always taken its nav as [id, icon, label, href?, target?].
33
- // Accept that shape and nav.js's object shape side by side, so a consumer's
34
- // existing tuples and the exported ACCOUNT_NAV both work. A nav that is not a
35
- // list at all falls back to the default; an entry inside one that is neither
36
- // shape is dropped, because sideItem() reads `.items` off whatever it is given.
37
- // An empty list is an answer, not a mistake — it stays empty.
22
+ // A default parameter covers `undefined` and nothing else, so `nav: null` from an
23
+ // /auth/me and a `crumbs` string from somebody reading the migration note both arrived
24
+ // as they were. A shell that throws mid-render takes the page with it, so each option is
25
+ // settled here, before the first sink sees it. Adding one to appShell() means adding it
26
+ // to SHAPES or deciding in the open that it needs nothing.
27
+
28
+ // accountShell()'s tuple nav [id, icon, label, href?, target?] and nav.js's object
29
+ // shape are accepted side by side. A nav that is not a list falls back to the default;
30
+ // an entry that is neither shape is dropped. An empty list is an answer and stays.
38
31
  const toItems = (nav) => (Array.isArray(nav) ? nav : ACCOUNT_NAV)
39
32
  .filter(isRecord)
40
33
  .map((n) => (Array.isArray(n)
41
34
  ? { id: n[0], icon: n[1], label: n[2], href: n[3], target: n[4] }
42
35
  : n));
43
36
 
44
- // The trail is the caller's, and `crumbs` is the one option whose shape changed
45
- // in this release: the old API was `crumb: 'Payouts'`, a string, and the new one
46
- // is `crumbs: [{ label }]`. One letter apart. A value that is not a list is not
47
- // read as a one-crumb trail — that would draw a plausible page and hide the
48
- // migration mistake — so it is no trail at all, which is the visible answer. A
37
+ // The trail is the caller's, and `crumbs` changed shape this release: `crumb: 'Payouts'`
38
+ // became `crumbs: [{ label }]`, one letter apart. A non-list is NOT read as a one-crumb
39
+ // trail that hides the migration mistake so it is no trail at all, which shows. A
49
40
  // crumb with no label would draw an empty cell, so it goes too.
50
41
  const toCrumbs = (crumbs) => (Array.isArray(crumbs) ? crumbs : [])
51
42
  .filter((c) => isRecord(c) && !Array.isArray(c) && str(c.label) !== '');
@@ -56,23 +47,16 @@ const toReader = (a) => (isRecord(a) ? { name: str(a.name), email: str(a.email)
56
47
 
57
48
  // ---- the topbar, which interpolates where the rail escapes ----------------
58
49
  //
59
- // brand() writes `word` straight into its markup and accountMenu() does the
60
- // same with the reader's name, address and menu entries. So the escaping is
61
- // here, on the one path into productTopbar(), and not in each caller:
62
- // accountShell() escaped for itself and the `topbar` option underneath it
63
- // escaped for nobody. The caller passes text either way.
64
-
65
- // The reader the menu draws. Both fields are always written, empty when the
66
- // caller gave noneaccountMenu()'s own defaults are a demo identity, and a
67
- // key dropped here is a key its default fills in, which is how a consumer's
68
- // page came to name its own reader in the rail and the kit's fixture beside it.
69
- //
70
- // `initials` travels with them because it is derived, and a derived value comes
71
- // from what the caller passed, not from the entities made of it: `<Ada>` and
72
- // `&lt;Ada&gt;` do not start with the same character. The entries land in the
73
- // same sink and toMenuTuple() is what escapes them; it reads item objects, so
74
- // tuples go through toItems() first. A nav nobody passed stays unpassed, and
75
- // accountMenu() falls back to its own derived default rather than to nothing.
50
+ // brand() writes `word` straight into its markup and accountMenu() does the same with
51
+ // the reader's name, address and menu entries, so the escaping is here on the one path
52
+ // into productTopbar() rather than in each caller. The caller passes text either way.
53
+
54
+ // The reader the menu draws. Both fields are always written, empty when the caller gave
55
+ // none: accountMenu()'s defaults are a demo identity, so a key dropped here is a key its
56
+ // fixture fills in. `initials` is derived from what the caller passed rather than from
57
+ // the entities made of it `<Ada>` and `&lt;Ada&gt;` do not start with the same
58
+ // character. toMenuTuple() escapes the entries and reads item objects, so tuples go
59
+ // through toItems() first; a nav nobody passed stays unpassed.
76
60
  const toMenuReader = (a) => {
77
61
  const { name, email } = toReader(a);
78
62
  const rest = isRecord(a) && !Array.isArray(a) ? a : {};
@@ -89,12 +73,11 @@ const toTopbar = (t) => {
89
73
  return out;
90
74
  };
91
75
 
92
- // `maxWidth` lands inside a style attribute, so a length is all this accepts —
93
- // a number and a unit, or `none`. Anything else yields '' and the caller writes
94
- // no style attribute, letting layout.css fall through to --measure. It must
95
- // REMOVE the property rather than pass a default or a bad value on: a custom
96
- // property accepts any token stream, so garbage is a valid declaration that
97
- // drops the column to `none`, the full track.
76
+ // `maxWidth` lands inside a style attribute, so a length is all this accepts — a number
77
+ // and a unit, or `none`. Anything else yields '' and the caller writes no style
78
+ // attribute, falling through to --measure. It must REMOVE the property rather than pass
79
+ // a bad value on: a custom property accepts any token stream, so garbage is a valid
80
+ // declaration that drops the column to `none`, the full track.
98
81
  // why: docs/specification.md#widths
99
82
  const LENGTH = /^(?:\d+|\d*\.\d+)(?:px|rem|em|ch|%|vw)$/;
100
83
  const mainMax = (v) => {
@@ -108,11 +91,9 @@ const SHAPES = {
108
91
  nav: toItems, crumbs: toCrumbs, account: toReader, maxWidth: mainMax, topbar: toTopbar,
109
92
  };
110
93
 
111
- // The text options settle too, by the same argument the rest of them do. A
112
- // default parameter covers `undefined` and not `null`, so `body: null` from a
113
- // record with no description drew the word "null" on the page, and `word: null`
114
- // left the brand link with no accessible name at all. Dropping the key is what
115
- // lets the declared default apply.
94
+ // The text options settle by the same argument. `body: null` from a record with no
95
+ // description drew the word "null" on the page and `word: null` left the brand link with
96
+ // no accessible name; dropping the key is what lets the declared default apply.
116
97
  const TEXT = ['word', 'brandHref', 'navLabel', 'title', 'sub', 'body', 'signOutHref', 'active'];
117
98
 
118
99
  function settle(options) {
@@ -122,23 +103,18 @@ function settle(options) {
122
103
  return out;
123
104
  }
124
105
 
125
- // Signing out is a navigation action, so it belongs in the rail nav's footer
126
- // slot. Opt-in: a shell that renders it unasked puts a dead link on a page with
127
- // no session behind it.
106
+ // Signing out is a navigation action, so it belongs in the rail nav's footer slot.
107
+ // Opt-in: rendering it unasked puts a dead link on a page with no session behind it.
128
108
  const signOut = (href) =>
129
109
  `<a class="ui-nav__item is-danger" href="${esc(href)}" aria-label="Sign out">` +
130
110
  `<span class="ui-nav__ic">${icon('logout')}</span>` +
131
111
  `<span class="ui-nav__label">Sign out</span></a>`;
132
112
 
133
- // Who is signed in. A sibling of the <nav>, not its footer: a reader's name and
134
- // address are not navigation, and inside the landmark a screen reader announces
135
- // the address as an entry. Empty when nobody is a shell must not invent an
136
- // identity for a reader it does not know.
137
- //
138
- // The initials carry the name, and the spelled-out half is aria-hidden. The
139
- // narrow rail folds `.ui-app__who` out of view, so a name that lived only there
140
- // left the initials on screen with nothing at all in the accessibility tree.
141
- // Naming the mark instead makes the two agree at every width, and says it once.
113
+ // Who is signed in. A sibling of the <nav>, not its footer: a name and address are not
114
+ // navigation, and inside the landmark a screen reader announces the address as an entry.
115
+ // Empty when nobody is. The initials carry the name and the spelled-out half is
116
+ // aria-hidden, because the narrow rail folds `.ui-app__who` out of view and a name that
117
+ // lived only there left nothing in the accessibility tree.
142
118
  function railUser({ name, email }) {
143
119
  if (!name && !email) return '';
144
120
  const who = [name, email].filter(Boolean).join(', ');
@@ -179,16 +155,14 @@ export function appShell(options = {}) {
179
155
  ariaLabel: navLabel,
180
156
  footer: signOutHref ? signOut(signOutHref) : '',
181
157
  });
182
- // The topbar already says the product word. Two lockups on one screen is one
183
- // product word too many, so the rail head steps aside when there is a topbar.
184
- // The word is the link's only text and the narrow rail folds it out of view,
185
- // so the name is written out — the mark itself is aria-hidden.
158
+ // The topbar already says the product word, so the rail head steps aside when there is
159
+ // one. The word is the link's only text and the narrow rail folds it out of view, so
160
+ // the name is written out the mark itself is aria-hidden.
186
161
  const brand = topbar ? '' : `<a class="ui-app__brand" href="${esc(brandHref)}" aria-label="${esc(word)}">`
187
162
  + `${prism(`appb-${++_shellUid}`, 24)}<span>${esc(word)}</span></a>`;
188
- // A <div>, not an <aside>: <aside> is the `complementary` landmark content
189
- // related to the page but separable from it and this holds the page's
190
- // primary navigation and the reader who is signed in. The <nav> inside it is
191
- // already the landmark that names the menu.
163
+ // A <div>, not an <aside>: <aside> is the `complementary` landmark, and this holds the
164
+ // page's primary navigation and the signed-in reader. The <nav> inside it is already
165
+ // the landmark that names the menu.
192
166
  const grid = `<div class="ui-app">
193
167
  <div class="ui-app__rail">
194
168
  ${brand}
@@ -225,11 +199,9 @@ export function accountShell({
225
199
  // topbar menu are handed one list rather than two readings of `nav`.
226
200
  const items = toItems(nav);
227
201
  const trail = [{ label: cap }, { label: crumb || title }];
228
- // The preset hands the topbar the caller's text, exactly as it hands the rail
229
- // the caller's text. toTopbar() is what escapes for the menu's raw sinks, and
230
- // it runs once inside appShell() — escaping here as well would reach the menu
231
- // as entities, and escaping nowhere is what the public `topbar` option used
232
- // to do.
202
+ // The preset hands the topbar the caller's text, as it does the rail. toTopbar()
203
+ // escapes for the menu's raw sinks and runs once inside appShell(); escaping here as
204
+ // well would reach the menu as entities.
233
205
  return appShell({
234
206
  word,
235
207
  nav: items,
@@ -34,14 +34,10 @@
34
34
  .ui-table tbody tr { transition: background var(--dur-fast) var(--ease); }
35
35
 
36
36
  /* Row hover: an inset, rounded fill so the highlight never sits flush against a
37
- * container's border (issue #71 the full-bleed rectangle collided with a card's
38
- * left edge). Separate border model with zero spacing keeps the row separators
39
- * continuous while letting the row ends round; table padding (which only applies
40
- * in the separate model) insets the rows so the fill clears the container edge.
41
- * Zebra tables keep the original full-bleed tint (finance ledger), so they're
42
- * excluded here — their hover rule lives further down.
43
- * The inset was 6px, which is a tie between --space-1 and --space-2; it rounds
44
- * up, because what it buys is clearance and clearance rounds away from the edge. */
37
+ * container's border (#71). The separate border model with zero spacing keeps the row
38
+ * separators continuous while letting the row ends round, and table padding — which
39
+ * only applies in that model insets the rows. Zebra keeps the original full-bleed
40
+ * tint (finance ledger) and is excluded here; its hover rule is below. */
45
41
  .ui-table--hover:not(.ui-table--zebra) {
46
42
  border-collapse: separate;
47
43
  border-spacing: 0;
@@ -54,9 +50,7 @@
54
50
  border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); }
55
51
 
56
52
  /* Data-table modifiers, composable: .ui-table--dense --zebra --hover.
57
- * --dense --space-3 across, --space-2 down — one step in from the base
58
- * table, and both round DOWN, because the modifier exists to fit
59
- * more rows and rounding up spends the distinction it is for.
53
+ * --dense --space-3 across, --space-2 down — one step in from the base table
60
54
  * --zebra row striping; 2px header rule; the hover tint out-ranks the stripe
61
55
  * Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
62
56
  * why: docs/specification.md#spacing-and-rhythm
@@ -71,15 +65,11 @@
71
65
  .ui-table--zebra th { border-bottom-width: 2px; }
72
66
  .ui-table--zebra td { border-bottom: 0; }
73
67
  .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
74
- /* The stripe is full-bleed on purpose the hover note above inset the hover fill for
75
- * #71 and deliberately left the ledger stripe alone. But full-bleed only reads right if
76
- * the end cells are inset from the tint's own edge, and the base rhythm does not inset
77
- * them: `.ui-table td` sets `padding-left: 0`. Only `--dense` supplied the inset, so a
78
- * `--zebra` table composed WITHOUT `--dense` put the first cell's content at exactly
79
- * x=0 of the stripe. `DataTable` is that composition — it writes `ui-table
80
- * ui-table--hover ui-table--zebra` and no `--dense` — so its select-all and per-row
81
- * checkboxes sat flush against the tint with a measured 0px gap.
82
- * Header and body are inset together, or the columns stop lining up. */
68
+ /* The stripe stays full-bleed (the #71 inset above is hover only), but it only reads right
69
+ * if the end cells are inset from the tint's own edge, and `.ui-table td` sets
70
+ * `padding-left: 0`. Only `--dense` supplied that inset, so `--zebra` without `--dense`
71
+ * `DataTable`'s composition sat its checkboxes at x=0 of the stripe. Head and body are
72
+ * inset together, or the columns stop lining up. */
83
73
  .ui-table--zebra th:first-child,
84
74
  .ui-table--zebra td:first-child { padding-left: var(--space-3); }
85
75
  .ui-table--zebra th:last-child,
@@ -62,25 +62,15 @@
62
62
  --purple: #1156b8;
63
63
  --purple-light: #1568d6;
64
64
  --purple-mid: #1560c8;
65
- /* Deepened for WCAG AA on the raised grey panel and its wash: #1568d6 read
66
- 4.54:1 on --surface-3 and 3.98:1 on --glow-purple over it; #005bc8 clears
67
- both at 5.43:1 and 4.76:1. Both washed figures are at the 0.10 alpha below,
68
- which is what this cell ships — an earlier version of this comment quoted
69
- them at the 0.05 the wash briefly carried, which flattered both inks.
70
- Light Ocean is the one light accent #96 never deepened, because it happened
71
- to clear plain white. Same kind of move, smaller: hue and chroma unchanged
72
- in OKLCH (H 258, C 0.185) and four points of lightness down, where #96's
73
- two took about ten points and gave up 15.9% of the chroma on Phoenix and
74
- 17.6% on Emerald. See issues #96 and #157. */
65
+ /* Deepened for WCAG AA on the raised grey panel and its wash hue and chroma held
66
+ in OKLCH (H 258, C 0.185), four points of lightness down. The washed figures are
67
+ read at the 0.10 alpha below, which is what this cell ships. See issues #96, #157. */
75
68
  --accent: #005bc8;
76
69
  --accent-strong: var(--accent);
77
70
  --accent-contrast: #ffffff;
78
- /* Back to 0.10, the alpha this cell carried before #157. #157 thinned it to
79
- 0.05 while --accent was still #1568d6, because no alpha could carry that
80
- blue over the grey panel. Deepening the ink is what closed the pair, so the
81
- wash does not have to be the thing that gives. The rgb stays --purple-light
82
- rather than the accent — in light a glow tints --purple-light, which
83
- stories/signal-contrast.test.js holds. See issue #157. */
71
+ /* Back to the 0.10 this cell carried before #157: deepening the ink closed the pair,
72
+ so the wash does not have to give. The rgb stays --purple-light rather than the
73
+ accent in light a glow tints --purple-light, which signal-contrast.test.js holds. */
84
74
  --glow-purple: rgba(21, 104, 214, 0.10);
85
75
  --grad-from: #1568d6;
86
76
  --grad-to: #17a2c0;
@@ -1,14 +1,8 @@
1
1
  /* ============================================================================
2
- * apliteni-ui — design tokens
3
- * Single source of truth for the Apliteni deck theme.
4
- *
5
- * Colour semantics are identical to viz/ (deck index.html, account.mjs):
6
- * purple accent, near-black surfaces, green = live, cyan = link/flag.
7
- * Two themes: dark (default) and light. Toggle with `data-theme` on <html>.
8
- *
9
- * Everything downstream references the SEMANTIC tokens (--bg, --surface,
10
- * --accent, --text…), never the raw ramp. Swap a ramp value once, and every
11
- * component follows.
2
+ * apliteni-ui — design tokens. Single source of truth for the Apliteni deck theme,
3
+ * with viz/'s colour semantics: purple accent, near-black surfaces, green = live,
4
+ * cyan = link/flag. Two themes, dark (default) and light, toggled with `data-theme`
5
+ * on <html>. Everything downstream references the SEMANTIC tokens, never the ramp.
12
6
  * ========================================================================== */
13
7
 
14
8
  :root {
@@ -44,7 +38,6 @@
44
38
  /* -- Measure ------------------------------------------------------------
45
39
  * --container is the page, gutter to gutter; --measure is the reading column
46
40
  * INSIDE a track, beside a sidebar. Different axes, never interchangeable.
47
- * Below --measure are --panel-* and --prose-*, at the end of this file (#208).
48
41
  * why: docs/specification.md#widths */
49
42
  --container: 1120px;
50
43
  --measure: 860px;
@@ -79,13 +72,12 @@
79
72
  --tracking-caps: 0.14em;
80
73
 
81
74
  /* -- Motion -------------------------------------------------------------
82
- * Durations + easings are the Apliteni brand's motion vocabulary, synced
83
- * from design-system into brand.generated.css (--duration-*, --easing-*).
84
- * The kit's legacy --dur-* and --ease names alias onto them, so every existing
85
- * component transitively consumes the brand primitives one vocabulary,
86
- * zero drift. Fallbacks keep tokens.css self-sufficient when imported
87
- * without brand.generated.css. Delays are the kit's own (not a brand
88
- * concern). The reusable effect layer lives in styles/motion.css. */
75
+ * The kit's legacy --dur-* and --ease names alias onto the brand's
76
+ * --duration-* and --easing-* primitives, so every component consumes one
77
+ * vocabulary; the fallbacks keep tokens.css self-sufficient when imported
78
+ * without brand.generated.css. Delays are the kit's own, and the reusable
79
+ * effect layer lives in styles/motion.css.
80
+ * why: CONTRIBUTING.md#brand-tokens-synced-from-design-system */
89
81
  --ease: var(--easing-ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
90
82
  --ease-out: var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1));
91
83
  --ease-in: var(--easing-ease-in, cubic-bezier(0.4, 0, 1, 1));
@@ -119,22 +111,16 @@
119
111
  color-scheme: dark;
120
112
 
121
113
  --purple: #6a2dcc;
122
- /* The ramp moves up one step so --accent is --purple-light again, which is
123
- the shape light Nebula and light Ocean already ship and which dark Nebula
124
- held until #157 lifted the accent onto the old --purple-mid. Left alone,
125
- the two tokens would resolve to one colour and the ramp would carry a
126
- duplicate step. #bd8cff is the value docs/specification.md#colour-and-contrast reasons about and
127
- rejects as the accent; it becomes the ramp's top step instead. Light
128
- Nebula's ramp does not move. See issue #157. */
114
+ /* The ramp moves up one step so --accent is --purple-light again, the shape light
115
+ Nebula and light Ocean already ship; left alone the two tokens would resolve to
116
+ one colour. #bd8cff becomes the ramp's top step instead. See issue #157. */
129
117
  --purple-light: #b479ff;
130
118
  --purple-mid: #bd8cff;
131
119
  --cyan: #20dcf5;
132
120
  --green: #98ff8f;
133
- /* Lighter than it looks like it should be, and deliberately so: --pink is read
134
- mostly ON its own 16% glow, and that wash lifts the ground faster than the
135
- hue lifts the ink. #e35b8f measured 3.82:1 on --glow-pink over a card
136
- under AA — so danger moved AWAY from the canvas, not toward it. Light moves
137
- the opposite way for the same reason. See issue #131. */
121
+ /* Lighter than it looks like it should be: --pink is read mostly ON its own 16%
122
+ glow, and that wash lifts the ground faster than the hue lifts the ink, so danger
123
+ moved AWAY from the canvas. Light moves the opposite way. See issue #131. */
138
124
  --pink: #e97ca5;
139
125
  --amber: #ffcf6a;
140
126
 
@@ -144,9 +130,8 @@
144
130
  --surface-2: #1b1927;
145
131
  --surface-3: #2a2739;
146
132
  --seg-active-bg: #34314a; /* raised pill — clearly above the inset track in dark */
147
- /* The raised handle on a control track (switch knob). Stays white in both
148
- themes and under every accent: it has to read on the plain track AND on
149
- the accent fill the track takes when the control is on. */
133
+ /* The raised handle on a control track (switch knob). White in both themes and under
134
+ every accent: it reads on the plain track AND on the accent fill of an on control. */
150
135
  --control-knob: #ffffff;
151
136
  --border: #332f45;
152
137
  --border-strong: #453f5c;
@@ -156,53 +141,40 @@
156
141
  --dim: #c6c2d6;
157
142
  --muted: #948fa8;
158
143
 
159
- /* The paint of a control that is off. NOT an opacity: `opacity` is a group
160
- property, so it fades a label and the box under it toward the ground
161
- TOGETHER and the pair a reader is left with is whatever that composite
162
- lands on a disabled primary button measured 1.48:1 that way, white on a
163
- faded accent. These three composite predictably instead: the ink is read on
164
- the surface beside it, and both are opaque. They are aliases, not new
165
- colours — the ramp gains nothing to keep in sync. A disabled control drops
166
- its accent, which is most of what makes it read as inert; the ink going
167
- quiet is the rest. Held by stories/guidelines/accessibility-floor.test.js.
144
+ /* The paint of a control that is off. NOT an opacity: `opacity` is a group property,
145
+ so it fades label and box toward the ground TOGETHER a disabled primary button
146
+ measured 1.48:1 that way. These three are aliases that composite predictably, the
147
+ ink read on the surface beside it. Held by guidelines/accessibility-floor.test.js.
168
148
  why: docs/specification.md#colour-and-contrast */
169
149
  --disabled-ink: var(--muted);
170
150
  --disabled-surface: var(--surface-2);
171
151
  --disabled-border: var(--border);
172
152
 
173
- /* Lifted from #9b5dff, which cleared the flat surfaces and failed on its own
174
- wash over a card. This value was already in the ramp as --purple-mid, so no
175
- new colour entered it; the ramp then moved up a step above so --accent is
176
- --purple-light, which is how every other Nebula and Ocean cell is written.
177
- --glow-purple follows it down the file; --ring is written as var(--accent)
178
- and so follows it by reference. Held by
179
- stories/accent-contrast.test.js. See issue #157.
153
+ /* Lifted from #9b5dff, which cleared the flat surfaces and failed on its own wash over
154
+ a card; the value was already in the ramp. --ring is var(--accent) and follows by
155
+ reference. Held by stories/accent-contrast.test.js. #157.
180
156
  why: docs/specification.md#colour-and-contrast */
181
157
  --accent: #b479ff;
182
158
  --accent-strong: #7c3aed; /* button bg — white text clears WCAG AA at 5.70:1 */
183
159
  --accent-contrast: #ffffff;
184
160
  --ink: #e9e7f0;
185
161
 
186
- /* Ink that reads on a SIGNAL colour once that colour becomes a fill — the
187
- glyph in a success circle, the glyph on a toast's status circle. One
188
- near-black ink clears all five in dark; danger keeps a token of its own
189
- because LIGHT still needs one.
162
+ /* Ink that reads on a SIGNAL colour once that colour becomes a fill — the glyph in a
163
+ success circle, or on a toast's status circle. One near-black ink clears all five in
164
+ dark; danger keeps its own token because LIGHT still needs one.
190
165
  why: docs/specification.md#colour-and-contrast */
191
166
  --signal-contrast: #0c0c0c;
192
167
  --danger-contrast: var(--signal-contrast);
193
168
 
194
169
  --glow-green: rgba(152, 255, 143, 0.16);
195
- /* Dropped from 0.18: the wash, not the surface beneath it, is what put --accent
196
- under AA it lifts the ground toward the ink faster than the ink lifts off
197
- it. Re-tinted from the new --accent, which signal-contrast.test.js requires.
198
- See issue #157. */
170
+ /* Dropped from 0.18: the wash, not the surface beneath it, is what put --accent under
171
+ AA. Re-tinted from the new --accent, which signal-contrast.test.js requires. #157. */
199
172
  --glow-purple: rgba(180, 121, 255, 0.12);
200
173
  --glow-cyan: rgba(32, 220, 245, 0.14);
201
174
  --glow-pink: rgba(233, 124, 165, 0.16);
202
175
 
203
- /* Status-chip paint — the ink + fill of a badge / pill. Dark tints with the
204
- signal's own glow; light needs a solid, deepened pair (the glows wash out
205
- on white), which is why these are a token pair and not just --green etc. */
176
+ /* Status-chip paint — the ink + fill of a badge / pill. Dark tints with the signal's
177
+ own glow; light needs a solid, deepened pair, which is why these are a token pair. */
206
178
  --chip-success-ink: var(--green);
207
179
  --chip-success-fill: var(--glow-green);
208
180
  --chip-warn-ink: var(--amber);
@@ -212,14 +184,10 @@
212
184
  --chip-info-ink: var(--cyan);
213
185
  --chip-info-fill: var(--glow-cyan);
214
186
 
215
- /* Solid signal surfaces — a status that has stopped being ink and become the
216
- FILL (a solid toast). The fill and its ink are ONE pair, never two
217
- independent picks: the fill is the status at this theme's extreme, and the
218
- ink is the pole opposite it, so a single ink clears all five. Dark fills are
219
- the bright signals, so the ink is near-black — it measures 15.87 on --green,
220
- 7.35 on --pink, 13.40 on --amber, 11.75 on --cyan and 6.29 on --muted.
221
- Neutral needs no chip pair to take part: --muted is already its
222
- fill-strength form. See issues #131 and #149. */
187
+ /* Solid signal surfaces — a status that has stopped being ink and become the FILL (a
188
+ solid toast). Fill and ink are ONE pair, never two independent picks: the fill is the
189
+ status at this theme's extreme and the ink the pole opposite it, so a single ink
190
+ clears all five. --muted is already its own fill-strength form. See #131, #149. */
223
191
  --signal-solid-ink: var(--signal-contrast);
224
192
  --signal-solid-success: var(--green);
225
193
  --signal-solid-danger: var(--pink);
@@ -232,19 +200,18 @@
232
200
  --shadow-lg: 0 18px 50px rgba(0, 0, 0, 0.5);
233
201
  /* Tight elevation for the active segmented pill — stays contained in its track. */
234
202
  --shadow-seg: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 4px rgba(0, 0, 0, 0.3);
235
- /* Resting elevation for a card. Dark cards are borderless — the surface does
236
- the grouping on its own — so they cast nothing. */
203
+ /* Resting elevation for a card. Dark cards are borderless — the surface groups on
204
+ its own — so they cast nothing. */
237
205
  --shadow-card: none;
238
- /* THE ONLY --ring in the kit — light and every data-accent sub-theme declare
239
- an --accent and inherit this line. Opaque because a translucent ring is a
240
- glow: the eight rgba() literals this replaced all missed the 3:1 of WCAG
241
- 1.4.11, at 1.35:1 worst. Held at 4.22:1 worst by
242
- stories/guidelines/accessibility-floor.test.js. See issue #218.
206
+ /* THE ONLY --ring in the kit — light and every data-accent sub-theme declare an
207
+ --accent and inherit this line. Opaque because a translucent ring is a glow, and the
208
+ eight rgba() literals it replaced all missed WCAG 1.4.11's 3:1 (#218). Held by
209
+ stories/guidelines/accessibility-floor.test.js.
243
210
  why: docs/specification.md#the-focus-ring */
244
211
  --ring: 0 0 0 3px var(--accent);
245
212
 
246
- /* The ink a cast shadow is made of, and the specular white that reads as
247
- gloss on a raised surface. Effects mix these instead of writing rgba(). */
213
+ /* The ink a cast shadow is made of, and the specular white that reads as gloss on a
214
+ raised surface. Effects mix these instead of writing rgba(). */
248
215
  --shadow-ink: #000000;
249
216
  --sheen: #ffffff;
250
217
 
@@ -252,10 +219,8 @@
252
219
  --scrim: rgba(0, 0, 0, 0.58);
253
220
 
254
221
  /* Accent gradient stops (hero headline, marketing) — overridden per sub-theme.
255
- --grad-from is pinned to the literal rather than tracking --purple-light,
256
- which #157 moved up a step: following it would lighten the hero gradient's
257
- first stop, a visible change nobody asked for. Every --grad-from in
258
- src/tokens/accents.css is already written as a literal for the same reason. */
222
+ --grad-from is pinned to a literal rather than tracking --purple-light, which #157
223
+ moved up a step; every --grad-from in accents.css is a literal for the same reason. */
259
224
  --grad-from: #9b5dff;
260
225
  --grad-to: var(--cyan);
261
226
  }
@@ -271,15 +236,13 @@
271
236
  --purple-mid: #7a3be0;
272
237
  --cyan: #0c8fa8;
273
238
  --green: #1c8a2c;
274
- /* Deeper than the dark pink for the same reason dark's is lighter: the wash
275
- under it is white, so the ink has to come down to meet it. #d63c72 measured
276
- 3.87:1 on --glow-pink over white and 4.42:1 on plain white — the required
277
- marker and the form error missed AA on nothing at all. See issue #131. */
239
+ /* Deeper than the dark pink for the same reason dark's is lighter: the wash under it
240
+ is white, so the ink has to come down to meet it. See issue #131. */
278
241
  --pink: #b63361;
279
242
  --amber: #b5730a;
280
243
 
281
- /* A WHITE app: the page and cards are both white; cards are delineated by a
282
- hairline border + soft shadow (see card.css), not a grey canvas. */
244
+ /* A WHITE app: page and cards are both white, and cards are delineated by a hairline
245
+ border + soft shadow (see card.css), not a grey canvas. */
283
246
  --bg: #ffffff;
284
247
  --bg-elevated: #ffffff;
285
248
  --surface: #ffffff;
@@ -295,9 +258,8 @@
295
258
  --dim: #3a404b;
296
259
  --muted: #5c6270;
297
260
 
298
- /* Same three aliases as dark, same reasoning. Light moves the disabled
299
- surface AWAY from white for the same reason dark moves it away from the
300
- card: a control that is off should sink, not rise. */
261
+ /* Same three aliases as dark. Light moves the disabled surface AWAY from white for the
262
+ same reason dark moves it away from the card: a control that is off should sink. */
301
263
  --disabled-ink: var(--muted);
302
264
  --disabled-surface: var(--surface-2);
303
265
  --disabled-border: var(--border);
@@ -310,45 +272,33 @@
310
272
  --signal-contrast: #0c0c0c;
311
273
  --danger-contrast: #ffffff;
312
274
 
313
- /* Re-tinted from --green. It was rgba(30, 150, 50, .1) a colour that exists
314
- nowhere else in the kit, carried unedited since the initial commit while the
315
- other three light glows are exact tints of their own token. The wash over
316
- white moves #e9f5eb -> #e8f3ea, at most two levels on a channel, which is
317
- 0.86 of a deltaE — under the 1.0 that is normally called imperceptible. No
318
- verdict moves with it: body copy on the success wash goes 9.29 -> 9.15
319
- against the 4.5 AA bar, and the --green icon on it 3.97 -> 3.91 against the
320
- 3.0 bar WCAG 1.4.11 sets for a graphic. The success chips are untouched —
321
- light's --chip-success-fill is the solid #dff3e4, not this. Gated for all
322
- four glows in stories/signal-contrast.test.js. See issue #131. */
275
+ /* Re-tinted from --green; it was rgba(30, 150, 50, .1), a colour that exists nowhere
276
+ else in the kit while the other three light glows are exact tints of their own token.
277
+ The wash over white moves at most two levels on a channel and no verdict moves with
278
+ it. Gated for all four glows in stories/signal-contrast.test.js. */
323
279
  --glow-green: rgba(28, 138, 44, 0.1);
324
280
  --glow-purple: rgba(106, 45, 204, 0.09);
325
281
  --glow-cyan: rgba(12, 143, 168, 0.1);
326
282
  --glow-pink: rgba(182, 51, 97, 0.1);
327
283
 
328
- /* Solid chip pairs for the white app. Each ink is deepened past its signal
329
- hue so 10px uppercase text clears WCAG AA on its own pale fill
330
- e.g. --pink (#b63361) on #fbe0ea is 4.66:1, #b7295f is 4.85:1. */
284
+ /* Solid chip pairs for the white app. Each ink is deepened past its signal hue so 10px
285
+ uppercase text clears WCAG AA on its own pale fill. */
331
286
  --chip-success-ink: #1f7a38;
332
287
  --chip-success-fill: #dff3e4;
333
288
  --chip-warn-ink: #8a5e00;
334
289
  --chip-warn-fill: #fbedd2;
335
290
  --chip-danger-ink: #b7295f;
336
291
  --chip-danger-fill: #fbe0ea;
337
- /* Info was the one signal family without a pair, so the info badge was still
338
- painting --cyan (#0c8fa8) on --glow-cyan over white, which is 3.39:1
339
- under AA. #0a7286 on #ddeff3 is 4.71:1, inside the 4.64–4.93 band the other
340
- three pairs already sit in. See issue #131. */
292
+ /* Info was the one signal family without a pair, so the info badge painted --cyan on
293
+ --glow-cyan over white and missed AA. This pair sits in the band the other three
294
+ already occupy. See issue #131. */
341
295
  --chip-info-ink: #0a7286;
342
296
  --chip-info-fill: #ddeff3;
343
297
 
344
- /* Light runs the same rule the other way. The signals are mid-tone here a
345
- solid --green fill measures 4.40 against near-black and 4.45 against white,
346
- so NO ink rescues it and the fill is what has to move. It moves to the
347
- deepened form the chip pair already carries, and the pole above it, white,
348
- clears every one: #ffffff measures 5.39 on --chip-success-ink, 6.01 on
349
- --chip-danger-ink, 5.70 on --chip-warn-ink, 5.58 on --chip-info-ink and 6.11
350
- on --muted. A future status supplies one --signal-solid-* value dark enough
351
- for white and inherits the ink. See issues #131 and #149. */
298
+ /* Light runs the same rule the other way. The signals are mid-tone here, so NO ink
299
+ rescues them and the fill is what has to move: it takes the deepened form the chip
300
+ pair already carries, and white clears every one. A future status supplies one
301
+ --signal-solid-* value dark enough for white and inherits the ink. See #131, #149. */
352
302
  --signal-solid-ink: #ffffff;
353
303
  --signal-solid-success: var(--chip-success-ink);
354
304
  --signal-solid-danger: var(--chip-danger-ink);
@@ -361,8 +311,8 @@
361
311
  --shadow-lg: 0 18px 50px rgba(30, 30, 50, 0.16);
362
312
  /* Tight elevation for the active segmented pill — stays contained in its track. */
363
313
  --shadow-seg: 0 1px 2px rgba(30, 30, 50, 0.12), 0 2px 4px rgba(30, 30, 50, 0.1);
364
- /* A white card on a white page needs its own, flatter-than-sm elevation:
365
- a hairline contact shadow plus a wide soft one (see card.css). */
314
+ /* A white card on a white page needs its own flatter-than-sm elevation: a hairline
315
+ contact shadow plus a wide soft one (see card.css). */
366
316
  --shadow-card: 0 1px 2px rgba(20, 25, 45, 0.04), 0 4px 14px rgba(20, 25, 45, 0.05);
367
317
  /* No --ring here. :root's is var(--accent), and light re-points --accent. */
368
318
 
@@ -376,42 +326,32 @@
376
326
  }
377
327
 
378
328
  /* ============================================================================
379
- * WIDTHS BELOW THE PAGE — the box, and the line
380
- *
381
- * --container is the page and --measure the reading column inside it (both
382
- * above). Underneath them the kit has two scales and one rule for choosing
383
- * between them: THE UNIT SAYS WHICH SCALE YOU ARE ON.
329
+ * WIDTHS BELOW THE PAGE — the box, and the line. Two scales, and one rule for
330
+ * choosing between them: THE UNIT SAYS WHICH SCALE YOU ARE ON.
384
331
  *
385
332
  * a box that holds a COMPONENT -> --panel-* (px)
386
333
  * a box that holds a LINE -> --prose-* (ch)
387
334
  *
388
- * Both are theme-invariant, which is why they sit outside the palette blocks.
335
+ * Both are theme-invariant, so they sit outside the palette blocks.
389
336
  * why: docs/specification.md#boxes-below-the-page
390
337
  * ========================================================================== */
391
338
  :root {
392
339
  /* -- Panel widths -------------------------------------------------------
393
- * Not a new scale: the drawer has shipped sm/md/lg at exactly these three
394
- * values since it was written, and confirm, the auth card and the toast each
395
- * wrote one of them out again on their own. --panel-md answers the question
396
- * #208 asked — the two 420s were three (--confirm-w, --drawer-w md and
397
- * .ui-auth__card), and they are one measure now. */
340
+ * Not a new scale: the drawer has shipped sm/md/lg at these three values, and
341
+ * confirm, the auth card and the toast each wrote one out again (#208). */
398
342
  --panel-sm: 320px;
399
343
  --panel-md: 420px;
400
344
  --panel-lg: 560px;
401
345
 
402
346
  /* -- Prose measures -----------------------------------------------------
403
- * ch resolves against the font-size of the element it is DECLARED on, so a
404
- * step goes on the paragraph itself never on a wrapper holding two type
405
- * sizes, where the same token would mean two different widths.
406
- *
407
- * The four steps are the reading band (44–72ch), named for what is being
408
- * read rather than sized s/m/l, so the choice is made by the writer:
347
+ * ch resolves against the font-size of the element it is DECLARED on, so a step
348
+ * goes on the paragraph itself, never on a wrapper holding two type sizes. The
349
+ * four steps are the reading band (44–72ch), named for what is being read:
409
350
  * caption one or two sentences under a glyph, centred
410
351
  * lede the line under a title
411
352
  * body a left-aligned column of copy
412
353
  * dense reference prose set below 13px, where the band runs wider
413
- * --prose-display is NOT a measure. It is where a display headline rags, and
414
- * it is far under the band on purpose. */
354
+ * --prose-display is NOT a measure it is where a display headline rags. */
415
355
  --prose-display: 14ch;
416
356
  --prose-caption: 44ch;
417
357
  --prose-lede: 54ch;