@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.
- package/CLAUDE.md +52 -5
- package/THIRD_PARTY_NOTICES.md +31 -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/AnimatedNumber.svelte +42 -1
- package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- 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/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/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SegmentedControl.svelte +263 -0
- package/dist/components/SegmentedControl.svelte.d.ts +48 -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/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/settings/SettingsRow.svelte +177 -0
- package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
- package/dist/components/settings/SettingsSection.svelte +139 -0
- package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
- package/dist/components/stats/ActivityHeatmap.svelte +412 -0
- package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
- package/dist/components/stats/BarList.svelte +262 -0
- package/dist/components/stats/BarList.svelte.d.ts +57 -0
- package/dist/components/stats/ChartCard.svelte +230 -0
- package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
- package/dist/components/stats/StatCard.svelte +387 -0
- package/dist/components/stats/StatCard.svelte.d.ts +37 -0
- package/dist/components/stats/StatGroup.svelte +67 -0
- package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
- package/dist/components/stats/heatmap.d.ts +76 -0
- package/dist/components/stats/heatmap.js +151 -0
- package/dist/components/tree/TreeView.svelte +680 -0
- package/dist/components/tree/TreeView.svelte.d.ts +123 -0
- package/dist/components/tree/flattenTree.d.ts +57 -0
- package/dist/components/tree/flattenTree.js +91 -0
- package/dist/components/tree/treeKeys.d.ts +46 -0
- package/dist/components/tree/treeKeys.js +80 -0
- package/dist/data/grid/cells.d.ts +40 -0
- package/dist/data/grid/cells.js +89 -0
- package/dist/data/grid/clipboard.d.ts +46 -0
- package/dist/data/grid/clipboard.js +131 -0
- package/dist/data/grid/edits.d.ts +148 -0
- package/dist/data/grid/edits.js +222 -0
- package/dist/data/grid/index.d.ts +12 -0
- package/dist/data/grid/index.js +12 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +31 -1
- 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 +5 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* By default includes all 33 standard DaisyUI themes. Pass custom `themes` prop to override.
|
|
7
7
|
*/
|
|
8
8
|
import { onMount } from 'svelte';
|
|
9
|
+
import { themeWipe } from '../utils/themeWipe';
|
|
9
10
|
|
|
10
11
|
interface ThemeOption {
|
|
11
12
|
name: string;
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
onThemeChange,
|
|
56
57
|
compact = false,
|
|
57
58
|
inline = false,
|
|
59
|
+
reveal = false,
|
|
58
60
|
}: {
|
|
59
61
|
dropUp?: boolean;
|
|
60
62
|
themes?: ThemeOption[];
|
|
@@ -65,6 +67,12 @@
|
|
|
65
67
|
compact?: boolean;
|
|
66
68
|
/** Render a collapsible `<details>` theme list instead of a dropdown — for embedding inside another menu. */
|
|
67
69
|
inline?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Reveal the new theme with a circular View Transition wipe from the
|
|
72
|
+
* clicked item (`themeWipe`). `true`/'hard' = crisp edge, 'soft' =
|
|
73
|
+
* feathered. Level-scaled; instant under reduced motion. Default off.
|
|
74
|
+
*/
|
|
75
|
+
reveal?: boolean | 'hard' | 'soft';
|
|
68
76
|
} = $props();
|
|
69
77
|
|
|
70
78
|
// svelte-ignore state_referenced_locally
|
|
@@ -87,9 +95,26 @@
|
|
|
87
95
|
}
|
|
88
96
|
});
|
|
89
97
|
|
|
90
|
-
function
|
|
98
|
+
function applyTheme(themeName: string) {
|
|
99
|
+
currentTheme = themeName;
|
|
100
|
+
document.documentElement.setAttribute('data-theme', themeName);
|
|
101
|
+
localStorage.setItem(storageKey, themeName);
|
|
102
|
+
onThemeChange?.(themeName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleThemeChange(themeName: string, event?: MouseEvent) {
|
|
91
106
|
if (themeName === currentTheme) return;
|
|
92
107
|
|
|
108
|
+
if (reveal) {
|
|
109
|
+
// Close the menu first so the snapshot doesn't freeze it open. The
|
|
110
|
+
// origin (pointer, or the item's centre on keyboard) is read now.
|
|
111
|
+
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
|
112
|
+
void themeWipe(event ?? null, () => applyTheme(themeName), {
|
|
113
|
+
edge: reveal === 'soft' ? 'soft' : 'hard'
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
93
118
|
// Close dropdown first so the trigger is visible during animation
|
|
94
119
|
if (document.activeElement instanceof HTMLElement) {
|
|
95
120
|
document.activeElement.blur();
|
|
@@ -97,12 +122,7 @@
|
|
|
97
122
|
|
|
98
123
|
isAnimating = true;
|
|
99
124
|
|
|
100
|
-
setTimeout(() =>
|
|
101
|
-
currentTheme = themeName;
|
|
102
|
-
document.documentElement.setAttribute('data-theme', themeName);
|
|
103
|
-
localStorage.setItem(storageKey, themeName);
|
|
104
|
-
onThemeChange?.(themeName);
|
|
105
|
-
}, 150);
|
|
125
|
+
setTimeout(() => applyTheme(themeName), 150);
|
|
106
126
|
|
|
107
127
|
setTimeout(() => {
|
|
108
128
|
isAnimating = false;
|
|
@@ -133,7 +153,7 @@
|
|
|
133
153
|
{#each themes as theme}
|
|
134
154
|
<button
|
|
135
155
|
class="flex items-center gap-2 w-full px-2 py-1 rounded transition-colors hover:bg-base-300 {currentTheme === theme.name ? 'bg-primary/20' : ''}"
|
|
136
|
-
onclick={() => handleThemeChange(theme.name)}
|
|
156
|
+
onclick={(e) => handleThemeChange(theme.name, e)}
|
|
137
157
|
>
|
|
138
158
|
<div
|
|
139
159
|
data-theme={theme.name}
|
|
@@ -193,7 +213,7 @@
|
|
|
193
213
|
<li>
|
|
194
214
|
<button
|
|
195
215
|
class="gap-3 px-2"
|
|
196
|
-
onclick={() => handleThemeChange(theme.name)}
|
|
216
|
+
onclick={(e) => handleThemeChange(theme.name, e)}
|
|
197
217
|
class:active={currentTheme === theme.name}
|
|
198
218
|
>
|
|
199
219
|
<div
|
|
@@ -12,6 +12,12 @@ type $$ComponentProps = {
|
|
|
12
12
|
compact?: boolean;
|
|
13
13
|
/** Render a collapsible `<details>` theme list instead of a dropdown — for embedding inside another menu. */
|
|
14
14
|
inline?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Reveal the new theme with a circular View Transition wipe from the
|
|
17
|
+
* clicked item (`themeWipe`). `true`/'hard' = crisp edge, 'soft' =
|
|
18
|
+
* feathered. Level-scaled; instant under reduced motion. Default off.
|
|
19
|
+
*/
|
|
20
|
+
reveal?: boolean | 'hard' | 'soft';
|
|
15
21
|
};
|
|
16
22
|
declare const ThemeSelector: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
23
|
type ThemeSelector = ReturnType<typeof ThemeSelector>;
|
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
* motion.css global floor and a component-local guard, since not every
|
|
20
20
|
* consumer loads motion.css). Colours are token-only — no literals.
|
|
21
21
|
*/
|
|
22
|
+
import { flushSync } from 'svelte';
|
|
23
|
+
import { themeWipe } from '../utils/themeWipe';
|
|
24
|
+
|
|
22
25
|
let {
|
|
23
26
|
theme = $bindable('light'),
|
|
24
27
|
lightTheme = 'light',
|
|
25
28
|
darkTheme = 'dark',
|
|
26
29
|
onchange,
|
|
27
|
-
size = 'md'
|
|
30
|
+
size = 'md',
|
|
31
|
+
reveal = false
|
|
28
32
|
}: {
|
|
29
33
|
/** Current theme name (bindable). Anything that is not `darkTheme` reads as light. */
|
|
30
34
|
theme?: string;
|
|
@@ -36,6 +40,15 @@
|
|
|
36
40
|
onchange?: (theme: string) => void;
|
|
37
41
|
/** Hit-target size — sm 40px, md 44px, lg 48px. Never below the 40px touch floor. */
|
|
38
42
|
size?: 'sm' | 'md' | 'lg';
|
|
43
|
+
/**
|
|
44
|
+
* Reveal the new theme with a circular View Transition wipe from the
|
|
45
|
+
* button (`themeWipe`): `true`/'hard' = crisp edge, 'soft' = feathered.
|
|
46
|
+
* The flip (bound `theme` + `onchange`) runs inside the transition, so
|
|
47
|
+
* the consumer's theme write must be synchronous in `onchange` or in an
|
|
48
|
+
* effect (it is flushed). Keyboard activation grows from the button's
|
|
49
|
+
* centre; reduced motion swaps instantly. Default off.
|
|
50
|
+
*/
|
|
51
|
+
reveal?: boolean | 'hard' | 'soft';
|
|
39
52
|
} = $props();
|
|
40
53
|
|
|
41
54
|
let isDark = $derived(theme === darkTheme);
|
|
@@ -50,11 +63,23 @@
|
|
|
50
63
|
|
|
51
64
|
const label = $derived(isDark ? 'Switch to light mode' : 'Switch to dark mode');
|
|
52
65
|
|
|
53
|
-
function
|
|
54
|
-
const next = isDark ? lightTheme : darkTheme;
|
|
66
|
+
function flip(next: string) {
|
|
55
67
|
theme = next;
|
|
56
68
|
onchange?.(next);
|
|
57
69
|
}
|
|
70
|
+
|
|
71
|
+
function toggle(event: MouseEvent) {
|
|
72
|
+
const next = isDark ? lightTheme : darkTheme;
|
|
73
|
+
if (!reveal) {
|
|
74
|
+
flip(next);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// The origin is read synchronously: pointer position, or this button's
|
|
78
|
+
// centre for a keyboard activation.
|
|
79
|
+
void themeWipe(event, () => flushSync(() => flip(next)), {
|
|
80
|
+
edge: reveal === 'soft' ? 'soft' : 'hard'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
58
83
|
</script>
|
|
59
84
|
|
|
60
85
|
<button
|
|
@@ -9,6 +9,15 @@ type $$ComponentProps = {
|
|
|
9
9
|
onchange?: (theme: string) => void;
|
|
10
10
|
/** Hit-target size — sm 40px, md 44px, lg 48px. Never below the 40px touch floor. */
|
|
11
11
|
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
/**
|
|
13
|
+
* Reveal the new theme with a circular View Transition wipe from the
|
|
14
|
+
* button (`themeWipe`): `true`/'hard' = crisp edge, 'soft' = feathered.
|
|
15
|
+
* The flip (bound `theme` + `onchange`) runs inside the transition, so
|
|
16
|
+
* the consumer's theme write must be synchronous in `onchange` or in an
|
|
17
|
+
* effect (it is flushed). Keyboard activation grows from the button's
|
|
18
|
+
* centre; reduced motion swaps instantly. Default off.
|
|
19
|
+
*/
|
|
20
|
+
reveal?: boolean | 'hard' | 'soft';
|
|
12
21
|
};
|
|
13
22
|
declare const ThemeToggle: import("svelte").Component<$$ComponentProps, {}, "theme">;
|
|
14
23
|
type ThemeToggle = ReturnType<typeof ThemeToggle>;
|
|
@@ -1,27 +1,242 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Breadcrumb — hierarchical location trail.
|
|
4
|
+
*
|
|
5
|
+
* <Breadcrumb items={[{ label: 'Home', href: '/' }, …, { label: 'README.md' }]} />
|
|
6
|
+
* <Breadcrumb {items} maxItems={4} /> Home / … / y / z
|
|
7
|
+
* <Breadcrumb items={[{ label: 'Home', href: '/', icon: HomeIcon }, …]} />
|
|
8
|
+
*
|
|
9
|
+
* `maxItems` (BoardUI wave 1, jatui-owg2z) keeps the root and the last
|
|
10
|
+
* `maxItems - 1` items and folds the middle into an ellipsis button whose
|
|
11
|
+
* menu lists the hidden levels (arrow keys, Escape, click-outside). The last
|
|
12
|
+
* item is the current page (`aria-current="page"`). Icons come from
|
|
13
|
+
* `item.icon` (any component taking `{ size, class }`, e.g. lucide-svelte)
|
|
14
|
+
* or the `icon` snippet for full control.
|
|
15
|
+
*/
|
|
16
|
+
import { tick, type Component, type Snippet } from 'svelte';
|
|
17
|
+
import { floating } from '../../actions/floating';
|
|
18
|
+
import { collapseBreadcrumb } from './breadcrumbCollapse';
|
|
19
|
+
|
|
2
20
|
export interface BreadcrumbItem {
|
|
3
21
|
label: string;
|
|
4
22
|
href?: string;
|
|
23
|
+
/** Leading icon component (receives `size` and `class`). */
|
|
24
|
+
icon?: Component<{ size?: number; class?: string }>;
|
|
5
25
|
}
|
|
6
26
|
|
|
7
27
|
interface Props {
|
|
8
28
|
items: BreadcrumbItem[];
|
|
29
|
+
/** Collapse long trails to root / … / last (maxItems - 1). Unset = never collapse. */
|
|
30
|
+
maxItems?: number;
|
|
31
|
+
/** Custom leading icon per item — overrides `item.icon`. */
|
|
32
|
+
icon?: Snippet<[BreadcrumbItem, number]>;
|
|
33
|
+
/** Accessible name of the nav landmark. Default "Breadcrumb". */
|
|
34
|
+
ariaLabel?: string;
|
|
9
35
|
class?: string;
|
|
10
36
|
}
|
|
11
37
|
|
|
12
|
-
let { items, class: className = '' }: Props = $props();
|
|
38
|
+
let { items, maxItems, icon, ariaLabel = 'Breadcrumb', class: className = '' }: Props = $props();
|
|
39
|
+
|
|
40
|
+
const slots = $derived(collapseBreadcrumb(items, maxItems));
|
|
41
|
+
|
|
42
|
+
let menuOpen = $state(false);
|
|
43
|
+
let triggerEl: HTMLButtonElement | undefined = $state();
|
|
44
|
+
let menuEl: HTMLUListElement | undefined = $state();
|
|
45
|
+
|
|
46
|
+
function openMenu() {
|
|
47
|
+
menuOpen = true;
|
|
48
|
+
// Focus the first hidden level once the menu has mounted.
|
|
49
|
+
void tick().then(() => menuEl?.querySelector<HTMLElement>('[role="menuitem"]')?.focus());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function closeMenu(refocus = true) {
|
|
53
|
+
menuOpen = false;
|
|
54
|
+
if (refocus) triggerEl?.focus();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// use:floating dismisses on a capture-phase click outside the menu — which
|
|
58
|
+
// includes the trigger, so a click meant to CLOSE would re-open it.
|
|
59
|
+
let dismissedAt = 0;
|
|
60
|
+
function dismiss() {
|
|
61
|
+
dismissedAt = performance.now();
|
|
62
|
+
closeMenu(false);
|
|
63
|
+
}
|
|
64
|
+
function onTriggerClick() {
|
|
65
|
+
if (performance.now() - dismissedAt < 100) return;
|
|
66
|
+
if (menuOpen) closeMenu();
|
|
67
|
+
else openMenu();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onMenuKeydown(e: KeyboardEvent) {
|
|
71
|
+
const entries = [...(menuEl?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
|
|
72
|
+
const at = entries.indexOf(document.activeElement as HTMLElement);
|
|
73
|
+
let next = -1;
|
|
74
|
+
if (e.key === 'ArrowDown') next = (at + 1) % entries.length;
|
|
75
|
+
else if (e.key === 'ArrowUp') next = (at - 1 + entries.length) % entries.length;
|
|
76
|
+
else if (e.key === 'Home') next = 0;
|
|
77
|
+
else if (e.key === 'End') next = entries.length - 1;
|
|
78
|
+
else if (e.key === 'Escape') {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
closeMenu();
|
|
82
|
+
return;
|
|
83
|
+
} else if (e.key === 'Tab') {
|
|
84
|
+
closeMenu(false);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (next >= 0) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
entries[next]?.focus();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function onTriggerKeydown(e: KeyboardEvent) {
|
|
94
|
+
if (e.key === 'ArrowDown' && !menuOpen) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
openMenu();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
13
99
|
</script>
|
|
14
100
|
|
|
15
|
-
|
|
101
|
+
{#snippet lead(item: BreadcrumbItem, i: number)}
|
|
102
|
+
{#if icon}
|
|
103
|
+
{@render icon(item, i)}
|
|
104
|
+
{:else if item.icon}
|
|
105
|
+
{@const Icon = item.icon}
|
|
106
|
+
<Icon size={14} class="jatui-crumb-icon" />
|
|
107
|
+
{/if}
|
|
108
|
+
{/snippet}
|
|
109
|
+
|
|
110
|
+
<nav class="breadcrumbs text-sm {className}" aria-label={ariaLabel}>
|
|
16
111
|
<ul>
|
|
17
|
-
{#each
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
112
|
+
{#each slots as slot (slot.kind === 'item' ? slot.index : 'ellipsis')}
|
|
113
|
+
{#if slot.kind === 'item'}
|
|
114
|
+
{@const isLast = slot.index === items.length - 1}
|
|
115
|
+
<li>
|
|
116
|
+
{#if slot.item.href && !isLast}
|
|
117
|
+
<a href={slot.item.href} class="jatui-crumb">
|
|
118
|
+
{@render lead(slot.item, slot.index)}{slot.item.label}
|
|
119
|
+
</a>
|
|
120
|
+
{:else}
|
|
121
|
+
<span class="jatui-crumb" aria-current={isLast ? 'page' : undefined}>
|
|
122
|
+
{@render lead(slot.item, slot.index)}{slot.item.label}
|
|
123
|
+
</span>
|
|
124
|
+
{/if}
|
|
125
|
+
</li>
|
|
126
|
+
{:else}
|
|
127
|
+
<li>
|
|
128
|
+
<button
|
|
129
|
+
bind:this={triggerEl}
|
|
130
|
+
type="button"
|
|
131
|
+
class="jatui-crumb-more"
|
|
132
|
+
aria-haspopup="menu"
|
|
133
|
+
aria-expanded={menuOpen}
|
|
134
|
+
aria-label="Show {slot.hidden.length} hidden {slot.hidden.length === 1 ? 'level' : 'levels'}"
|
|
135
|
+
onclick={onTriggerClick}
|
|
136
|
+
onkeydown={onTriggerKeydown}
|
|
137
|
+
>
|
|
138
|
+
<span aria-hidden="true">…</span>
|
|
139
|
+
</button>
|
|
140
|
+
{#if menuOpen}
|
|
141
|
+
<ul
|
|
142
|
+
bind:this={menuEl}
|
|
143
|
+
role="menu"
|
|
144
|
+
aria-label="Hidden levels"
|
|
145
|
+
class="jatui-crumb-menu bg-base-100 border-base-300 z-50 border p-1"
|
|
146
|
+
use:floating={{
|
|
147
|
+
anchor: triggerEl ?? null,
|
|
148
|
+
placement: 'bottom-start',
|
|
149
|
+
offset: 4,
|
|
150
|
+
onDismiss: dismiss
|
|
151
|
+
}}
|
|
152
|
+
onkeydown={onMenuKeydown}
|
|
153
|
+
>
|
|
154
|
+
{#each slot.hidden as h (h.index)}
|
|
155
|
+
<li role="none">
|
|
156
|
+
{#if h.item.href}
|
|
157
|
+
<a
|
|
158
|
+
role="menuitem"
|
|
159
|
+
href={h.item.href}
|
|
160
|
+
class="jatui-crumb-menuitem"
|
|
161
|
+
onclick={() => closeMenu(false)}
|
|
162
|
+
>
|
|
163
|
+
{@render lead(h.item, h.index)}{h.item.label}
|
|
164
|
+
</a>
|
|
165
|
+
{:else}
|
|
166
|
+
<span role="menuitem" tabindex="-1" aria-disabled="true" class="jatui-crumb-menuitem">
|
|
167
|
+
{@render lead(h.item, h.index)}{h.item.label}
|
|
168
|
+
</span>
|
|
169
|
+
{/if}
|
|
170
|
+
</li>
|
|
171
|
+
{/each}
|
|
172
|
+
</ul>
|
|
173
|
+
{/if}
|
|
174
|
+
</li>
|
|
175
|
+
{/if}
|
|
25
176
|
{/each}
|
|
26
177
|
</ul>
|
|
27
|
-
</
|
|
178
|
+
</nav>
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
.jatui-crumb,
|
|
182
|
+
.jatui-crumb-menuitem {
|
|
183
|
+
display: inline-flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: var(--jatui-space-md, 6px);
|
|
186
|
+
}
|
|
187
|
+
.jatui-crumb-more {
|
|
188
|
+
display: inline-flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
min-width: 1.75rem;
|
|
192
|
+
height: 1.5rem;
|
|
193
|
+
padding: 0 var(--jatui-space-sm, 4px);
|
|
194
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
195
|
+
color: inherit;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
transition: background-color var(--motion-fast, 150ms) var(--motion-glide, ease-out);
|
|
198
|
+
}
|
|
199
|
+
.jatui-crumb-more:hover,
|
|
200
|
+
.jatui-crumb-more[aria-expanded='true'] {
|
|
201
|
+
background-color: var(
|
|
202
|
+
--jatui-hover-bg,
|
|
203
|
+
color-mix(in oklab, var(--color-base-content) 6%, transparent)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
.jatui-crumb-more:focus-visible,
|
|
207
|
+
.jatui-crumb-menuitem:focus-visible {
|
|
208
|
+
outline: var(--jatui-focus-ring-width, 2px) solid
|
|
209
|
+
var(--jatui-focus-ring, color-mix(in oklab, var(--color-primary) 70%, transparent));
|
|
210
|
+
outline-offset: var(--jatui-focus-ring-offset, 2px);
|
|
211
|
+
}
|
|
212
|
+
.jatui-crumb-menu {
|
|
213
|
+
min-width: 10rem;
|
|
214
|
+
max-width: 20rem;
|
|
215
|
+
border-radius: var(--jatui-radius-box, 0.5rem);
|
|
216
|
+
list-style: none;
|
|
217
|
+
}
|
|
218
|
+
/* DaisyUI's breadcrumbs paints a separator before every li — not inside the menu. */
|
|
219
|
+
.jatui-crumb-menu :global(li::before) {
|
|
220
|
+
display: none !important;
|
|
221
|
+
}
|
|
222
|
+
.jatui-crumb-menuitem {
|
|
223
|
+
display: flex;
|
|
224
|
+
width: 100%;
|
|
225
|
+
padding: var(--jatui-space-sm, 4px) var(--jatui-space-base, 8px);
|
|
226
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
227
|
+
white-space: nowrap;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
text-overflow: ellipsis;
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
}
|
|
232
|
+
.jatui-crumb-menuitem:hover,
|
|
233
|
+
.jatui-crumb-menuitem:focus-visible {
|
|
234
|
+
background-color: var(
|
|
235
|
+
--jatui-hover-bg,
|
|
236
|
+
color-mix(in oklab, var(--color-base-content) 6%, transparent)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
.jatui-crumb-menuitem[aria-disabled='true'] {
|
|
240
|
+
opacity: 0.6;
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -1,11 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb — hierarchical location trail.
|
|
3
|
+
*
|
|
4
|
+
* <Breadcrumb items={[{ label: 'Home', href: '/' }, …, { label: 'README.md' }]} />
|
|
5
|
+
* <Breadcrumb {items} maxItems={4} /> Home / … / y / z
|
|
6
|
+
* <Breadcrumb items={[{ label: 'Home', href: '/', icon: HomeIcon }, …]} />
|
|
7
|
+
*
|
|
8
|
+
* `maxItems` (BoardUI wave 1, jatui-owg2z) keeps the root and the last
|
|
9
|
+
* `maxItems - 1` items and folds the middle into an ellipsis button whose
|
|
10
|
+
* menu lists the hidden levels (arrow keys, Escape, click-outside). The last
|
|
11
|
+
* item is the current page (`aria-current="page"`). Icons come from
|
|
12
|
+
* `item.icon` (any component taking `{ size, class }`, e.g. lucide-svelte)
|
|
13
|
+
* or the `icon` snippet for full control.
|
|
14
|
+
*/
|
|
15
|
+
import { type Component, type Snippet } from 'svelte';
|
|
1
16
|
export interface BreadcrumbItem {
|
|
2
17
|
label: string;
|
|
3
18
|
href?: string;
|
|
19
|
+
/** Leading icon component (receives `size` and `class`). */
|
|
20
|
+
icon?: Component<{
|
|
21
|
+
size?: number;
|
|
22
|
+
class?: string;
|
|
23
|
+
}>;
|
|
4
24
|
}
|
|
5
25
|
interface Props {
|
|
6
26
|
items: BreadcrumbItem[];
|
|
27
|
+
/** Collapse long trails to root / … / last (maxItems - 1). Unset = never collapse. */
|
|
28
|
+
maxItems?: number;
|
|
29
|
+
/** Custom leading icon per item — overrides `item.icon`. */
|
|
30
|
+
icon?: Snippet<[BreadcrumbItem, number]>;
|
|
31
|
+
/** Accessible name of the nav landmark. Default "Breadcrumb". */
|
|
32
|
+
ariaLabel?: string;
|
|
7
33
|
class?: string;
|
|
8
34
|
}
|
|
9
|
-
declare const Breadcrumb:
|
|
35
|
+
declare const Breadcrumb: Component<Props, {}, "">;
|
|
10
36
|
type Breadcrumb = ReturnType<typeof Breadcrumb>;
|
|
11
37
|
export default Breadcrumb;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb overflow collapse (jatui-owg2z). Pure.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the first item and the last `maxItems - 1`, folding the middle into
|
|
5
|
+
* one ellipsis entry: "Home / … / y / z". The current page (last item) is
|
|
6
|
+
* always visible; the root is visible whenever maxItems ≥ 2.
|
|
7
|
+
*/
|
|
8
|
+
export type BreadcrumbSlot<T> = {
|
|
9
|
+
kind: 'item';
|
|
10
|
+
item: T;
|
|
11
|
+
index: number;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'ellipsis';
|
|
14
|
+
hidden: {
|
|
15
|
+
item: T;
|
|
16
|
+
index: number;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
export declare function collapseBreadcrumb<T>(items: readonly T[], maxItems?: number): BreadcrumbSlot<T>[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function collapseBreadcrumb(items, maxItems) {
|
|
2
|
+
const all = items.map((item, index) => ({ kind: 'item', item, index }));
|
|
3
|
+
if (maxItems == null || !(maxItems > 0) || items.length <= maxItems)
|
|
4
|
+
return all;
|
|
5
|
+
const max = Math.max(1, Math.floor(maxItems));
|
|
6
|
+
if (max === 1) {
|
|
7
|
+
return [{ kind: 'ellipsis', hidden: all.slice(0, -1) }, all[all.length - 1]];
|
|
8
|
+
}
|
|
9
|
+
const tail = all.slice(items.length - (max - 1));
|
|
10
|
+
const hidden = all.slice(1, items.length - (max - 1));
|
|
11
|
+
return [all[0], { kind: 'ellipsis', hidden }, ...tail];
|
|
12
|
+
}
|
|
@@ -76,7 +76,7 @@ declare function $$render<N extends Node = Node, E extends Edge = Edge>(): {
|
|
|
76
76
|
screenToFlowPosition: (p: import("@xyflow/system").XYPosition) => import("@xyflow/system").XYPosition;
|
|
77
77
|
getViewport: () => Viewport;
|
|
78
78
|
};
|
|
79
|
-
bindings: "viewport" | "
|
|
79
|
+
bindings: "viewport" | "edges" | "nodes";
|
|
80
80
|
slots: {};
|
|
81
81
|
events: {};
|
|
82
82
|
};
|
|
@@ -84,7 +84,7 @@ declare class __sveltets_Render<N extends Node = Node, E extends Edge = Edge> {
|
|
|
84
84
|
props(): ReturnType<typeof $$render<N, E>>['props'];
|
|
85
85
|
events(): ReturnType<typeof $$render<N, E>>['events'];
|
|
86
86
|
slots(): ReturnType<typeof $$render<N, E>>['slots'];
|
|
87
|
-
bindings(): "viewport" | "
|
|
87
|
+
bindings(): "viewport" | "edges" | "nodes";
|
|
88
88
|
exports(): {
|
|
89
89
|
/**
|
|
90
90
|
* Imperative handle (`bind:this={canvas}` → `canvas.api.fitView()`). Every
|