@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.
- package/CLAUDE.md +54 -5
- package/THIRD_PARTY_NOTICES.md +67 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/BulkActionBar.svelte +32 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -1
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/FloatingActionBar.svelte +102 -34
- package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaExplorer.svelte +249 -0
- package/dist/components/MediaExplorer.svelte.d.ts +37 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/NotificationCenter.svelte +481 -0
- package/dist/components/NotificationCenter.svelte.d.ts +29 -0
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SearchDropdown.svelte +54 -8
- package/dist/components/SearchDropdown.svelte.d.ts +12 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/agent/AgentLog.svelte +217 -0
- package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
- package/dist/components/agent/AgentLogRow.svelte +281 -0
- package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
- package/dist/components/agent/AgentTaskList.svelte +607 -0
- package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
- package/dist/components/agent/AgentThinking.svelte +280 -0
- package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
- package/dist/components/agent/ComposerGlow.svelte +265 -0
- package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
- package/dist/components/agent/ContextMeter.svelte +277 -0
- package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
- package/dist/components/agent/Questionnaire.svelte +767 -0
- package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
- package/dist/components/agent/ShimmerText.svelte +100 -0
- package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
- package/dist/components/agent/StreamedText.svelte +135 -0
- package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
- package/dist/components/agent/agentLog.d.ts +22 -0
- package/dist/components/agent/agentLog.js +26 -0
- package/dist/components/agent/agentTaskList.d.ts +90 -0
- package/dist/components/agent/agentTaskList.js +93 -0
- package/dist/components/agent/agentThinking.d.ts +46 -0
- package/dist/components/agent/agentThinking.js +78 -0
- package/dist/components/agent/contextMeter.d.ts +97 -0
- package/dist/components/agent/contextMeter.js +156 -0
- package/dist/components/agent/questionnaire.d.ts +104 -0
- package/dist/components/agent/questionnaire.js +225 -0
- package/dist/components/agent/revealCore.d.ts +74 -0
- package/dist/components/agent/revealCore.js +160 -0
- package/dist/components/agent/revealTicker.d.ts +34 -0
- package/dist/components/agent/revealTicker.js +73 -0
- package/dist/components/agent/streamedText.d.ts +28 -0
- package/dist/components/agent/streamedText.js +59 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/components/messaging/MessageInput.svelte +34 -4
- package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
- package/dist/components/messaging/MessageItem.svelte +2 -2
- package/dist/components/notificationCenter.d.ts +65 -0
- package/dist/components/notificationCenter.js +110 -0
- package/dist/components/progress/Steps.svelte +269 -5
- package/dist/components/progress/Steps.svelte.d.ts +16 -1
- package/dist/components/progress/stepsSummary.d.ts +14 -0
- package/dist/components/progress/stepsSummary.js +32 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.js +39 -1
- package/dist/selection/clickSelection.d.ts +89 -0
- package/dist/selection/clickSelection.js +152 -0
- package/dist/selection/createClickSelection.d.ts +79 -0
- package/dist/selection/createClickSelection.js +134 -0
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- 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
|
-
<
|
|
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
|
-
<
|
|
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}
|