@apliteni/apliteni-ui 0.23.1 → 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.
@@ -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,
@@ -1,22 +1,13 @@
1
- // Success / confirmation surface the emotional high point of a flow, done
2
- // with craft. One factory, three layouts and three backdrops, an SVG check
3
- // that draws itself in, optional confetti and an optional auto-redirect
4
- // countdown. Every motion path is reduced-motion safe (static check, no burst,
5
- // no confetti, no sweep). Accent-aware via the kit tokens; the success mark
6
- // stays on the --green family. Styles ship in styles/success.css.
1
+ // Success / confirmation surface. One factory, three layouts and three
2
+ // backdrops, an SVG check that draws itself in, optional confetti and an
3
+ // optional auto-redirect countdown. Every motion path is reduced-motion safe.
4
+ // Accent-aware, but the success mark stays on the --green family.
7
5
  //
8
- // container.innerHTML = success({
9
- // title: 'Feedback sent',
10
- // body: 'It goes straight to the strategy owner.',
11
- // actions: [
12
- // { label: 'Back to strategy', variant: 'primary', icon: 'compass' },
13
- // { label: 'Send another', variant: 'ghost' },
14
- // ],
15
- // });
6
+ // container.innerHTML = success({ title, body, actions: [{ label, variant }] });
16
7
  //
17
- // The check animation is pure CSS, so string-rendered markup animates on its
18
- // own once mounted. A live countdown (ticking numbers + redirect) is opt-in via
19
- // wireSuccess(); the markup alone shows the ring sweep + a static number.
8
+ // The check animation is pure CSS, so string-rendered markup animates on its own
9
+ // once mounted. A live countdown is opt-in via wireSuccess(); the markup alone
10
+ // shows the ring sweep and a static number.
20
11
  import { esc, button } from './index.js';
21
12
 
22
13
  // Self-drawing check: a faint track disc, a filled accent disc that springs in,
@@ -52,19 +52,15 @@ export function versionSwitcher(versions = [], activeIdx = 0) {
52
52
  `<div class="vsw__menu" data-dropdown-panel role="listbox" aria-label="Version">${opts}</div></div>`;
53
53
  }
54
54
 
55
- // `nav` ([id, icon, label, href?, target?][]) mirrors the account sidebar so the
56
- // dropdown and the sidebar stay in sync. The fallback is derived from the one
57
- // ACCOUNT_NAV definition rather than restated here: a second literal agreed
58
- // with it by hand about the icon and disagreed about the encoding, which is the
59
- // drift #127 was filed about. Every field below is interpolated raw, so what
60
- // arrives has to arrive escaped — accountMenuNav() is what does that.
55
+ // `nav` mirrors the account sidebar, DERIVED from the one ACCOUNT_NAV definition
56
+ // rather than restated: a second literal agreed with it by hand about the icon
57
+ // and disagreed about the encoding, which is the drift #127 was filed about.
58
+ // Every field below is interpolated raw, so what arrives has to arrive escaped.
61
59
  //
62
- // `initials` is the avatar, for a caller that escapes on the way in. A mark is
63
- // derived from the reader's name, and a derived value has to be derived before
64
- // the escaping: `<Ada>` and `&lt;Ada&gt;` do not begin with the same character,
65
- // so shell.js which escapes both fields for this sink computes the mark
66
- // from the caller's own strings and passes it down beside them. Left out, it is
67
- // computed here from `name` and `email`, exactly where it always came from.
60
+ // `initials` is the avatar. A derived value has to be derived BEFORE the
61
+ // escaping `<Ada>` and `&lt;Ada&gt;` do not begin with the same character so
62
+ // shell.js computes the mark from the caller's own strings and passes it down
63
+ // beside them. Left out, it is computed here from `name` and `email`.
68
64
  export function accountMenu({
69
65
  name = 'Ada Lovelace', email = 'ada@apliteni.com', active = 'prefs', nav, initials: mark,
70
66
  } = {}) {
@@ -102,23 +102,10 @@ a {
102
102
 
103
103
  /* Sensible default size for inline icons that a parent rule doesn't size.
104
104
  A floor, not a ceiling: :where() holds the whole filter at zero specificity,
105
- so this weighs (0,0,1) one element, nothing more and any component rule
106
- that sizes an icon outranks it. Written bare, :not([width]):not([height])
107
- would count both attribute selectors and weigh (0,2,1), which beats every
108
- `.ui-btn svg`-shaped rule in the kit no matter what order the files load in.
109
- It did, for every one of them bar three, and for `.ui-fbck` besides — that
110
- one is a class on the svg itself, so it is easy to miss when you go looking.
111
- The count is deliberately not written here: it moved once already. Two gates
112
- keep the rules that compete with this one honest, each over the ground it
113
- actually sweeps: src/styles/icon-size.test.js reads the stylesheets
114
- src/index.css imports, and scripts/icon-size-surfaces.test.js reads the
115
- surfaces the kit renders — the landing site's pages and the Storybook stories.
116
- Neither claims more, and the second one's header lists what it declines to
117
- claim; read it before trusting a green run. Two gaps are worth knowing here: a
118
- reset scoped to an ancestor that a subject's own selector does not name is out
119
- of reach of both, and .storybook/preview.js imports src/index.css into every
120
- story iframe, which makes .storybook/ a third rendering surface neither gate
121
- sweeps. Nothing in there sizes an icon today. */
105
+ so this weighs (0,0,1) and any component rule that sizes an icon outranks it.
106
+ The count of those rules is deliberately not written here — it moved once
107
+ already, and two gates hold it over the ground each actually sweeps.
108
+ why: CONTRIBUTING.md#the-reset-is-a-floor-and-its-specificity-is-the-whole-of-that */
122
109
  svg:where(:not([width]):not([height])) {
123
110
  width: 1.1em;
124
111
  height: 1.1em;
@@ -49,18 +49,10 @@
49
49
 
50
50
  /* status → the paint tokens every style below consumes.
51
51
  --toast-accent is the status as a line or a small mark, --toast-glow its wash,
52
- --toast-on the glyph ink on the accent circle. --toast-solid is separate on
53
- purpose: a full fill is a different job from a 22px circle, and it is chosen
54
- as a PAIR with --signal-solid-ink rather than against a global. See #149.
55
-
56
- --toast-action-ink is separate from --toast-accent for the same reason the
57
- fill is: the accent is a graphic colour, sized for a 3px rule and a 22px
58
- circle, and in the light theme it is not a colour text can be set in. The
59
- trailing action is the one part of a toast that is BOTH the status colour and
60
- a piece of text, so it takes the chip inks — the text-grade version of the
61
- same five statuses, and the same value as the accent in dark by construction.
62
- neutral's --toast-on is --signal-solid-ink because the neutral circle IS
63
- --signal-solid-neutral: one fill, so one ink, chosen once. #206 */
52
+ --toast-on the glyph ink on the accent circle. --toast-solid and
53
+ --toast-action-ink are separate from the accent on purpose: a fill and a piece
54
+ of text are different jobs from a 22px circle.
55
+ why: docs/specification.md#colour-and-contrast */
64
56
  .ui-toast--success { --toast-accent: var(--green); --toast-action-ink: var(--chip-success-ink); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
65
57
  .ui-toast--danger { --toast-accent: var(--pink); --toast-action-ink: var(--chip-danger-ink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
66
58
  .ui-toast--warn { --toast-accent: var(--amber); --toast-action-ink: var(--chip-warn-ink); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
@@ -1,19 +1,16 @@
1
- /* ============================================================================
2
- * Confirm the kit's confirmation dialog: a centred panel over a scrim, for
3
- * the question a destructive action has to ask. Fully token-driven so it
4
- * re-themes across accents + light/dark. Shares its scrim + focus-trap
5
- * behaviour with the drawer (see overlay.js).
1
+ /* Confirm — the kit's confirmation dialog: a centred panel over a scrim, for the
2
+ * question a destructive action has to ask. Fully token-driven, and it shares
3
+ * its scrim and focus-trap behaviour with the drawer (see overlay.js).
6
4
  *
7
- * Motion is the drawer's, deliberately: --dur-med/--ease, and `visibility`
8
- * switched rather than transitioned. A transitioned visibility is still
5
+ * `visibility` is SWITCHED rather than transitioned: transitioned, it is still
9
6
  * `hidden` in the frame the class lands, so openConfirm()'s focus() would have
10
- * nothing to focus and the reader would be left on <body>. The root owns the
11
- * switch; the panel and scrim inherit it.
7
+ * nothing to focus. The root owns the switch; panel and scrim inherit it.
12
8
  *
13
- * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
9
+ * Local tokens so the panel and scrim re-theme cleanly:
14
10
  * --confirm-surface panel background --confirm-shadow panel elevation
15
- * --confirm-scrim backdrop colour (--scrim already re-themes per theme)
16
- * ========================================================================== */
11
+ * --confirm-scrim backdrop colour
12
+ *
13
+ * why: docs/specification.md#motion */
17
14
 
18
15
  .ui-confirm {
19
16
  --confirm-surface: var(--surface-2);
@@ -1,30 +1,15 @@
1
- /* ============================================================================
2
- * Loading and permission-denied the screen-scale states.
1
+ /* Loading and permission-denied — the screen-scale states. Markup comes from
2
+ * components/loading.js; this file owns layout and nothing else.
3
3
  *
4
- * Markup comes from components/loading.js:
4
+ * The shimmer is .m-skeleton from styles/motion.css, so reduced motion is
5
+ * already handled by the global net and there is one shimmer in the kit.
5
6
  *
6
- * <div class="ui-busy" role="status" aria-live="polite" aria-busy="true">
7
- * <span class="ui-sr">Loading your report…</span>
8
- * <div class="ui-busy__body">
9
- * <div class="ui-skel" aria-hidden="true">
10
- * <span class="ui-skel__bar m-skeleton"></span>…
7
+ * .ui-denied deliberately borrows every colour .ui-empty uses: to a reader the
8
+ * two are the same event — what you came for is not here — and giving denial its
9
+ * own red made it read as a fault the reader had committed. The lock says which
10
+ * of the two it is.
11
11
  *
12
- * <div class="ui-denied">
13
- * <div class="ui-denied__seal">…lock…</div>
14
- * <div class="ui-denied__title">You don’t have access</div>
15
- * <div class="ui-denied__sub">…</div>
16
- * <div class="ui-denied__need">Needs <code class="ui-code">reports.read</code></div>
17
- * <div class="ui-denied__actions">…</div>
18
- *
19
- * The shimmer is .m-skeleton from styles/motion.css — this file owns layout and
20
- * nothing else about the animation, so reduced motion is already handled by the
21
- * global block at the end of that file and there is one shimmer in the kit.
22
- *
23
- * .ui-denied deliberately borrows every colour .ui-empty uses. To a reader the
24
- * two are the same event — what you came for is not here — and giving denial
25
- * its own red made it read as a fault the reader had committed. The lock says
26
- * which of the two it is.
27
- * ========================================================================== */
12
+ * why: docs/specification.md#pending-and-denied-states */
28
13
 
29
14
  /* Visually hidden, still read aloud. The kit had no such utility, and the live
30
15
  region needs one: its message is for assistive tech only — the sighted
@@ -1,27 +1,19 @@
1
- /* ============================================================================
2
- * apliteni-ui MOTION LIBRARY
3
- *
4
- * A small, token-driven, framework-agnostic motion system: reusable entrance,
5
- * micro-interaction, attention, and scroll-reveal effects as plain classes.
6
- * Every timing reads the brand motion vocabulary (--duration-* / --easing-*,
7
- * synced from design-system) so motion re-themes and stays consistent.
1
+ /* apliteni-ui — motion library. Reusable entrance, micro-interaction, attention
2
+ * and scroll-reveal effects as plain classes, every timing reading the brand
3
+ * motion vocabulary.
8
4
  *
9
5
  * Per-effect overrides via custom properties on the element:
10
- * --m-dur duration (default: per effect)
11
- * --m-ease timing function (default: per effect)
12
- * --m-delay start delay (default: 0s)
13
- * --m-dist travel distance for slides (default: 16px)
6
+ * --m-dur duration --m-ease timing function
7
+ * --m-delay start delay --m-dist slide travel (default 16px)
8
+ *
9
+ * Scroll reveals need `initReveal()` from apliteni-ui/motion. Without JS,
10
+ * [data-reveal] content stays fully visible.
14
11
  *
15
- * Scroll reveals need one line of JS the optional `initReveal()` hook from
16
- * apliteni-ui/motion. Without JS, [data-reveal] content stays fully visible
17
- * (progressive enhancement, never hidden behind a script that didn't run).
12
+ * The global prefers-reduced-motion net lives in reduced-motion.css and is NOT
13
+ * @imported from here the icon gates read each stylesheet on its own and
14
+ * refuse a sheet that pulls in another.
18
15
  *
19
- * The global prefers-reduced-motion net that neutralises ALL of the above — plus
20
- * any component animation in the kit — lives in reduced-motion.css, because the
21
- * React bundle needs the same net and two copies would drift. index.css imports
22
- * it beside this file; it is NOT @imported from here, because the icon gates read
23
- * each stylesheet on its own and refuse a sheet that pulls in another.
24
- * ========================================================================== */
16
+ * why: docs/specification.md#motion */
25
17
 
26
18
  /* -- Entrances -------------------------------------------------------------
27
19
  * `both` fill so the element starts at the "from" frame (hidden) and holds
@@ -1,24 +1,13 @@
1
- /* ============================================================================
2
- * apliteni-ui REDUCED-MOTION NET
1
+ /* apliteni-ui — reduced-motion net. One file, because it has to reach every
2
+ * bundle the kit publishes and a second copy would drift; a consumer who imports
3
+ * both bundles gets it twice, which costs nothing, since every rule here is
4
+ * idempotent and `!important`.
3
5
  *
4
- * One file, because it has to reach every bundle the kit publishes and a second
5
- * copy of it would drift. `src/styles/motion.css` imports it, so `apliteni-ui/css`
6
- * carries it; `react/src/index.ts` imports it, so `apliteni-ui/react/css` carries
7
- * it too. A consumer who imports both gets it twice, which costs nothing: every
8
- * rule here is idempotent and `!important`.
6
+ * `iteration-count: 1` lets one-shot animations settle on their final frame a
7
+ * checkmark stays drawn instead of snapping back to the start.
9
8
  *
10
- * It neutralises every animation and transition in whatever bundle it lands in —
11
- * the motion library's effects, component motion (button loader, badge pulse,
12
- * feedback check), and smooth scroll. `iteration-count: 1` lets one-shot
13
- * animations settle on their final frame (a checkmark stays drawn) instead of
14
- * snapping back to the start.
15
- *
16
- * `0.01ms` is the kill-switch idiom, not a duration: it is short enough that
17
- * nothing is perceptible and non-zero so `transitionend`/`animationend` still
18
- * fire, which is what scripts that wait for a close animation depend on. It is
19
- * the one time literal in the kit that is not a timing — see
20
- * docs/specification.md#motion.
21
- * ========================================================================== */
9
+ * why: docs/specification.md#reduced-motion-travels-with-the-stylesheet
10
+ * why: docs/specification.md#motion */
22
11
 
23
12
  @media (prefers-reduced-motion: reduce) {
24
13
  html { scroll-behavior: auto !important; }
@@ -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;
@@ -53,18 +49,10 @@
53
49
  .ui-table--hover:not(.ui-table--zebra) tbody tr:hover td:last-child {
54
50
  border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); }
55
51
 
56
- /* ── Data-table modifiers (composable) ──────────────────────────────────────
57
- * Compose for a dense financial ledger: .ui-table--dense --zebra --hover.
58
- * --dense compact row rhythm for many-row / many-column data: --space-3
59
- * across and --space-2 down, one step in from the base table's
60
- * --space-4 across and --space-4 down. Both of its old numbers sat
61
- * exactly between two steps — 14px between 12 and 16, 10px between 8
62
- * and 12 — and both round DOWN, because the modifier exists to fit
63
- * more rows and rounding up spends the distinction it is for.
64
- * --zebra row striping instead of per-row rules; 2px header rule; the
65
- * (accent) hover tint out-ranks the stripe (defined after it)
66
- * Cell helpers: .ui-table__num (right, tabular), .ui-table__code (plain
67
- * monospace id — never a boxed code chip inside a table).
52
+ /* Data-table modifiers, composable: .ui-table--dense --zebra --hover.
53
+ * --dense --space-3 across, --space-2 down one step in from the base table
54
+ * --zebra row striping; 2px header rule; the hover tint out-ranks the stripe
55
+ * Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
68
56
  * why: docs/specification.md#spacing-and-rhythm
69
57
  * held by: stories/table-rhythm.test.js */
70
58
  .ui-table--dense th { padding: 0 var(--space-3) var(--space-2) 0; }
@@ -77,15 +65,11 @@
77
65
  .ui-table--zebra th { border-bottom-width: 2px; }
78
66
  .ui-table--zebra td { border-bottom: 0; }
79
67
  .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
80
- /* The stripe is full-bleed on purpose the hover note above inset the hover fill for
81
- * #71 and deliberately left the ledger stripe alone. But full-bleed only reads right if
82
- * the end cells are inset from the tint's own edge, and the base rhythm does not inset
83
- * them: `.ui-table td` sets `padding-left: 0`. Only `--dense` supplied the inset, so a
84
- * `--zebra` table composed WITHOUT `--dense` put the first cell's content at exactly
85
- * x=0 of the stripe. `DataTable` is that composition — it writes `ui-table
86
- * ui-table--hover ui-table--zebra` and no `--dense` — so its select-all and per-row
87
- * checkboxes sat flush against the tint with a measured 0px gap.
88
- * 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. */
89
73
  .ui-table--zebra th:first-child,
90
74
  .ui-table--zebra td:first-child { padding-left: var(--space-3); }
91
75
  .ui-table--zebra th:last-child,
@@ -1,23 +1,19 @@
1
- /* ============================================================================
2
- * Accent sub-themes — an orthogonal `data-accent` dimension on top of
3
- * `data-theme` (dark/light). Set both on <html>:
1
+ /* Accent sub-themes — an orthogonal `data-accent` dimension on top of
2
+ * `data-theme`. Set both on <html>:
4
3
  *
5
4
  * <html data-theme="dark" data-accent="phoenix">
6
5
  *
7
- * Each sub-theme only re-points the accent family (--accent, --purple*,
8
- * --glow-purple, --grad-*). Surfaces, text and signal colours (green =
9
- * live, pink = danger) stay put, so every accent works in light AND dark and
10
- * every component follows with zero component-level changes.
6
+ * Each sub-theme only re-points the accent family. Surfaces, text and signal
7
+ * colours (green = live, pink = danger) stay put, so every accent works in
8
+ * light AND dark and every component follows with no component-level change.
11
9
  *
12
- * --ring is NOT here. It is declared once, in tokens.css, as var(--accent), so
13
- * re-pointing --accent above re-points the focus ring too. Six hand-written
14
- * rgba() rings used to sit in this file and every one of them missed the 3:1
15
- * of WCAG 1.4.11 — 1.35:1 at worst. See #218 and
16
- * docs/specification.md#the-focus-ring.
10
+ * --ring is NOT here: it is declared once in tokens.css as var(--accent), so
11
+ * re-pointing --accent re-points the focus ring too.
17
12
  *
18
13
  * Selectors carry two attributes + :root, so they always out-specify the
19
14
  * single-attribute theme blocks in tokens.css regardless of import order.
20
- * ========================================================================== */
15
+ *
16
+ * why: docs/specification.md#the-focus-ring */
21
17
 
22
18
  /* ---- Phoenix — ember / rising fire (the strategy's namesake) ------------- */
23
19
  :root[data-theme="dark"][data-accent="phoenix"] {
@@ -66,25 +62,15 @@
66
62
  --purple: #1156b8;
67
63
  --purple-light: #1568d6;
68
64
  --purple-mid: #1560c8;
69
- /* Deepened for WCAG AA on the raised grey panel and its wash: #1568d6 read
70
- 4.54:1 on --surface-3 and 3.98:1 on --glow-purple over it; #005bc8 clears
71
- both at 5.43:1 and 4.76:1. Both washed figures are at the 0.10 alpha below,
72
- which is what this cell ships — an earlier version of this comment quoted
73
- them at the 0.05 the wash briefly carried, which flattered both inks.
74
- Light Ocean is the one light accent #96 never deepened, because it happened
75
- to clear plain white. Same kind of move, smaller: hue and chroma unchanged
76
- in OKLCH (H 258, C 0.185) and four points of lightness down, where #96's
77
- two took about ten points and gave up 15.9% of the chroma on Phoenix and
78
- 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. */
79
68
  --accent: #005bc8;
80
69
  --accent-strong: var(--accent);
81
70
  --accent-contrast: #ffffff;
82
- /* Back to 0.10, the alpha this cell carried before #157. #157 thinned it to
83
- 0.05 while --accent was still #1568d6, because no alpha could carry that
84
- blue over the grey panel. Deepening the ink is what closed the pair, so the
85
- wash does not have to be the thing that gives. The rgb stays --purple-light
86
- rather than the accent — in light a glow tints --purple-light, which
87
- 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. */
88
74
  --glow-purple: rgba(21, 104, 214, 0.10);
89
75
  --grad-from: #1568d6;
90
76
  --grad-to: #17a2c0;