@lyeve-labs/ui-kit 0.44.0 → 0.46.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.
Files changed (36) hide show
  1. package/dist/components/AccountMenu.svelte +4 -1
  2. package/dist/components/AppShell.svelte +42 -10
  3. package/dist/components/AppShell.svelte.d.ts +8 -0
  4. package/dist/components/AuthShell.svelte +9 -2
  5. package/dist/components/Badge.svelte +7 -6
  6. package/dist/components/Button.svelte +29 -5
  7. package/dist/components/Card.svelte +9 -4
  8. package/dist/components/Drawer.svelte +1 -1
  9. package/dist/components/EmptyState.svelte +9 -3
  10. package/dist/components/Modal.svelte +1 -1
  11. package/dist/components/PageHeader.svelte +10 -4
  12. package/dist/components/Pagination.svelte +9 -22
  13. package/dist/components/PluginCart.svelte +32 -10
  14. package/dist/components/PluginCart.svelte.d.ts +8 -0
  15. package/dist/components/Progress.svelte +17 -3
  16. package/dist/components/SegmentedControl.svelte +2 -2
  17. package/dist/components/SidebarNav.svelte +14 -1
  18. package/dist/components/SortableList.svelte +1 -1
  19. package/dist/components/Stat.svelte +13 -3
  20. package/dist/components/Table.svelte +20 -9
  21. package/dist/components/Tabs.svelte +77 -10
  22. package/dist/components/Toaster.svelte +1 -1
  23. package/dist/components/Tooltip.svelte +1 -1
  24. package/dist/components/dialog/Dialog.svelte +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/internal/field.js +12 -1
  28. package/dist/internal/layout.d.ts +29 -4
  29. package/dist/internal/layout.js +37 -5
  30. package/dist/internal/panel.d.ts +4 -1
  31. package/dist/internal/panel.js +4 -1
  32. package/dist/internal/scroll-edges.d.ts +9 -8
  33. package/dist/internal/scroll-edges.js +9 -8
  34. package/dist/styles/theme.css +203 -0
  35. package/package.json +1 -1
  36. package/src/lib/styles/theme.css +203 -0
@@ -8,6 +8,7 @@
8
8
  * use this one instead.
9
9
  */
10
10
  import type { StatusTone } from '../internal/tone.js';
11
+ import { CARD_SURFACE } from '../internal/layout.js';
11
12
 
12
13
  type Trend = 'up' | 'down' | 'flat';
13
14
  type Accent = 'brand' | 'violet' | 'success' | 'neutral';
@@ -74,8 +75,8 @@
74
75
 
75
76
  const pads: Record<Size, string> = { sm: 'p-4', md: 'p-5' };
76
77
  const values: Record<Size, string> = {
77
- sm: 'mt-1 text-lg font-semibold',
78
- md: 'mt-2 text-2xl font-bold',
78
+ sm: 'mt-1 text-lg font-semibold tracking-tight',
79
+ md: 'mt-2 text-2xl font-bold tracking-tight',
79
80
  };
80
81
 
81
82
  const valueColor = $derived(tone ? tones[tone] : accents[accent]);
@@ -84,7 +85,16 @@
84
85
  const trendMark = { up: '↑', down: '↓', flat: '→' } as const;
85
86
  </script>
86
87
 
87
- <div class="rounded-xl border border-line bg-surface {pads[size]} {klass}">
88
+ <!-- A brand to violet line along the top edge, so a row of figures reads as a
89
+ set. It is a mark and carries nothing a reader needs, so where gradients
90
+ are not drawn it falls back to solid brand or to nothing at all. It is
91
+ mirrored under right-to-left, so cyan stays at the start edge.
92
+ overflow-hidden lets the card's corners trim it. -->
93
+ <div
94
+ class="{CARD_SURFACE} relative overflow-hidden before:absolute before:inset-x-0 before:top-0 before:h-0.5 before:bg-brand before:bg-accent rtl:before:-scale-x-100 {pads[
95
+ size
96
+ ]} {klass}"
97
+ >
88
98
  <p class="text-xs font-medium uppercase tracking-wide text-faint">{label}</p>
89
99
  <p class="{values[size]} {valueColor} {mono ? 'font-mono tabular-nums' : ''}">{value}</p>
90
100
  <div class="mt-1 flex items-center gap-2">
@@ -17,6 +17,7 @@
17
17
  SCROLL_EDGE_START,
18
18
  scrollEdges,
19
19
  } from '../internal/scroll-edges.js';
20
+ import { CARD_SURFACE } from '../internal/layout.js';
20
21
 
21
22
  interface Props {
22
23
  striped?: boolean;
@@ -71,12 +72,13 @@
71
72
  * start cell in either direction and the two agree in left-to-right text.
72
73
  */
73
74
  const HOVER_RING = [
74
- '[&_tbody_tr:hover]:bg-surface-2/60',
75
+ '[&_tbody_tr:hover]:bg-tint',
75
76
  '[&_tbody_tr:hover>td]:shadow-[inset_0_1px_var(--color-brand),inset_0_-1px_var(--color-brand)]',
76
77
  '[&_tbody_tr:hover>td:first-child]:shadow-[inset_1px_1px_var(--color-brand),inset_1px_-1px_var(--color-brand)]',
77
78
  '[&_tbody_tr:hover>td:last-child]:shadow-[inset_-1px_1px_var(--color-brand),inset_-1px_-1px_var(--color-brand)]',
78
79
  '[&_tbody_tr:hover>td:first-child:last-child]:shadow-[inset_1px_1px_var(--color-brand),inset_-1px_-1px_var(--color-brand)]',
79
80
  '[&_tbody_tr]:transition-colors',
81
+ '[&_tbody_tr>td]:transition-shadow',
80
82
  ].join(' ');
81
83
 
82
84
  let {
@@ -239,10 +241,15 @@
239
241
 
240
242
  <!-- overflow-hidden on the frame, not only on the scroller: the hovered row's
241
243
  ring is square and the frame is not, and the last row's corners drew past
242
- the frame's rounded ones. -->
244
+ the frame's rounded ones.
245
+
246
+ The frame is a card: the same surface, radius and shadows, so a table and
247
+ the cards above it sit at one depth. It paints its own surface rather than
248
+ showing the page through, which the shadow under it needs to read as a
249
+ raised sheet. -->
243
250
  <div
244
251
  data-testid="table-frame"
245
- class="relative w-full overflow-hidden rounded-xl border border-line {cls}"
252
+ class="relative w-full overflow-hidden {CARD_SURFACE} {cls}"
246
253
  >
247
254
  <!--
248
255
  data-print: the box exists to clip. On paper there is no viewport to clip
@@ -255,7 +262,7 @@
255
262
  onfocusin={reveal}
256
263
  data-print="unclip"
257
264
  data-testid="table-scroll"
258
- class="w-full overflow-x-auto rounded-xl"
265
+ class="w-full overflow-x-auto rounded-card"
259
266
  {...scrollAttrs}
260
267
  >
261
268
  <!--
@@ -273,21 +280,25 @@
273
280
  which is why the lines moved down to the cells they were already drawn
274
281
  between. With border-spacing at zero the result is the same line in the
275
282
  same place.
283
+
284
+ The head row and a hovered row take the brand tint. It is one opaque
285
+ color, so the contrast suite measures the head's labels and a row's text
286
+ on it, and a hovered row is told apart by more than its ring.
276
287
  -->
277
288
  <table
278
289
  style="--cell-truncate: {truncateAt}"
279
290
  class="w-full border-separate border-spacing-0 text-sm text-start
280
291
  {fixed ? 'table-fixed' : ''}
281
292
  {CELL_FIT[cell]} {CELL_NAMED} {COLUMN_ALIGN} {HEAD_CONTROLS}
282
- [&_thead_th]:border-b [&_thead_th]:border-line [&_thead]:bg-surface-2/40
293
+ [&_thead_th]:border-b [&_thead_th]:border-line [&_thead]:bg-tint
283
294
  [&_thead_th]:px-4 [&_thead_th]:py-3 [&_thead_th]:text-xs
284
295
  [&_thead_th]:font-medium [&_thead_th]:text-faint
285
296
  [&_thead_th]:uppercase [&_thead_th]:tracking-wider [&_thead_th]:whitespace-nowrap
286
297
  [&_tbody_td]:px-4 [&_tbody_td]:py-3 [&_tbody_td]:text-fg [&_tbody_td]:align-middle
287
298
  [&_tbody_td]:border-b [&_tbody_td]:border-line [&_tbody_tr:last-child_td]:border-0
288
299
  {striped ? '[&_tbody_tr:nth-child(even)]:bg-surface-2/40' : ''}
289
- [&_tbody_tr:last-child>td:first-child]:rounded-es-[calc(var(--radius-xl)-1px)]
290
- [&_tbody_tr:last-child>td:last-child]:rounded-ee-[calc(var(--radius-xl)-1px)]
300
+ [&_tbody_tr:last-child>td:first-child]:rounded-es-[calc(var(--radius-card)-1px)]
301
+ [&_tbody_tr:last-child>td:last-child]:rounded-ee-[calc(var(--radius-card)-1px)]
291
302
  {hoverable ? HOVER_RING : ''}"
292
303
  >
293
304
  {@render children()}
@@ -305,7 +316,7 @@
305
316
  aria-hidden="true"
306
317
  data-print="hide"
307
318
  data-testid="table-more-before"
308
- class="{SCROLL_EDGE} {SCROLL_EDGE_START} rounded-s-xl"
319
+ class="{SCROLL_EDGE} {SCROLL_EDGE_START} rounded-s-card"
309
320
  ></div>
310
321
  {/if}
311
322
  {#if moreAfter}
@@ -313,7 +324,7 @@
313
324
  aria-hidden="true"
314
325
  data-print="hide"
315
326
  data-testid="table-more-after"
316
- class="{SCROLL_EDGE} {SCROLL_EDGE_END} rounded-e-xl"
327
+ class="{SCROLL_EDGE} {SCROLL_EDGE_END} rounded-e-card"
317
328
  ></div>
318
329
  {/if}
319
330
  </div>
@@ -29,8 +29,8 @@
29
29
  *
30
30
  * The scrollbar is not drawn. A strip of tabs with a bar under it reads as
31
31
  * a second rule under the real one. The fades carry the same message, the
32
- * tabs are buttons so the keyboard reaches every one, and the active tab
33
- * is scrolled into view when it changes.
32
+ * arrow keys reach every tab, and the active tab is scrolled into view when
33
+ * it changes.
34
34
  */
35
35
  let scroller: HTMLElement | undefined = $state();
36
36
  let moreBefore = $state(false);
@@ -62,20 +62,81 @@
62
62
  * page down to it as soon as it mounted. A test DOM lays nothing out, so
63
63
  * every box is empty and nothing moves.
64
64
  */
65
- $effect(() => {
65
+ function reveal(tab: HTMLElement) {
66
66
  const el = scroller;
67
- const id = active;
68
67
  if (!el) return;
69
- const tab = [...el.querySelectorAll<HTMLElement>('[role="tab"]')].find(
70
- (t) => t.dataset.tab === id,
71
- );
72
- if (!tab) return;
73
68
  const box = el.getBoundingClientRect();
74
69
  const at = tab.getBoundingClientRect();
75
70
  if (at.left < box.left) el.scrollLeft -= box.left - at.left;
76
71
  else if (at.right > box.right) el.scrollLeft += at.right - box.right;
72
+ }
73
+
74
+ function tabAt(index: number): HTMLElement | undefined {
75
+ return scroller?.querySelectorAll<HTMLElement>('[role="tab"]')[index];
76
+ }
77
+
78
+ const activeIndex = $derived(items.findIndex((t) => t.id === active));
79
+
80
+ $effect(() => {
81
+ const tab = activeIndex === -1 ? undefined : tabAt(activeIndex);
82
+ if (tab) reveal(tab);
77
83
  });
78
84
 
85
+ /*
86
+ * The strip is one stop in the tab order. A strip of five tabs cost five
87
+ * presses of Tab to step over, and the arrows did nothing.
88
+ *
89
+ * Activation is manual: Left, Right, Home and End move focus and the stop
90
+ * with it, and Enter, Space or a click chooses. A caller may navigate to
91
+ * another page when a tab is chosen, and a strip that chose on every arrow
92
+ * would then load a page per key press and take the reader off the strip.
93
+ *
94
+ * While focus is inside the strip the stop follows it, by key or by
95
+ * pointer. When focus leaves, the stop goes back to the selected tab. A
96
+ * selection matching no item would leave every tab at -1 and drop the
97
+ * strip out of the tab order, so the first tab holds it then.
98
+ */
99
+ let roving: number | null = $state(null);
100
+ const tabStop = $derived(roving ?? (activeIndex === -1 ? 0 : activeIndex));
101
+
102
+ function moveTo(index: number) {
103
+ const tab = tabAt(index);
104
+ if (!tab) return;
105
+ // The strip is in view when a key reaches it, and focus() would scroll
106
+ // every box above the tab, the page among them.
107
+ tab.focus({ preventScroll: true });
108
+ reveal(tab);
109
+ }
110
+
111
+ function onTabKeydown(e: KeyboardEvent, index: number) {
112
+ const last = items.length - 1;
113
+ let next: number;
114
+ switch (e.key) {
115
+ case 'ArrowRight':
116
+ next = index === last ? 0 : index + 1;
117
+ break;
118
+ case 'ArrowLeft':
119
+ next = index === 0 ? last : index - 1;
120
+ break;
121
+ case 'Home':
122
+ next = 0;
123
+ break;
124
+ case 'End':
125
+ next = last;
126
+ break;
127
+ default:
128
+ return;
129
+ }
130
+ // The arrows scroll the strip and the page, and Home and End jump the page.
131
+ e.preventDefault();
132
+ moveTo(next);
133
+ }
134
+
135
+ function onStripFocusOut(e: FocusEvent) {
136
+ if (scroller && e.relatedTarget instanceof Node && scroller.contains(e.relatedTarget)) return;
137
+ roving = null;
138
+ }
139
+
79
140
  /*
80
141
  * The focus ring is inset: the tabs sit in a box that clips to scroll, and
81
142
  * the global outline sits 2px outside its element, so on the strip's top
@@ -92,20 +153,26 @@
92
153
  <div
93
154
  bind:this={scroller}
94
155
  onscroll={measure}
156
+ onfocusout={onStripFocusOut}
95
157
  role="tablist"
96
158
  data-testid="tabs-scroll"
97
159
  class="flex gap-0.5 overflow-x-auto border-b border-line [scrollbar-width:none]"
98
160
  >
99
- {#each items as tab (tab.id)}
161
+ <!-- The active indicator is the cyan to violet accent, with solid brand
162
+ under it for wherever gradients are not drawn. -->
163
+ {#each items as tab, i (tab.id)}
100
164
  <button
101
165
  type="button"
102
166
  role="tab"
103
167
  data-tab={tab.id}
104
168
  aria-selected={active === tab.id}
169
+ tabindex={i === tabStop ? 0 : -1}
105
170
  onclick={() => onchange(tab.id)}
171
+ onfocus={() => (roving = i)}
172
+ onkeydown={(e) => onTabKeydown(e, i)}
106
173
  class="{TAB}
107
174
  {active === tab.id
108
- ? 'text-fg after:absolute after:bottom-0 after:inset-x-0 after:h-0.5 after:bg-brand after:rounded-t'
175
+ ? 'text-fg after:absolute after:bottom-0 after:inset-x-0 after:h-0.5 after:bg-brand after:bg-accent after:rounded-t rtl:after:-scale-x-100'
109
176
  : 'text-faint hover:text-fg'}"
110
177
  >
111
178
  {tab.label}
@@ -30,7 +30,7 @@
30
30
  transition:motion.toast|global
31
31
  animate:motion.reorder
32
32
  class="pointer-events-auto flex items-start gap-3 overflow-hidden rounded-lg border border-line
33
- bg-surface ps-0 pe-3 py-3 shadow-xl"
33
+ bg-surface ps-0 pe-3 py-3 shadow-popover inset-shadow-highlight"
34
34
  >
35
35
  <span class="w-1 self-stretch shrink-0 {styles[statusTone(t.tone)].bar}"></span>
36
36
  <span
@@ -138,7 +138,7 @@
138
138
  aria-hidden={describe ? undefined : 'true'}
139
139
  style="top: {anchor.top}px; left: {anchor.left}px"
140
140
  class="fixed {pos[position]} z-tooltip w-max max-w-[min(16rem,calc(100vw-2rem))]
141
- rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-xl
141
+ rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-popover inset-shadow-highlight
142
142
  transition {visible
143
143
  ? 'duration-base ease-enter'
144
144
  : 'invisible scale-[0.96] opacity-0 duration-fast ease-exit'}"
@@ -110,7 +110,7 @@
110
110
  use:overlay
111
111
  transition:motion.dialog|global
112
112
  class="relative w-full {sizeClass(entry.options.size ?? 'md')} mx-4
113
- bg-surface border border-line rounded-xl shadow-2xl
113
+ bg-surface bg-card border border-line rounded-card shadow-overlay inset-shadow-highlight
114
114
  transition-transform duration-slow
115
115
  focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
116
116
  style="transform: scale({1 - offset * 0.03}) translateY({offset * 16}px);
package/dist/index.d.ts CHANGED
@@ -113,4 +113,4 @@ export { cn, type ClassValue } from './utils/cn.js';
113
113
  export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, watchThemePreference, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
114
114
  export * as motion from './motion.js';
115
115
  export type { Rung as MotionRung, Curve as MotionCurve } from './motion.js';
116
- export declare const VERSION = "0.44.0";
116
+ export declare const VERSION = "0.46.0";
package/dist/index.js CHANGED
@@ -122,4 +122,4 @@ export * as motion from './motion.js';
122
122
  // ── Version ────────────────────────────────────────────────────────────────
123
123
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
124
124
  // this line. The build and the test suite fail when the two disagree.
125
- export const VERSION = '0.44.0';
125
+ export const VERSION = '0.46.0';
@@ -40,6 +40,15 @@ export const FIELD_LABEL = 'text-sm font-medium text-fg';
40
40
  export const FIELD_HINT = 'text-xs text-faint';
41
41
  /** Error text below a control. Replaces the hint rather than stacking with it. */
42
42
  export const FIELD_ERROR = 'text-xs text-danger';
43
+ /**
44
+ * The depth every field shares: an inner shadow along the top edge that sinks
45
+ * it into the surface, and the cyan glow around the focus ring. Both are
46
+ * shadows, so the field keeps its height and its border, and the ring stays
47
+ * the indicator that clears 3:1. The glow appears with the ring rather than
48
+ * easing in, because a focus indicator that fades is late for the key press
49
+ * that moved it.
50
+ */
51
+ const CONTROL_DEPTH = 'inset-shadow-well focus-visible:shadow-glow';
43
52
  /**
44
53
  * Everything a single-line control needs except its border color.
45
54
  *
@@ -58,11 +67,13 @@ export const FIELD_ERROR = 'text-xs text-danger';
58
67
  */
59
68
  export const CONTROL_BASE = 'w-full h-control rounded-lg bg-surface-2 border px-3 text-sm text-fg ' +
60
69
  'placeholder:text-faint transition-colors outline-none ' +
70
+ `${CONTROL_DEPTH} ` +
61
71
  'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand ' +
62
72
  'disabled:opacity-50 disabled:cursor-not-allowed';
63
73
  /** As CONTROL_BASE, for controls whose height is driven by their content. */
64
74
  export const CONTROL_MULTILINE = 'w-full min-h-control rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg ' +
65
75
  'placeholder:text-faint transition-colors outline-none ' +
76
+ `${CONTROL_DEPTH} ` +
66
77
  'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand ' +
67
78
  'disabled:opacity-50 disabled:cursor-not-allowed';
68
79
  /**
@@ -111,7 +122,7 @@ export function describedBy(id, error, hint) {
111
122
  * held the caret.
112
123
  */
113
124
  export const CONTROL_SEGMENTED = 'inline-flex h-control items-center gap-0.5 rounded-lg bg-surface-2 border px-3 ' +
114
- 'text-sm text-fg transition-colors';
125
+ 'text-sm text-fg transition-colors inset-shadow-well has-[:focus-visible]:shadow-glow';
115
126
  /** As controlBorder, for a wrapper that is only ever focused through a child. */
116
127
  export function segmentedBorder(error) {
117
128
  return error
@@ -91,8 +91,15 @@ export declare const PAGE_STACK = "flex flex-col gap-section";
91
91
  * children flush to the border. `overflow-hidden` is deliberately absent: the
92
92
  * focus ring sits 2px outside the element it belongs to, so a clipping surface
93
93
  * crops the ring of every button inside it down to whichever edge fits.
94
+ *
95
+ * The depth is a few percent of light at the top, a 1px light along the top
96
+ * edge and a soft drop under the card. The light is a translucent background
97
+ * image laid over the background color, so a caller who passes its own bg-*
98
+ * keeps that color under it. The rest are shadows, so the card is the same
99
+ * size with or without them and the 1px border stays the boundary a forced
100
+ * colors scheme keeps.
94
101
  */
95
- export declare const CARD_SURFACE = "bg-surface border border-line rounded-xl";
102
+ export declare const CARD_SURFACE = "bg-surface bg-card border border-line rounded-card shadow-card inset-shadow-highlight";
96
103
  /**
97
104
  * The card surface in the danger tone, for a group of destructive actions.
98
105
  *
@@ -101,7 +108,7 @@ export declare const CARD_SURFACE = "bg-surface border border-line rounded-xl";
101
108
  * percent keeps the controls of a form inside it on a ground they were drawn
102
109
  * for. The contrast suite reads the share from this string.
103
110
  */
104
- export declare const PANEL_DANGER_SURFACE = "bg-[color-mix(in_srgb,var(--color-danger)_5%,var(--color-surface))] border border-danger/40 rounded-xl";
111
+ export declare const PANEL_DANGER_SURFACE = "bg-[color-mix(in_srgb,var(--color-danger)_5%,var(--color-surface))] border border-danger/40 rounded-card shadow-card inset-shadow-highlight";
105
112
  /**
106
113
  * Card padding by name.
107
114
  *
@@ -181,8 +188,20 @@ export declare function sectionHeading(level: 2 | 3, variant?: SectionVariant):
181
188
  *
182
189
  * `border-e` and not `border-r`: the shell is the one place a right-to-left
183
190
  * locale flips, and a physical border leaves the rule on the wrong edge.
191
+ *
192
+ * The sidebar runs from the surface at the top a step toward ink at the foot,
193
+ * and its edge is a rule that fades in and out at its ends, drawn over a
194
+ * transparent border. The border keeps the column's width, and a forced
195
+ * colors scheme, which draws no gradient, paints that border in the system
196
+ * text color, so the edge is still there.
184
197
  */
185
- export declare const APP_SIDEBAR = "h-full shrink-0 flex-col border-e border-line bg-surface transition-[width] duration-base ease-move";
198
+ export declare const APP_SIDEBAR: string;
199
+ /**
200
+ * The faded edge of the sidebar, a 1px line laid over its transparent
201
+ * border. It sets a width of its own, so it stays out of APP_SIDEBAR, which
202
+ * holds no width at all.
203
+ */
204
+ export declare const APP_SIDEBAR_EDGE = "after:pointer-events-none after:absolute after:inset-y-0 after:-end-px after:w-px after:bg-rule-y";
186
205
  /**
187
206
  * The sidebar's two widths. Expanded is the 224px column every authed screen
188
207
  * had. The rail is the 56px icon column the theme had named and nothing used:
@@ -201,8 +220,14 @@ export declare const APP_SIDEBAR_RAIL = "w-nav-rail";
201
220
  /**
202
221
  * The header bar. Its gutter is the page gutter, so the section name in the
203
222
  * header sits directly above the page title under it rather than 8px off.
223
+ *
224
+ * The surface is translucent, so the light at the top of the canvas carries
225
+ * up under it and the header reads as part of the page rather than a lid on
226
+ * it. Its rule fades out toward the end edge and stays solid where it meets
227
+ * the brand row's, so the two read as one line across the top. The rule sits
228
+ * over a transparent border, for the same reason as the sidebar's.
204
229
  */
205
- export declare const APP_HEADER = "flex h-header shrink-0 items-center gap-2 border-b border-line bg-surface px-page-x";
230
+ export declare const APP_HEADER: string;
206
231
  /**
207
232
  * The sidebar's brand row. Exactly as tall as the header beside it: the two
208
233
  * meet at the top left corner and a 4px disagreement there reads as a seam.
@@ -102,8 +102,15 @@ export const PAGE_STACK = 'flex flex-col gap-section';
102
102
  * children flush to the border. `overflow-hidden` is deliberately absent: the
103
103
  * focus ring sits 2px outside the element it belongs to, so a clipping surface
104
104
  * crops the ring of every button inside it down to whichever edge fits.
105
+ *
106
+ * The depth is a few percent of light at the top, a 1px light along the top
107
+ * edge and a soft drop under the card. The light is a translucent background
108
+ * image laid over the background color, so a caller who passes its own bg-*
109
+ * keeps that color under it. The rest are shadows, so the card is the same
110
+ * size with or without them and the 1px border stays the boundary a forced
111
+ * colors scheme keeps.
105
112
  */
106
- export const CARD_SURFACE = 'bg-surface border border-line rounded-xl';
113
+ export const CARD_SURFACE = 'bg-surface bg-card border border-line rounded-card shadow-card inset-shadow-highlight';
107
114
  /**
108
115
  * The card surface in the danger tone, for a group of destructive actions.
109
116
  *
@@ -112,7 +119,7 @@ export const CARD_SURFACE = 'bg-surface border border-line rounded-xl';
112
119
  * percent keeps the controls of a form inside it on a ground they were drawn
113
120
  * for. The contrast suite reads the share from this string.
114
121
  */
115
- export const PANEL_DANGER_SURFACE = 'bg-[color-mix(in_srgb,var(--color-danger)_5%,var(--color-surface))] border border-danger/40 rounded-xl';
122
+ export const PANEL_DANGER_SURFACE = 'bg-[color-mix(in_srgb,var(--color-danger)_5%,var(--color-surface))] border border-danger/40 rounded-card shadow-card inset-shadow-highlight';
116
123
  /**
117
124
  * Card padding by name.
118
125
  *
@@ -188,8 +195,12 @@ export const MODAL_PAD = 'px-card py-card-sm';
188
195
  export function sectionHeading(level, variant = 'default') {
189
196
  // The eyebrow reads the same at both levels. It is a label for the band under
190
197
  // it rather than a title, so it does not take the level's size at all.
198
+ //
199
+ // It is muted and semibold rather than faint and medium. A band label in the
200
+ // faintest gray read as metadata beside the stronger page title, so it
201
+ // stepped up once in weight and once in color.
191
202
  if (variant === 'eyebrow')
192
- return 'text-xs font-medium uppercase tracking-wide text-faint';
203
+ return 'text-xs font-semibold uppercase tracking-wider text-muted';
193
204
  // Level 2 is the brand ramp's H3, which the guideline describes as the card
194
205
  // and group head this is. Its leading and tracking come with it: a --text-*
195
206
  // token sets font-size and nothing else, so the ratio has to be asked for by
@@ -215,8 +226,21 @@ export function sectionHeading(level, variant = 'default') {
215
226
  *
216
227
  * `border-e` and not `border-r`: the shell is the one place a right-to-left
217
228
  * locale flips, and a physical border leaves the rule on the wrong edge.
229
+ *
230
+ * The sidebar runs from the surface at the top a step toward ink at the foot,
231
+ * and its edge is a rule that fades in and out at its ends, drawn over a
232
+ * transparent border. The border keeps the column's width, and a forced
233
+ * colors scheme, which draws no gradient, paints that border in the system
234
+ * text color, so the edge is still there.
218
235
  */
219
- export const APP_SIDEBAR = 'h-full shrink-0 flex-col border-e border-line bg-surface transition-[width] duration-base ease-move';
236
+ export const APP_SIDEBAR = 'relative h-full shrink-0 flex-col border-e border-transparent bg-surface bg-sidebar ' +
237
+ 'transition-[width] duration-base ease-move';
238
+ /**
239
+ * The faded edge of the sidebar, a 1px line laid over its transparent
240
+ * border. It sets a width of its own, so it stays out of APP_SIDEBAR, which
241
+ * holds no width at all.
242
+ */
243
+ export const APP_SIDEBAR_EDGE = 'after:pointer-events-none after:absolute after:inset-y-0 after:-end-px after:w-px after:bg-rule-y';
220
244
  /**
221
245
  * The sidebar's two widths. Expanded is the 224px column every authed screen
222
246
  * had. The rail is the 56px icon column the theme had named and nothing used:
@@ -235,8 +259,16 @@ export const APP_SIDEBAR_RAIL = 'w-nav-rail';
235
259
  /**
236
260
  * The header bar. Its gutter is the page gutter, so the section name in the
237
261
  * header sits directly above the page title under it rather than 8px off.
262
+ *
263
+ * The surface is translucent, so the light at the top of the canvas carries
264
+ * up under it and the header reads as part of the page rather than a lid on
265
+ * it. Its rule fades out toward the end edge and stays solid where it meets
266
+ * the brand row's, so the two read as one line across the top. The rule sits
267
+ * over a transparent border, for the same reason as the sidebar's.
238
268
  */
239
- export const APP_HEADER = 'flex h-header shrink-0 items-center gap-2 border-b border-line bg-surface px-page-x';
269
+ export const APP_HEADER = 'relative flex h-header shrink-0 items-center gap-2 border-b border-transparent bg-surface/70 px-page-x ' +
270
+ 'after:pointer-events-none after:absolute after:inset-x-0 after:-bottom-px after:h-px after:bg-rule-end ' +
271
+ 'rtl:after:-scale-x-100';
240
272
  /**
241
273
  * The sidebar's brand row. Exactly as tall as the header beside it: the two
242
274
  * meet at the top left corner and a 4px disagreement there reads as a seam.
@@ -52,8 +52,11 @@
52
52
  *
53
53
  * No vertical offset and no side. Both belong to placePanel, which measures
54
54
  * the room and puts the panel above the anchor when there is none below.
55
+ *
56
+ * shadow-popover is the rung above a card, so a menu opened from a card reads
57
+ * as lifted off it rather than as part of it.
55
58
  */
56
- export declare const PANEL_SURFACE = "absolute z-dropdown overflow-hidden rounded-xl border border-line-strong bg-surface text-fg shadow-2xl";
59
+ export declare const PANEL_SURFACE = "absolute z-dropdown overflow-hidden rounded-xl border border-line-strong bg-surface text-fg shadow-popover inset-shadow-highlight";
57
60
  /**
58
61
  * The scrolling region inside it.
59
62
  *
@@ -53,8 +53,11 @@ import { TOUCH_GROW } from './touch.js';
53
53
  *
54
54
  * No vertical offset and no side. Both belong to placePanel, which measures
55
55
  * the room and puts the panel above the anchor when there is none below.
56
+ *
57
+ * shadow-popover is the rung above a card, so a menu opened from a card reads
58
+ * as lifted off it rather than as part of it.
56
59
  */
57
- export const PANEL_SURFACE = 'absolute z-dropdown overflow-hidden rounded-xl border border-line-strong bg-surface text-fg shadow-2xl';
60
+ export const PANEL_SURFACE = 'absolute z-dropdown overflow-hidden rounded-xl border border-line-strong bg-surface text-fg shadow-popover inset-shadow-highlight';
58
61
  /**
59
62
  * The scrolling region inside it.
60
63
  *
@@ -20,14 +20,15 @@ export interface ScrollMetrics {
20
20
  }
21
21
  export declare function scrollEdges(el: ScrollMetrics): ScrollEdges;
22
22
  /**
23
- * The fade itself: a dark gradient on the edge, outside the scroll box so it
24
- * does not scroll away with the content it describes. Dark rather than a
25
- * palette color, because the box sits on the page in one place and inside
26
- * a card in another and no surface token is the color to fade to. The
27
- * scrims use black at an alpha for the same reason and it reads in both
28
- * themes. The direction is logical, so a right-to-left page fades the other
23
+ * The fade itself: a wash of the foreground color on the edge, outside the
24
+ * scroll box so it does not scroll away with the content it describes. The
25
+ * foreground token inverts with the theme, so the fade darkens a light card
26
+ * and lightens a dark one. A black fade had nothing to darken on a dark card
27
+ * and read as no fade at all. No surface token is the color to fade to,
28
+ * because the box sits on the page in one place and inside a card in
29
+ * another. The direction is logical, so a right-to-left page fades the other
29
30
  * way.
30
31
  */
31
32
  export declare const SCROLL_EDGE = "pointer-events-none absolute inset-y-px w-6";
32
- export declare const SCROLL_EDGE_START = "start-0 bg-linear-to-r rtl:bg-linear-to-l from-black/25 to-transparent";
33
- export declare const SCROLL_EDGE_END = "end-0 bg-linear-to-l rtl:bg-linear-to-r from-black/25 to-transparent";
33
+ export declare const SCROLL_EDGE_START = "start-0 bg-linear-to-r rtl:bg-linear-to-l from-fg/25 to-transparent";
34
+ export declare const SCROLL_EDGE_END = "end-0 bg-linear-to-l rtl:bg-linear-to-r from-fg/25 to-transparent";
@@ -20,14 +20,15 @@ export function scrollEdges(el) {
20
20
  return { before: traveled > 1, after: traveled < slack - 1 };
21
21
  }
22
22
  /**
23
- * The fade itself: a dark gradient on the edge, outside the scroll box so it
24
- * does not scroll away with the content it describes. Dark rather than a
25
- * palette color, because the box sits on the page in one place and inside
26
- * a card in another and no surface token is the color to fade to. The
27
- * scrims use black at an alpha for the same reason and it reads in both
28
- * themes. The direction is logical, so a right-to-left page fades the other
23
+ * The fade itself: a wash of the foreground color on the edge, outside the
24
+ * scroll box so it does not scroll away with the content it describes. The
25
+ * foreground token inverts with the theme, so the fade darkens a light card
26
+ * and lightens a dark one. A black fade had nothing to darken on a dark card
27
+ * and read as no fade at all. No surface token is the color to fade to,
28
+ * because the box sits on the page in one place and inside a card in
29
+ * another. The direction is logical, so a right-to-left page fades the other
29
30
  * way.
30
31
  */
31
32
  export const SCROLL_EDGE = 'pointer-events-none absolute inset-y-px w-6';
32
- export const SCROLL_EDGE_START = 'start-0 bg-linear-to-r rtl:bg-linear-to-l from-black/25 to-transparent';
33
- export const SCROLL_EDGE_END = 'end-0 bg-linear-to-l rtl:bg-linear-to-r from-black/25 to-transparent';
33
+ export const SCROLL_EDGE_START = 'start-0 bg-linear-to-r rtl:bg-linear-to-l from-fg/25 to-transparent';
34
+ export const SCROLL_EDGE_END = 'end-0 bg-linear-to-l rtl:bg-linear-to-r from-fg/25 to-transparent';