@apliteni/apliteni-ui 0.8.0 → 0.9.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
@@ -105,7 +105,12 @@ import { DataTable, Modal } from '@apliteni/apliteni-ui/react';
105
105
  ```
106
106
 
107
107
  The source lives in [`react/`](./react) — a private workspace with its own build
108
- (tsup) and Storybook on port 6007. Details in [`react/README.md`](./react/README.md).
108
+ (tsup) and Storybook on port 6007. When 6007 is taken Storybook moves to the next
109
+ free port, so the root Storybook does not trust the number: it probes the range
110
+ 6007 can drift into and composes the first port that proves it is this workspace's
111
+ Storybook. A stranger on the port is never composed — the "React components"
112
+ section is absent instead, and the terminal says why. Details in
113
+ [`react/README.md`](./react/README.md).
109
114
 
110
115
  ## Theming
111
116
 
@@ -147,7 +152,7 @@ react/ # React components — private workspace, built to reac
147
152
 
148
153
  ```bash
149
154
  npm install
150
- npm run storybook # http://localhost:6006
155
+ npm run storybook # http://localhost:6006, or the next free port
151
156
  npm run build-storybook # -> storybook-static/
152
157
  node site/build.mjs # -> site/public/ (landing + kit.css + /storybook)
153
158
  ```
@@ -156,7 +161,7 @@ The React components build and test through the workspace (`npm install` at the
156
161
  root covers them — there is no second install):
157
162
 
158
163
  ```bash
159
- npm run storybook -w react # http://localhost:6007
164
+ npm run storybook -w react # http://localhost:6007, or the next free port
160
165
  npm test -w react # vitest
161
166
  npm run build # tsup -> react/dist/ (also runs on prepare)
162
167
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -41,6 +41,17 @@ npm run storybook -w react # http://localhost:6007
41
41
  npm run build # tsup -> react/dist/
42
42
  ```
43
43
 
44
+ 6007 is a request, not a promise: if something else already holds it, Storybook
45
+ walks upward to the next free port. The root Storybook composes these components
46
+ by following that drift — it probes 6007 through 6016 (the window Storybook's own
47
+ port-finder searches) and takes the first one whose `index.json` lists every story
48
+ file this workspace has on disk. A stranger on the port fails that check and is
49
+ never shown under "React components"; the section is simply absent, and the root
50
+ Storybook's terminal says which ports it tried and what it found.
51
+
52
+ The probe runs once, while the root Storybook boots. Start this one afterwards and
53
+ you have to restart the root Storybook before it appears.
54
+
44
55
  The bare `@apliteni/apliteni-ui` specifier in this source resolves to the kit itself
45
56
  once installed. In the repo there is no copy to resolve to, so `kit-alias.ts` points
46
57
  vitest and Storybook straight at `../src/` — which is why the class-name parity tests
@@ -235,6 +235,14 @@ export function toast({
235
235
  + `<div class="ui-toast__body">${title ? `<div class="ui-toast__title">${esc(title)}</div>` : ''}${bodyHtml}</div>`
236
236
  + `${actBtn}${closeBtn}${timerBar}</div>`;
237
237
  }
238
+ // The inline confirmation. A check, a title and one line of sub, dropped into
239
+ // the page the user is already on — under a form that just submitted, or inside
240
+ // a card. Two strings, no options; styles are .ui-success in styles/callout.css.
241
+ //
242
+ // The kit also publishes success() from components/success.js, which is the same
243
+ // idea at page size: layouts, a backdrop, follow-up buttons and an auto-redirect
244
+ // countdown. Pick by how much of the screen the confirmation owns. The two share
245
+ // no CSS, so changing one never moves the other.
238
246
  export function successPanel({ title = 'Done', sub = '' } = {}) {
239
247
  return `<div class="ui-success"><div class="ui-success__check">${icon('check')}</div><div class="ui-success__title">${esc(title)}</div>${sub ? `<div class="ui-success__sub">${esc(sub)}</div>` : ''}</div>`;
240
248
  }
@@ -61,6 +61,16 @@ function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
61
61
  </div>`;
62
62
  }
63
63
 
64
+ // The page-sized confirmation — what a flow lands on once it is over. It fills
65
+ // the space it is given, and carries the things a screen needs that a block does
66
+ // not: somewhere to go next, and optionally a countdown that takes the user
67
+ // there.
68
+ //
69
+ // For a confirmation that stays inside the page the user is already on, the kit
70
+ // publishes successPanel() from components/index.js — a check, a title and one
71
+ // line of sub, and nothing to configure. Pick by how much of the screen the
72
+ // confirmation owns. The two share no CSS (.ui-sx here, .ui-success there), so
73
+ // changing one never moves the other.
64
74
  export function success({
65
75
  layout = 'hero', // 'hero' | 'split' | 'compact'
66
76
  backdrop = 'aurora', // 'aurora' | 'glow' | 'flat'
@@ -7,10 +7,28 @@ import { wireDropdown } from './dropdown.js';
7
7
 
8
8
  const THEME_KEY = 'apliteni-strategy-theme';
9
9
 
10
- export function themeToggle() {
11
- // Single icon-only switch (sun in dark, moon in light). Icon pre-filled so it
12
- // renders before applyTheme() runs; aria-label carries the accessible name.
13
- return `<button class="toggle" data-theme-toggle aria-label="Toggle theme" title="Toggle light / dark"><span class="ic" data-theme-icon>${sun}</span></button>`;
10
+ // A stateful control in this kit reports the state it is IN, never the state a
11
+ // click would produce the same reading as segmented()'s aria-pressed, the
12
+ // Deck/Text switch's aria-current, and the accent chips. So: moon while dark,
13
+ // sun while light. Storybook's own toolbar toggle (.storybook/theme-toggle.jsx)
14
+ // reads the same way, and topbar stories render directly beneath it.
15
+ export const themeIcon = (t) => (t === 'light' ? sun : moon);
16
+
17
+ // The name carries both the state and what the click does, which is why there
18
+ // is no aria-pressed and no role="switch": dark and light are peers, not on and
19
+ // off, and an ARIA state on top of this name would announce the theme twice.
20
+ // The button is icon-only, so `title` is the only sighted reading of the same
21
+ // fact and carries the identical string — nothing for WCAG 2.5.3 to disagree
22
+ // with. Both are rewritten by applyTheme on every flip; a name that is right
23
+ // once and never again tells a screen-reader user nothing.
24
+ export const themeName = (t) =>
25
+ (t === 'light' ? 'Theme: Light. Switch to dark.' : 'Theme: Dark. Switch to light.');
26
+
27
+ export function themeToggle(theme = 'dark') {
28
+ // Single icon-only switch. Icon and name are pre-filled for `theme` so the
29
+ // first paint is already truthful; applyTheme() keeps them so afterwards.
30
+ const name = esc(themeName(theme));
31
+ return `<button class="toggle" data-theme-toggle aria-label="${name}" title="${name}"><span class="ic" data-theme-icon>${themeIcon(theme)}</span></button>`;
14
32
  }
15
33
 
16
34
  export function deckTextSwitch(active = 'deck') {
@@ -69,11 +87,14 @@ export function topbar({
69
87
  // ---- Behaviours (call once after markup mounts) --------------------------
70
88
  export function applyTheme(t, root = document.documentElement) {
71
89
  root.setAttribute('data-theme', t);
90
+ const name = themeName(t);
72
91
  root.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
73
92
  const ic = btn.querySelector('[data-theme-icon]');
74
- const lbl = btn.querySelector('[data-theme-label]');
75
- if (ic) ic.innerHTML = t === 'dark' ? sun : moon;
76
- if (lbl) lbl.textContent = t === 'dark' ? 'Light' : 'Dark';
93
+ if (ic) ic.innerHTML = themeIcon(t);
94
+ // The announced name is the point of the control, so it moves with the
95
+ // theme like the glyph does.
96
+ btn.setAttribute('aria-label', name);
97
+ btn.setAttribute('title', name);
77
98
  });
78
99
  try { localStorage.setItem(THEME_KEY, t); } catch (e) { /* no-op */ }
79
100
  }
package/src/index.js CHANGED
@@ -7,8 +7,10 @@ export * from './components/nav.js';
7
7
  export * from './components/drawer.js';
8
8
  export * from './components/topbar.js';
9
9
  export * from './components/shell.js';
10
+ export * from './components/footer.js';
10
11
  export * from './components/feedback.js';
11
12
  export * from './components/toasts.js';
13
+ export * from './components/success.js';
12
14
  export * from './assets/icons.js';
13
15
  export * from './assets/brand.js';
14
16
  export * from './motion.js';
package/src/inline.js CHANGED
@@ -39,6 +39,7 @@ export const styles = {
39
39
  nav: read('styles/nav.css'),
40
40
  drawer: read('styles/drawer.css'),
41
41
  table: read('styles/table.css'),
42
+ empty: read('styles/empty.css'),
42
43
  callout: read('styles/callout.css'),
43
44
  code: read('styles/code.css'),
44
45
  topbar: topbarCss,
@@ -63,6 +64,7 @@ export const cssText = [
63
64
  styles.nav,
64
65
  styles.drawer,
65
66
  styles.table,
67
+ styles.empty,
66
68
  styles.callout,
67
69
  styles.code,
68
70
  styles.topbar,
@@ -24,7 +24,7 @@
24
24
  wash out and the ink has to deepen to clear AA, and that is a token decision. */
25
25
  .ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
26
26
  .ui-badge--soon { color: var(--purple-mid); background: var(--glow-purple); }
27
- .ui-badge--info { color: var(--cyan); background: var(--glow-cyan); }
27
+ .ui-badge--info { color: var(--chip-info-ink); background: var(--chip-info-fill); }
28
28
  .ui-badge--warn { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
29
29
  .ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); }
30
30
  .ui-badge--archive { color: var(--muted); background: var(--surface); }
@@ -54,7 +54,10 @@
54
54
  padding: 4px 11px;
55
55
  font-weight: var(--weight-semibold);
56
56
  }
57
- .ui-pill--live { color: var(--green); background: var(--glow-green); }
57
+ /* The live pill is the same status paint as the live badge, so it takes the
58
+ same pair. On its own it was --green on --glow-green, which is 3.97:1 over
59
+ white — under AA, while the badge beside it cleared at 4.64:1. See issue #131. */
60
+ .ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
58
61
  .ui-pill--soon { color: var(--purple-mid); background: var(--glow-purple); }
59
62
 
60
63
  /* Status dot */
@@ -101,9 +101,17 @@ a {
101
101
  }
102
102
 
103
103
  /* Sensible default size for inline icons that a parent rule doesn't size.
104
- Component rules (.ui-btn svg, .ui-card__icon svg…) come later and win the
105
- tie, so they always override this it only catches bare icon() calls. */
106
- svg:not([width]):not([height]) {
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, and the
112
+ rules outside this package (site/, the Iconography story) are not in it.
113
+ src/styles/icon-size.test.js keeps the ones in src/styles honest. */
114
+ svg:where(:not([width]):not([height])) {
107
115
  width: 1.1em;
108
116
  height: 1.1em;
109
117
  flex: none;
@@ -29,7 +29,7 @@
29
29
  /* ----------------------------------------------------------------------------
30
30
  * Toast — a floating, dismissible notification.
31
31
  * A status (colour) × style (surface) matrix, entirely token-driven:
32
- * status success · danger · warn · info · neutral → sets --toast-accent/-glow/-on
32
+ * status success · danger · warn · info · neutral → sets --toast-accent/-glow/-on/-solid
33
33
  * style soft (tinted) · solid (filled) · outline (bordered)
34
34
  * -------------------------------------------------------------------------- */
35
35
  .ui-toast {
@@ -47,13 +47,16 @@
47
47
  animation: ui-toast-in 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
48
48
  }
49
49
 
50
- /* status → the paint tokens every style below consumes (--toast-on = the ink
51
- that reads on the accent when it becomes a fill) */
52
- .ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); }
53
- .ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); }
54
- .ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); }
55
- .ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); }
56
- .ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); }
50
+ /* status → the paint tokens every style below consumes.
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
+ .ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
56
+ .ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
57
+ .ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
58
+ .ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
59
+ .ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); --toast-solid: var(--signal-solid-neutral); }
57
60
 
58
61
  /* left status marker (soft + outline; solid is already a full fill) */
59
62
  .ui-toast--soft::before,
@@ -83,17 +86,32 @@
83
86
  /* style: soft — tinted surface */
84
87
  .ui-toast--soft { background: var(--toast-glow); box-shadow: var(--shadow-sm); }
85
88
 
86
- /* style: solid — filled accent, ink flips to --toast-on */
87
- .ui-toast--solid { background: var(--toast-accent); color: var(--toast-on); box-shadow: var(--shadow-lg); }
88
- .ui-toast--solid .ui-toast__title,
89
- .ui-toast--solid .ui-toast__text { color: var(--toast-on); }
90
- .ui-toast--solid .ui-toast__text { opacity: 0.82; }
91
- .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-on) 22%, transparent); color: var(--toast-on); }
92
- .ui-toast--solid .ui-toast__action { color: var(--toast-on); }
93
- .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-on) 18%, transparent); }
94
- .ui-toast--solid .ui-toast__close { color: color-mix(in srgb, var(--toast-on) 68%, transparent); }
95
- .ui-toast--solid .ui-toast__close:hover { color: var(--toast-on); }
96
- .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-on) 55%, transparent); }
89
+ /* style: solid — the status at fill strength, inked with the pole opposite it.
90
+ The fill and the ink are one pair, taken together from --signal-solid-*: dark
91
+ fills are the bright signals and take near-black, light fills are the deepened
92
+ ones and take white. What this replaced picked the fill from the status and
93
+ the ink from one of two globals, on independent axes, so nothing made them
94
+ clear each other and four of the ten status x theme combinations did not.
95
+ Gated for all ten in stories/signal-contrast.test.js. See #149. */
96
+ .ui-toast--solid {
97
+ --toast-ink: var(--signal-solid-ink);
98
+ background: var(--toast-solid);
99
+ color: var(--toast-ink);
100
+ box-shadow: var(--shadow-lg);
101
+ }
102
+ .ui-toast--solid .ui-toast__title { color: var(--toast-ink); }
103
+ /* The body copy is the quieter of the two, but it stays quiet by dilution the
104
+ pair can carry: 90% of the ink measures 4.71 on the worst fill in the matrix
105
+ (--chip-success-ink in light). The opacity: 0.82 this replaced measured 4.19
106
+ there, and 2.25 on the dark danger fill — the pair was correct and the
107
+ dilution then spent it. */
108
+ .ui-toast--solid .ui-toast__text { color: color-mix(in srgb, var(--toast-ink) 90%, transparent); }
109
+ .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-ink) 22%, transparent); color: var(--toast-ink); }
110
+ .ui-toast--solid .ui-toast__action { color: var(--toast-ink); }
111
+ .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-ink) 18%, transparent); }
112
+ .ui-toast--solid .ui-toast__close { color: color-mix(in srgb, var(--toast-ink) 68%, transparent); }
113
+ .ui-toast--solid .ui-toast__close:hover { color: var(--toast-ink); }
114
+ .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }
97
115
 
98
116
  /* style: outline — elevated card with a status-tinted border */
99
117
  .ui-toast--outline { background: var(--bg-elevated); box-shadow: var(--shadow-md); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
@@ -102,7 +102,7 @@
102
102
  color: var(--muted);
103
103
  background: var(--surface);
104
104
  }
105
- .ui-dropdown__badge.is-live { color: var(--green); background: var(--glow-green); }
105
+ .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
106
106
  .ui-dropdown__badge.is-accent { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
107
107
 
108
108
  /* Disabled + danger rows */
@@ -67,10 +67,15 @@
67
67
  margin-top: 12px;
68
68
  }
69
69
 
70
- /* Legacy line-icon slot — kept so existing .ui-empty__icon markup still works. */
70
+ /* Legacy line-icon slot — kept so existing .ui-empty__icon markup still works.
71
+ 44px until #148. The icon inside is `width: 100%`, and that rule lost to the
72
+ base.css reset for as long as this slot has existed, so the slot's number was
73
+ never the size anyone saw: it rendered at 15.94px. Once the reset stopped
74
+ winning, 44px was a size nobody had chosen with their eyes open. 32px is the
75
+ size that was chosen, against the alternatives, on a rendered comparison. */
71
76
  .ui-empty__icon {
72
- width: 44px;
73
- height: 44px;
77
+ width: 32px;
78
+ height: 32px;
74
79
  margin: 0 auto 14px;
75
80
  color: var(--muted);
76
81
  opacity: 0.6;
@@ -6,7 +6,7 @@
6
6
  * ========================================================================== */
7
7
 
8
8
  .ui-nav { font-family: var(--font-sans); }
9
- .ui-nav ul, .ui-nav ol { list-style: none; margin: 0; padding: 0; }
9
+ .ui-nav :where(ul, ol) { list-style: none; margin: 0; padding: 0; } /* floor, not ceiling */
10
10
 
11
11
  /* -- Sidebar rail -------------------------------------------------------- */
12
12
  .ui-nav--side {
@@ -49,7 +49,7 @@
49
49
  }
50
50
  .ui-nav__item:hover { background: var(--surface-2); color: var(--strong); }
51
51
  .ui-nav__item.is-active {
52
- background: var(--surface);
52
+ background: var(--surface-3);
53
53
  color: var(--strong);
54
54
  font-weight: 500;
55
55
  }
@@ -75,7 +75,9 @@
75
75
  .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: var(--accent); }
76
76
  .ui-nav__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
77
77
 
78
- .ui-nav__item.is-danger { color: var(--pink); }
78
+ /* Destructive row (sign out, revoke). Quiet at rest, --pink once the pointer
79
+ reaches it — same two-step as .ui-btn--danger and .ui-dropdown__item.is-danger. */
80
+ .ui-nav__item.is-danger { color: var(--muted); }
79
81
  .ui-nav__item.is-danger:hover { background: var(--glow-pink); color: var(--pink); }
80
82
  .ui-nav__item.is-disabled { color: var(--muted); opacity: 0.55; cursor: default; pointer-events: none; }
81
83
 
@@ -96,7 +98,7 @@
96
98
  color: var(--dim);
97
99
  }
98
100
  .ui-nav__badge.is-accent { background: var(--glow-purple); color: var(--accent); }
99
- .ui-nav__badge.is-live { background: var(--glow-green); color: var(--green); }
101
+ .ui-nav__badge.is-live { background: var(--chip-success-fill); color: var(--chip-success-ink); }
100
102
  .ui-nav__badge.is-danger { background: var(--glow-pink); color: var(--pink); }
101
103
  .ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface-3); color: var(--strong); }
102
104
 
@@ -115,14 +117,18 @@
115
117
  display: flex;
116
118
  flex-direction: column;
117
119
  gap: 2px;
118
- margin: 2px 0 2px 15px;
119
- padding-left: 11px;
120
+ margin: var(--space-1) 0 var(--space-1) var(--space-4);
121
+ padding-left: var(--space-3);
120
122
  border-left: 1px solid var(--border);
121
123
  }
122
124
  .ui-nav__sub[hidden] { display: none; }
123
125
  .ui-nav__item--sub { font-size: 14px; padding: 7px 12px; }
124
- .ui-nav--side .ui-nav__item--sub.is-active { box-shadow: none; }
125
- .ui-nav--side .ui-nav__item--sub.is-active::before { left: -12px; height: 15px; }
126
+ /* No hairline on a nested current row — but only at rest. box-shadow carries
127
+ both the hairline and the focus ring, so a rule about the resting look must
128
+ say so, or it silently cancels the ring (WCAG 2.4.7). */
129
+ .ui-nav--side .ui-nav__item--sub.is-active:not(:focus-visible) { box-shadow: none; }
130
+ /* Hangs back onto the guide hairline the indent above draws. */
131
+ .ui-nav--side .ui-nav__item--sub.is-active::before { left: calc(-1 * var(--space-3)); height: 15px; }
126
132
 
127
133
  /* Footer slot (divider + trailing links, e.g. sign-out) */
128
134
  .ui-nav__foot { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
@@ -68,6 +68,19 @@
68
68
  .ui-table--zebra th { border-bottom-width: 2px; }
69
69
  .ui-table--zebra td { border-bottom: 0; }
70
70
  .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
71
+ /* The stripe is full-bleed on purpose — the hover note above inset the hover fill for
72
+ * #71 and deliberately left the ledger stripe alone. But full-bleed only reads right if
73
+ * the end cells are inset from the tint's own edge, and the base rhythm does not inset
74
+ * them: `.ui-table td` sets `padding-left: 0`. Only `--dense` supplied the inset, so a
75
+ * `--zebra` table composed WITHOUT `--dense` put the first cell's content at exactly
76
+ * x=0 of the stripe. `DataTable` is that composition — it writes `ui-table
77
+ * ui-table--hover ui-table--zebra` and no `--dense` — so its select-all and per-row
78
+ * checkboxes sat flush against the tint with a measured 0px gap.
79
+ * Header and body are inset together, or the columns stop lining up. */
80
+ .ui-table--zebra th:first-child,
81
+ .ui-table--zebra td:first-child { padding-left: 12px; }
82
+ .ui-table--zebra th:last-child,
83
+ .ui-table--zebra td:last-child { padding-right: 12px; }
71
84
  .ui-table--zebra.ui-table--hover tbody tr:hover td,
72
85
  .ui-table--zebra tbody tr:hover td { background: color-mix(in srgb, var(--accent) 8%, transparent); }
73
86
 
@@ -149,7 +149,7 @@
149
149
  flex: none;
150
150
  margin-top: 1px;
151
151
  }
152
- .vbadge.live { color: var(--green); background: var(--glow-green); }
152
+ .vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
153
153
  .vbadge.arch { color: var(--muted); background: var(--surface); }
154
154
  @media (max-width: 600px) { .vsw__btn .lbl { display: none; } }
155
155
 
@@ -109,7 +109,12 @@
109
109
  --purple-mid: #b479ff;
110
110
  --cyan: #20dcf5;
111
111
  --green: #98ff8f;
112
- --pink: #e35b8f;
112
+ /* Lighter than it looks like it should be, and deliberately so: --pink is read
113
+ mostly ON its own 16% glow, and that wash lifts the ground faster than the
114
+ hue lifts the ink. #e35b8f measured 3.82:1 on --glow-pink over a card —
115
+ under AA — so danger moved AWAY from the canvas, not toward it. Light moves
116
+ the opposite way for the same reason. See issue #131. */
117
+ --pink: #e97ca5;
113
118
  --amber: #ffcf6a;
114
119
 
115
120
  --bg: #16151f;
@@ -136,16 +141,25 @@
136
141
  --ink: #e9e7f0;
137
142
 
138
143
  /* Ink that reads on a SIGNAL colour once that colour becomes a fill — the
139
- glyph in a success circle, the text in a solid toast. The signal hues sit
140
- high enough in dark and mid-tone in light that near-black wins on all of
141
- them; the pink danger fill deliberately takes light ink instead. */
144
+ glyph in a success circle, the glyph on a toast's status circle.
145
+ In dark every signal is a bright hue, so one near-black ink clears all
146
+ five: 15.87 on --green, 7.35 on --pink, 13.40 on --amber, 11.75 on --cyan
147
+ and 6.29 on --muted. Danger keeps a token of its own only because LIGHT
148
+ still needs it: there the signals are deepened to read as ink on white,
149
+ which parks them mid-luminance, and --pink is the one that comes out ahead
150
+ against white (5.78) rather than against near-black (3.39).
151
+ Dark danger was white until now, which was correct while --pink was
152
+ #e35b8f. Issue #131 lightened it to #e97ca5 to clear AA as INK on its own
153
+ glow, and that carried the fill up past the point where white could read on
154
+ it — 2.66 on the toast's status circle, under the 3:1 of WCAG 1.4.11. The
155
+ ten status x theme icon pairs are gated in stories/signal-contrast.test.js. */
142
156
  --signal-contrast: #0c0c0c;
143
- --danger-contrast: #ffffff;
157
+ --danger-contrast: var(--signal-contrast);
144
158
 
145
159
  --glow-green: rgba(152, 255, 143, 0.16);
146
160
  --glow-purple: rgba(155, 93, 255, 0.18);
147
161
  --glow-cyan: rgba(32, 220, 245, 0.14);
148
- --glow-pink: rgba(227, 91, 143, 0.16);
162
+ --glow-pink: rgba(233, 124, 165, 0.16);
149
163
 
150
164
  /* Status-chip paint — the ink + fill of a badge / pill. Dark tints with the
151
165
  signal's own glow; light needs a solid, deepened pair (the glows wash out
@@ -156,6 +170,23 @@
156
170
  --chip-warn-fill: color-mix(in srgb, var(--amber) 15%, transparent);
157
171
  --chip-danger-ink: var(--pink);
158
172
  --chip-danger-fill: var(--glow-pink);
173
+ --chip-info-ink: var(--cyan);
174
+ --chip-info-fill: var(--glow-cyan);
175
+
176
+ /* Solid signal surfaces — a status that has stopped being ink and become the
177
+ FILL (a solid toast). The fill and its ink are ONE pair, never two
178
+ independent picks: the fill is the status at this theme's extreme, and the
179
+ ink is the pole opposite it, so a single ink clears all five. Dark fills are
180
+ the bright signals, so the ink is near-black — it measures 15.87 on --green,
181
+ 7.35 on --pink, 13.40 on --amber, 11.75 on --cyan and 6.29 on --muted.
182
+ Neutral needs no chip pair to take part: --muted is already its
183
+ fill-strength form. See issues #131 and #149. */
184
+ --signal-solid-ink: var(--signal-contrast);
185
+ --signal-solid-success: var(--green);
186
+ --signal-solid-danger: var(--pink);
187
+ --signal-solid-warn: var(--amber);
188
+ --signal-solid-info: var(--cyan);
189
+ --signal-solid-neutral: var(--muted);
159
190
 
160
191
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.28);
161
192
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);
@@ -191,7 +222,11 @@
191
222
  --purple-mid: #7a3be0;
192
223
  --cyan: #0c8fa8;
193
224
  --green: #1c8a2c;
194
- --pink: #d63c72;
225
+ /* Deeper than the dark pink for the same reason dark's is lighter: the wash
226
+ under it is white, so the ink has to come down to meet it. #d63c72 measured
227
+ 3.87:1 on --glow-pink over white and 4.42:1 on plain white — the required
228
+ marker and the form error missed AA on nothing at all. See issue #131. */
229
+ --pink: #b63361;
195
230
  --amber: #b5730a;
196
231
 
197
232
  /* A WHITE app: the page and cards are both white; cards are delineated by a
@@ -219,20 +254,51 @@
219
254
  --signal-contrast: #0c0c0c;
220
255
  --danger-contrast: #ffffff;
221
256
 
222
- --glow-green: rgba(30, 150, 50, 0.1);
257
+ /* Re-tinted from --green. It was rgba(30, 150, 50, .1) — a colour that exists
258
+ nowhere else in the kit, carried unedited since the initial commit while the
259
+ other three light glows are exact tints of their own token. The wash over
260
+ white moves #e9f5eb -> #e8f3ea, at most two levels on a channel, which is
261
+ 0.86 of a deltaE — under the 1.0 that is normally called imperceptible. No
262
+ verdict moves with it: body copy on the success wash goes 9.29 -> 9.15
263
+ against the 4.5 AA bar, and the --green icon on it 3.97 -> 3.91 against the
264
+ 3.0 bar WCAG 1.4.11 sets for a graphic. The success chips are untouched —
265
+ light's --chip-success-fill is the solid #dff3e4, not this. Gated for all
266
+ four glows in stories/signal-contrast.test.js. See issue #131. */
267
+ --glow-green: rgba(28, 138, 44, 0.1);
223
268
  --glow-purple: rgba(106, 45, 204, 0.09);
224
269
  --glow-cyan: rgba(12, 143, 168, 0.1);
225
- --glow-pink: rgba(214, 60, 114, 0.1);
270
+ --glow-pink: rgba(182, 51, 97, 0.1);
226
271
 
227
272
  /* Solid chip pairs for the white app. Each ink is deepened past its signal
228
273
  hue so 10px uppercase text clears WCAG AA on its own pale fill —
229
- e.g. --pink (#d63c72) on #fbe0ea is 3.6:1, #b7295f is 4.9:1. */
274
+ e.g. --pink (#b63361) on #fbe0ea is 4.66:1, #b7295f is 4.85:1. */
230
275
  --chip-success-ink: #1f7a38;
231
276
  --chip-success-fill: #dff3e4;
232
277
  --chip-warn-ink: #8a5e00;
233
278
  --chip-warn-fill: #fbedd2;
234
279
  --chip-danger-ink: #b7295f;
235
280
  --chip-danger-fill: #fbe0ea;
281
+ /* Info was the one signal family without a pair, so the info badge was still
282
+ painting --cyan (#0c8fa8) on --glow-cyan over white, which is 3.39:1 —
283
+ under AA. #0a7286 on #ddeff3 is 4.71:1, inside the 4.64–4.93 band the other
284
+ three pairs already sit in. See issue #131. */
285
+ --chip-info-ink: #0a7286;
286
+ --chip-info-fill: #ddeff3;
287
+
288
+ /* Light runs the same rule the other way. The signals are mid-tone here — a
289
+ solid --green fill measures 4.40 against near-black and 4.45 against white,
290
+ so NO ink rescues it and the fill is what has to move. It moves to the
291
+ deepened form the chip pair already carries, and the pole above it, white,
292
+ clears every one: #ffffff measures 5.39 on --chip-success-ink, 6.01 on
293
+ --chip-danger-ink, 5.70 on --chip-warn-ink, 5.58 on --chip-info-ink and 6.11
294
+ on --muted. A future status supplies one --signal-solid-* value dark enough
295
+ for white and inherits the ink. See issues #131 and #149. */
296
+ --signal-solid-ink: #ffffff;
297
+ --signal-solid-success: var(--chip-success-ink);
298
+ --signal-solid-danger: var(--chip-danger-ink);
299
+ --signal-solid-warn: var(--chip-warn-ink);
300
+ --signal-solid-info: var(--chip-info-ink);
301
+ --signal-solid-neutral: var(--muted);
236
302
 
237
303
  --shadow-sm: 0 1px 3px rgba(30, 30, 50, 0.08);
238
304
  --shadow-md: 0 8px 24px rgba(30, 30, 50, 0.1);