@joewinke/jatui 0.39.0 → 0.41.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 (75) hide show
  1. package/CLAUDE.md +52 -5
  2. package/THIRD_PARTY_NOTICES.md +31 -0
  3. package/dist/actions/scrollEdge.d.ts +55 -0
  4. package/dist/actions/scrollEdge.js +144 -0
  5. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
  6. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
  7. package/dist/components/AnimatedNumber.svelte +42 -1
  8. package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
  9. package/dist/components/BottomSheet.svelte +7 -6
  10. package/dist/components/BottomSheet.svelte.d.ts +2 -0
  11. package/dist/components/CloseButton.svelte +75 -0
  12. package/dist/components/CloseButton.svelte.d.ts +29 -0
  13. package/dist/components/Drawer.svelte +10 -11
  14. package/dist/components/Drawer.svelte.d.ts +2 -8
  15. package/dist/components/IconButton.svelte +200 -0
  16. package/dist/components/IconButton.svelte.d.ts +46 -0
  17. package/dist/components/LinkButton.svelte +117 -0
  18. package/dist/components/LinkButton.svelte.d.ts +32 -0
  19. package/dist/components/MediaLightbox.svelte +2 -7
  20. package/dist/components/Modal.svelte +2 -10
  21. package/dist/components/PinInput.svelte +94 -31
  22. package/dist/components/PinInput.svelte.d.ts +4 -0
  23. package/dist/components/SegmentedControl.svelte +263 -0
  24. package/dist/components/SegmentedControl.svelte.d.ts +48 -0
  25. package/dist/components/TextArea.svelte +67 -5
  26. package/dist/components/TextArea.svelte.d.ts +4 -6
  27. package/dist/components/ThemeSelector.svelte +29 -9
  28. package/dist/components/ThemeSelector.svelte.d.ts +6 -0
  29. package/dist/components/ThemeToggle.svelte +28 -3
  30. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  31. package/dist/components/basic/Breadcrumb.svelte +226 -11
  32. package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
  33. package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
  34. package/dist/components/basic/breadcrumbCollapse.js +12 -0
  35. package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
  36. package/dist/components/settings/SettingsRow.svelte +177 -0
  37. package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
  38. package/dist/components/settings/SettingsSection.svelte +139 -0
  39. package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
  40. package/dist/components/stats/ActivityHeatmap.svelte +412 -0
  41. package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
  42. package/dist/components/stats/BarList.svelte +262 -0
  43. package/dist/components/stats/BarList.svelte.d.ts +57 -0
  44. package/dist/components/stats/ChartCard.svelte +230 -0
  45. package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
  46. package/dist/components/stats/StatCard.svelte +387 -0
  47. package/dist/components/stats/StatCard.svelte.d.ts +37 -0
  48. package/dist/components/stats/StatGroup.svelte +67 -0
  49. package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
  50. package/dist/components/stats/heatmap.d.ts +76 -0
  51. package/dist/components/stats/heatmap.js +151 -0
  52. package/dist/components/tree/TreeView.svelte +680 -0
  53. package/dist/components/tree/TreeView.svelte.d.ts +123 -0
  54. package/dist/components/tree/flattenTree.d.ts +57 -0
  55. package/dist/components/tree/flattenTree.js +91 -0
  56. package/dist/components/tree/treeKeys.d.ts +46 -0
  57. package/dist/components/tree/treeKeys.js +80 -0
  58. package/dist/data/grid/cells.d.ts +40 -0
  59. package/dist/data/grid/cells.js +89 -0
  60. package/dist/data/grid/clipboard.d.ts +46 -0
  61. package/dist/data/grid/clipboard.js +131 -0
  62. package/dist/data/grid/edits.d.ts +148 -0
  63. package/dist/data/grid/edits.js +222 -0
  64. package/dist/data/grid/index.d.ts +12 -0
  65. package/dist/data/grid/index.js +12 -0
  66. package/dist/index.d.ts +36 -1
  67. package/dist/index.js +31 -1
  68. package/dist/styles/tokens.css +80 -1
  69. package/dist/utils/pinInput.d.ts +33 -0
  70. package/dist/utils/pinInput.js +63 -0
  71. package/dist/utils/textareaRows.d.ts +23 -0
  72. package/dist/utils/textareaRows.js +13 -0
  73. package/dist/utils/themeWipe.d.ts +36 -17
  74. package/dist/utils/themeWipe.js +145 -35
  75. package/package.json +5 -1
@@ -8,6 +8,8 @@
8
8
  * class (e.g. `'w-[42rem]'`) which takes precedence over `size`.
9
9
  */
10
10
 
11
+ import CloseButton from './CloseButton.svelte';
12
+ import { scrollEdge as scrollEdgeAction } from '../actions/scrollEdge';
11
13
  import { untrack, type Snippet } from 'svelte';
12
14
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
13
15
 
@@ -22,6 +24,8 @@
22
24
  width?: string;
23
25
  showCloseButton?: boolean;
24
26
  contentClass?: string;
27
+ /** Soft progressive-blur fade at the top of the body once it scrolls (`use:scrollEdge`). Default true. */
28
+ scrollEdge?: boolean;
25
29
  /**
26
30
  * When true (default), keydown events fired inside the drawer panel are
27
31
  * stopped from propagating to window — preventing page-level list/keyboard
@@ -59,6 +63,7 @@
59
63
  width,
60
64
  showCloseButton = true,
61
65
  contentClass = '',
66
+ scrollEdge = true,
62
67
  trapKeys = true,
63
68
  shortcutRegistry,
64
69
  shortcutContext = 'drawer',
@@ -150,22 +155,16 @@
150
155
  {@render headerActions()}
151
156
  {/if}
152
157
  {#if showCloseButton}
153
- <button
154
- type="button"
155
- class="btn btn-sm btn-circle btn-ghost"
156
- onclick={handleClose}
157
- aria-label="Close drawer"
158
- >
159
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
160
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
161
- </svg>
162
- </button>
158
+ <CloseButton label="Close drawer" onclick={handleClose} />
163
159
  {/if}
164
160
  </div>
165
161
  </div>
166
162
  {/if}
167
163
 
168
- <div class="flex-1 overflow-y-auto {contentClass}">
164
+ <div
165
+ class="flex-1 overflow-y-auto {contentClass}"
166
+ use:scrollEdgeAction={{ enabled: scrollEdge, surface: 'var(--color-base-200)' }}
167
+ >
169
168
  {#if open && children}
170
169
  {@render children()}
171
170
  {/if}
@@ -1,11 +1,3 @@
1
- /**
2
- * Drawer Component
3
- *
4
- * Sliding panel overlay with backdrop, escape key, and header actions.
5
- * Supports a `footer` snippet (pinned below the scrollable body —
6
- * matches mardukos DrawerShell) and an arbitrary Tailwind `width`
7
- * class (e.g. `'w-[42rem]'`) which takes precedence over `size`.
8
- */
9
1
  import { type Snippet } from 'svelte';
10
2
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
11
3
  interface Props {
@@ -19,6 +11,8 @@ interface Props {
19
11
  width?: string;
20
12
  showCloseButton?: boolean;
21
13
  contentClass?: string;
14
+ /** Soft progressive-blur fade at the top of the body once it scrolls (`use:scrollEdge`). Default true. */
15
+ scrollEdge?: boolean;
22
16
  /**
23
17
  * When true (default), keydown events fired inside the drawer panel are
24
18
  * stopped from propagating to window — preventing page-level list/keyboard
@@ -0,0 +1,200 @@
1
+ <script module lang="ts">
2
+ export type IconButtonVariant =
3
+ | 'ghost'
4
+ | 'outline'
5
+ | 'soft'
6
+ | 'primary'
7
+ | 'secondary'
8
+ | 'error'
9
+ | 'overlay';
10
+ export type IconButtonSize = 'xs' | 'sm' | 'md' | 'lg';
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ /**
15
+ * IconButton — an icon-only button with a REQUIRED accessible name.
16
+ *
17
+ * The square/circle `btn` that consumers hand-write hundreds of times
18
+ * (`btn btn-sm btn-circle btn-ghost` + an inline svg + an aria-label that is
19
+ * sometimes forgotten). `label` is required by the types, becomes the
20
+ * aria-label, and — with `tooltip` — the hover/focus hint too.
21
+ *
22
+ * <IconButton label="Edit" onclick={edit}><PencilIcon size={16} /></IconButton>
23
+ * <IconButton label="Pin" pressed={pinned} tooltip onclick={toggle}>…</IconButton>
24
+ * <IconButton label="Open" href="/x" shape="circle" variant="outline">…</IconButton>
25
+ *
26
+ * `variant="overlay"` is light-on-dark translucent chrome for controls that
27
+ * sit over media (lightboxes, video stages) where a ghost button vanishes.
28
+ *
29
+ * Idea from BoardUI's icon-button (MIT); written fresh for Svelte 5 + jatui
30
+ * tokens.
31
+ */
32
+ import type { Snippet } from 'svelte';
33
+ import type { HTMLButtonAttributes } from 'svelte/elements';
34
+ import { tooltip as tooltipAction, type TooltipOptions } from '../actions/tooltip';
35
+
36
+ interface Props
37
+ extends Omit<
38
+ HTMLButtonAttributes,
39
+ 'type' | 'disabled' | 'class' | 'onclick' | 'children' | 'aria-label'
40
+ > {
41
+ /** Accessible name — required. Rendered as aria-label (and the tooltip text). */
42
+ label: string;
43
+ variant?: IconButtonVariant;
44
+ size?: IconButtonSize;
45
+ shape?: 'square' | 'circle';
46
+ /** Render an `<a>` instead of a `<button>`. */
47
+ href?: string;
48
+ type?: 'button' | 'submit' | 'reset';
49
+ disabled?: boolean;
50
+ /** Toggle semantics — renders aria-pressed and the pressed surface. */
51
+ pressed?: boolean;
52
+ /** Show a tooltip: `true` uses `label`; a string shows that text instead (e.g. with a shortcut). */
53
+ tooltip?: boolean | string;
54
+ tooltipPlacement?: TooltipOptions['placement'];
55
+ class?: string;
56
+ onclick?: (e: MouseEvent) => void;
57
+ /** The icon. Size it to the button (16px for sm/md is the house default). */
58
+ children?: Snippet;
59
+ }
60
+
61
+ let {
62
+ label,
63
+ variant = 'ghost',
64
+ size = 'md',
65
+ shape = 'square',
66
+ href,
67
+ type = 'button',
68
+ disabled = false,
69
+ pressed,
70
+ tooltip = false,
71
+ tooltipPlacement = 'top',
72
+ class: className = '',
73
+ onclick,
74
+ children,
75
+ ...rest
76
+ }: Props = $props();
77
+
78
+ const VARIANT: Record<IconButtonVariant, string> = {
79
+ ghost: 'btn-ghost',
80
+ outline: 'btn-outline',
81
+ soft: 'btn-soft',
82
+ primary: 'btn-primary',
83
+ secondary: 'btn-secondary',
84
+ error: 'btn-error',
85
+ overlay: 'jatui-icon-btn--overlay'
86
+ };
87
+ const SIZE: Record<IconButtonSize, string> = {
88
+ xs: 'btn-xs',
89
+ sm: 'btn-sm',
90
+ md: '',
91
+ lg: 'btn-lg'
92
+ };
93
+
94
+ const classes = $derived(
95
+ [
96
+ 'btn jatui-icon-btn',
97
+ shape === 'circle' ? 'btn-circle' : 'btn-square',
98
+ VARIANT[variant] ?? 'btn-ghost',
99
+ SIZE[size],
100
+ pressed ? 'jatui-icon-btn--pressed' : '',
101
+ className
102
+ ]
103
+ .filter(Boolean)
104
+ .join(' ')
105
+ );
106
+
107
+ const ariaPressed = $derived(pressed === undefined ? undefined : pressed ? 'true' : 'false');
108
+ const tipText = $derived(typeof tooltip === 'string' ? tooltip : tooltip ? label : '');
109
+
110
+ /** use:tooltip only when asked for — no aria-describedby noise otherwise. */
111
+ function maybeTooltip(node: HTMLElement, text: string) {
112
+ let handle: ReturnType<typeof tooltipAction> | undefined;
113
+ const apply = (t: string) => {
114
+ if (t && !handle) handle = tooltipAction(node, { text: t, placement: tooltipPlacement });
115
+ else if (t && handle) handle.update?.({ text: t, placement: tooltipPlacement });
116
+ else if (!t && handle) {
117
+ handle.destroy?.();
118
+ handle = undefined;
119
+ }
120
+ };
121
+ apply(text);
122
+ return { update: apply, destroy: () => handle?.destroy?.() };
123
+ }
124
+ </script>
125
+
126
+ {#if href && !disabled}
127
+ <a
128
+ {...rest as Record<string, unknown>}
129
+ {href}
130
+ class={classes}
131
+ aria-label={label}
132
+ aria-pressed={ariaPressed}
133
+ {onclick}
134
+ use:maybeTooltip={tipText}
135
+ >
136
+ {#if children}{@render children()}{/if}
137
+ </a>
138
+ {:else}
139
+ <button
140
+ {...rest}
141
+ {type}
142
+ {disabled}
143
+ class={classes}
144
+ aria-label={label}
145
+ aria-pressed={ariaPressed}
146
+ {onclick}
147
+ use:maybeTooltip={tipText}
148
+ >
149
+ {#if children}{@render children()}{/if}
150
+ </button>
151
+ {/if}
152
+
153
+ <style>
154
+ /* Flat (no DaisyUI v5 variant shadow), press squish, token focus ring. */
155
+ .jatui-icon-btn {
156
+ --btn-shadow: 0 0 0 0 transparent;
157
+ transition:
158
+ transform 500ms var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1)),
159
+ background-color var(--motion-fast, 150ms) var(--motion-glide, ease-out),
160
+ color var(--motion-fast, 150ms) var(--motion-glide, ease-out);
161
+ }
162
+ .jatui-icon-btn:active:not(:disabled) {
163
+ transform: scale(0.92);
164
+ transition: transform var(--motion-instant, 80ms) ease-out;
165
+ }
166
+ .jatui-icon-btn:focus-visible {
167
+ outline: var(--jatui-focus-ring-width, 2px) solid
168
+ var(--jatui-focus-ring, color-mix(in oklab, var(--color-primary) 70%, transparent));
169
+ outline-offset: var(--jatui-focus-ring-offset, 2px);
170
+ }
171
+ .jatui-icon-btn--pressed:not(.btn-primary):not(.btn-secondary):not(.btn-error) {
172
+ background-color: var(
173
+ --jatui-selection-bg,
174
+ color-mix(in oklab, var(--color-primary) 14%, transparent)
175
+ );
176
+ color: var(--color-primary);
177
+ }
178
+
179
+ /* Over media: translucent light chrome that reads on any image or video. */
180
+ .jatui-icon-btn--overlay {
181
+ --btn-color: color-mix(in oklab, white 10%, transparent);
182
+ --btn-fg: color-mix(in oklab, white 95%, transparent);
183
+ --btn-border: color-mix(in oklab, white 15%, transparent);
184
+ --btn-noise: none;
185
+ background-color: color-mix(in oklab, white 10%, transparent);
186
+ border: 1px solid color-mix(in oklab, white 15%, transparent);
187
+ color: color-mix(in oklab, white 95%, transparent);
188
+ }
189
+ .jatui-icon-btn--overlay:hover {
190
+ background-color: color-mix(in oklab, white 20%, transparent);
191
+ }
192
+
193
+ @media (prefers-reduced-motion: reduce) {
194
+ .jatui-icon-btn,
195
+ .jatui-icon-btn:active:not(:disabled) {
196
+ transform: none;
197
+ transition: none;
198
+ }
199
+ }
200
+ </style>
@@ -0,0 +1,46 @@
1
+ export type IconButtonVariant = 'ghost' | 'outline' | 'soft' | 'primary' | 'secondary' | 'error' | 'overlay';
2
+ export type IconButtonSize = 'xs' | 'sm' | 'md' | 'lg';
3
+ /**
4
+ * IconButton — an icon-only button with a REQUIRED accessible name.
5
+ *
6
+ * The square/circle `btn` that consumers hand-write hundreds of times
7
+ * (`btn btn-sm btn-circle btn-ghost` + an inline svg + an aria-label that is
8
+ * sometimes forgotten). `label` is required by the types, becomes the
9
+ * aria-label, and — with `tooltip` — the hover/focus hint too.
10
+ *
11
+ * <IconButton label="Edit" onclick={edit}><PencilIcon size={16} /></IconButton>
12
+ * <IconButton label="Pin" pressed={pinned} tooltip onclick={toggle}>…</IconButton>
13
+ * <IconButton label="Open" href="/x" shape="circle" variant="outline">…</IconButton>
14
+ *
15
+ * `variant="overlay"` is light-on-dark translucent chrome for controls that
16
+ * sit over media (lightboxes, video stages) where a ghost button vanishes.
17
+ *
18
+ * Idea from BoardUI's icon-button (MIT); written fresh for Svelte 5 + jatui
19
+ * tokens.
20
+ */
21
+ import type { Snippet } from 'svelte';
22
+ import type { HTMLButtonAttributes } from 'svelte/elements';
23
+ import { type TooltipOptions } from '../actions/tooltip';
24
+ interface Props extends Omit<HTMLButtonAttributes, 'type' | 'disabled' | 'class' | 'onclick' | 'children' | 'aria-label'> {
25
+ /** Accessible name — required. Rendered as aria-label (and the tooltip text). */
26
+ label: string;
27
+ variant?: IconButtonVariant;
28
+ size?: IconButtonSize;
29
+ shape?: 'square' | 'circle';
30
+ /** Render an `<a>` instead of a `<button>`. */
31
+ href?: string;
32
+ type?: 'button' | 'submit' | 'reset';
33
+ disabled?: boolean;
34
+ /** Toggle semantics — renders aria-pressed and the pressed surface. */
35
+ pressed?: boolean;
36
+ /** Show a tooltip: `true` uses `label`; a string shows that text instead (e.g. with a shortcut). */
37
+ tooltip?: boolean | string;
38
+ tooltipPlacement?: TooltipOptions['placement'];
39
+ class?: string;
40
+ onclick?: (e: MouseEvent) => void;
41
+ /** The icon. Size it to the button (16px for sm/md is the house default). */
42
+ children?: Snippet;
43
+ }
44
+ declare const IconButton: import("svelte").Component<Props, {}, "">;
45
+ type IconButton = ReturnType<typeof IconButton>;
46
+ export default IconButton;
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LinkButton — a text action that looks like a link: no box, underline on
4
+ * hover/focus. Renders `<a>` when given `href`, otherwise a `<button>` —
5
+ * pick by what it DOES (navigates vs acts), not by how it looks.
6
+ *
7
+ * <LinkButton onclick={resend}>Resend code</LinkButton>
8
+ * <LinkButton href="/docs" tone="primary">Read the docs</LinkButton>
9
+ * <LinkButton href="https://x.dev" external>Changelog</LinkButton>
10
+ *
11
+ * `external` opens a new tab with rel="noopener noreferrer" and tells
12
+ * screen-reader users so. Idea from BoardUI's link-button (MIT).
13
+ */
14
+ import type { Snippet } from 'svelte';
15
+
16
+ interface Props {
17
+ href?: string;
18
+ /** Ink: inherit the surrounding text, primary accent, muted, or error. */
19
+ tone?: 'inherit' | 'primary' | 'muted' | 'error';
20
+ size?: 'xs' | 'sm' | 'md';
21
+ /** Underline: on hover/focus only (default) or always. */
22
+ underline?: 'hover' | 'always';
23
+ /** Open in a new tab (adds rel=noopener and an SR-only "(opens in a new tab)"). */
24
+ external?: boolean;
25
+ type?: 'button' | 'submit' | 'reset';
26
+ disabled?: boolean;
27
+ class?: string;
28
+ onclick?: (e: MouseEvent) => void;
29
+ children?: Snippet;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ let {
34
+ href,
35
+ tone = 'primary',
36
+ size = 'sm',
37
+ underline = 'hover',
38
+ external = false,
39
+ type = 'button',
40
+ disabled = false,
41
+ class: className = '',
42
+ onclick,
43
+ children,
44
+ ...rest
45
+ }: Props = $props();
46
+
47
+ const TONE = {
48
+ inherit: '',
49
+ primary: 'link-primary',
50
+ muted: 'jatui-link-btn--muted',
51
+ error: 'link-error'
52
+ } as const;
53
+ const SIZE = { xs: 'text-xs', sm: 'text-sm', md: 'text-base' } as const;
54
+
55
+ const classes = $derived(
56
+ [
57
+ 'link jatui-link-btn',
58
+ underline === 'hover' ? 'link-hover' : '',
59
+ TONE[tone] ?? '',
60
+ SIZE[size] ?? '',
61
+ disabled ? 'jatui-link-btn--disabled' : '',
62
+ className
63
+ ]
64
+ .filter(Boolean)
65
+ .join(' ')
66
+ );
67
+ </script>
68
+
69
+ {#if href && !disabled}
70
+ <a
71
+ {...rest}
72
+ {href}
73
+ class={classes}
74
+ target={external ? '_blank' : rest.target as string | undefined}
75
+ rel={external ? 'noopener noreferrer' : rest.rel as string | undefined}
76
+ {onclick}
77
+ >
78
+ {#if children}{@render children()}{/if}
79
+ {#if external}<span class="sr-only"> (opens in a new tab)</span>{/if}
80
+ </a>
81
+ {:else}
82
+ <button {...rest} {type} {disabled} class={classes} {onclick}>
83
+ {#if children}{@render children()}{/if}
84
+ </button>
85
+ {/if}
86
+
87
+ <style>
88
+ .jatui-link-btn {
89
+ display: inline;
90
+ padding: 0;
91
+ background: none;
92
+ border: 0;
93
+ font-weight: 500;
94
+ text-underline-offset: 0.2em;
95
+ cursor: pointer;
96
+ border-radius: var(--jatui-radius-field, 0.25rem);
97
+ transition: color var(--motion-fast, 150ms) var(--motion-glide, ease-out);
98
+ }
99
+ .jatui-link-btn:focus-visible {
100
+ outline: var(--jatui-focus-ring-width, 2px) solid
101
+ var(--jatui-focus-ring, color-mix(in oklab, var(--color-primary) 70%, transparent));
102
+ outline-offset: var(--jatui-focus-ring-offset, 2px);
103
+ text-decoration-line: underline;
104
+ }
105
+ .jatui-link-btn--muted {
106
+ color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
107
+ }
108
+ .jatui-link-btn--muted:hover {
109
+ color: var(--color-base-content);
110
+ }
111
+ .jatui-link-btn--disabled,
112
+ .jatui-link-btn:disabled {
113
+ opacity: 0.45;
114
+ cursor: not-allowed;
115
+ text-decoration: none;
116
+ }
117
+ </style>
@@ -0,0 +1,32 @@
1
+ /**
2
+ * LinkButton — a text action that looks like a link: no box, underline on
3
+ * hover/focus. Renders `<a>` when given `href`, otherwise a `<button>` —
4
+ * pick by what it DOES (navigates vs acts), not by how it looks.
5
+ *
6
+ * <LinkButton onclick={resend}>Resend code</LinkButton>
7
+ * <LinkButton href="/docs" tone="primary">Read the docs</LinkButton>
8
+ * <LinkButton href="https://x.dev" external>Changelog</LinkButton>
9
+ *
10
+ * `external` opens a new tab with rel="noopener noreferrer" and tells
11
+ * screen-reader users so. Idea from BoardUI's link-button (MIT).
12
+ */
13
+ import type { Snippet } from 'svelte';
14
+ interface Props {
15
+ href?: string;
16
+ /** Ink: inherit the surrounding text, primary accent, muted, or error. */
17
+ tone?: 'inherit' | 'primary' | 'muted' | 'error';
18
+ size?: 'xs' | 'sm' | 'md';
19
+ /** Underline: on hover/focus only (default) or always. */
20
+ underline?: 'hover' | 'always';
21
+ /** Open in a new tab (adds rel=noopener and an SR-only "(opens in a new tab)"). */
22
+ external?: boolean;
23
+ type?: 'button' | 'submit' | 'reset';
24
+ disabled?: boolean;
25
+ class?: string;
26
+ onclick?: (e: MouseEvent) => void;
27
+ children?: Snippet;
28
+ [key: string]: unknown;
29
+ }
30
+ declare const LinkButton: import("svelte").Component<Props, {}, "">;
31
+ type LinkButton = ReturnType<typeof LinkButton>;
32
+ export default LinkButton;
@@ -24,6 +24,7 @@
24
24
  * ]} initialIndex={0} onClose={close} />
25
25
  */
26
26
 
27
+ import CloseButton from './CloseButton.svelte';
27
28
  import { fade } from 'svelte/transition';
28
29
 
29
30
  export type MediaItem = {
@@ -175,9 +176,7 @@
175
176
  >
176
177
  Download
177
178
  </a>
178
- <button type="button" class="mlb-btn mlb-close" onclick={close} aria-label="Close">
179
- ×
180
- </button>
179
+ <CloseButton label="Close" variant="overlay" shape="square" onclick={close} />
181
180
  </div>
182
181
  </div>
183
182
 
@@ -288,10 +287,6 @@
288
287
  .mlb-btn:hover {
289
288
  background: oklch(1 0 0 / 0.2);
290
289
  }
291
- .mlb-close {
292
- font-size: 1.25rem;
293
- line-height: 1;
294
- }
295
290
  .mlb-nav {
296
291
  position: absolute;
297
292
  top: 50%;
@@ -6,6 +6,7 @@
6
6
  * Converted from Svelte 4 to Svelte 5 runes.
7
7
  */
8
8
 
9
+ import CloseButton from './CloseButton.svelte';
9
10
  import { onMount, untrack } from 'svelte';
10
11
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
11
12
  import { fly, fade } from 'svelte/transition';
@@ -241,16 +242,7 @@
241
242
  {/if}
242
243
 
243
244
  {#if showCloseButton && dismissible}
244
- <button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={close} aria-label="Close modal">
245
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
246
- <path
247
- stroke-linecap="round"
248
- stroke-linejoin="round"
249
- stroke-width="2"
250
- d="M6 18L18 6M6 6l12 12"
251
- />
252
- </svg>
253
- </button>
245
+ <CloseButton label="Close modal" onclick={close} />
254
246
  {/if}
255
247
  </div>
256
248
  {/if}