@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,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CloseButton — the one ✕ every dialog, drawer, sheet, toast and lightbox
|
|
4
|
+
* needs. An IconButton with a built-in X glyph whose stroke is a true CSS
|
|
5
|
+
* pixel width at every size (`vector-effect: non-scaling-stroke`), so the
|
|
6
|
+
* cross never turns hairline at xs or chunky at lg.
|
|
7
|
+
*
|
|
8
|
+
* <CloseButton onclick={close} />
|
|
9
|
+
* <CloseButton label="Close drawer" size="md" onclick={close} />
|
|
10
|
+
* <CloseButton variant="overlay" onclick={close} /> <!-- over media -->
|
|
11
|
+
*
|
|
12
|
+
* Idea from BoardUI's close-button (MIT); written fresh for Svelte 5.
|
|
13
|
+
*/
|
|
14
|
+
import IconButton, { type IconButtonSize, type IconButtonVariant } from './IconButton.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Accessible name. Default "Close" — name the thing ("Close drawer") when several are on screen. */
|
|
18
|
+
label?: string;
|
|
19
|
+
size?: IconButtonSize;
|
|
20
|
+
variant?: IconButtonVariant;
|
|
21
|
+
shape?: 'square' | 'circle';
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Show the label as a tooltip (string = custom text, e.g. "Close (Esc)"). */
|
|
24
|
+
tooltip?: boolean | string;
|
|
25
|
+
class?: string;
|
|
26
|
+
onclick?: (e: MouseEvent) => void;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
label = 'Close',
|
|
32
|
+
size = 'sm',
|
|
33
|
+
variant = 'ghost',
|
|
34
|
+
shape = 'circle',
|
|
35
|
+
disabled = false,
|
|
36
|
+
tooltip = false,
|
|
37
|
+
class: className = '',
|
|
38
|
+
onclick,
|
|
39
|
+
...rest
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
/** Glyph box per size — the X itself is inset to ~58% of it. */
|
|
43
|
+
const GLYPH: Record<IconButtonSize, number> = { xs: 12, sm: 16, md: 18, lg: 22 };
|
|
44
|
+
const STROKE: Record<IconButtonSize, number> = { xs: 1.5, sm: 1.75, md: 2, lg: 2 };
|
|
45
|
+
const px = $derived(GLYPH[size] ?? 16);
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<IconButton
|
|
49
|
+
{...rest}
|
|
50
|
+
{label}
|
|
51
|
+
{size}
|
|
52
|
+
{variant}
|
|
53
|
+
{shape}
|
|
54
|
+
{disabled}
|
|
55
|
+
{tooltip}
|
|
56
|
+
{onclick}
|
|
57
|
+
class="jatui-close-btn {className}"
|
|
58
|
+
>
|
|
59
|
+
<svg
|
|
60
|
+
width={px}
|
|
61
|
+
height={px}
|
|
62
|
+
viewBox="0 0 24 24"
|
|
63
|
+
fill="none"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
stroke-linecap="round"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
focusable="false"
|
|
68
|
+
>
|
|
69
|
+
<path
|
|
70
|
+
d="M17 7L7 17M7 7l10 10"
|
|
71
|
+
stroke-width={STROKE[size] ?? 1.75}
|
|
72
|
+
vector-effect="non-scaling-stroke"
|
|
73
|
+
/>
|
|
74
|
+
</svg>
|
|
75
|
+
</IconButton>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloseButton — the one ✕ every dialog, drawer, sheet, toast and lightbox
|
|
3
|
+
* needs. An IconButton with a built-in X glyph whose stroke is a true CSS
|
|
4
|
+
* pixel width at every size (`vector-effect: non-scaling-stroke`), so the
|
|
5
|
+
* cross never turns hairline at xs or chunky at lg.
|
|
6
|
+
*
|
|
7
|
+
* <CloseButton onclick={close} />
|
|
8
|
+
* <CloseButton label="Close drawer" size="md" onclick={close} />
|
|
9
|
+
* <CloseButton variant="overlay" onclick={close} /> <!-- over media -->
|
|
10
|
+
*
|
|
11
|
+
* Idea from BoardUI's close-button (MIT); written fresh for Svelte 5.
|
|
12
|
+
*/
|
|
13
|
+
import { type IconButtonSize, type IconButtonVariant } from './IconButton.svelte';
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Accessible name. Default "Close" — name the thing ("Close drawer") when several are on screen. */
|
|
16
|
+
label?: string;
|
|
17
|
+
size?: IconButtonSize;
|
|
18
|
+
variant?: IconButtonVariant;
|
|
19
|
+
shape?: 'square' | 'circle';
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Show the label as a tooltip (string = custom text, e.g. "Close (Esc)"). */
|
|
22
|
+
tooltip?: boolean | string;
|
|
23
|
+
class?: string;
|
|
24
|
+
onclick?: (e: MouseEvent) => void;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
declare const CloseButton: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type CloseButton = ReturnType<typeof CloseButton>;
|
|
29
|
+
export default CloseButton;
|
|
@@ -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
|
-
<
|
|
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
|
|
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
|
|
@@ -20,6 +20,22 @@
|
|
|
20
20
|
* Per-action `disabled` renders the button dimmed and non-interactive;
|
|
21
21
|
* per-action `loadingText` disables it and swaps the label for a
|
|
22
22
|
* spinner + that text (mardukos fork) while other actions stay active.
|
|
23
|
+
*
|
|
24
|
+
* `placement="inline"` puts the bar in the normal flow at its container's
|
|
25
|
+
* full width (for a host with its own sticky bar); the default floats
|
|
26
|
+
* bottom-centre. Either way it wraps rather than run off a narrow screen.
|
|
27
|
+
*
|
|
28
|
+
* The root class is `fab-bar`, not `fab`: daisyUI 5 ships a `.fab`
|
|
29
|
+
* (speed-dial) component whose rules hid every button of this bar in a
|
|
30
|
+
* daisyUI 5 host (qimg-x9ifp.1).
|
|
31
|
+
*
|
|
32
|
+
* Theming: every colour is a custom property with the original dark value
|
|
33
|
+
* as its fallback, so a host can map the bar onto its theme without
|
|
34
|
+
* overriding internals: --fab-bg, --fab-fg, --fab-muted, --fab-border,
|
|
35
|
+
* --fab-shadow, --fab-radius, --fab-btn-bg/-fg/-border/-hover/-radius,
|
|
36
|
+
* --fab-primary-bg/-fg/-border/-hover, --fab-danger-bg/-fg/-border/-hover,
|
|
37
|
+
* --fab-menu-bg/-radius/-shadow, --fab-focus, --fab-spinner. Set them on
|
|
38
|
+
* any ancestor (the bar inherits them).
|
|
23
39
|
*/
|
|
24
40
|
|
|
25
41
|
import { fade } from 'svelte/transition';
|
|
@@ -49,6 +65,8 @@
|
|
|
49
65
|
label: string;
|
|
50
66
|
/** Optional colored dot (oklch string) */
|
|
51
67
|
dotColor?: string;
|
|
68
|
+
/** Optional key hint shown at the item's right edge (display only; the host binds it). */
|
|
69
|
+
hint?: string;
|
|
52
70
|
/** Optional SVG snippet rendered before the label */
|
|
53
71
|
iconSnippet?: import('svelte').Snippet;
|
|
54
72
|
/** Called when this item is clicked */
|
|
@@ -86,6 +104,13 @@
|
|
|
86
104
|
onClear?: () => void;
|
|
87
105
|
/** Label for the selection count (default: "selected") */
|
|
88
106
|
countLabel?: string;
|
|
107
|
+
/**
|
|
108
|
+
* 'floating' (default): fixed, bottom-centre over the page.
|
|
109
|
+
* 'inline': in the normal flow, full width of its container — for a
|
|
110
|
+
* host that already has a sticky bar to put it in. Both wrap onto more
|
|
111
|
+
* lines rather than run off a narrow screen.
|
|
112
|
+
*/
|
|
113
|
+
placement?: 'floating' | 'inline';
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
let {
|
|
@@ -96,6 +121,7 @@
|
|
|
96
121
|
loadingMessage = null,
|
|
97
122
|
onClear,
|
|
98
123
|
countLabel = 'selected',
|
|
124
|
+
placement = 'floating',
|
|
99
125
|
}: Props = $props();
|
|
100
126
|
|
|
101
127
|
let openDropdownId = $state<string | null>(null);
|
|
@@ -129,7 +155,7 @@
|
|
|
129
155
|
</script>
|
|
130
156
|
|
|
131
157
|
{#if count > 0}
|
|
132
|
-
<div class="fab" transition:fade={{ duration: 150 }}>
|
|
158
|
+
<div class="fab-bar" class:fab-inline={placement === 'inline'} role="group" aria-label="{count} {countLabel}" transition:fade={{ duration: 150 }}>
|
|
133
159
|
<span class="fab-count">{count} {countLabel}</span>
|
|
134
160
|
|
|
135
161
|
{#if loadingMessage}
|
|
@@ -176,13 +202,22 @@
|
|
|
176
202
|
class="fab-dropdown-wrapper"
|
|
177
203
|
role="group"
|
|
178
204
|
onclick={(e) => e.stopPropagation()}
|
|
179
|
-
onkeydown={(e) =>
|
|
205
|
+
onkeydown={(e) => {
|
|
206
|
+
e.stopPropagation();
|
|
207
|
+
if (e.key === 'Escape' && openDropdownId === dropdown.id) {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
openDropdownId = null;
|
|
210
|
+
(e.currentTarget.querySelector('.fab-btn') as HTMLElement | null)?.focus();
|
|
211
|
+
}
|
|
212
|
+
}}
|
|
180
213
|
>
|
|
181
214
|
<button
|
|
182
215
|
type="button"
|
|
183
216
|
class="fab-btn {variantClass(dropdown.variant)}"
|
|
184
217
|
onclick={() => openDropdownId = openDropdownId === dropdown.id ? null : dropdown.id}
|
|
185
218
|
disabled={loading}
|
|
219
|
+
aria-haspopup="true"
|
|
220
|
+
aria-expanded={openDropdownId === dropdown.id}
|
|
186
221
|
>
|
|
187
222
|
{#if dropdown.icon}
|
|
188
223
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
@@ -205,7 +240,10 @@
|
|
|
205
240
|
{#if item.iconSnippet}
|
|
206
241
|
{@render item.iconSnippet()}
|
|
207
242
|
{/if}
|
|
208
|
-
{item.label}
|
|
243
|
+
<span class="fab-item-label">{item.label}</span>
|
|
244
|
+
{#if item.hint}
|
|
245
|
+
<kbd class="fab-key fab-item-hint">{item.hint}</kbd>
|
|
246
|
+
{/if}
|
|
209
247
|
</button>
|
|
210
248
|
{/each}
|
|
211
249
|
</div>
|
|
@@ -229,8 +267,11 @@
|
|
|
229
267
|
{/if}
|
|
230
268
|
|
|
231
269
|
<style>
|
|
232
|
-
.fab {
|
|
270
|
+
.fab-bar {
|
|
233
271
|
position: fixed;
|
|
272
|
+
max-width: calc(100vw - 1rem);
|
|
273
|
+
flex-wrap: wrap;
|
|
274
|
+
justify-content: center;
|
|
234
275
|
bottom: 1.5rem;
|
|
235
276
|
left: 50%;
|
|
236
277
|
transform: translateX(-50%);
|
|
@@ -239,23 +280,35 @@
|
|
|
239
280
|
align-items: center;
|
|
240
281
|
gap: 0.5rem;
|
|
241
282
|
padding: 0.5rem 0.75rem;
|
|
242
|
-
background: oklch(0.16 0.02 250);
|
|
243
|
-
border: 1px solid oklch(0.30 0.02 250);
|
|
244
|
-
border-radius: 0.75rem;
|
|
245
|
-
box-shadow: 0 8px 32px oklch(0.05 0 0 / 0.6), 0 2px 8px oklch(0.05 0 0 / 0.3);
|
|
283
|
+
background: var(--fab-bg, oklch(0.16 0.02 250));
|
|
284
|
+
border: 1px solid var(--fab-border, oklch(0.30 0.02 250));
|
|
285
|
+
border-radius: var(--fab-radius, 0.75rem);
|
|
286
|
+
box-shadow: var(--fab-shadow, 0 8px 32px oklch(0.05 0 0 / 0.6), 0 2px 8px oklch(0.05 0 0 / 0.3));
|
|
287
|
+
color: var(--fab-fg, oklch(0.85 0.02 250));
|
|
288
|
+
}
|
|
289
|
+
/* Inline: in the host's own bar, so no float, no shadow, its full width. */
|
|
290
|
+
.fab-bar.fab-inline {
|
|
291
|
+
position: static;
|
|
292
|
+
left: auto;
|
|
293
|
+
bottom: auto;
|
|
294
|
+
transform: none;
|
|
295
|
+
max-width: none;
|
|
296
|
+
width: 100%;
|
|
297
|
+
justify-content: flex-start;
|
|
298
|
+
box-shadow: var(--fab-shadow-inline, none);
|
|
246
299
|
}
|
|
247
300
|
|
|
248
301
|
.fab-count {
|
|
249
302
|
font-size: 0.8125rem;
|
|
250
303
|
font-weight: 600;
|
|
251
|
-
color: oklch(0.85 0.02 250);
|
|
304
|
+
color: var(--fab-fg, oklch(0.85 0.02 250));
|
|
252
305
|
white-space: nowrap;
|
|
253
306
|
}
|
|
254
307
|
|
|
255
308
|
.fab-divider {
|
|
256
309
|
width: 1px;
|
|
257
310
|
height: 1.25rem;
|
|
258
|
-
background: oklch(0.30 0.02 250);
|
|
311
|
+
background: var(--fab-border, oklch(0.30 0.02 250));
|
|
259
312
|
}
|
|
260
313
|
|
|
261
314
|
.fab-btn {
|
|
@@ -264,7 +317,7 @@
|
|
|
264
317
|
gap: 0.375rem;
|
|
265
318
|
padding: 0.375rem 0.625rem;
|
|
266
319
|
border: 1px solid transparent;
|
|
267
|
-
border-radius: 0.375rem;
|
|
320
|
+
border-radius: var(--fab-btn-radius, 0.375rem);
|
|
268
321
|
font-size: 0.75rem;
|
|
269
322
|
font-weight: 500;
|
|
270
323
|
cursor: pointer;
|
|
@@ -278,25 +331,25 @@
|
|
|
278
331
|
|
|
279
332
|
/* Variants */
|
|
280
333
|
.fab-btn-default {
|
|
281
|
-
background: oklch(0.25 0.02 250 / 0.5);
|
|
282
|
-
color: oklch(0.75 0.02 250);
|
|
283
|
-
border-color: oklch(0.35 0.02 250);
|
|
334
|
+
background: var(--fab-btn-bg, oklch(0.25 0.02 250 / 0.5));
|
|
335
|
+
color: var(--fab-btn-fg, oklch(0.75 0.02 250));
|
|
336
|
+
border-color: var(--fab-btn-border, oklch(0.35 0.02 250));
|
|
284
337
|
}
|
|
285
|
-
.fab-btn-default:hover { background: oklch(0.30 0.02 250); }
|
|
338
|
+
.fab-btn-default:hover { background: var(--fab-btn-hover, oklch(0.30 0.02 250)); }
|
|
286
339
|
|
|
287
340
|
.fab-btn-primary {
|
|
288
|
-
background: oklch(0.65 0.18 250 / 0.15);
|
|
289
|
-
color: oklch(0.82 0.14 250);
|
|
290
|
-
border-color: oklch(0.65 0.18 250 / 0.3);
|
|
341
|
+
background: var(--fab-primary-bg, oklch(0.65 0.18 250 / 0.15));
|
|
342
|
+
color: var(--fab-primary-fg, oklch(0.82 0.14 250));
|
|
343
|
+
border-color: var(--fab-primary-border, oklch(0.65 0.18 250 / 0.3));
|
|
291
344
|
}
|
|
292
|
-
.fab-btn-primary:hover { background: oklch(0.65 0.18 250 / 0.25); }
|
|
345
|
+
.fab-btn-primary:hover { background: var(--fab-primary-hover, oklch(0.65 0.18 250 / 0.25)); }
|
|
293
346
|
|
|
294
347
|
.fab-btn-danger {
|
|
295
|
-
background: oklch(0.55 0.18 30 / 0.15);
|
|
296
|
-
color: oklch(0.80 0.15 30);
|
|
297
|
-
border-color: oklch(0.55 0.18 30 / 0.3);
|
|
348
|
+
background: var(--fab-danger-bg, oklch(0.55 0.18 30 / 0.15));
|
|
349
|
+
color: var(--fab-danger-fg, oklch(0.80 0.15 30));
|
|
350
|
+
border-color: var(--fab-danger-border, oklch(0.55 0.18 30 / 0.3));
|
|
298
351
|
}
|
|
299
|
-
.fab-btn-danger:hover { background: oklch(0.55 0.18 30 / 0.25); }
|
|
352
|
+
.fab-btn-danger:hover { background: var(--fab-danger-hover, oklch(0.55 0.18 30 / 0.25)); }
|
|
300
353
|
|
|
301
354
|
.fab-btn-warning {
|
|
302
355
|
background: oklch(0.75 0.15 85 / 0.12);
|
|
@@ -328,19 +381,23 @@
|
|
|
328
381
|
|
|
329
382
|
.fab-btn-clear {
|
|
330
383
|
background: transparent;
|
|
331
|
-
color: oklch(0.65 0.02 250);
|
|
384
|
+
color: var(--fab-muted, oklch(0.65 0.02 250));
|
|
332
385
|
border-color: transparent;
|
|
333
386
|
}
|
|
334
387
|
.fab-btn-clear:hover {
|
|
335
|
-
background: oklch(0.25 0.02 250);
|
|
336
|
-
color: oklch(0.80 0.02 250);
|
|
388
|
+
background: var(--fab-btn-hover, oklch(0.25 0.02 250));
|
|
389
|
+
color: var(--fab-fg, oklch(0.80 0.02 250));
|
|
390
|
+
}
|
|
391
|
+
.fab-btn:focus-visible {
|
|
392
|
+
outline: 2px solid var(--fab-focus, currentColor);
|
|
393
|
+
outline-offset: 1px;
|
|
337
394
|
}
|
|
338
395
|
|
|
339
396
|
.fab-spinner {
|
|
340
397
|
width: 14px;
|
|
341
398
|
height: 14px;
|
|
342
|
-
border: 2px solid oklch(0.35 0.02 250);
|
|
343
|
-
border-top-color: oklch(0.70 0.15 240);
|
|
399
|
+
border: 2px solid var(--fab-border, oklch(0.35 0.02 250));
|
|
400
|
+
border-top-color: var(--fab-spinner, oklch(0.70 0.15 240));
|
|
344
401
|
border-radius: 50%;
|
|
345
402
|
animation: fab-spin 0.6s linear infinite;
|
|
346
403
|
flex-shrink: 0;
|
|
@@ -389,13 +446,17 @@
|
|
|
389
446
|
left: 50%;
|
|
390
447
|
transform: translateX(-50%);
|
|
391
448
|
min-width: 160px;
|
|
392
|
-
background: oklch(0.18 0.02 250);
|
|
393
|
-
border: 1px solid oklch(0.30 0.02 250);
|
|
394
|
-
border-radius: 0.5rem;
|
|
449
|
+
background: var(--fab-menu-bg, oklch(0.18 0.02 250));
|
|
450
|
+
border: 1px solid var(--fab-border, oklch(0.30 0.02 250));
|
|
451
|
+
border-radius: var(--fab-menu-radius, 0.5rem);
|
|
395
452
|
padding: 0.375rem;
|
|
396
|
-
box-shadow: 0 8px 24px oklch(0.05 0 0 / 0.5);
|
|
453
|
+
box-shadow: var(--fab-menu-shadow, 0 8px 24px oklch(0.05 0 0 / 0.5));
|
|
397
454
|
z-index: 60;
|
|
398
455
|
}
|
|
456
|
+
.fab-inline .fab-dropdown {
|
|
457
|
+
left: 0;
|
|
458
|
+
transform: none;
|
|
459
|
+
}
|
|
399
460
|
.fab-dropdown-item {
|
|
400
461
|
display: flex;
|
|
401
462
|
align-items: center;
|
|
@@ -404,7 +465,7 @@
|
|
|
404
465
|
padding: 0.375rem 0.625rem;
|
|
405
466
|
border: none;
|
|
406
467
|
background: transparent;
|
|
407
|
-
color: oklch(0.80 0.02 250);
|
|
468
|
+
color: var(--fab-fg, oklch(0.80 0.02 250));
|
|
408
469
|
font-size: 0.75rem;
|
|
409
470
|
text-align: left;
|
|
410
471
|
border-radius: 0.375rem;
|
|
@@ -412,8 +473,15 @@
|
|
|
412
473
|
transition: background 0.1s;
|
|
413
474
|
white-space: nowrap;
|
|
414
475
|
}
|
|
415
|
-
.fab-dropdown-item:hover
|
|
476
|
+
.fab-dropdown-item:hover,
|
|
477
|
+
.fab-dropdown-item:focus-visible { background: var(--fab-btn-hover, oklch(0.25 0.02 250)); outline: none; }
|
|
416
478
|
|
|
479
|
+
.fab-item-label {
|
|
480
|
+
flex: 1;
|
|
481
|
+
}
|
|
482
|
+
.fab-item-hint {
|
|
483
|
+
margin-left: 1rem;
|
|
484
|
+
}
|
|
417
485
|
.fab-dot {
|
|
418
486
|
width: 8px;
|
|
419
487
|
height: 8px;
|
|
@@ -22,6 +22,8 @@ export interface DropdownItem {
|
|
|
22
22
|
label: string;
|
|
23
23
|
/** Optional colored dot (oklch string) */
|
|
24
24
|
dotColor?: string;
|
|
25
|
+
/** Optional key hint shown at the item's right edge (display only; the host binds it). */
|
|
26
|
+
hint?: string;
|
|
25
27
|
/** Optional SVG snippet rendered before the label */
|
|
26
28
|
iconSnippet?: import('svelte').Snippet;
|
|
27
29
|
/** Called when this item is clicked */
|
|
@@ -57,6 +59,13 @@ interface Props {
|
|
|
57
59
|
onClear?: () => void;
|
|
58
60
|
/** Label for the selection count (default: "selected") */
|
|
59
61
|
countLabel?: string;
|
|
62
|
+
/**
|
|
63
|
+
* 'floating' (default): fixed, bottom-centre over the page.
|
|
64
|
+
* 'inline': in the normal flow, full width of its container — for a
|
|
65
|
+
* host that already has a sticky bar to put it in. Both wrap onto more
|
|
66
|
+
* lines rather than run off a narrow screen.
|
|
67
|
+
*/
|
|
68
|
+
placement?: 'floating' | 'inline';
|
|
60
69
|
}
|
|
61
70
|
declare const FloatingActionBar: import("svelte").Component<Props, {}, "">;
|
|
62
71
|
type FloatingActionBar = ReturnType<typeof FloatingActionBar>;
|
|
@@ -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>
|