@apliteni/apliteni-ui 0.30.0 → 0.32.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.
@@ -0,0 +1,75 @@
1
+ // Stat band — a row of key figures, as an HTML string.
2
+ // why: docs/specification.md#stat-bands
3
+ //
4
+ // A band is a <dl>: each figure is a <div> holding its label as a <dt> and its
5
+ // value, change and trend as <dd>s, which is the grouping HTML allows inside a
6
+ // description list and the one a screen reader reads as "term, then its values".
7
+ import { esc } from './index.js';
8
+ import { icon } from '../assets/icons.js';
9
+
10
+ export const STAT_VARIANTS = ['band', 'tiles', 'open'];
11
+ export const STAT_TONES = ['good', 'bad', 'neutral'];
12
+
13
+ const cx = (...a) => a.filter(Boolean).join(' ');
14
+ let seq = 0;
15
+
16
+ // The arrow is read off the sign the caller printed, so the figure and the
17
+ // glyph beside it cannot disagree. U+2212, the en dash and an accounting
18
+ // bracket are negatives too: a formatter that typesets its figures uses them.
19
+ const directionOf = (text) => {
20
+ const t = text.trim();
21
+ return /^[-−–(]/.test(t) ? 'down' : /^\+/.test(t) ? 'up' : 'flat';
22
+ };
23
+ const GLYPH = { up: 'arrowUp', down: 'arrowDown', flat: 'minus' };
24
+ const hasChange = (delta) => delta && delta.value != null && delta.value !== '';
25
+
26
+ const deltaHtml = (delta, basisId) => {
27
+ if (!delta) return '';
28
+ if (!hasChange(delta)) {
29
+ return `<dd class="ui-stat__delta ui-stat__delta--none">${esc(delta.none || 'No earlier figure')}</dd>`;
30
+ }
31
+ const text = String(delta.value);
32
+ const dir = GLYPH[delta.direction] ? delta.direction : directionOf(text);
33
+ const own = delta.basis ? ` <span class="ui-stat__basis">${esc(delta.basis)}</span>` : '';
34
+ const describedby = !delta.basis && basisId ? ` aria-describedby="${basisId}"` : '';
35
+ return `<dd class="ui-stat__delta"${describedby}>${icon(GLYPH[dir])}<span class="ui-stat__change">${esc(text)}</span>${own}</dd>`;
36
+ };
37
+
38
+ // `tone` says whether the change is good news, and nothing else. It is never
39
+ // inferred from the direction: costs going up and unclassified rows going down
40
+ // are both real, and a band that paints every rise green is editorialising.
41
+ // A figure with no change has no news to colour.
42
+ const figure = ({ label = '', value = '', delta, trend = '' }, tile, basisId) => {
43
+ const tone = hasChange(delta) && STAT_TONES.includes(delta.tone) && delta.tone !== 'neutral' ? delta.tone : '';
44
+ const cls = cx('ui-stat', tone && `ui-stat--${tone}`, tile && 'ui-card ui-card--pad-sm');
45
+ // `trend` is trusted markup — an <svg> the caller drew. The kit sizes and
46
+ // colours it and draws no chart of its own.
47
+ return `<div class="${cls}">`
48
+ + `<dt class="ui-stat__label">${esc(label)}</dt>`
49
+ + `<dd class="ui-stat__value">${esc(value)}</dd>`
50
+ + deltaHtml(delta, basisId)
51
+ + (trend ? `<dd class="ui-stat__trend">${trend}</dd>` : '')
52
+ + '</div>';
53
+ };
54
+
55
+ // `basis` is the band's caption, said once above the figures: what every change
56
+ // is measured against, or on a band with no changes, what the figures cover. A
57
+ // figure measured against something else carries its own `delta.basis`, printed
58
+ // beside the change.
59
+ //
60
+ // The caption comes before the list, the way a table's <caption> does, in every
61
+ // layout. It is one statement about all the figures, so it is read before them
62
+ // and it sits outside every one of them — under a row of tiles it would read as
63
+ // a note on the last card. why: docs/specification.md#stat-bands
64
+ export function statBand({ stats = [], variant = 'tiles', basis = '', label, id } = {}) {
65
+ const v = STAT_VARIANTS.includes(variant) ? variant : 'tiles';
66
+ const base = id ? esc(id) : `ui-stats-${++seq}`;
67
+ const basisId = basis ? `${base}-basis` : '';
68
+ const cls = cx('ui-stats', `ui-stats--${v}`, v === 'band' && 'ui-card');
69
+ const named = label ? ` role="group" aria-label="${esc(label)}"` : '';
70
+ const items = stats.map((s) => figure(s, v === 'tiles', basisId)).join('');
71
+ return `<div class="${cls}"${named}>`
72
+ + (basis ? `<p class="ui-stats__basis" id="${basisId}">${esc(basis)}</p>` : '')
73
+ + `<dl class="ui-stats__list">${items}</dl>`
74
+ + '</div>';
75
+ }
package/src/index.css CHANGED
@@ -16,6 +16,7 @@
16
16
  @import "./tokens/accents.css";
17
17
  @import "./styles/base.css";
18
18
  @import "./styles/reduced-motion.css";
19
+ @import "./styles/field-zoom.css";
19
20
  @import "./styles/motion.css";
20
21
  @import "./styles/button.css";
21
22
  @import "./styles/card.css";
@@ -32,6 +33,7 @@
32
33
  @import "./styles/command-palette.css";
33
34
  @import "./styles/table.css";
34
35
  @import "./styles/pagination.css";
36
+ @import "./styles/stat.css";
35
37
  @import "./styles/empty.css";
36
38
  @import "./styles/callout.css";
37
39
  @import "./styles/code.css";
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ export * from './components/toasts.js';
17
17
  export * from './components/success.js';
18
18
  export * from './components/loading.js';
19
19
  export * from './components/pagination.js';
20
+ export * from './components/stat.js';
20
21
  export * from './assets/icons.js';
21
22
  export * from './assets/brand.js';
22
23
  export * from './motion.js';
package/src/inline.js CHANGED
@@ -29,6 +29,7 @@ export const topbarCss = read('styles/topbar.css');
29
29
  export const styles = {
30
30
  base: baseCss,
31
31
  reducedMotion: read('styles/reduced-motion.css'),
32
+ fieldZoom: read('styles/field-zoom.css'),
32
33
  motion: read('styles/motion.css'),
33
34
  button: read('styles/button.css'),
34
35
  card: read('styles/card.css'),
@@ -45,6 +46,7 @@ export const styles = {
45
46
  commandPalette: read('styles/command-palette.css'),
46
47
  table: read('styles/table.css'),
47
48
  pagination: read('styles/pagination.css'),
49
+ stat: read('styles/stat.css'),
48
50
  empty: read('styles/empty.css'),
49
51
  callout: read('styles/callout.css'),
50
52
  code: read('styles/code.css'),
@@ -61,6 +63,7 @@ export const cssText = [
61
63
  tokensCss,
62
64
  styles.base,
63
65
  styles.reducedMotion,
66
+ styles.fieldZoom,
64
67
  styles.motion,
65
68
  styles.button,
66
69
  styles.card,
@@ -77,6 +80,7 @@ export const cssText = [
77
80
  styles.commandPalette,
78
81
  styles.table,
79
82
  styles.pagination,
83
+ styles.stat,
80
84
  styles.empty,
81
85
  styles.callout,
82
86
  styles.code,
@@ -102,11 +102,12 @@
102
102
  border-color: transparent;
103
103
  color: var(--disabled-ink-bare);
104
104
  }
105
- /* With no box, the ink is read on whatever is behind it. --disabled-ink read
106
- 5.18:1 on a card and 4.66:1 on --surface-3 in dark, under #220's 5.56, so the
107
- ghost takes an ink set for the dullest ground: in dark 7.00 / 6.24 / 6.69 /
108
- 5.62 on --bg / --surface / --surface-2 / --surface-3, in light 6.50 / 6.50 /
109
- 6.01 / 5.60. The enabled ghost's --dim reads 1.5 times that in dark, 1.6 in light.
105
+ /* With no box, the ink is read on whatever is behind it, so the ghost takes an ink
106
+ set for the dullest ground: in dark 7.49 / 6.31 / 7.00 / 5.44 on --bg / --surface /
107
+ --surface-2 / --surface-3, in light 5.70 / 6.17 / 5.20 / 5.39. The enabled ghost's
108
+ --dim reads about 1.5 times that in both themes. #273 cut this ink to rescue two
109
+ dark pairs from under #220's floor; #295 re-picked --muted against the elevation
110
+ ladder and lifted both clear, so it is the margin now rather than the rescue.
110
111
 
111
112
  #273 got here the long way. It first gave the ghost the flat disabled box
112
113
  instead, on the claim that nothing would get less readable. That was false:
@@ -110,8 +110,12 @@
110
110
  .ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
111
111
  .ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the bar spends the toast's dismiss timer, which the caller sets */ }
112
112
 
113
- /* style: soft — tinted surface */
114
- .ui-toast--soft { background: var(--toast-glow); box-shadow: var(--shadow-sm); }
113
+ /* style: soft — tinted surface. The shadow used to be its edge, so it takes the
114
+ status-tinted hairline the outline style carries, at half the weight. #295 */
115
+ .ui-toast--soft {
116
+ background: var(--toast-glow);
117
+ border: 1px solid color-mix(in srgb, var(--toast-accent) 22%, transparent);
118
+ }
115
119
 
116
120
  /* style: solid — the status at fill strength, inked with the pole opposite it.
117
121
  The fill and the ink are one pair, taken together from --signal-solid-*: dark
@@ -124,7 +128,6 @@
124
128
  --toast-ink: var(--signal-solid-ink);
125
129
  background: var(--toast-solid);
126
130
  color: var(--toast-ink);
127
- box-shadow: var(--shadow-lg);
128
131
  }
129
132
  .ui-toast--solid .ui-toast__title { color: var(--toast-ink); }
130
133
  /* The body copy is the quieter of the two, but it stays quiet by dilution the
@@ -144,7 +147,7 @@
144
147
  .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }
145
148
 
146
149
  /* style: outline — elevated card with a status-tinted border */
147
- .ui-toast--outline { background: var(--bg-elevated); box-shadow: var(--shadow-md); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
150
+ .ui-toast--outline { background: var(--bg-elevated); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
148
151
 
149
152
  /* compact — single line, no body text */
150
153
  .ui-toast--compact { align-items: center; padding-top: 10px; padding-bottom: 10px; }
@@ -1,25 +1,18 @@
1
1
  /* ============================================================================
2
- * Card — .ui-card (filled surface, no borders the deck groups with
3
- * surface + radius, never outlines)
2
+ * Card — .ui-card (a filled surface one step off the page, edged by a hairline)
4
3
  * Rows: .ui-card__row / headers: .ui-card__title / .ui-card__sub
5
4
  * ========================================================================== */
6
5
 
6
+ /* Flat since #284 and edged in both themes since #295: the surface is the step, the
7
+ line is what makes it legible. why: docs/specification.md#elevation */
7
8
  .ui-card {
8
9
  background: var(--surface);
10
+ border: 1px solid var(--border);
9
11
  border-radius: var(--radius-xl);
10
12
  padding: var(--space-6) 26px;
11
13
  position: relative;
12
14
  }
13
15
 
14
- /* Light theme is an all-white app, so a filled card can't rely on a darker-
15
- than-page surface the way the dark deck does — a hairline border delineates
16
- it instead. Flat, no drop shadow (#284). `none` rather than no declaration:
17
- at (0,3,0) it outranks .ui-card--accent's inset ring, which light never showed. */
18
- :root[data-theme="light"] .ui-card {
19
- border: 1px solid var(--border);
20
- box-shadow: none;
21
- }
22
-
23
16
  /* A table wider than its card scrolls INSIDE the card instead of bleeding past
24
17
  the rounded corners / clipping its last column. Clipping to the card's
25
18
  border-box also keeps zebra stripes from escaping the radius. */
@@ -32,10 +25,11 @@
32
25
  .ui-card--pad-sm { padding: var(--space-5); }
33
26
  .ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
34
27
 
35
- /* An accent-tinted card, e.g. a highlighted plan / success surface */
28
+ /* An accent-tinted card, e.g. a highlighted plan / success surface. The accent edge is
29
+ the card's own hairline re-coloured: an inset ring over it would draw two. #295 */
36
30
  .ui-card--accent {
37
31
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
38
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
32
+ border-color: color-mix(in srgb, var(--accent) 22%, transparent);
39
33
  }
40
34
  .ui-card--live {
41
35
  background: color-mix(in srgb, var(--green) 10%, var(--surface));
@@ -44,16 +38,15 @@
44
38
  /* Interactive card (link / button semantics) */
45
39
  .ui-card--interactive {
46
40
  cursor: pointer;
47
- /* Written as a <button>, the card would take the browser's frame, centred text
48
- and `font: 400 13.3333px Arial` — a shorthand, so `font: inherit` and not the
49
- family longhand. .ui-card already states its own background, and the light
50
- theme's border rule is (0,3,0) so `border: 0` here cannot reach it. #251 */
51
- border: 0;
41
+ /* Written as a <button>, the card would take the browser's frame, centred text and
42
+ `font: 400 13.3333px Arial` — a shorthand, so `font: inherit` and not the family
43
+ longhand. .ui-card states the background and, since #295, the border, so a
44
+ `border: 0` here would only delete the hairline. #251 */
52
45
  text-align: left;
53
46
  font: inherit;
54
47
  transition: transform var(--dur-fast) var(--ease);
55
48
  }
56
- /* The lift alone, no shadow: cards are flat (#284), and light never showed one. */
49
+ /* The lift alone: the kit casts no shadow anywhere (#295), and cards are flat (#284). */
57
50
  .ui-card--interactive:hover { transform: translateY(-2px); }
58
51
 
59
52
  /* card() emits the title as an h2, so the text face is named here rather than
@@ -15,8 +15,8 @@
15
15
  * why: docs/specification.md#the-command-palette */
16
16
 
17
17
  .ui-cmdk {
18
- --cmdk-surface: var(--surface-2);
19
- --cmdk-shadow: var(--shadow-lg);
18
+ /* The floating step; nothing casts. why: docs/specification.md#elevation */
19
+ --cmdk-surface: var(--bg-elevated);
20
20
  --cmdk-scrim: var(--scrim);
21
21
  --cmdk-w: 560px;
22
22
  --cmdk-top: 12vh;
@@ -71,7 +71,6 @@
71
71
  background: var(--cmdk-surface);
72
72
  border: 1px solid var(--border);
73
73
  border-radius: var(--radius-lg);
74
- box-shadow: var(--cmdk-shadow);
75
74
  overflow: hidden;
76
75
  opacity: 0;
77
76
  transform: translate(-50%, -8px);
@@ -80,7 +79,7 @@
80
79
  transform var(--dur-med) var(--ease);
81
80
  }
82
81
  .ui-cmdk.is-open .ui-cmdk__panel { opacity: 1; transform: translate(-50%, 0); }
83
- .ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--cmdk-shadow), var(--ring); }
82
+ .ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--ring); }
84
83
 
85
84
  /* -- The text box --------------------------------------------------------- */
86
85
 
@@ -250,7 +249,9 @@
250
249
  padding: 2px 5px;
251
250
  border: 1px solid var(--border);
252
251
  border-radius: var(--radius-xs);
253
- background: var(--surface);
252
+ /* A key cap reads as raised, so it takes the step above the panel rather than
253
+ the card step below it. why: docs/specification.md#elevation */
254
+ background: var(--surface-3);
254
255
  font-family: var(--font-sans);
255
256
  font-size: var(--text-xs);
256
257
  line-height: 1.5;
@@ -1,20 +1,11 @@
1
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).
4
- *
5
- * `visibility` is SWITCHED rather than transitioned: transitioned, it is still
6
- * `hidden` in the frame the class lands, so openConfirm()'s focus() would have
7
- * nothing to focus. The root owns the switch; panel and scrim inherit it.
8
- *
9
- * Local tokens so the panel and scrim re-theme cleanly:
10
- * --confirm-surface panel background --confirm-shadow panel elevation
11
- * --confirm-scrim backdrop colour
12
- *
2
+ * question a destructive action has to ask. Shares its scrim and focus trap with
3
+ * the drawer (see overlay.js). Local tokens: --confirm-surface, --confirm-scrim.
13
4
  * why: docs/specification.md#motion */
14
5
 
15
6
  .ui-confirm {
16
- --confirm-surface: var(--surface-2);
17
- --confirm-shadow: var(--shadow-lg);
7
+ /* The floating step; nothing casts. why: docs/specification.md#elevation */
8
+ --confirm-surface: var(--bg-elevated);
18
9
  --confirm-scrim: var(--scrim);
19
10
  --confirm-w: var(--panel-md);
20
11
 
@@ -26,10 +17,11 @@
26
17
  z-index: calc(var(--z-overlay) + 2);
27
18
  pointer-events: none; /* container never blocks; scrim/panel opt back in */
28
19
 
29
- /* Only the hide is transitioned, so the panel is still on screen while it
30
- fades out; the show is instant, because focus moves in the same frame.
31
- `linear` and not var(--ease): visibility is discrete, easing a step buys
32
- nothing, and a curve that leaves [0, 1] would flip it mid-fade. */
20
+ /* SWITCHED, not transitioned: transitioned, the panel is still `hidden` in the
21
+ frame the class lands and openConfirm()'s focus() has nothing to focus. Only
22
+ the hide is timed, so the panel is on screen while it fades out. `linear` and
23
+ not var(--ease): visibility is discrete, and a curve leaving [0, 1] flips it
24
+ mid-fade. The root owns the switch; panel and scrim inherit it. */
33
25
  visibility: hidden;
34
26
  transition: visibility var(--dur-med) linear;
35
27
  }
@@ -72,7 +64,6 @@
72
64
  background: var(--confirm-surface);
73
65
  border: 1px solid var(--border);
74
66
  border-radius: var(--radius-lg);
75
- box-shadow: var(--confirm-shadow);
76
67
  opacity: 0;
77
68
  transform: translateY(8px);
78
69
  transition:
@@ -80,7 +71,7 @@
80
71
  transform var(--dur-med) var(--ease);
81
72
  }
82
73
  .ui-confirm.is-open .ui-confirm__panel { opacity: 1; transform: none; }
83
- .ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--confirm-shadow), var(--ring); }
74
+ .ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--ring); }
84
75
 
85
76
  /* -- Slots ----------------------------------------------------------------- */
86
77
  /* The question and the answers keep their size; the consequence between them is
@@ -6,13 +6,14 @@
6
6
  * (reduced-motion.css) makes the slide and the fade instant.
7
7
  *
8
8
  * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
9
- * --drawer-surface panel background --drawer-shadow panel elevation
9
+ * --drawer-surface panel background
10
10
  * --drawer-scrim backdrop colour (--scrim already re-themes per theme)
11
11
  * ========================================================================== */
12
12
 
13
13
  .ui-drawer {
14
- --drawer-surface: var(--surface-2);
15
- --drawer-shadow: var(--shadow-lg);
14
+ /* The floating step; the edge is the hairline each --drawer--<edge> rule below draws.
15
+ why: docs/specification.md#elevation */
16
+ --drawer-surface: var(--bg-elevated);
16
17
  --drawer-scrim: var(--scrim);
17
18
  --drawer-w: var(--panel-md); /* left|right panels: width */
18
19
  --drawer-h: 45vh; /* top|bottom panels: height */
@@ -57,10 +58,9 @@
57
58
  display: flex;
58
59
  flex-direction: column;
59
60
  background: var(--drawer-surface);
60
- box-shadow: var(--drawer-shadow);
61
61
  transition: transform var(--dur-med) var(--ease);
62
62
  }
63
- .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--drawer-shadow), var(--ring); }
63
+ .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring); }
64
64
  .ui-drawer.is-open .ui-drawer__panel { transform: none; }
65
65
 
66
66
  /* -- Edge anchoring + slide-in direction ---------------------------------- */
@@ -149,7 +149,7 @@
149
149
  background var(--dur-fast) var(--ease),
150
150
  border-color var(--dur-fast) var(--ease);
151
151
  }
152
- .ui-drawer__close:hover { color: var(--strong); background: var(--surface); }
152
+ .ui-drawer__close:hover { color: var(--strong); background: var(--surface-3); }
153
153
  .ui-drawer__close:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
154
154
  .ui-drawer__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.1; }
155
155
 
@@ -57,11 +57,12 @@
57
57
  top: calc(100% + var(--ui-dropdown-gap));
58
58
  left: 0;
59
59
  min-width: 240px;
60
- background: var(--surface-2);
60
+ /* The floating step, not the sunken one — a panel held open over a card cannot be
61
+ painted the colour of an inset. why: docs/specification.md#elevation */
62
+ background: var(--bg-elevated);
61
63
  border: 1px solid var(--border);
62
64
  border-radius: var(--radius-lg);
63
65
  padding: 6px;
64
- box-shadow: var(--shadow-lg);
65
66
  opacity: 0;
66
67
  visibility: hidden;
67
68
  transform: translateY(-6px);
@@ -126,8 +127,9 @@
126
127
  color: var(--text);
127
128
  transition: background var(--dur-fast) var(--ease);
128
129
  }
129
- .ui-dropdown__item:hover { background: var(--surface); }
130
- .ui-dropdown__item:focus-visible { outline: none; background: var(--surface); box-shadow: var(--ring); }
130
+ /* A row in a raised panel lifts: the step above it, never the card step below. why: docs/specification.md#elevation */
131
+ .ui-dropdown__item:hover { background: var(--surface-3); }
132
+ .ui-dropdown__item:focus-visible { outline: none; background: var(--surface-3); box-shadow: var(--ring); }
131
133
  .ui-dropdown__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
132
134
  .ui-dropdown__label { font-family: var(--font-sans); font-size: 12.5px; color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
133
135
  .ui-dropdown__desc { font-size: 11px; color: var(--muted); line-height: 1.35; }
@@ -153,10 +155,15 @@
153
155
  margin-left: auto;
154
156
  flex: none;
155
157
  color: var(--muted);
156
- background: var(--surface);
158
+ /* A chip is the top step. why: docs/specification.md#elevation */
159
+ background: var(--surface-3);
157
160
  }
158
161
  .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
159
- .ui-dropdown__badge.is-accent { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
162
+ /* The accent counter keeps the badge's flat surface and changes only the ink — the shape
163
+ src/styles/nav.css:128 `.ui-nav__item.is-active .ui-nav__badge.is-accent` already holds.
164
+ The accent wash goes on a base surface, never a raised one, and #295 made this panel a
165
+ raised one. why: docs/specification.md#elevation */
166
+ .ui-dropdown__badge.is-accent { color: var(--accent); background: var(--surface-3); }
160
167
 
161
168
  /* Disabled + danger rows */
162
169
  .ui-dropdown__item.is-disabled { color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
@@ -209,12 +216,16 @@
209
216
  }
210
217
  /* 16px at 2.4 paints 1.6px, over the 1.5px floor stories/glyph-stroke.test.js holds. */
211
218
  .ui-dropdown__search-ic svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.4; }
219
+ /* 12.5px with a mouse only. On a touch screen the touch-zoom net in
220
+ src/styles/field-zoom.css takes this field, and every other field the kit
221
+ ships, to 16px. why: docs/specification.md#a-field-is-16px-on-a-touch-screen */
212
222
  .ui-dropdown__search-input {
213
223
  width: 100%;
214
224
  font: inherit;
215
225
  font-size: 12.5px;
216
226
  color: var(--strong);
217
- background: var(--surface);
227
+ /* A field is the sunken step, the same one .ui-input takes. why: docs/specification.md#elevation */
228
+ background: var(--surface-2);
218
229
  border: 1px solid var(--border);
219
230
  border-radius: var(--radius-sm);
220
231
  padding: 8px 12px 8px 34px;
@@ -223,10 +234,6 @@
223
234
  min-height: 38px;
224
235
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
225
236
  }
226
- /* iOS Safari zooms the page into a focused field under 16px, and opening the
227
- panel focuses this one. A real 16px, not a scaled one: the zoom reads the
228
- computed size, and a transform would shrink the ring and border with it. */
229
- @media (pointer: coarse) { .ui-dropdown__search-input { font-size: 16px; } }
230
237
  .ui-dropdown__search-input::placeholder { color: var(--muted); }
231
238
  .ui-dropdown__search-input:hover { border-color: var(--border-strong); }
232
239
  .ui-dropdown__search-input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
@@ -238,7 +245,7 @@
238
245
 
239
246
  /* The row Enter would pick. Focus stays in the field, which carries the ring,
240
247
  so this row takes the hover fill and a bar rather than a second ring. */
241
- .ui-dropdown__item.is-active { background: var(--surface); box-shadow: inset 2px 0 0 var(--accent); }
248
+ .ui-dropdown__item.is-active { background: var(--surface-3); box-shadow: inset 2px 0 0 var(--accent); }
242
249
 
243
250
  /* No match — a nudge and no action, per Guidelines / Microcopy. Empty while
244
251
  anything matches, so it takes no room. */
@@ -9,7 +9,6 @@
9
9
 
10
10
  :root {
11
11
  --ui-fb-pill-grad: linear-gradient(135deg, var(--accent), var(--accent-strong));
12
- --ui-fb-pill-glow: color-mix(in srgb, var(--accent) 45%, transparent);
13
12
  --ui-fb-sel: color-mix(in srgb, var(--accent) 22%, transparent);
14
13
  }
15
14
 
@@ -34,23 +33,21 @@
34
33
  border: 0; text-align: center; font: inherit;
35
34
  font-family: var(--font-sans); font-size: 13px; font-weight: var(--weight-semibold); letter-spacing: .01em;
36
35
  padding: 9px 17px 9px 15px; border-radius: 999px; cursor: pointer; white-space: nowrap; overflow: hidden;
37
- box-shadow: 0 10px 26px var(--ui-fb-pill-glow),
38
- 0 2px 6px color-mix(in srgb, var(--shadow-ink) 28%, transparent),
39
- inset 0 1px 0 color-mix(in srgb, var(--sheen) 28%, transparent);
36
+ /* The pill cast two layers of ink and an accent glow until #295, and grew both on
37
+ hover. Nothing casts now, and it takes no hairline either: it is the accent at full
38
+ fill strength on the page, found the way the solid toast is found. What is left is an
39
+ inset sheen — gloss on the surface, not a shadow under it — so the hover is the travel
40
+ alone and the box-shadow it transitioned went with it.
41
+ why: docs/specification.md#elevation */
42
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--sheen) 28%, transparent);
40
43
  transition:
41
44
  transform var(--dur-fast) var(--ease-spring),
42
- opacity var(--dur-fast) var(--ease),
43
- box-shadow var(--dur-fast) var(--ease);
45
+ opacity var(--dur-fast) var(--ease);
44
46
  }
45
47
  .ui-fbpill.show { opacity: 1; pointer-events: auto; transform: translate(-50%, -100%) translateY(-9px) scale(1); }
46
- .ui-fbpill:hover {
47
- box-shadow: 0 14px 34px var(--ui-fb-pill-glow),
48
- 0 3px 9px color-mix(in srgb, var(--shadow-ink) 30%, transparent),
49
- inset 0 1px 0 color-mix(in srgb, var(--sheen) 34%, transparent);
50
- }
51
48
  .ui-fbpill.show:hover { transform: translate(-50%, -100%) translateY(-12px) scale(1.04); }
52
49
  .ui-fbpill:active { transform: translate(-50%, -100%) translateY(-8px) scale(.98); }
53
- .ui-fbpill svg { width: 16px; height: 16px; stroke-width: 2.4; color: currentColor; filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--shadow-ink) 25%, transparent)); }
50
+ .ui-fbpill svg { width: 16px; height: 16px; stroke-width: 2.4; color: currentColor; }
54
51
  .ui-fbpill::before {
55
52
  content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%;
56
53
  background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--sheen) 55%, transparent), transparent); transform: skewX(-18deg);
@@ -70,7 +67,8 @@
70
67
  .ui-fbcomposer {
71
68
  position: fixed; z-index: 71; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(14px);
72
69
  width: min(520px, calc(100vw - 32px)); background: var(--bg-elevated); border-radius: 18px;
73
- box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none;
70
+ /* The shadow was its only edge until #295; now it takes the hairline. */
71
+ border: 1px solid var(--border); opacity: 0; pointer-events: none;
74
72
  transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease); overflow: hidden;
75
73
  }
76
74
  .ui-fbcomposer.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
@@ -0,0 +1,20 @@
1
+ /* apliteni-ui — touch-zoom net. iOS Safari zooms the page into a focused field
2
+ * whose text is under 16px and does not zoom back out, so under (pointer: coarse)
3
+ * every field the kit ships is 16px. Element selectors rather than kit classes,
4
+ * and !important for the reason reduced-motion.css takes it; left out are the
5
+ * controls with nothing to type into, which never zoom.
6
+ *
7
+ * The two costs, argued in the spec below: a 16px scaled back down with a
8
+ * transform still zooms, and this is a flat size rather than a floor, so a host
9
+ * field designed ABOVE 16px is shrunk here. The override recipe is there too.
10
+ *
11
+ * why: docs/specification.md#a-field-is-16px-on-a-touch-screen */
12
+
13
+ @media (pointer: coarse) {
14
+ input:not(:where(
15
+ [type="button"], [type="checkbox"], [type="color"], [type="file"], [type="hidden"],
16
+ [type="image"], [type="radio"], [type="range"], [type="reset"], [type="submit"]
17
+ )),
18
+ select,
19
+ textarea { font-size: 16px !important; }
20
+ }
@@ -80,14 +80,24 @@
80
80
  }
81
81
  .ui-input-group__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.2; }
82
82
 
83
+ /* The chevron is a data URI, and a data URI cannot read a token: `var()` does not
84
+ substitute inside url() and `currentColor` does not cross into the image's own
85
+ document. So the ink is written twice, once per theme, each the theme's --muted —
86
+ which is the colour every other quiet glyph on a field takes. It was one frozen
87
+ hex until #295, dark --muted's old value, which the ladder's re-pick left behind
88
+ at 2.49:1 on the light field. Held by stories/colour-tokens.test.js.
89
+ why: docs/specification.md#colour-and-contrast */
83
90
  .ui-select {
84
91
  appearance: none;
85
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23948fa8' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
92
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a29db6' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
86
93
  background-repeat: no-repeat;
87
94
  background-position: right 15px center;
88
95
  padding-right: 40px;
89
96
  cursor: pointer;
90
97
  }
98
+ :root[data-theme="light"] .ui-select {
99
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235c6270' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
100
+ }
91
101
 
92
102
  /* Checkbox / radio — custom, token-driven */
93
103
  .ui-check {
@@ -178,8 +188,13 @@
178
188
  width: calc(var(--sw-h) - 6px);
179
189
  height: calc(var(--sw-h) - 6px);
180
190
  background: var(--control-knob);
191
+ /* --shadow-sm was the knob's only edge until #295, and a white knob on a light
192
+ off-track has 1.20:1 to spare. --border-strong and not --border, which measures
193
+ 1.03:1 against that track and draws nothing — the checkbox above edges itself
194
+ the same way. why: docs/specification.md#elevation */
195
+ border: 1px solid var(--border-strong);
196
+ box-sizing: border-box;
181
197
  border-radius: 50%;
182
- box-shadow: var(--shadow-sm);
183
198
  transition: transform var(--dur-med) var(--ease);
184
199
  }
185
200
  .ui-switch input:checked + .ui-switch__track { background: var(--accent); }
@@ -76,7 +76,10 @@
76
76
  step down so the current one is the brightest whatever --accent is doing, and
77
77
  the bar beside it carries the hue. A 3x17px bar on a 45x44 tile was the
78
78
  smallest of three weak signals doing the most work, so it grows with the row. */
79
- .ui-app__rail .ui-nav__ic svg { opacity: 0.62; }
79
+ /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
80
+ the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
81
+ stories/apps/shell-states.test.js:427 `a resting glyph is dimmer but still legible`. */
82
+ .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
80
83
  .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
81
84
  .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
82
85
  .ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
@@ -226,10 +229,12 @@
226
229
  z-index: 1;
227
230
  width: 100%;
228
231
  max-width: var(--panel-md);
229
- background: var(--surface);
232
+ /* Floats over a spotlit page, so it takes the floating step and the hairline.
233
+ why: docs/specification.md#elevation */
234
+ background: var(--bg-elevated);
235
+ border: 1px solid var(--border);
230
236
  border-radius: 22px;
231
237
  padding: 38px 36px 32px;
232
- box-shadow: var(--shadow-lg);
233
238
  }
234
239
  .ui-auth__brand { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 26px; }
235
240
  .ui-auth__brand span { font-size: 15px; font-weight: 600; color: var(--strong); }
@@ -64,13 +64,13 @@
64
64
  to { opacity: 1; filter: blur(0); }
65
65
  }
66
66
 
67
- /* -- Micro-interactions ---------------------------------------------------- */
67
+ /* -- Micro-interactions ----------------------------------------------------
68
+ * .m-lift is the travel only; it cast --shadow-md until #295. */
68
69
  .m-lift {
69
- transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-med) var(--ease);
70
+ transition: transform var(--dur-fast) var(--ease);
70
71
  }
71
72
  .m-lift:hover {
72
73
  transform: translateY(-3px);
73
- box-shadow: var(--shadow-md);
74
74
  }
75
75
  .m-press {
76
76
  transition: transform var(--dur-instant) var(--ease-sharp);
@@ -4,6 +4,7 @@
4
4
  * --single one page of content: the size control alone, no steps
5
5
  * The controls are .ui-btn ghost/sm and the size control is a .ui-select, so
6
6
  * everything here is layout and the two states those sheets do not own.
7
+ * why: CONTRIBUTING.md#pagination-layout
7
8
  * ========================================================================== */
8
9
 
9
10
  .ui-pager {