@joewinke/jatui 0.1.31 → 0.2.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 +22 -0
- package/dist/actions/attention.d.ts +24 -0
- package/dist/actions/attention.js +94 -0
- package/dist/actions/collapse.js +29 -15
- package/dist/actions/floating.d.ts +53 -0
- package/dist/actions/floating.js +158 -0
- package/dist/actions/listNav.js +137 -108
- package/dist/actions/railNav.js +89 -72
- package/dist/actions/staggerEntrance.d.ts +28 -0
- package/dist/actions/staggerEntrance.js +71 -0
- package/dist/actions/tooltip.d.ts +28 -0
- package/dist/actions/tooltip.js +186 -0
- package/dist/assets/sounds/README.md +47 -0
- package/dist/assets/sounds/error.d.ts +1 -0
- package/dist/assets/sounds/error.js +4 -0
- package/dist/assets/sounds/notify.d.ts +1 -0
- package/dist/assets/sounds/notify.js +4 -0
- package/dist/assets/sounds/pop.d.ts +1 -0
- package/dist/assets/sounds/pop.js +4 -0
- package/dist/assets/sounds/send.d.ts +1 -0
- package/dist/assets/sounds/send.js +4 -0
- package/dist/assets/sounds/success.d.ts +1 -0
- package/dist/assets/sounds/success.js +4 -0
- package/dist/assets/sounds/toggle.d.ts +1 -0
- package/dist/assets/sounds/toggle.js +4 -0
- package/dist/components/AnimatedNumber.svelte +112 -0
- package/dist/components/AnimatedNumber.svelte.d.ts +8 -0
- package/dist/components/Badge.svelte +75 -4
- package/dist/components/Badge.svelte.d.ts +6 -1
- package/dist/components/Button.svelte +196 -21
- package/dist/components/Button.svelte.d.ts +10 -1
- package/dist/components/Carousel.svelte +265 -0
- package/dist/components/Carousel.svelte.d.ts +21 -0
- package/dist/components/ChipInput.svelte +63 -3
- package/dist/components/Collapse.svelte +70 -14
- package/dist/components/Collapse.svelte.d.ts +11 -0
- package/dist/components/ColorSelector.svelte +21 -24
- package/dist/components/CommandPalette.svelte +475 -0
- package/dist/components/CommandPalette.svelte.d.ts +3 -0
- package/dist/components/ContextMenu.svelte +9 -73
- package/dist/components/CopyButton.svelte +156 -0
- package/dist/components/CopyButton.svelte.d.ts +12 -0
- package/dist/components/HoldToConfirm.svelte +183 -0
- package/dist/components/HoldToConfirm.svelte.d.ts +11 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +54 -8
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +3 -0
- package/dist/components/PasswordMeter.svelte +87 -0
- package/dist/components/PasswordMeter.svelte.d.ts +8 -0
- package/dist/components/PinInput.svelte +186 -0
- package/dist/components/PinInput.svelte.d.ts +13 -0
- package/dist/components/QuantityStepper.svelte +185 -0
- package/dist/components/QuantityStepper.svelte.d.ts +12 -0
- package/dist/components/Skeleton.svelte +123 -0
- package/dist/components/Skeleton.svelte.d.ts +12 -0
- package/dist/components/SpeechForm.svelte +37 -0
- package/dist/components/StatusBadge.svelte +31 -3
- package/dist/components/StatusBadge.svelte.d.ts +0 -5
- package/dist/components/Tabs.svelte +270 -0
- package/dist/components/Tabs.svelte.d.ts +15 -0
- package/dist/components/TextInput.svelte +138 -38
- package/dist/components/TextInput.svelte.d.ts +9 -1
- package/dist/components/VoicePlayer.svelte +51 -0
- package/dist/components/basic/Alert.svelte +36 -0
- package/dist/components/basic/Alert.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +27 -0
- package/dist/components/basic/Breadcrumb.svelte.d.ts +11 -0
- package/dist/components/basic/ButtonGroup.svelte +15 -0
- package/dist/components/basic/ButtonGroup.svelte.d.ts +8 -0
- package/dist/components/basic/Checkbox.svelte +85 -0
- package/dist/components/basic/Checkbox.svelte.d.ts +15 -0
- package/dist/components/basic/Empty.svelte +17 -0
- package/dist/components/basic/Empty.svelte.d.ts +8 -0
- package/dist/components/basic/Kbd.svelte +16 -0
- package/dist/components/basic/Kbd.svelte.d.ts +8 -0
- package/dist/components/basic/Pagination.svelte +104 -0
- package/dist/components/basic/Pagination.svelte.d.ts +12 -0
- package/dist/components/basic/RadioGroup.svelte +71 -0
- package/dist/components/basic/RadioGroup.svelte.d.ts +18 -0
- package/dist/components/basic/Separator.svelte +19 -0
- package/dist/components/basic/Separator.svelte.d.ts +8 -0
- package/dist/components/basic/Slider.svelte +72 -0
- package/dist/components/basic/Slider.svelte.d.ts +17 -0
- package/dist/components/basic/Switch.svelte +88 -0
- package/dist/components/basic/Switch.svelte.d.ts +14 -0
- package/dist/components/basic/Toggle.svelte +43 -0
- package/dist/components/basic/Toggle.svelte.d.ts +14 -0
- package/dist/components/graph/GraphCanvas.svelte +103 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +22 -0
- package/dist/components/graph/JatuiNode.svelte +128 -0
- package/dist/components/graph/JatuiNode.svelte.d.ts +14 -0
- package/dist/components/graph/theme.d.ts +27 -0
- package/dist/components/graph/theme.js +136 -0
- package/dist/components/markup.js +62 -59
- package/dist/components/messaging/MessageItem.svelte +92 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +5 -0
- package/dist/components/messaging/TypingIndicator.svelte +63 -0
- package/dist/components/messaging/TypingIndicator.svelte.d.ts +7 -0
- package/dist/components/pipeline/PipelineColumn.svelte +44 -3
- package/dist/components/progress/ProgressBar.svelte +189 -0
- package/dist/components/progress/ProgressBar.svelte.d.ts +17 -0
- package/dist/components/progress/ProgressRing.svelte +138 -0
- package/dist/components/progress/ProgressRing.svelte.d.ts +12 -0
- package/dist/components/progress/Steps.svelte +195 -0
- package/dist/components/progress/Steps.svelte.d.ts +14 -0
- package/dist/components/session-nav/transcriptModel.js +66 -49
- package/dist/components/shell/NavigationMenu.svelte +422 -0
- package/dist/components/shell/NavigationMenu.svelte.d.ts +43 -0
- package/dist/components/shell/Sidebar.svelte +276 -0
- package/dist/components/shell/Sidebar.svelte.d.ts +32 -0
- package/dist/components/shell/SidebarGroup.svelte +180 -0
- package/dist/components/shell/SidebarGroup.svelte.d.ts +19 -0
- package/dist/components/shell/SidebarItem.svelte +170 -0
- package/dist/components/shell/SidebarItem.svelte.d.ts +24 -0
- package/dist/components/shell/shell.d.ts +30 -0
- package/dist/components/shell/shell.js +65 -0
- package/dist/components/table/DataTable.svelte +364 -0
- package/dist/components/table/DataTable.svelte.d.ts +4 -0
- package/dist/components/table/flexRender.d.ts +31 -0
- package/dist/components/table/flexRender.js +32 -0
- package/dist/components/table/types.d.ts +28 -0
- package/dist/components/table/types.js +1 -0
- package/dist/components/toast/ToastHost.svelte +21 -0
- package/dist/components/toast/ToastHost.svelte.d.ts +3 -0
- package/dist/components/toast/ToastItem.svelte +287 -0
- package/dist/components/toast/ToastItem.svelte.d.ts +9 -0
- package/dist/data/emojis.js +4 -4
- package/dist/dnd/autoScroll.js +12 -10
- package/dist/dnd/createDndBoard.svelte.js +110 -76
- package/dist/dnd/createDndBoard.test.js +161 -85
- package/dist/dnd/draggable.js +23 -15
- package/dist/dnd/dropTarget.js +29 -15
- package/dist/dnd/index.d.ts +0 -1
- package/dist/dnd/index.js +0 -1
- package/dist/dnd/types.d.ts +0 -9
- package/dist/dnd/types.js +14 -2
- package/dist/index.d.ts +60 -6
- package/dist/index.js +73 -6
- package/dist/showcase/AnimatedBeam.svelte +183 -0
- package/dist/showcase/AnimatedBeam.svelte.d.ts +26 -0
- package/dist/showcase/AuroraDrift.svelte +74 -0
- package/dist/showcase/AuroraDrift.svelte.d.ts +9 -0
- package/dist/showcase/BentoCard.svelte +50 -0
- package/dist/showcase/BentoCard.svelte.d.ts +14 -0
- package/dist/showcase/BentoGrid.svelte +43 -0
- package/dist/showcase/BentoGrid.svelte.d.ts +12 -0
- package/dist/showcase/BorderBeam.svelte +79 -0
- package/dist/showcase/BorderBeam.svelte.d.ts +12 -0
- package/dist/showcase/ClipWipe.svelte +82 -0
- package/dist/showcase/ClipWipe.svelte.d.ts +12 -0
- package/dist/showcase/Confetti.svelte +71 -0
- package/dist/showcase/Confetti.svelte.d.ts +10 -0
- package/dist/showcase/CursorSpotlight.svelte +85 -0
- package/dist/showcase/CursorSpotlight.svelte.d.ts +9 -0
- package/dist/showcase/DeviceMock.svelte +186 -0
- package/dist/showcase/DeviceMock.svelte.d.ts +11 -0
- package/dist/showcase/Dock.svelte +123 -0
- package/dist/showcase/Dock.svelte.d.ts +17 -0
- package/dist/showcase/FlipCard.svelte +103 -0
- package/dist/showcase/FlipCard.svelte.d.ts +10 -0
- package/dist/showcase/FocusReveal.svelte +83 -0
- package/dist/showcase/FocusReveal.svelte.d.ts +11 -0
- package/dist/showcase/Globe.svelte +202 -0
- package/dist/showcase/Globe.svelte.d.ts +16 -0
- package/dist/showcase/KenBurns.svelte +105 -0
- package/dist/showcase/KenBurns.svelte.d.ts +13 -0
- package/dist/showcase/Marquee.svelte +89 -0
- package/dist/showcase/Marquee.svelte.d.ts +9 -0
- package/dist/showcase/ParallaxTilt.svelte +98 -0
- package/dist/showcase/ParallaxTilt.svelte.d.ts +10 -0
- package/dist/showcase/ScrambleText.svelte +98 -0
- package/dist/showcase/ScrambleText.svelte.d.ts +11 -0
- package/dist/showcase/ShineSweep.svelte +81 -0
- package/dist/showcase/ShineSweep.svelte.d.ts +9 -0
- package/dist/showcase/Terminal.svelte +230 -0
- package/dist/showcase/Terminal.svelte.d.ts +18 -0
- package/dist/showcase/TextSplitReveal.svelte +146 -0
- package/dist/showcase/TextSplitReveal.svelte.d.ts +13 -0
- package/dist/showcase/TextWave.svelte +55 -0
- package/dist/showcase/TextWave.svelte.d.ts +8 -0
- package/dist/showcase/TypewriterText.svelte +130 -0
- package/dist/showcase/TypewriterText.svelte.d.ts +9 -0
- package/dist/showcase/backdrop/GrainGradient.svelte +126 -0
- package/dist/showcase/backdrop/GrainGradient.svelte.d.ts +23 -0
- package/dist/showcase/backdrop/MeshGradient.svelte +104 -0
- package/dist/showcase/backdrop/MeshGradient.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/Metaballs.svelte +116 -0
- package/dist/showcase/backdrop/Metaballs.svelte.d.ts +18 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte +115 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte +48 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte.d.ts +12 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte +234 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte.d.ts +9 -0
- package/dist/showcase/generative/intent.d.ts +7 -0
- package/dist/showcase/generative/intent.js +67 -0
- package/dist/showcase/generative/manifest.d.ts +9 -0
- package/dist/showcase/generative/manifest.js +97 -0
- package/dist/showcase/generative/types.d.ts +48 -0
- package/dist/showcase/generative/types.js +8 -0
- package/dist/showcase/index.d.ts +36 -0
- package/dist/showcase/index.js +46 -0
- package/dist/showcase/scroll/SmoothScroll.svelte +49 -0
- package/dist/showcase/scroll/SmoothScroll.svelte.d.ts +12 -0
- package/dist/showcase/scroll/scrollProgress.d.ts +24 -0
- package/dist/showcase/scroll/scrollProgress.js +65 -0
- package/dist/showcase/scroll/scrollReveal.d.ts +30 -0
- package/dist/showcase/scroll/scrollReveal.js +70 -0
- package/dist/stores/commandPalette.svelte.d.ts +24 -0
- package/dist/stores/commandPalette.svelte.js +82 -0
- package/dist/stores/confirmDialog.svelte.js +5 -5
- package/dist/stores/inputDialog.svelte.js +5 -5
- package/dist/stores/toast.svelte.d.ts +28 -0
- package/dist/stores/toast.svelte.js +33 -0
- package/dist/styles/motion.css +46 -0
- package/dist/styles/tokens.css +31 -0
- package/dist/transitions/glide.d.ts +31 -0
- package/dist/transitions/glide.js +58 -0
- package/dist/types/booking.js +4 -4
- package/dist/types/comments.js +2 -2
- package/dist/types/messaging.js +3 -3
- package/dist/types/pipeline.js +2 -2
- package/dist/utils/currency.js +4 -2
- package/dist/utils/daisyuiColors.js +27 -25
- package/dist/utils/dateFormatters.d.ts +20 -0
- package/dist/utils/dateFormatters.js +65 -44
- package/dist/utils/googleMapsLoader.js +100 -46
- package/dist/utils/mentionParser.js +30 -30
- package/dist/utils/phone.js +27 -21
- package/dist/utils/phoneFormat.js +9 -9
- package/dist/utils/sound.d.ts +6 -0
- package/dist/utils/sound.js +113 -0
- package/dist/utils/splitText.d.ts +25 -0
- package/dist/utils/splitText.js +94 -0
- package/dist/utils/taskUtils.js +1 -1
- package/dist/utils/themeWipe.d.ts +21 -0
- package/dist/utils/themeWipe.js +94 -0
- package/dist/utils/viewTransition.d.ts +23 -0
- package/dist/utils/viewTransition.js +75 -0
- package/package.json +56 -4
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { fly, fade } from 'svelte/transition';
|
|
4
|
+
import { cubicOut } from 'svelte/easing';
|
|
5
|
+
import { commandPalette, type CommandItem } from '../stores/commandPalette.svelte.js';
|
|
6
|
+
|
|
7
|
+
// Respect prefers-reduced-motion
|
|
8
|
+
const reducedMotion =
|
|
9
|
+
typeof window !== 'undefined' &&
|
|
10
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
11
|
+
|
|
12
|
+
let searchQuery = $state('');
|
|
13
|
+
let highlightedIndex = $state(0);
|
|
14
|
+
let searchInput: HTMLInputElement | undefined;
|
|
15
|
+
let listRef: HTMLUListElement | undefined;
|
|
16
|
+
|
|
17
|
+
// Fuzzy score: return a score > 0 if query matches, else 0.
|
|
18
|
+
// Simple: all query chars must appear in label+keywords in order (subsequence).
|
|
19
|
+
function fuzzyScore(item: CommandItem, q: string): number {
|
|
20
|
+
if (!q) return 1;
|
|
21
|
+
const haystack = [item.label, ...(item.keywords ?? [])].join(' ').toLowerCase();
|
|
22
|
+
let pos = 0;
|
|
23
|
+
let score = 0;
|
|
24
|
+
let lastMatchPos = -1;
|
|
25
|
+
for (const ch of q) {
|
|
26
|
+
const idx = haystack.indexOf(ch, pos);
|
|
27
|
+
if (idx === -1) return 0;
|
|
28
|
+
// Bonus for consecutive chars
|
|
29
|
+
if (lastMatchPos >= 0 && idx === lastMatchPos + 1) score += 2;
|
|
30
|
+
score += 1;
|
|
31
|
+
lastMatchPos = idx;
|
|
32
|
+
pos = idx + 1;
|
|
33
|
+
}
|
|
34
|
+
// Bonus for label prefix match
|
|
35
|
+
if (item.label.toLowerCase().startsWith(q)) score += 10;
|
|
36
|
+
return score;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ScoredGroup = { label: string; items: CommandItem[] };
|
|
40
|
+
|
|
41
|
+
const filteredGroups = $derived.by((): ScoredGroup[] => {
|
|
42
|
+
const q = searchQuery.trim().toLowerCase();
|
|
43
|
+
|
|
44
|
+
// Show recents when query is empty
|
|
45
|
+
if (!q) {
|
|
46
|
+
const recent = commandPalette.getRecentItems();
|
|
47
|
+
const base: ScoredGroup[] = [];
|
|
48
|
+
if (recent.length > 0) {
|
|
49
|
+
base.push({ label: 'Recent', items: recent });
|
|
50
|
+
}
|
|
51
|
+
for (const g of commandPalette.groups) {
|
|
52
|
+
base.push({ label: g.label, items: g.items });
|
|
53
|
+
}
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result: ScoredGroup[] = [];
|
|
58
|
+
for (const g of commandPalette.groups) {
|
|
59
|
+
const scored = g.items
|
|
60
|
+
.map((item) => ({ item, score: fuzzyScore(item, q) }))
|
|
61
|
+
.filter((x) => x.score > 0)
|
|
62
|
+
.sort((a, b) => b.score - a.score)
|
|
63
|
+
.map((x) => x.item);
|
|
64
|
+
if (scored.length > 0) result.push({ label: g.label, items: scored });
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const flatItems = $derived(filteredGroups.flatMap((g) => g.items));
|
|
70
|
+
|
|
71
|
+
// Reset highlight when results change
|
|
72
|
+
$effect(() => {
|
|
73
|
+
void filteredGroups;
|
|
74
|
+
highlightedIndex = 0;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Focus input when opened
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (commandPalette.open) {
|
|
80
|
+
searchQuery = '';
|
|
81
|
+
tick().then(() => searchInput?.focus());
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function scrollHighlightIntoView() {
|
|
86
|
+
tick().then(() => {
|
|
87
|
+
const el = listRef?.querySelector<HTMLElement>('[data-highlighted="true"]');
|
|
88
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
93
|
+
if (e.key === 'Escape') {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
commandPalette.closePalette();
|
|
96
|
+
} else if (e.key === 'ArrowDown') {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
highlightedIndex = Math.min(highlightedIndex + 1, flatItems.length - 1);
|
|
99
|
+
scrollHighlightIntoView();
|
|
100
|
+
} else if (e.key === 'ArrowUp') {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
103
|
+
scrollHighlightIntoView();
|
|
104
|
+
} else if (e.key === 'Enter') {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const item = flatItems[highlightedIndex];
|
|
107
|
+
if (item) commandPalette.runCommand(item);
|
|
108
|
+
} else if (e.key === 'Tab') {
|
|
109
|
+
// Tab between groups — find next group boundary
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
const dir = e.shiftKey ? -1 : 1;
|
|
112
|
+
const currentItem = flatItems[highlightedIndex];
|
|
113
|
+
const currentGroup = filteredGroups.find((g) => g.items.includes(currentItem));
|
|
114
|
+
const groupIdx = currentGroup ? filteredGroups.indexOf(currentGroup) : 0;
|
|
115
|
+
const nextGroupIdx = (groupIdx + dir + filteredGroups.length) % filteredGroups.length;
|
|
116
|
+
const nextGroup = filteredGroups[nextGroupIdx];
|
|
117
|
+
if (nextGroup) {
|
|
118
|
+
const nextItem = nextGroup.items[0];
|
|
119
|
+
highlightedIndex = flatItems.indexOf(nextItem);
|
|
120
|
+
scrollHighlightIntoView();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
126
|
+
if (e.target === e.currentTarget) commandPalette.closePalette();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function handleWindowKeydown(e: KeyboardEvent) {
|
|
130
|
+
// Open on Ctrl+K / Cmd+K (ignore if typing in an input)
|
|
131
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
132
|
+
const tag = (document.activeElement as HTMLElement)?.tagName;
|
|
133
|
+
const isTyping =
|
|
134
|
+
tag === 'INPUT' || tag === 'TEXTAREA' ||
|
|
135
|
+
(document.activeElement as HTMLElement)?.isContentEditable;
|
|
136
|
+
if (!isTyping) {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
if (commandPalette.open) {
|
|
139
|
+
commandPalette.closePalette();
|
|
140
|
+
} else {
|
|
141
|
+
commandPalette.openPalette();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<svelte:window onkeydown={handleWindowKeydown} />
|
|
149
|
+
|
|
150
|
+
{#if commandPalette.open}
|
|
151
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
152
|
+
<div
|
|
153
|
+
class="cp-backdrop"
|
|
154
|
+
onclick={handleBackdropClick}
|
|
155
|
+
transition:fade={{ duration: reducedMotion ? 0 : 150 }}
|
|
156
|
+
>
|
|
157
|
+
<div
|
|
158
|
+
class="cp-panel"
|
|
159
|
+
role="combobox"
|
|
160
|
+
aria-expanded="true"
|
|
161
|
+
aria-haspopup="listbox"
|
|
162
|
+
aria-owns="cp-listbox"
|
|
163
|
+
in:fly={{ y: reducedMotion ? 0 : -8, duration: reducedMotion ? 0 : 200, easing: cubicOut }}
|
|
164
|
+
out:fade={{ duration: reducedMotion ? 0 : 120 }}
|
|
165
|
+
>
|
|
166
|
+
<!-- Search input -->
|
|
167
|
+
<div class="cp-search">
|
|
168
|
+
<svg class="cp-search-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
169
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
170
|
+
</svg>
|
|
171
|
+
<input
|
|
172
|
+
bind:this={searchInput}
|
|
173
|
+
bind:value={searchQuery}
|
|
174
|
+
onkeydown={handleKeydown}
|
|
175
|
+
type="text"
|
|
176
|
+
placeholder="Search commands…"
|
|
177
|
+
aria-label="Search commands"
|
|
178
|
+
aria-autocomplete="list"
|
|
179
|
+
aria-controls="cp-listbox"
|
|
180
|
+
aria-activedescendant={flatItems[highlightedIndex] ? `cp-item-${flatItems[highlightedIndex].id}` : undefined}
|
|
181
|
+
class="cp-search-input"
|
|
182
|
+
autocomplete="off"
|
|
183
|
+
spellcheck="false"
|
|
184
|
+
/>
|
|
185
|
+
{#if searchQuery}
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
aria-label="Clear search"
|
|
189
|
+
onclick={() => { searchQuery = ''; searchInput?.focus(); }}
|
|
190
|
+
class="cp-search-clear"
|
|
191
|
+
>
|
|
192
|
+
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
193
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
194
|
+
</svg>
|
|
195
|
+
</button>
|
|
196
|
+
{:else}
|
|
197
|
+
<kbd class="cp-hint-kbd">ESC</kbd>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- Results -->
|
|
202
|
+
<ul
|
|
203
|
+
id="cp-listbox"
|
|
204
|
+
role="listbox"
|
|
205
|
+
aria-label="Commands"
|
|
206
|
+
bind:this={listRef}
|
|
207
|
+
class="cp-list"
|
|
208
|
+
>
|
|
209
|
+
{#if filteredGroups.length > 0}
|
|
210
|
+
{#each filteredGroups as group}
|
|
211
|
+
<li role="presentation" class="cp-group-label">{group.label}</li>
|
|
212
|
+
{#each group.items as item}
|
|
213
|
+
{@const flatIdx = flatItems.indexOf(item)}
|
|
214
|
+
{@const isHighlighted = flatIdx === highlightedIndex}
|
|
215
|
+
<li
|
|
216
|
+
id="cp-item-{item.id}"
|
|
217
|
+
role="option"
|
|
218
|
+
aria-selected={isHighlighted}
|
|
219
|
+
data-highlighted={isHighlighted ? 'true' : undefined}
|
|
220
|
+
class="cp-item"
|
|
221
|
+
class:cp-item-highlighted={isHighlighted}
|
|
222
|
+
>
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
class="cp-item-btn"
|
|
226
|
+
onclick={() => commandPalette.runCommand(item)}
|
|
227
|
+
onmouseenter={() => { highlightedIndex = flatIdx; }}
|
|
228
|
+
tabindex="-1"
|
|
229
|
+
>
|
|
230
|
+
{#if item.icon}
|
|
231
|
+
<span class="cp-item-icon" aria-hidden="true">{item.icon}</span>
|
|
232
|
+
{:else}
|
|
233
|
+
<span class="cp-item-icon-placeholder" aria-hidden="true"></span>
|
|
234
|
+
{/if}
|
|
235
|
+
<span class="cp-item-label">{item.label}</span>
|
|
236
|
+
{#if item.shortcut && item.shortcut.length > 0}
|
|
237
|
+
<span class="cp-item-shortcut" aria-label="Shortcut: {item.shortcut.join(' ')}">
|
|
238
|
+
{#each item.shortcut as key}
|
|
239
|
+
<kbd class="cp-kbd">{key}</kbd>
|
|
240
|
+
{/each}
|
|
241
|
+
</span>
|
|
242
|
+
{/if}
|
|
243
|
+
</button>
|
|
244
|
+
</li>
|
|
245
|
+
{/each}
|
|
246
|
+
{/each}
|
|
247
|
+
{:else}
|
|
248
|
+
<li class="cp-empty">No commands match "{searchQuery}"</li>
|
|
249
|
+
{/if}
|
|
250
|
+
</ul>
|
|
251
|
+
|
|
252
|
+
<!-- Footer hint -->
|
|
253
|
+
<div class="cp-footer" aria-hidden="true">
|
|
254
|
+
<span class="cp-footer-hint"><kbd class="cp-kbd">↑↓</kbd> navigate</span>
|
|
255
|
+
<span class="cp-footer-hint"><kbd class="cp-kbd">↵</kbd> run</span>
|
|
256
|
+
<span class="cp-footer-hint"><kbd class="cp-kbd">Tab</kbd> jump group</span>
|
|
257
|
+
<span class="cp-footer-hint"><kbd class="cp-kbd">Esc</kbd> close</span>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
{/if}
|
|
262
|
+
|
|
263
|
+
<style>
|
|
264
|
+
.cp-backdrop {
|
|
265
|
+
position: fixed;
|
|
266
|
+
inset: 0;
|
|
267
|
+
z-index: 9000;
|
|
268
|
+
background: oklch(0 0 0 / 0.5);
|
|
269
|
+
display: flex;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
align-items: flex-start;
|
|
272
|
+
padding-top: 15vh;
|
|
273
|
+
padding-inline: 1rem;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.cp-panel {
|
|
277
|
+
width: 100%;
|
|
278
|
+
max-width: 560px;
|
|
279
|
+
background: oklch(var(--b1));
|
|
280
|
+
border: var(--jatui-border-w) solid oklch(var(--b3));
|
|
281
|
+
border-radius: var(--jatui-radius-box);
|
|
282
|
+
overflow: hidden;
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-direction: column;
|
|
285
|
+
max-height: 60vh;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* Search row */
|
|
289
|
+
.cp-search {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 0.5rem;
|
|
293
|
+
padding: 0.625rem 0.875rem;
|
|
294
|
+
border-bottom: var(--jatui-border-w) solid oklch(var(--b2));
|
|
295
|
+
flex-shrink: 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.cp-search-icon {
|
|
299
|
+
width: 1rem;
|
|
300
|
+
height: 1rem;
|
|
301
|
+
flex-shrink: 0;
|
|
302
|
+
color: oklch(var(--bc) / 0.4);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.cp-search-input {
|
|
306
|
+
flex: 1;
|
|
307
|
+
background: transparent;
|
|
308
|
+
border: none;
|
|
309
|
+
outline: none;
|
|
310
|
+
font-size: 0.9375rem;
|
|
311
|
+
color: oklch(var(--bc));
|
|
312
|
+
line-height: 1.5;
|
|
313
|
+
min-width: 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.cp-search-input::placeholder {
|
|
317
|
+
color: oklch(var(--bc) / 0.35);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.cp-search-clear {
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
justify-content: center;
|
|
324
|
+
padding: 0.1875rem;
|
|
325
|
+
border-radius: var(--jatui-radius-field);
|
|
326
|
+
border: none;
|
|
327
|
+
background: none;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
color: oklch(var(--bc) / 0.45);
|
|
330
|
+
transition: color var(--motion-instant) var(--motion-glide),
|
|
331
|
+
background var(--motion-instant) var(--motion-glide);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.cp-search-clear:hover {
|
|
335
|
+
color: oklch(var(--bc));
|
|
336
|
+
background: oklch(var(--b2));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.cp-hint-kbd {
|
|
340
|
+
font-size: 0.5625rem;
|
|
341
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
342
|
+
color: oklch(var(--bc) / 0.3);
|
|
343
|
+
background: none;
|
|
344
|
+
border: none;
|
|
345
|
+
padding: 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Results list */
|
|
349
|
+
.cp-list {
|
|
350
|
+
flex: 1;
|
|
351
|
+
overflow-y: auto;
|
|
352
|
+
list-style: none;
|
|
353
|
+
margin: 0;
|
|
354
|
+
padding: 0.25rem 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.cp-group-label {
|
|
358
|
+
padding: 0.5rem 0.875rem 0.125rem;
|
|
359
|
+
font-size: 0.5625rem;
|
|
360
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
361
|
+
font-weight: 700;
|
|
362
|
+
text-transform: uppercase;
|
|
363
|
+
letter-spacing: 0.07em;
|
|
364
|
+
color: oklch(var(--bc) / 0.35);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.cp-item {
|
|
368
|
+
padding: 0 0.375rem;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.cp-item-btn {
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
gap: 0.625rem;
|
|
375
|
+
width: 100%;
|
|
376
|
+
padding: 0.5rem 0.5rem;
|
|
377
|
+
border: none;
|
|
378
|
+
border-radius: var(--jatui-radius-selector);
|
|
379
|
+
background: transparent;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
text-align: left;
|
|
382
|
+
color: oklch(var(--bc) / 0.85);
|
|
383
|
+
transition: background var(--motion-instant) var(--motion-glide),
|
|
384
|
+
color var(--motion-instant) var(--motion-glide);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.cp-item-highlighted .cp-item-btn {
|
|
388
|
+
background: oklch(var(--b2));
|
|
389
|
+
color: oklch(var(--bc));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.cp-item-icon {
|
|
393
|
+
flex-shrink: 0;
|
|
394
|
+
width: 1.25rem;
|
|
395
|
+
text-align: center;
|
|
396
|
+
font-size: 0.875rem;
|
|
397
|
+
line-height: 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.cp-item-icon-placeholder {
|
|
401
|
+
flex-shrink: 0;
|
|
402
|
+
width: 1.25rem;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.cp-item-label {
|
|
406
|
+
flex: 1;
|
|
407
|
+
font-size: 0.875rem;
|
|
408
|
+
white-space: nowrap;
|
|
409
|
+
overflow: hidden;
|
|
410
|
+
text-overflow: ellipsis;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.cp-item-shortcut {
|
|
414
|
+
display: flex;
|
|
415
|
+
gap: 0.25rem;
|
|
416
|
+
align-items: center;
|
|
417
|
+
flex-shrink: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Keyboard hint keys */
|
|
421
|
+
.cp-kbd {
|
|
422
|
+
display: inline-flex;
|
|
423
|
+
align-items: center;
|
|
424
|
+
justify-content: center;
|
|
425
|
+
padding: 0.0625rem 0.3125rem;
|
|
426
|
+
border-radius: var(--jatui-radius-field);
|
|
427
|
+
font-size: 0.5625rem;
|
|
428
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
429
|
+
font-weight: 600;
|
|
430
|
+
color: oklch(var(--bc) / 0.55);
|
|
431
|
+
background: oklch(var(--b2));
|
|
432
|
+
border: var(--jatui-border-w) solid oklch(var(--b3));
|
|
433
|
+
line-height: 1.4;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.cp-item-highlighted .cp-kbd {
|
|
437
|
+
color: oklch(var(--bc) / 0.7);
|
|
438
|
+
background: oklch(var(--b3));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Empty state */
|
|
442
|
+
.cp-empty {
|
|
443
|
+
padding: 1.5rem 0.875rem;
|
|
444
|
+
text-align: center;
|
|
445
|
+
font-size: 0.8125rem;
|
|
446
|
+
color: oklch(var(--bc) / 0.4);
|
|
447
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Footer */
|
|
451
|
+
.cp-footer {
|
|
452
|
+
display: flex;
|
|
453
|
+
gap: 1rem;
|
|
454
|
+
align-items: center;
|
|
455
|
+
padding: 0.5rem 0.875rem;
|
|
456
|
+
border-top: var(--jatui-border-w) solid oklch(var(--b2));
|
|
457
|
+
flex-shrink: 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.cp-footer-hint {
|
|
461
|
+
display: flex;
|
|
462
|
+
align-items: center;
|
|
463
|
+
gap: 0.25rem;
|
|
464
|
+
font-size: 0.5625rem;
|
|
465
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
466
|
+
color: oklch(var(--bc) / 0.35);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@media (prefers-reduced-motion: reduce) {
|
|
470
|
+
.cp-search-clear,
|
|
471
|
+
.cp-item-btn {
|
|
472
|
+
transition: none;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
</style>
|
|
@@ -3,15 +3,9 @@
|
|
|
3
3
|
* ContextMenu Component
|
|
4
4
|
*
|
|
5
5
|
* Floating right-click (or programmatic) menu positioned at a cursor point.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Viewport clamping is measured from the rendered DOM (offsetWidth /
|
|
10
|
-
* offsetHeight) — no estimatedWidth/estimatedHeight props needed. The menu
|
|
11
|
-
* stays inside the viewport regardless of how many items are passed in.
|
|
12
|
-
* `transform-origin: top left` is set on the menu element so a caller's
|
|
13
|
-
* scale-based entrance animation (via `class`) cannot shift the visual
|
|
14
|
-
* rect away from the layout-box position used for clamping.
|
|
6
|
+
* Positioning is handled by the use:floating action (@floating-ui/dom) via a
|
|
7
|
+
* virtual anchor at the cursor coordinates. Dismiss (click-outside, Escape),
|
|
8
|
+
* focus return, and entrance animation are all delegated to the action.
|
|
15
9
|
*
|
|
16
10
|
* Usage:
|
|
17
11
|
* let cm = $state({ open: false, x: 0, y: 0, target: null })
|
|
@@ -35,8 +29,7 @@
|
|
|
35
29
|
* />
|
|
36
30
|
*/
|
|
37
31
|
|
|
38
|
-
import {
|
|
39
|
-
import { cubicOut } from 'svelte/easing';
|
|
32
|
+
import { floating, makeVirtualAnchor } from '../actions/floating.js';
|
|
40
33
|
|
|
41
34
|
type ActionItem = {
|
|
42
35
|
label: string;
|
|
@@ -89,74 +82,17 @@
|
|
|
89
82
|
close();
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
// effect below corrects them once the layout box is known.
|
|
95
|
-
let menuEl = $state<HTMLDivElement | null>(null);
|
|
96
|
-
// svelte-ignore state_referenced_locally
|
|
97
|
-
let clampedX = $state(x);
|
|
98
|
-
// svelte-ignore state_referenced_locally
|
|
99
|
-
let clampedY = $state(y);
|
|
100
|
-
|
|
101
|
-
function measureAndClamp() {
|
|
102
|
-
if (typeof window === 'undefined' || !menuEl) return;
|
|
103
|
-
// offset* gives the layout box, ignoring any transform: scale() the
|
|
104
|
-
// caller might apply via a className. getBoundingClientRect would
|
|
105
|
-
// return the post-transform rect and underestimate during entrance
|
|
106
|
-
// animations.
|
|
107
|
-
const w = menuEl.offsetWidth;
|
|
108
|
-
const h = menuEl.offsetHeight;
|
|
109
|
-
const margin = 8;
|
|
110
|
-
clampedX =
|
|
111
|
-
x + w > window.innerWidth - margin
|
|
112
|
-
? Math.max(margin, window.innerWidth - w - margin)
|
|
113
|
-
: Math.max(margin, x);
|
|
114
|
-
clampedY =
|
|
115
|
-
y + h > window.innerHeight - margin
|
|
116
|
-
? Math.max(margin, window.innerHeight - h - margin)
|
|
117
|
-
: Math.max(margin, y);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Re-measure whenever the menu opens, the anchor moves, or the item list
|
|
121
|
-
// changes (which can change the menu height).
|
|
122
|
-
$effect(() => {
|
|
123
|
-
if (!open) return;
|
|
124
|
-
// Touch reactive deps so the effect re-runs.
|
|
125
|
-
void x; void y; void items.length;
|
|
126
|
-
// Wait for the menu element to mount before measuring.
|
|
127
|
-
if (menuEl) {
|
|
128
|
-
measureAndClamp();
|
|
129
|
-
} else {
|
|
130
|
-
// Element not mounted yet — fall back to cursor position; the next
|
|
131
|
-
// effect tick will fire once `bind:this` resolves.
|
|
132
|
-
clampedX = x;
|
|
133
|
-
clampedY = y;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
85
|
+
// Virtual anchor tracks the cursor position; rebuilt whenever x or y change.
|
|
86
|
+
let anchor = $derived(makeVirtualAnchor(x, y));
|
|
136
87
|
</script>
|
|
137
88
|
|
|
138
89
|
{#if open}
|
|
139
|
-
<!-- Invisible full-viewport overlay — captures click/right-click to dismiss -->
|
|
140
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
141
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
142
|
-
<div
|
|
143
|
-
class="fixed inset-0"
|
|
144
|
-
style="z-index: {zIndex - 1};"
|
|
145
|
-
onclick={close}
|
|
146
|
-
oncontextmenu={(e) => {
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
close();
|
|
149
|
-
}}
|
|
150
|
-
></div>
|
|
151
|
-
|
|
152
90
|
<div
|
|
153
|
-
bind:this={menuEl}
|
|
154
91
|
role="menu"
|
|
155
92
|
tabindex="-1"
|
|
156
|
-
class="
|
|
157
|
-
style="
|
|
158
|
-
|
|
159
|
-
out:fade={{ duration: 100 }}
|
|
93
|
+
class="min-w-[200px] bg-base-200 border border-base-300 rounded-md py-1 {className}"
|
|
94
|
+
style="z-index: {zIndex};"
|
|
95
|
+
use:floating={{ anchor, placement: 'bottom-start', flip: true, shift: true, offset: 2, onDismiss: close }}
|
|
160
96
|
onkeydown={(e) => e.key === 'Escape' && close()}
|
|
161
97
|
>
|
|
162
98
|
{#if header}
|