@lyeve-labs/ui-kit 0.17.1 → 0.18.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 (37) hide show
  1. package/README.md +16 -13
  2. package/dist/components/AccordionItem.svelte +1 -1
  3. package/dist/components/AccountMenu.svelte +1 -1
  4. package/dist/components/AppShell.svelte +20 -6
  5. package/dist/components/Autocomplete.svelte +10 -1
  6. package/dist/components/Card.svelte +1 -0
  7. package/dist/components/Collapsible.svelte +1 -1
  8. package/dist/components/DatePicker.svelte +1 -1
  9. package/dist/components/Drawer.svelte +6 -26
  10. package/dist/components/Dropdown.svelte +196 -10
  11. package/dist/components/Dropdown.svelte.d.ts +12 -0
  12. package/dist/components/Logo.svelte +77 -0
  13. package/dist/components/Logo.svelte.d.ts +35 -0
  14. package/dist/components/Modal.svelte +2 -15
  15. package/dist/components/MultiSelect.svelte +12 -3
  16. package/dist/components/PageHeader.svelte +23 -1
  17. package/dist/components/Panel.svelte +1 -1
  18. package/dist/components/SearchInput.svelte +10 -1
  19. package/dist/components/Table.svelte +143 -10
  20. package/dist/components/Table.svelte.d.ts +7 -0
  21. package/dist/components/ThemeToggle.svelte +75 -12
  22. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  23. package/dist/components/Toaster.svelte +3 -15
  24. package/dist/components/Tooltip.svelte +1 -1
  25. package/dist/components/dialog/Dialog.svelte +9 -4
  26. package/dist/components/dialog/dialog-manager.svelte.d.ts +20 -1
  27. package/dist/components/dialog/dialog-manager.svelte.js +13 -2
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.js +6 -2
  30. package/dist/internal/layout.js +11 -2
  31. package/dist/internal/panel.d.ts +1 -1
  32. package/dist/internal/panel.js +1 -1
  33. package/dist/styles/theme.css +235 -0
  34. package/dist/utils/theme.d.ts +68 -3
  35. package/dist/utils/theme.js +118 -3
  36. package/package.json +18 -19
  37. package/src/lib/styles/theme.css +235 -0
@@ -199,11 +199,20 @@
199
199
  event.stopPropagation();
200
200
  remove(option.value);
201
201
  }}
202
- class="transition-colors duration-150 hover:text-brand-light"
202
+ class="-m-1.5 p-1.5 transition-colors duration-150 hover:text-brand-light"
203
203
  >
204
+ <!--
205
+ 12px inside 6px of padding is a 24px target, which is the floor
206
+ SC 2.5.8 puts on a control that cannot claim the spacing
207
+ exception, and this one sits inside the trigger. The padding is
208
+ taken straight back off as margin, so the chip is laid out
209
+ exactly as it was and only the reachable area grew. The glyph
210
+ is the same 12px cross Autocomplete clears its value with; it
211
+ was drawn a pixel smaller here for no reason anyone recorded.
212
+ -->
204
213
  <svg
205
- width="11"
206
- height="11"
214
+ width="12"
215
+ height="12"
207
216
  viewBox="0 0 24 24"
208
217
  fill="none"
209
218
  stroke="currentColor"
@@ -43,9 +43,31 @@
43
43
  <div class="min-w-0">
44
44
  <!-- The app shells render their own h1 for the current route, so a heading
45
45
  lookup by name matches two elements. This names the page's own title. -->
46
+ <!--
47
+ The page title is the one place the brand type scale had a component to
48
+ land on and did not: eighteen type tokens shipped and nothing referenced
49
+ one of them, while this rendered at 24px, a size the ramp does not
50
+ contain.
51
+
52
+ text-h2 and not text-h1. The brand ramp is a marketing ramp - it runs
53
+ 64 / 44 / 32 / 22 / 16 and holds nothing between 16 and 22, which is
54
+ where a console's own type lives. A 44px page title over a 14px table on
55
+ a 390px screen is the ramp applied rather than adopted. 32px is the step
56
+ the ramp does hold and the step this was already reaching for.
57
+
58
+ The weight stays at 700, which is the ramp's H1 weight, because this is
59
+ still the page's h1. Taking H2's 600 along with its size would move the
60
+ title twice for one decision.
61
+
62
+ Each size token names its own leading and tracking. A `text-*` token
63
+ built from `--text-*` alone sets font-size and leaves line-height to
64
+ whatever it inherits, which is not the ratio the ramp specifies.
65
+ -->
46
66
  <h1
47
67
  data-testid="page-title"
48
- class={compact ? 'text-sm font-semibold text-fg' : 'text-2xl font-bold text-fg'}
68
+ class={compact
69
+ ? 'text-sm font-semibold text-fg'
70
+ : 'text-h2 leading-h2 tracking-h2 font-bold text-fg'}
49
71
  >
50
72
  {title}
51
73
  </h1>
@@ -68,7 +68,7 @@
68
68
  const surface = $derived(`${SURFACE[variant]} ${CARD_PAD[pad]}`.trim());
69
69
  </script>
70
70
 
71
- <div class="flex flex-col gap-stack {surface} {klass}">
71
+ <div data-print="keep" class="flex flex-col gap-stack {surface} {klass}">
72
72
  {#snippet headingText()}
73
73
  {#if icon}
74
74
  {@const Icon = icon}
@@ -71,11 +71,20 @@
71
71
  />
72
72
 
73
73
  {#if value}
74
+ <!--
75
+ 14px inside 6px of padding is a 26px target, past the 24px floor SC
76
+ 2.5.8 puts on a control with no spacing exception to claim, and this one
77
+ sits inside the field. The padding is taken back off the horizontal axis
78
+ only: the box is positioned from its right edge, so a negative right
79
+ margin holds the glyph exactly where it was, while a negative top margin
80
+ would fight the translate that centres it and lift it 6px off the middle
81
+ of the field.
82
+ -->
74
83
  <button
75
84
  type="button"
76
85
  onclick={clear}
77
86
  aria-label="Clear search"
78
- class="absolute right-2.5 top-1/2 -translate-y-1/2 text-faint
87
+ class="absolute right-2.5 top-1/2 -mx-1.5 -translate-y-1/2 p-1.5 text-faint
79
88
  transition-colors duration-150 hover:text-fg"
80
89
  >
81
90
  <svg
@@ -5,6 +5,13 @@
5
5
  striped?: boolean;
6
6
  hoverable?: boolean;
7
7
  fixed?: boolean;
8
+ /**
9
+ * Names the scroll box, so a screen reader announces what the region it
10
+ * just landed in holds. Without it the box is still focusable and simply
11
+ * carries no role: an unnamed region is announced as "region" and nothing
12
+ * else, which is a landmark the reader has to enter to identify.
13
+ */
14
+ label?: string;
8
15
  class?: string;
9
16
  children: Snippet;
10
17
  }
@@ -13,26 +20,152 @@
13
20
  striped = false,
14
21
  hoverable = true,
15
22
  fixed = false,
23
+ label = undefined,
16
24
  class: cls = '',
17
25
  children,
18
26
  }: Props = $props();
27
+
28
+ /*
29
+ * The scroll box is a tab stop. A wide table scrolls sideways, and a mouse is
30
+ * the only thing that could reach the columns past the right edge: nothing
31
+ * inside a plain table takes focus, so the arrow keys never had an element to
32
+ * act on (SC 2.1.1).
33
+ *
34
+ * Spread rather than written on the element. The compiler reads a literal
35
+ * tabindex on any element it does not consider interactive as a defect, and
36
+ * a focusable scroll container is the one case where that is the fix rather
37
+ * than the fault. The rule cannot express it and the gate stays on for
38
+ * everything else, so the attributes are assembled here.
39
+ */
40
+ const scrollAttrs = $derived(
41
+ label === undefined ? { tabindex: 0 } : { tabindex: 0, role: 'region', 'aria-label': label },
42
+ );
43
+
44
+ /*
45
+ * Reachable is not the same as discoverable.
46
+ *
47
+ * Making the box a tab stop answered SC 2.1.1 and told nobody the columns
48
+ * were there. A desktop has a scrollbar to give it away; a phone has an
49
+ * overlay scrollbar that is painted while a finger is moving and is invisible
50
+ * the rest of the time. On the admin's plugin list at 390px the column that
51
+ * falls off the edge is ACTIONS, which is the only thing on the page a reader
52
+ * came to do, and nothing on screen says so.
53
+ *
54
+ * A fade on the edge that has more behind it. Only when the box really
55
+ * overflows, and only on the side the content continues on, so it reads as a
56
+ * property of this table rather than as decoration every table wears.
57
+ */
58
+ let scroller: HTMLElement | undefined = $state();
59
+ let moreBefore = $state(false);
60
+ let moreAfter = $state(false);
61
+
62
+ function measure() {
63
+ const el = scroller;
64
+ if (!el) return;
65
+ const slack = el.scrollWidth - el.clientWidth;
66
+ // A sub-pixel layout leaves scrollLeft a fraction short of its own maximum
67
+ // at the far end, so an exact comparison never reports the end as reached.
68
+ if (slack <= 1) {
69
+ moreBefore = false;
70
+ moreAfter = false;
71
+ return;
72
+ }
73
+ // A right-to-left box reports the offset as a negative number, and the two
74
+ // edges are the same two edges either way round.
75
+ const travelled = Math.abs(el.scrollLeft);
76
+ moreBefore = travelled > 1;
77
+ moreAfter = travelled < slack - 1;
78
+ }
79
+
80
+ $effect(() => {
81
+ const el = scroller;
82
+ if (!el) return;
83
+ measure();
84
+ // The width that matters changes without a scroll and without a resize of
85
+ // the window: a column added, a filter applied, a sidebar collapsed. Both
86
+ // boxes are watched because either one moving changes whether the other
87
+ // overflows.
88
+ if (typeof ResizeObserver === 'undefined') return;
89
+ const observer = new ResizeObserver(measure);
90
+ observer.observe(el);
91
+ if (el.firstElementChild) observer.observe(el.firstElementChild);
92
+ return () => observer.disconnect();
93
+ });
94
+
95
+ /*
96
+ * A dark fade rather than a palette colour. The box has no background of its
97
+ * own and sits on the page in one place and inside a card in another, so no
98
+ * surface token is the colour to fade to. The scrims use black at an alpha
99
+ * for the same reason and it reads in both themes.
100
+ */
101
+ const EDGE = 'pointer-events-none absolute inset-y-px w-6';
19
102
  </script>
20
103
 
21
- <div class="w-full overflow-x-auto rounded-xl border border-line {cls}">
22
- <table
23
- class="w-full text-sm text-left
104
+ <div data-testid="table-frame" class="relative w-full rounded-xl border border-line {cls}">
105
+ <!--
106
+ data-print: the box exists to clip. On paper there is no viewport to clip
107
+ to, and every column past the edge would simply not be printed.
108
+ -->
109
+ <div
110
+ bind:this={scroller}
111
+ onscroll={measure}
112
+ data-print="unclip"
113
+ data-testid="table-scroll"
114
+ class="w-full overflow-x-auto rounded-xl"
115
+ {...scrollAttrs}
116
+ >
117
+ <!--
118
+ border-separate with no spacing, and the row separators on the cells.
119
+
120
+ The hovered row draws a ring, and a ring is a box-shadow: a browser paints
121
+ no box-shadow on a table row while the table collapses its borders, so the
122
+ same row treatment the panels use would have rendered nothing at all here.
123
+ The separate model paints it. Borders on a row are ignored in that model,
124
+ which is why the lines moved down to the cells they were already drawn
125
+ between; with border-spacing at zero the result is the same line in the
126
+ same place.
127
+ -->
128
+ <table
129
+ class="w-full border-separate border-spacing-0 text-sm text-left
24
130
  {fixed ? 'table-fixed' : ''}
25
- [&_thead_tr]:border-b [&_thead_tr]:border-line [&_thead]:bg-surface-2/40
131
+ [&_thead_th]:border-b [&_thead_th]:border-line [&_thead]:bg-surface-2/40
26
132
  [&_thead_th]:px-4 [&_thead_th]:py-3 [&_thead_th]:text-xs
27
133
  [&_thead_th]:font-medium [&_thead_th]:text-faint
28
134
  [&_thead_th]:uppercase [&_thead_th]:tracking-wider [&_thead_th]:whitespace-nowrap
29
135
  [&_tbody_td]:px-4 [&_tbody_td]:py-3 [&_tbody_td]:text-fg [&_tbody_td]:align-middle
30
- [&_tbody_tr]:border-b [&_tbody_tr]:border-line [&_tbody_tr:last-child]:border-0
136
+ [&_tbody_td]:border-b [&_tbody_td]:border-line [&_tbody_tr:last-child_td]:border-0
31
137
  {striped ? '[&_tbody_tr:nth-child(even)]:bg-surface-2/40' : ''}
32
138
  {hoverable
33
- ? '[&_tbody_tr:hover]:bg-surface-2/60 [&_tbody_tr]:transition-colors [&_tbody_tr]:duration-150'
34
- : ''}"
35
- >
36
- {@render children()}
37
- </table>
139
+ ? '[&_tbody_tr:hover]:bg-surface-2/60 [&_tbody_tr:hover]:ring-1 [&_tbody_tr:hover]:ring-inset [&_tbody_tr:hover]:ring-brand [&_tbody_tr]:transition-colors [&_tbody_tr]:duration-150'
140
+ : ''}"
141
+ >
142
+ {@render children()}
143
+ </table>
144
+ </div>
145
+
146
+ <!--
147
+ aria-hidden and outside the scroll box. It is a picture of the scrollbar
148
+ the phone will not draw, it says nothing a screen reader has not already
149
+ been told by the region, and a child of the box would scroll away with the
150
+ content it is describing.
151
+
152
+ The gradient direction is logical: in a right-to-left locale the start edge
153
+ is the right one and the fade has to run the other way with it.
154
+ -->
155
+ {#if moreBefore}
156
+ <div
157
+ aria-hidden="true"
158
+ data-print="hide"
159
+ data-testid="table-more-before"
160
+ class="{EDGE} start-0 rounded-s-xl bg-linear-to-r rtl:bg-linear-to-l from-black/25 to-transparent"
161
+ ></div>
162
+ {/if}
163
+ {#if moreAfter}
164
+ <div
165
+ aria-hidden="true"
166
+ data-print="hide"
167
+ data-testid="table-more-after"
168
+ class="{EDGE} end-0 rounded-e-xl bg-linear-to-l rtl:bg-linear-to-r from-black/25 to-transparent"
169
+ ></div>
170
+ {/if}
38
171
  </div>
@@ -3,6 +3,13 @@ interface Props {
3
3
  striped?: boolean;
4
4
  hoverable?: boolean;
5
5
  fixed?: boolean;
6
+ /**
7
+ * Names the scroll box, so a screen reader announces what the region it
8
+ * just landed in holds. Without it the box is still focusable and simply
9
+ * carries no role: an unnamed region is announced as "region" and nothing
10
+ * else, which is a landmark the reader has to enter to identify.
11
+ */
12
+ label?: string;
6
13
  class?: string;
7
14
  children: Snippet;
8
15
  }
@@ -1,33 +1,96 @@
1
1
  <script lang="ts">
2
- import { Moon, Sun } from '@lucide/svelte';
3
- import { getTheme, toggleTheme, type Theme } from '../utils/theme.js';
2
+ /**
3
+ * The control that picks the palette.
4
+ *
5
+ * Three states, not two. It offered `dark` and `light` and had no way to say
6
+ * "whatever the operating system is on", which is the state a reader who
7
+ * already answered that question at the OS level expects to find, and the
8
+ * reason one console shipped its own copy of this file rather than adopt it.
9
+ *
10
+ * One button rather than three radios: it sits in the header bar of every
11
+ * authed screen in three applications, next to the account menu, and a
12
+ * segmented control there costs the width of the section name beside it. The
13
+ * cost of the cycle is that the button's name has to carry the current state
14
+ * as well as the action, because with three states the action no longer
15
+ * implies the state.
16
+ */
17
+ import { Monitor, Moon, Sun } from '@lucide/svelte';
18
+ import {
19
+ THEME_PREFERENCES,
20
+ getThemePreference,
21
+ nextThemePreference,
22
+ setThemePreference,
23
+ watchSystemTheme,
24
+ type ThemePreference,
25
+ } from '../utils/theme.js';
4
26
 
5
27
  interface Props {
28
+ /**
29
+ * Which preferences to offer, in the order the button cycles through them.
30
+ *
31
+ * A surface that must not offer `system` - a screen printed to a fixed
32
+ * palette, a kiosk - passes `['light', 'dark']` and gets the two-state
33
+ * control back with no other change.
34
+ */
35
+ order?: readonly ThemePreference[];
6
36
  class?: string;
7
37
  }
8
38
 
9
- let { class: klass = '' }: Props = $props();
39
+ let { order = THEME_PREFERENCES, class: klass = '' }: Props = $props();
10
40
 
11
- let theme = $state<Theme>('dark');
41
+ /**
42
+ * `system` until the browser says otherwise, which is what an unanswered
43
+ * question resolves to. The server cannot read storage or the OS, so a
44
+ * hardcoded `dark` here would render the wrong icon on every light-OS
45
+ * reader's first paint.
46
+ */
47
+ let preference = $state<ThemePreference>('system');
12
48
 
13
49
  $effect(() => {
14
- theme = getTheme();
50
+ preference = getThemePreference();
15
51
  });
16
52
 
17
- function flip() {
18
- theme = toggleTheme();
53
+ /**
54
+ * A reader on `system` whose OS flips at dusk gets the new palette without
55
+ * touching anything. Without this the preference is honoured once, at load,
56
+ * and reads as ignored for the rest of the session.
57
+ */
58
+ $effect(() =>
59
+ watchSystemTheme(() => {
60
+ if (preference === 'system') setThemePreference('system');
61
+ }),
62
+ );
63
+
64
+ const upcoming = $derived(nextThemePreference(preference, order));
65
+
66
+ const LABEL: Record<ThemePreference, string> = {
67
+ light: 'light',
68
+ dark: 'dark',
69
+ system: 'system',
70
+ };
71
+
72
+ function advance() {
73
+ preference = upcoming;
74
+ setThemePreference(preference);
19
75
  }
20
76
  </script>
21
77
 
22
78
  <button
23
79
  type="button"
24
- onclick={flip}
25
- aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
80
+ onclick={advance}
81
+ data-print="hide"
82
+ data-theme-preference={preference}
83
+ aria-label="Theme: {LABEL[preference]}. Switch to {LABEL[upcoming]}."
26
84
  class="rounded-lg p-2 text-faint transition-colors duration-150 hover:bg-surface-2 hover:text-fg {klass}"
27
85
  >
28
- {#if theme === 'dark'}
29
- <Sun size={16} aria-hidden="true" />
30
- {:else}
86
+ <!-- The icon shows the state the control is in, not the one it moves to. It
87
+ showed the destination while the label named the same destination, which
88
+ said the action twice and the state not at all. -->
89
+ {#if preference === 'system'}
90
+ <Monitor size={16} aria-hidden="true" />
91
+ {:else if preference === 'dark'}
31
92
  <Moon size={16} aria-hidden="true" />
93
+ {:else}
94
+ <Sun size={16} aria-hidden="true" />
32
95
  {/if}
33
96
  </button>
@@ -1,4 +1,13 @@
1
+ import { type ThemePreference } from '../utils/theme.js';
1
2
  interface Props {
3
+ /**
4
+ * Which preferences to offer, in the order the button cycles through them.
5
+ *
6
+ * A surface that must not offer `system` - a screen printed to a fixed
7
+ * palette, a kiosk - passes `['light', 'dark']` and gets the two-state
8
+ * control back with no other change.
9
+ */
10
+ order?: readonly ThemePreference[];
2
11
  class?: string;
3
12
  }
4
13
  declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
@@ -17,7 +17,8 @@
17
17
  changes, and a region that arrives already holding its text is not a
18
18
  change. -->
19
19
  <div
20
- class="pointer-events-none fixed bottom-4 right-4 z-[60] flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
20
+ data-print="hide"
21
+ class="pointer-events-none fixed bottom-4 right-4 z-toast flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
21
22
  role="status"
22
23
  aria-live="polite"
23
24
  aria-atomic="false"
@@ -25,7 +26,7 @@
25
26
  {#each toast.items as t (t.id)}
26
27
  <div
27
28
  class="pointer-events-auto flex items-start gap-3 overflow-hidden rounded-lg border border-line
28
- bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-in_140ms_ease-out]"
29
+ bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-in_var(--duration-toast-in)_ease-out]"
29
30
  >
30
31
  <span class="w-1 self-stretch shrink-0 {styles[statusTone(t.tone)].bar}"></span>
31
32
  <span
@@ -68,16 +69,3 @@
68
69
  </div>
69
70
  {/each}
70
71
  </div>
71
-
72
- <style>
73
- @keyframes toast-in {
74
- from {
75
- opacity: 0;
76
- transform: translateX(12px);
77
- }
78
- to {
79
- opacity: 1;
80
- transform: translateX(0);
81
- }
82
- }
83
- </style>
@@ -72,7 +72,7 @@
72
72
  {id}
73
73
  role="tooltip"
74
74
  hidden={!visible}
75
- class="absolute {pos[position]} z-50 max-w-[min(16rem,calc(100vw-2rem))]
75
+ class="absolute {pos[position]} z-tooltip max-w-[min(16rem,calc(100vw-2rem))]
76
76
  rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-xl"
77
77
  >
78
78
  {text}
@@ -32,9 +32,8 @@
32
32
  let visible = $state(false);
33
33
  let exiting = $state(false);
34
34
 
35
- // Stacked offset: each deeper dialog shrinks and shifts back
35
+ // Stacked offset: each deeper dialog shrinks, shifts back and rises
36
36
  let offset = $derived(entry.depth);
37
- let zIndex = $derived(50 + offset);
38
37
 
39
38
  // ──────────────────────────────────────────────────────
40
39
  // DOM refs
@@ -93,10 +92,16 @@
93
92
  <!-- z-index is an inline style, not a `z-[...]` class. Tailwind scans source text
94
93
  for complete class names, so a class built from a runtime value matches no
95
94
  candidate and no rule is ever generated: every stacked dialog rendered at
96
- `z-index: auto` and the stacking order came down to DOM order. -->
95
+ `z-index: auto` and the stacking order came down to DOM order.
96
+
97
+ The base is the modal layer rather than the literal 50 it used to be. 50
98
+ was the number every floating surface in the kit happened to hold, so a
99
+ dialog neither sat above a drawer nor below a tooltip on purpose; it sat
100
+ wherever the document put it. The offset climbs from there, and the layer
101
+ above is 100 clear of this one. -->
97
102
  <div
98
103
  class="fixed inset-0 flex items-center justify-center"
99
- style="z-index: {zIndex}"
104
+ style="z-index: calc(var(--z-index-modal) + {offset})"
100
105
  role="presentation"
101
106
  >
102
107
  <!-- Backdrop -->
@@ -40,13 +40,32 @@ export declare function dismissDialog(id?: string): void;
40
40
  * Close all open dialogs, rejecting their promises.
41
41
  */
42
42
  export declare function dismissAllDialogs(): void;
43
+ /** The labels a caller may override on a confirm dialog. */
44
+ export interface ConfirmOptions {
45
+ /**
46
+ * The verb on the accepting button. Defaults to `Confirm`.
47
+ *
48
+ * Name the action rather than accepting the default wherever the caller has
49
+ * a verb of its own: a button reading `Delete` says what happens, and one
50
+ * reading `Confirm` only says that something does. ConfirmDialog has always
51
+ * read this, but until it was reachable from here a caller had to drop to
52
+ * openDialog and setDialogMeta to reach it, so every call site took the
53
+ * generic label instead.
54
+ */
55
+ confirmLabel?: string;
56
+ /** The verb on the dismissing button. Defaults to `Cancel`. */
57
+ cancelLabel?: string;
58
+ }
43
59
  /**
44
60
  * Convenience: confirm dialog.
45
61
  * Returns `true` if user confirmed, `false` if cancelled.
46
62
  *
47
63
  * const ok = await confirm('Delete item?', 'This cannot be undone.');
64
+ * const ok = await confirm('Delete item?', 'This cannot be undone.', {
65
+ * confirmLabel: 'Delete',
66
+ * });
48
67
  */
49
- export declare function confirm(title: string, message?: string): Promise<boolean>;
68
+ export declare function confirm(title: string, message?: string, options?: ConfirmOptions): Promise<boolean>;
50
69
  /**
51
70
  * Set metadata on a dialog entry after it's opened.
52
71
  * Used by convenience wrappers to pass extra data without polluting DialogOptions.
@@ -136,11 +136,22 @@ export function dismissAllDialogs() {
136
136
  * Returns `true` if user confirmed, `false` if cancelled.
137
137
  *
138
138
  * const ok = await confirm('Delete item?', 'This cannot be undone.');
139
+ * const ok = await confirm('Delete item?', 'This cannot be undone.', {
140
+ * confirmLabel: 'Delete',
141
+ * });
139
142
  */
140
- export function confirm(title, message) {
143
+ export function confirm(title, message, options) {
141
144
  const id = `confirm-${++idCounter}`;
142
145
  const promise = openDialog({ id, size: 'sm', title });
143
- setDialogMeta(id, { confirmTitle: title, confirmMessage: message ?? '' });
146
+ setDialogMeta(id, {
147
+ confirmTitle: title,
148
+ confirmMessage: message ?? '',
149
+ // Undefined keys are dropped rather than written, because ConfirmDialog
150
+ // decides its default by type-checking the meta value: writing undefined
151
+ // would be indistinguishable from an override to the empty case.
152
+ ...(options?.confirmLabel !== undefined ? { confirmLabel: options.confirmLabel } : {}),
153
+ ...(options?.cancelLabel !== undefined ? { cancelLabel: options.cancelLabel } : {}),
154
+ });
144
155
  // Cancelling a dialog dismisses it, and dismissal rejects. Callers write
145
156
  // `if (await confirm(...))`, so a rejection on Cancel is an unhandled
146
157
  // rejection on the ordinary path rather than an error anyone meant to
package/dist/index.d.ts CHANGED
@@ -75,6 +75,7 @@ export { default as Kbd } from './components/Kbd.svelte';
75
75
  export { default as CopyButton } from './components/CopyButton.svelte';
76
76
  export { default as Avatar } from './components/Avatar.svelte';
77
77
  export { default as AvatarGroup } from './components/AvatarGroup.svelte';
78
+ export { default as Logo } from './components/Logo.svelte';
78
79
  export { default as ThemeToggle } from './components/ThemeToggle.svelte';
79
80
  export { default as Toaster } from './components/Toaster.svelte';
80
81
  export { toast } from './stores/toast.svelte.js';
@@ -83,7 +84,8 @@ export { default as DialogContainer } from './components/dialog/DialogContainer.
83
84
  export { default as Dialog } from './components/dialog/Dialog.svelte';
84
85
  export { default as ConfirmDialog } from './components/dialog/ConfirmDialog.svelte';
85
86
  export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, setDialogMeta, getDialogStack, } from './components/dialog/dialog-manager.svelte.js';
87
+ export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.js';
86
88
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
87
89
  export { cn, type ClassValue } from './utils/cn.js';
88
- export { getTheme, setTheme, toggleTheme, themeBootScript, type Theme } from './utils/theme.js';
89
- export declare const VERSION = "0.17.1";
90
+ export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
91
+ export declare const VERSION = "0.18.0";
package/dist/index.js CHANGED
@@ -76,6 +76,10 @@ export { default as CopyButton } from './components/CopyButton.svelte';
76
76
  // ── Media ──────────────────────────────────────────────────────────────────
77
77
  export { default as Avatar } from './components/Avatar.svelte';
78
78
  export { default as AvatarGroup } from './components/AvatarGroup.svelte';
79
+ // The product mark. Two applications carried their own copy of the same lockup
80
+ // and the two had drifted apart in size, in weight and in how they answered the
81
+ // theme; the kit owns it now.
82
+ export { default as Logo } from './components/Logo.svelte';
79
83
  // ── Theming & toasts ───────────────────────────────────────────────────────
80
84
  export { default as ThemeToggle } from './components/ThemeToggle.svelte';
81
85
  export { default as Toaster } from './components/Toaster.svelte';
@@ -88,8 +92,8 @@ export { default as ConfirmDialog } from './components/dialog/ConfirmDialog.svel
88
92
  export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, setDialogMeta, getDialogStack, } from './components/dialog/dialog-manager.svelte.js';
89
93
  // ── Utilities ──────────────────────────────────────────────────────────────
90
94
  export { cn } from './utils/cn.js';
91
- export { getTheme, setTheme, toggleTheme, themeBootScript } from './utils/theme.js';
95
+ export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, } from './utils/theme.js';
92
96
  // ── Version ────────────────────────────────────────────────────────────────
93
97
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
94
98
  // this line; the build and the test suite fail when the two disagree.
95
- export const VERSION = '0.17.1';
99
+ export const VERSION = '0.18.0';
@@ -137,10 +137,19 @@ export function sectionHeading(level, variant = 'default') {
137
137
  // it rather than a title, so it does not take the level's size at all.
138
138
  if (variant === 'eyebrow')
139
139
  return 'text-xs font-medium uppercase tracking-wide text-faint';
140
+ // Level 2 is the brand ramp's H3, which the guideline describes as the card
141
+ // and group head this is. Its leading and tracking come with it: a --text-*
142
+ // token sets font-size and nothing else, so the ratio has to be asked for by
143
+ // name or the heading inherits whatever line-height its parent had.
144
+ //
140
145
  // Level 3 drops a size rather than a weight. Inside a card it sits under the
141
146
  // card's own semibold title, and two semibold lines at the same size read as
142
- // one heading broken in half.
143
- return level === 2 ? 'text-lg font-semibold text-fg' : 'text-sm font-semibold text-fg';
147
+ // one heading broken in half. It stays at text-sm: the ramp holds nothing
148
+ // between 16px and 22px, and 16px is exactly the size of the card title it
149
+ // has to sit below.
150
+ return level === 2
151
+ ? 'text-h3 leading-h3 tracking-h3 font-semibold text-fg'
152
+ : 'text-sm font-semibold text-fg';
144
153
  }
145
154
  /**
146
155
  * The authed app frame, stated once for every app that has one.
@@ -40,7 +40,7 @@
40
40
  * needs a boundary that clears 3:1, which line does not. It carries the
41
41
  * resting text colour so a row can override it with a single utility.
42
42
  */
43
- export declare const PANEL_SURFACE = "absolute z-50 mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl";
43
+ export declare const PANEL_SURFACE = "absolute z-dropdown mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl";
44
44
  /**
45
45
  * The scrolling region inside it.
46
46
  *
@@ -40,7 +40,7 @@
40
40
  * needs a boundary that clears 3:1, which line does not. It carries the
41
41
  * resting text colour so a row can override it with a single utility.
42
42
  */
43
- export const PANEL_SURFACE = 'absolute z-50 mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl';
43
+ export const PANEL_SURFACE = 'absolute z-dropdown mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl';
44
44
  /**
45
45
  * The scrolling region inside it.
46
46
  *