@joewinke/jatui 0.38.6 → 0.40.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 (98) hide show
  1. package/CLAUDE.md +54 -5
  2. package/THIRD_PARTY_NOTICES.md +67 -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/BottomSheet.svelte +7 -6
  8. package/dist/components/BottomSheet.svelte.d.ts +2 -0
  9. package/dist/components/BulkActionBar.svelte +32 -2
  10. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  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/FloatingActionBar.svelte +102 -34
  16. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  17. package/dist/components/IconButton.svelte +200 -0
  18. package/dist/components/IconButton.svelte.d.ts +46 -0
  19. package/dist/components/LinkButton.svelte +117 -0
  20. package/dist/components/LinkButton.svelte.d.ts +32 -0
  21. package/dist/components/MediaExplorer.svelte +249 -0
  22. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  23. package/dist/components/MediaLightbox.svelte +2 -7
  24. package/dist/components/Modal.svelte +2 -10
  25. package/dist/components/NotificationCenter.svelte +481 -0
  26. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  27. package/dist/components/PinInput.svelte +94 -31
  28. package/dist/components/PinInput.svelte.d.ts +4 -0
  29. package/dist/components/SearchDropdown.svelte +54 -8
  30. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  31. package/dist/components/TextArea.svelte +67 -5
  32. package/dist/components/TextArea.svelte.d.ts +4 -6
  33. package/dist/components/ThemeSelector.svelte +29 -9
  34. package/dist/components/ThemeSelector.svelte.d.ts +6 -0
  35. package/dist/components/ThemeToggle.svelte +28 -3
  36. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  37. package/dist/components/agent/AgentLog.svelte +217 -0
  38. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  39. package/dist/components/agent/AgentLogRow.svelte +281 -0
  40. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  41. package/dist/components/agent/AgentTaskList.svelte +607 -0
  42. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  43. package/dist/components/agent/AgentThinking.svelte +280 -0
  44. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  45. package/dist/components/agent/ComposerGlow.svelte +265 -0
  46. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  47. package/dist/components/agent/ContextMeter.svelte +277 -0
  48. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  49. package/dist/components/agent/Questionnaire.svelte +767 -0
  50. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  51. package/dist/components/agent/ShimmerText.svelte +100 -0
  52. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  53. package/dist/components/agent/StreamedText.svelte +135 -0
  54. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  55. package/dist/components/agent/agentLog.d.ts +22 -0
  56. package/dist/components/agent/agentLog.js +26 -0
  57. package/dist/components/agent/agentTaskList.d.ts +90 -0
  58. package/dist/components/agent/agentTaskList.js +93 -0
  59. package/dist/components/agent/agentThinking.d.ts +46 -0
  60. package/dist/components/agent/agentThinking.js +78 -0
  61. package/dist/components/agent/contextMeter.d.ts +97 -0
  62. package/dist/components/agent/contextMeter.js +156 -0
  63. package/dist/components/agent/questionnaire.d.ts +104 -0
  64. package/dist/components/agent/questionnaire.js +225 -0
  65. package/dist/components/agent/revealCore.d.ts +74 -0
  66. package/dist/components/agent/revealCore.js +160 -0
  67. package/dist/components/agent/revealTicker.d.ts +34 -0
  68. package/dist/components/agent/revealTicker.js +73 -0
  69. package/dist/components/agent/streamedText.d.ts +28 -0
  70. package/dist/components/agent/streamedText.js +59 -0
  71. package/dist/components/basic/Breadcrumb.svelte +226 -11
  72. package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
  73. package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
  74. package/dist/components/basic/breadcrumbCollapse.js +12 -0
  75. package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
  76. package/dist/components/messaging/MessageInput.svelte +34 -4
  77. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  78. package/dist/components/messaging/MessageItem.svelte +2 -2
  79. package/dist/components/notificationCenter.d.ts +65 -0
  80. package/dist/components/notificationCenter.js +110 -0
  81. package/dist/components/progress/Steps.svelte +269 -5
  82. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  83. package/dist/components/progress/stepsSummary.d.ts +14 -0
  84. package/dist/components/progress/stepsSummary.js +32 -0
  85. package/dist/index.d.ts +48 -1
  86. package/dist/index.js +39 -1
  87. package/dist/selection/clickSelection.d.ts +89 -0
  88. package/dist/selection/clickSelection.js +152 -0
  89. package/dist/selection/createClickSelection.d.ts +79 -0
  90. package/dist/selection/createClickSelection.js +134 -0
  91. package/dist/styles/tokens.css +80 -1
  92. package/dist/utils/pinInput.d.ts +33 -0
  93. package/dist/utils/pinInput.js +63 -0
  94. package/dist/utils/textareaRows.d.ts +23 -0
  95. package/dist/utils/textareaRows.js +13 -0
  96. package/dist/utils/themeWipe.d.ts +36 -17
  97. package/dist/utils/themeWipe.js +145 -35
  98. package/package.json +3 -2
@@ -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;
@@ -0,0 +1,249 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MediaExplorer — a two-pane source browser shell for files, images, or any
4
+ * per-source collection. The LEFT RAIL lists the sources (grouped, keyboard
5
+ * navigable); the RIGHT PANE renders whatever the host shows for the active
6
+ * source via the `pane` snippet. MediaExplorer owns the shell only — it is
7
+ * deliberately decoupled from any backend, media type, or selection model:
8
+ * the host keeps its own selection state in the pane and the footer.
9
+ *
10
+ * <MediaExplorer {sources} bind:activeId title="Add references" onClose={...}>
11
+ * {#snippet pane(source)}
12
+ * <MyBrowser source={source.id} />
13
+ * {/snippet}
14
+ * {#snippet footer()}…{/snippet}
15
+ * </MediaExplorer>
16
+ *
17
+ * Rail keyboard contract: the rail is one tab stop (roving tabindex).
18
+ * ArrowUp/ArrowDown/Home/End move between enabled sources and ACTIVATE them
19
+ * (the pane follows focus — arrowing through sources is the browsing gesture,
20
+ * no Enter needed). Click activates. Disabled sources are skipped; their
21
+ * `hint` explains why (rendered as the row's title).
22
+ *
23
+ * Dialog conventions follow Modal.svelte: native <dialog>, focus moved in on
24
+ * open and restored on close, Escape + backdrop close, body scroll locked,
25
+ * reduced-motion skips the entrance transitions.
26
+ */
27
+ import { fade, fly } from "svelte/transition"
28
+ import { cubicOut } from "svelte/easing"
29
+ import type { Snippet } from "svelte"
30
+
31
+ export interface ExplorerSource {
32
+ /** Stable source id, e.g. "refs", "out", "catalogs/costume". */
33
+ id: string
34
+ /** Rail label. */
35
+ label: string
36
+ /** Small mono suffix after the label, e.g. "refs/". */
37
+ hint?: string
38
+ /** Right-aligned count badge. */
39
+ badge?: string | number
40
+ /** Rail section heading; ungrouped sources render above every group. */
41
+ group?: string
42
+ /** Not clickable and skipped by the arrows; `hint` should say why. */
43
+ disabled?: boolean
44
+ }
45
+
46
+ interface Props {
47
+ sources: ExplorerSource[]
48
+ /** The active source id. Falls back to the first enabled source when unset or stale. */
49
+ activeId?: string
50
+ title: string
51
+ onClose: () => void
52
+ /** Content of the active source. */
53
+ pane: Snippet<[ExplorerSource]>
54
+ /** Rendered beside the title (filters, view toggles). */
55
+ toolbar?: Snippet
56
+ /** Below the pane (selection counts, actions). */
57
+ footer?: Snippet
58
+ /** Accessible label for the rail. Default "Sources". */
59
+ railLabel?: string
60
+ /** Accessible label for the pane column. Default "Contents". */
61
+ paneLabel?: string
62
+ /** Max width of the dialog as a CSS length. Default "72rem". */
63
+ maxWidth?: string
64
+ }
65
+
66
+ let {
67
+ sources,
68
+ activeId = $bindable(),
69
+ title,
70
+ onClose,
71
+ pane,
72
+ toolbar,
73
+ footer,
74
+ railLabel = "Sources",
75
+ paneLabel = "Contents",
76
+ maxWidth = "72rem",
77
+ }: Props = $props()
78
+
79
+ const enabled = $derived(sources.filter((s) => !s.disabled))
80
+
81
+ // An unset or stale activeId falls to the first enabled source, so a caller
82
+ // can bind a plain variable and read it back.
83
+ const active = $derived(sources.find((s) => s.id === activeId) ?? enabled[0])
84
+ $effect(() => {
85
+ if (active && active.id !== activeId) activeId = active.id
86
+ })
87
+
88
+ // Rail sections: ungrouped first, then groups in first-seen order.
89
+ const sections = $derived.by(() => {
90
+ const ungrouped = sources.filter((s) => !s.group)
91
+ const groups: { name: string; sources: ExplorerSource[] }[] = []
92
+ for (const s of sources) {
93
+ if (!s.group) continue
94
+ let g = groups.find((x) => x.name === s.group)
95
+ if (!g) groups.push((g = { name: s.group, sources: [] }))
96
+ g.sources.push(s)
97
+ }
98
+ return [...(ungrouped.length ? [{ name: "", sources: ungrouped }] : []), ...groups]
99
+ })
100
+
101
+ function activate(s: ExplorerSource) {
102
+ if (s.disabled) return
103
+ activeId = s.id
104
+ }
105
+
106
+ // Roving tabindex: one tab stop, arrows both move and activate.
107
+ let railEl = $state<HTMLElement | null>(null)
108
+ let dialogEl = $state<HTMLDialogElement | null>(null)
109
+ let previousFocus: HTMLElement | null = null
110
+
111
+ const reducedMotion =
112
+ typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
113
+
114
+ const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
115
+
116
+ $effect(() => {
117
+ if (typeof document === "undefined") return
118
+ previousFocus = document.activeElement as HTMLElement
119
+ document.body.style.overflow = "hidden"
120
+ requestAnimationFrame(() => dialogEl?.focus())
121
+ return () => {
122
+ document.body.style.overflow = ""
123
+ previousFocus?.focus?.()
124
+ previousFocus = null
125
+ }
126
+ })
127
+
128
+ function focusRow(id: string) {
129
+ railEl?.querySelector<HTMLElement>(`[data-source-id="${CSS.escape(id)}"]`)?.focus()
130
+ }
131
+ function onRailKey(e: KeyboardEvent) {
132
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp" && e.key !== "Home" && e.key !== "End") return
133
+ e.preventDefault()
134
+ if (!enabled.length) return
135
+ const i = enabled.findIndex((s) => s.id === active?.id)
136
+ const next =
137
+ e.key === "Home" ? 0 : e.key === "End" ? enabled.length - 1 : (i + (e.key === "ArrowDown" ? 1 : enabled.length - 1)) % enabled.length
138
+ const target = enabled[next]!
139
+ activate(target)
140
+ focusRow(target.id)
141
+ }
142
+ function onDialogKey(e: KeyboardEvent) {
143
+ if (e.key !== "Escape") return
144
+ e.preventDefault()
145
+ e.stopPropagation()
146
+ onClose()
147
+ }
148
+ function trapFocus(e: KeyboardEvent) {
149
+ if (e.key !== "Tab" || !dialogEl) return
150
+ const focusable = dialogEl.querySelectorAll<HTMLElement>(FOCUSABLE)
151
+ if (!focusable.length) return
152
+ const first = focusable[0]!
153
+ const last = focusable[focusable.length - 1]!
154
+ if (e.shiftKey && document.activeElement === first) {
155
+ e.preventDefault()
156
+ last.focus()
157
+ } else if (!e.shiftKey && document.activeElement === last) {
158
+ e.preventDefault()
159
+ first.focus()
160
+ }
161
+ }
162
+ </script>
163
+
164
+ <svelte:window onkeydown={onDialogKey} />
165
+
166
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
167
+ <dialog
168
+ bind:this={dialogEl}
169
+ class="modal"
170
+ aria-modal="true"
171
+ aria-label={title}
172
+ open
173
+ tabindex="-1"
174
+ onkeydown={(e) => {
175
+ trapFocus(e)
176
+ }}
177
+ >
178
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
179
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
180
+ <div
181
+ class="modal-backdrop"
182
+ onclick={(e) => {
183
+ // Only a direct click on the backdrop closes; clicks inside the sheet bubble
184
+ // here and must not (same guard as Modal.svelte).
185
+ if (e.target === e.currentTarget) onClose()
186
+ }}
187
+ transition:fade={{ duration: reducedMotion ? 0 : 150 }}
188
+ >
189
+ <div
190
+ class="bg-base-100 text-base-content relative mx-auto my-auto flex max-h-[92vh] flex-col rounded-lg shadow-2xl"
191
+ style="width: min(calc(100% - 2rem), {maxWidth})"
192
+ in:fly={{ y: reducedMotion ? 0 : 12, duration: reducedMotion ? 0 : 200, easing: cubicOut }}
193
+ out:fade={{ duration: reducedMotion ? 0 : 120 }}
194
+ >
195
+ <header class="flex items-center gap-3 px-4 pt-4 pb-2">
196
+ <h2 class="text-lg font-semibold">{title}</h2>
197
+ <span class="min-w-0 flex-1"></span>
198
+ {@render toolbar?.()}
199
+ <button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={onClose} aria-label="Close">✕</button>
200
+ </header>
201
+ <div class="grid min-h-0 flex-1 sm:grid-cols-[13rem_1fr]" style="height: min(70vh, 620px)">
202
+ <nav class="border-base-content/10 overflow-y-auto border-r pr-1" aria-label={railLabel}>
203
+ <div
204
+ class="flex flex-col gap-0.5 py-1"
205
+ role="tablist"
206
+ aria-orientation="vertical"
207
+ aria-label={railLabel}
208
+ bind:this={railEl}
209
+ onkeydown={onRailKey}
210
+ >
211
+ {#each sections as section (section.name)}
212
+ {#if section.name}
213
+ <p class="text-base-content/50 mt-2 px-2 text-[10px] font-semibold tracking-widest uppercase">{section.name}</p>
214
+ {/if}
215
+ {#each section.sources as s (s.id)}
216
+ <button
217
+ data-source-id={s.id}
218
+ role="tab"
219
+ aria-selected={active?.id === s.id}
220
+ tabindex={active?.id === s.id ? 0 : -1}
221
+ disabled={s.disabled}
222
+ title={s.disabled ? s.hint : undefined}
223
+ class="flex items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm
224
+ {active?.id === s.id ? 'bg-base-content/10 font-semibold' : 'hover:bg-base-content/5'}
225
+ {s.disabled ? 'opacity-50' : ''}"
226
+ onclick={() => activate(s)}
227
+ >
228
+ <span class="min-w-0 flex-1 truncate">{s.label}</span>
229
+ {#if s.hint}<span class="text-base-content/40 shrink-0 font-mono text-[10px]">{s.hint}</span>{/if}
230
+ {#if s.badge !== undefined}<span class="text-base-content/50 shrink-0 font-mono text-[10px] tnum">{s.badge}</span>{/if}
231
+ </button>
232
+ {/each}
233
+ {/each}
234
+ </div>
235
+ </nav>
236
+ <section class="min-h-0 overflow-y-auto p-4" aria-label={paneLabel}>
237
+ {#if active}
238
+ {@render pane(active)}
239
+ {/if}
240
+ </section>
241
+ </div>
242
+ {#if footer}
243
+ <div class="border-base-content/10 flex flex-wrap items-center gap-2 border-t px-4 py-3">
244
+ {@render footer()}
245
+ </div>
246
+ {/if}
247
+ </div>
248
+ </div>
249
+ </dialog>
@@ -0,0 +1,37 @@
1
+ import type { Snippet } from "svelte";
2
+ export interface ExplorerSource {
3
+ /** Stable source id, e.g. "refs", "out", "catalogs/costume". */
4
+ id: string;
5
+ /** Rail label. */
6
+ label: string;
7
+ /** Small mono suffix after the label, e.g. "refs/". */
8
+ hint?: string;
9
+ /** Right-aligned count badge. */
10
+ badge?: string | number;
11
+ /** Rail section heading; ungrouped sources render above every group. */
12
+ group?: string;
13
+ /** Not clickable and skipped by the arrows; `hint` should say why. */
14
+ disabled?: boolean;
15
+ }
16
+ interface Props {
17
+ sources: ExplorerSource[];
18
+ /** The active source id. Falls back to the first enabled source when unset or stale. */
19
+ activeId?: string;
20
+ title: string;
21
+ onClose: () => void;
22
+ /** Content of the active source. */
23
+ pane: Snippet<[ExplorerSource]>;
24
+ /** Rendered beside the title (filters, view toggles). */
25
+ toolbar?: Snippet;
26
+ /** Below the pane (selection counts, actions). */
27
+ footer?: Snippet;
28
+ /** Accessible label for the rail. Default "Sources". */
29
+ railLabel?: string;
30
+ /** Accessible label for the pane column. Default "Contents". */
31
+ paneLabel?: string;
32
+ /** Max width of the dialog as a CSS length. Default "72rem". */
33
+ maxWidth?: string;
34
+ }
35
+ declare const MediaExplorer: import("svelte").Component<Props, {}, "activeId">;
36
+ type MediaExplorer = ReturnType<typeof MediaExplorer>;
37
+ export default MediaExplorer;
@@ -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}