@joewinke/jatui 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/actions/collapse.d.ts +80 -0
  2. package/dist/actions/collapse.js +134 -0
  3. package/dist/actions/railNav.d.ts +167 -0
  4. package/dist/actions/railNav.js +351 -0
  5. package/dist/components/AnnotationLayer.svelte +108 -0
  6. package/dist/components/AnnotationLayer.svelte.d.ts +12 -0
  7. package/dist/components/AnnotationPanel.svelte +319 -0
  8. package/dist/components/AnnotationPanel.svelte.d.ts +13 -0
  9. package/dist/components/AudioWaveform.svelte +701 -0
  10. package/dist/components/AudioWaveform.svelte.d.ts +19 -0
  11. package/dist/components/AvailabilityModal.svelte +177 -0
  12. package/dist/components/AvailabilityModal.svelte.d.ts +14 -0
  13. package/dist/components/AvatarUpload.svelte +234 -0
  14. package/dist/components/AvatarUpload.svelte.d.ts +23 -0
  15. package/dist/components/Badge.svelte +38 -0
  16. package/dist/components/Badge.svelte.d.ts +14 -0
  17. package/dist/components/BookingForm.svelte +278 -0
  18. package/dist/components/BookingForm.svelte.d.ts +37 -0
  19. package/dist/components/BurndownChart.svelte +778 -0
  20. package/dist/components/BurndownChart.svelte.d.ts +17 -0
  21. package/dist/components/Button.svelte +81 -0
  22. package/dist/components/Button.svelte.d.ts +21 -0
  23. package/dist/components/CalendarPicker.svelte +284 -0
  24. package/dist/components/CalendarPicker.svelte.d.ts +22 -0
  25. package/dist/components/Card.svelte +67 -0
  26. package/dist/components/Card.svelte.d.ts +17 -0
  27. package/dist/components/CharacterCounter.svelte +82 -0
  28. package/dist/components/CharacterCounter.svelte.d.ts +10 -0
  29. package/dist/components/ChipInput.svelte +609 -0
  30. package/dist/components/ChipInput.svelte.d.ts +55 -0
  31. package/dist/components/Collapse.svelte +91 -0
  32. package/dist/components/Collapse.svelte.d.ts +18 -0
  33. package/dist/components/ColorSelector.svelte +167 -0
  34. package/dist/components/ColorSelector.svelte.d.ts +9 -0
  35. package/dist/components/CommentThread.svelte +773 -0
  36. package/dist/components/CommentThread.svelte.d.ts +32 -0
  37. package/dist/components/ConfirmDialog.svelte +348 -0
  38. package/dist/components/ConfirmDialog.svelte.d.ts +3 -0
  39. package/dist/components/ConfirmModal.svelte +142 -0
  40. package/dist/components/ConfirmModal.svelte.d.ts +15 -0
  41. package/dist/components/ContextMenu.svelte +188 -0
  42. package/dist/components/ContextMenu.svelte.d.ts +31 -0
  43. package/dist/components/CountdownTimer.svelte +94 -0
  44. package/dist/components/CountdownTimer.svelte.d.ts +10 -0
  45. package/dist/components/DateRangePicker.svelte +194 -0
  46. package/dist/components/DateRangePicker.svelte.d.ts +10 -0
  47. package/dist/components/Drawer.svelte +143 -0
  48. package/dist/components/Drawer.svelte.d.ts +33 -0
  49. package/dist/components/EmojiPicker.svelte +159 -0
  50. package/dist/components/EmojiPicker.svelte.d.ts +8 -0
  51. package/dist/components/EntityPreviewCard.svelte +104 -0
  52. package/dist/components/EntityPreviewCard.svelte.d.ts +9 -0
  53. package/dist/components/FileDropzone.svelte +493 -0
  54. package/dist/components/FileDropzone.svelte.d.ts +40 -0
  55. package/dist/components/FilePicker.svelte +488 -0
  56. package/dist/components/FilePicker.svelte.d.ts +20 -0
  57. package/dist/components/FileThumbnail.svelte +80 -0
  58. package/dist/components/FileThumbnail.svelte.d.ts +12 -0
  59. package/dist/components/FilterDropdown.svelte +203 -0
  60. package/dist/components/FilterDropdown.svelte.d.ts +48 -0
  61. package/dist/components/FloatingActionBar.svelte +354 -0
  62. package/dist/components/FloatingActionBar.svelte.d.ts +51 -0
  63. package/dist/components/GPSTracker.svelte +202 -0
  64. package/dist/components/GPSTracker.svelte.d.ts +34 -0
  65. package/dist/components/HunkDiffView.svelte +348 -0
  66. package/dist/components/HunkDiffView.svelte.d.ts +9 -0
  67. package/dist/components/ImageLightbox.svelte +274 -0
  68. package/dist/components/ImageLightbox.svelte.d.ts +25 -0
  69. package/dist/components/ImageUpload.svelte +147 -0
  70. package/dist/components/ImageUpload.svelte.d.ts +10 -0
  71. package/dist/components/InlineEdit.svelte +291 -0
  72. package/dist/components/InlineEdit.svelte.d.ts +45 -0
  73. package/dist/components/InputDialog.svelte +327 -0
  74. package/dist/components/InputDialog.svelte.d.ts +3 -0
  75. package/dist/components/KeyboardShortcutsOverlay.svelte +296 -0
  76. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +42 -0
  77. package/dist/components/LazyImage.svelte +123 -0
  78. package/dist/components/LazyImage.svelte.d.ts +14 -0
  79. package/dist/components/LinkShortener.svelte +274 -0
  80. package/dist/components/LinkShortener.svelte.d.ts +27 -0
  81. package/dist/components/LoadingSpinner.svelte +106 -0
  82. package/dist/components/LoadingSpinner.svelte.d.ts +21 -0
  83. package/dist/components/LocationMap.svelte +186 -0
  84. package/dist/components/LocationMap.svelte.d.ts +11 -0
  85. package/dist/components/MapView.svelte +341 -0
  86. package/dist/components/MapView.svelte.d.ts +20 -0
  87. package/dist/components/MarkupEditor.svelte +485 -0
  88. package/dist/components/MarkupEditor.svelte.d.ts +10 -0
  89. package/dist/components/MarkupOverlay.svelte +55 -0
  90. package/dist/components/MarkupOverlay.svelte.d.ts +8 -0
  91. package/dist/components/MediaWorkbench.svelte +871 -0
  92. package/dist/components/MediaWorkbench.svelte.d.ts +9 -0
  93. package/dist/components/MilestoneCard.svelte +98 -0
  94. package/dist/components/MilestoneCard.svelte.d.ts +16 -0
  95. package/dist/components/MilestoneTimeline.svelte +91 -0
  96. package/dist/components/MilestoneTimeline.svelte.d.ts +15 -0
  97. package/dist/components/Modal.svelte +244 -0
  98. package/dist/components/Modal.svelte.d.ts +35 -0
  99. package/dist/components/PDFViewer.svelte +105 -0
  100. package/dist/components/PDFViewer.svelte.d.ts +9 -0
  101. package/dist/components/PdfThumbnail.svelte +63 -0
  102. package/dist/components/PdfThumbnail.svelte.d.ts +9 -0
  103. package/dist/components/PhoneInput.svelte +212 -0
  104. package/dist/components/PhoneInput.svelte.d.ts +25 -0
  105. package/dist/components/ResizableDivider.svelte +307 -0
  106. package/dist/components/ResizableDivider.svelte.d.ts +26 -0
  107. package/dist/components/ResizablePanel.svelte +305 -0
  108. package/dist/components/ResizablePanel.svelte.d.ts +18 -0
  109. package/dist/components/SearchDropdown.svelte +686 -0
  110. package/dist/components/SearchDropdown.svelte.d.ts +40 -0
  111. package/dist/components/SelectInput.svelte +237 -0
  112. package/dist/components/SelectInput.svelte.d.ts +26 -0
  113. package/dist/components/SidebarUserFooter.svelte +34 -0
  114. package/dist/components/SidebarUserFooter.svelte.d.ts +16 -0
  115. package/dist/components/SignaturePad.svelte +175 -0
  116. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  117. package/dist/components/SmartImageEditor.svelte +720 -0
  118. package/dist/components/SmartImageEditor.svelte.d.ts +24 -0
  119. package/dist/components/SortDropdown.svelte +156 -0
  120. package/dist/components/SortDropdown.svelte.d.ts +40 -0
  121. package/dist/components/Sparkline.svelte +116 -0
  122. package/dist/components/Sparkline.svelte.d.ts +12 -0
  123. package/dist/components/SpeechForm.svelte +114 -0
  124. package/dist/components/SpeechForm.svelte.d.ts +17 -0
  125. package/dist/components/StatusBadge.svelte +157 -0
  126. package/dist/components/StatusBadge.svelte.d.ts +24 -0
  127. package/dist/components/TaskTypeIcon.svelte +54 -0
  128. package/dist/components/TaskTypeIcon.svelte.d.ts +17 -0
  129. package/dist/components/TextArea.svelte +162 -0
  130. package/dist/components/TextArea.svelte.d.ts +30 -0
  131. package/dist/components/TextInput.svelte +131 -0
  132. package/dist/components/TextInput.svelte.d.ts +29 -0
  133. package/dist/components/ThemeSelector.svelte +206 -0
  134. package/dist/components/ThemeSelector.svelte.d.ts +14 -0
  135. package/dist/components/TimeSlotPicker.svelte +162 -0
  136. package/dist/components/TimeSlotPicker.svelte.d.ts +18 -0
  137. package/dist/components/UserAvatar.svelte +72 -0
  138. package/dist/components/UserAvatar.svelte.d.ts +11 -0
  139. package/dist/components/VariablePicker.svelte +170 -0
  140. package/dist/components/VariablePicker.svelte.d.ts +40 -0
  141. package/dist/components/VoicePlayer.svelte +422 -0
  142. package/dist/components/VoicePlayer.svelte.d.ts +18 -0
  143. package/dist/components/linked-columns/LinkedColumns.svelte +526 -0
  144. package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +130 -0
  145. package/dist/components/markup.d.ts +47 -0
  146. package/dist/components/markup.js +201 -0
  147. package/dist/components/messaging/Avatar.svelte +81 -0
  148. package/dist/components/messaging/Avatar.svelte.d.ts +11 -0
  149. package/dist/components/messaging/ChannelInfoModal.svelte +163 -0
  150. package/dist/components/messaging/ChannelInfoModal.svelte.d.ts +18 -0
  151. package/dist/components/messaging/ChannelList.svelte +107 -0
  152. package/dist/components/messaging/ChannelList.svelte.d.ts +11 -0
  153. package/dist/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
  154. package/dist/components/messaging/ChannelMemberAvatarStack.svelte.d.ts +11 -0
  155. package/dist/components/messaging/ChannelMembersModal.svelte +182 -0
  156. package/dist/components/messaging/ChannelMembersModal.svelte.d.ts +16 -0
  157. package/dist/components/messaging/CreateChannelModal.svelte +190 -0
  158. package/dist/components/messaging/CreateChannelModal.svelte.d.ts +14 -0
  159. package/dist/components/messaging/DirectMessageList.svelte +145 -0
  160. package/dist/components/messaging/DirectMessageList.svelte.d.ts +12 -0
  161. package/dist/components/messaging/EmojiSelector.svelte +261 -0
  162. package/dist/components/messaging/EmojiSelector.svelte.d.ts +19 -0
  163. package/dist/components/messaging/MentionAutocomplete.svelte +193 -0
  164. package/dist/components/messaging/MentionAutocomplete.svelte.d.ts +17 -0
  165. package/dist/components/messaging/MessageAttachment.svelte +273 -0
  166. package/dist/components/messaging/MessageAttachment.svelte.d.ts +13 -0
  167. package/dist/components/messaging/MessageAttachmentUpload.svelte +243 -0
  168. package/dist/components/messaging/MessageAttachmentUpload.svelte.d.ts +19 -0
  169. package/dist/components/messaging/MessageInput.svelte +452 -0
  170. package/dist/components/messaging/MessageInput.svelte.d.ts +28 -0
  171. package/dist/components/messaging/MessageItem.svelte +341 -0
  172. package/dist/components/messaging/MessageItem.svelte.d.ts +41 -0
  173. package/dist/components/messaging/MessageThread.svelte +306 -0
  174. package/dist/components/messaging/MessageThread.svelte.d.ts +30 -0
  175. package/dist/components/messaging/NotificationSettingsModal.svelte +234 -0
  176. package/dist/components/messaging/NotificationSettingsModal.svelte.d.ts +13 -0
  177. package/dist/components/messaging/QuotedMessageDisplay.svelte +123 -0
  178. package/dist/components/messaging/QuotedMessageDisplay.svelte.d.ts +10 -0
  179. package/dist/components/messaging/StartDMModal.svelte +100 -0
  180. package/dist/components/messaging/StartDMModal.svelte.d.ts +14 -0
  181. package/dist/components/messaging/ThreadPanel.svelte +153 -0
  182. package/dist/components/messaging/ThreadPanel.svelte.d.ts +16 -0
  183. package/dist/components/pipeline/DESIGN.md +361 -0
  184. package/dist/components/pipeline/Pipeline.svelte +391 -0
  185. package/dist/components/pipeline/Pipeline.svelte.d.ts +28 -0
  186. package/dist/components/pipeline/PipelineCard.svelte +94 -0
  187. package/dist/components/pipeline/PipelineCard.svelte.d.ts +13 -0
  188. package/dist/components/pipeline/PipelineColumn.svelte +163 -0
  189. package/dist/components/pipeline/PipelineColumn.svelte.d.ts +30 -0
  190. package/dist/components/replay/ChapterTimeline.svelte +326 -0
  191. package/dist/components/replay/ChapterTimeline.svelte.d.ts +39 -0
  192. package/dist/components/session-nav/transcriptModel.d.ts +132 -0
  193. package/dist/components/session-nav/transcriptModel.js +285 -0
  194. package/dist/data/emojis.d.ts +18 -0
  195. package/dist/data/emojis.js +1196 -0
  196. package/dist/index.d.ts +142 -0
  197. package/dist/index.js +142 -0
  198. package/dist/stores/confirmDialog.svelte.d.ts +21 -0
  199. package/dist/stores/confirmDialog.svelte.js +31 -0
  200. package/dist/stores/inputDialog.svelte.d.ts +24 -0
  201. package/dist/stores/inputDialog.svelte.js +32 -0
  202. package/dist/styles/rail.css +63 -0
  203. package/dist/types/annotation.d.ts +32 -0
  204. package/dist/types/annotation.js +3 -0
  205. package/dist/types/booking.d.ts +115 -0
  206. package/dist/types/booking.js +24 -0
  207. package/dist/types/comments.d.ts +65 -0
  208. package/dist/types/comments.js +22 -0
  209. package/dist/types/entityPreview.d.ts +42 -0
  210. package/dist/types/entityPreview.js +10 -0
  211. package/dist/types/filePicker.d.ts +15 -0
  212. package/dist/types/filePicker.js +1 -0
  213. package/dist/types/googleMaps.d.ts +51 -0
  214. package/dist/types/maps.d.ts +43 -0
  215. package/dist/types/maps.js +4 -0
  216. package/dist/types/messaging.d.ts +282 -0
  217. package/dist/types/messaging.js +100 -0
  218. package/dist/types/milestone.d.ts +20 -0
  219. package/dist/types/milestone.js +7 -0
  220. package/dist/types/pipeline.d.ts +115 -0
  221. package/dist/types/pipeline.js +15 -0
  222. package/dist/types/smartImageEditor.d.ts +42 -0
  223. package/dist/types/smartImageEditor.js +1 -0
  224. package/dist/types/templateVars.d.ts +34 -0
  225. package/dist/types/templateVars.js +12 -0
  226. package/dist/utils/currency.d.ts +8 -0
  227. package/dist/utils/currency.js +17 -0
  228. package/dist/utils/daisyuiColors.d.ts +37 -0
  229. package/dist/utils/daisyuiColors.js +152 -0
  230. package/dist/utils/dateFormatters.d.ts +69 -0
  231. package/dist/utils/dateFormatters.js +199 -0
  232. package/dist/utils/googleMapsLoader.d.ts +19 -0
  233. package/dist/utils/googleMapsLoader.js +121 -0
  234. package/dist/utils/mentionParser.d.ts +67 -0
  235. package/dist/utils/mentionParser.js +132 -0
  236. package/dist/utils/phone.d.ts +13 -0
  237. package/dist/utils/phone.js +67 -0
  238. package/dist/utils/phoneFormat.d.ts +26 -0
  239. package/dist/utils/phoneFormat.js +69 -0
  240. package/dist/utils/taskUtils.d.ts +37 -0
  241. package/dist/utils/taskUtils.js +71 -0
  242. package/package.json +3 -2
@@ -0,0 +1,686 @@
1
+ <script module lang="ts">
2
+ export type SearchDropdownOption = {
3
+ value: string;
4
+ label: string;
5
+ icon?: string;
6
+ image?: string; // URL for avatar/thumbnail image
7
+ };
8
+
9
+ export type SearchDropdownGroup = {
10
+ label: string;
11
+ options: SearchDropdownOption[];
12
+ };
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import { tick } from 'svelte';
17
+ import { slide } from 'svelte/transition';
18
+
19
+ import type { Snippet } from 'svelte';
20
+
21
+ let {
22
+ value = '',
23
+ groups = [],
24
+ placeholder = 'Filter...',
25
+ disabled = false,
26
+ displayValue,
27
+ colorFn,
28
+ footer,
29
+ variant = 'default',
30
+ size = 'md',
31
+ dropup = false,
32
+ appendToBody = false,
33
+ clearable = false,
34
+ triggerAttrs,
35
+ embedded = false,
36
+ onChange,
37
+ }: {
38
+ value: string;
39
+ groups: SearchDropdownGroup[];
40
+ placeholder?: string;
41
+ disabled?: boolean;
42
+ displayValue?: string;
43
+ colorFn?: (value: string) => string | undefined;
44
+ footer?: Snippet;
45
+ /** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
46
+ variant?: 'default' | 'chip';
47
+ /** 'sm' renders a smaller trigger matching text-[11px] compact UIs */
48
+ size?: 'sm' | 'md';
49
+ /** Open the panel above the trigger instead of below */
50
+ dropup?: boolean;
51
+ /** Render panel with position:fixed using viewport coordinates — escapes table/overflow stacking contexts */
52
+ appendToBody?: boolean;
53
+ /** Show an × button on the trigger when a value is selected */
54
+ clearable?: boolean;
55
+ /** Extra attributes forwarded to the trigger button (e.g. data-rail-id) */
56
+ triggerAttrs?: Record<string, string>;
57
+ /** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
58
+ embedded?: boolean;
59
+ onChange: (value: string) => void;
60
+ } = $props();
61
+
62
+ let open = $state(false);
63
+ let searchQuery = $state('');
64
+ let highlightedIndex = $state(-1);
65
+ let panelStyle = $state('');
66
+ let searchInput: HTMLInputElement | undefined;
67
+ let optionListRef: HTMLUListElement | undefined;
68
+ let containerRef: HTMLDivElement | undefined;
69
+ let triggerRef: HTMLButtonElement | undefined;
70
+
71
+ // Find the currently selected option across all groups
72
+ const selectedOption = $derived.by(() => {
73
+ for (const group of groups) {
74
+ const found = group.options.find(o => o.value === value);
75
+ if (found) return found;
76
+ }
77
+ return null;
78
+ });
79
+
80
+ const triggerLabel = $derived(displayValue || selectedOption?.label || value || placeholder);
81
+ const triggerIcon = $derived(selectedOption?.icon || '');
82
+ const triggerImage = $derived(selectedOption?.image || '');
83
+
84
+ // Generate a deterministic hue from a string for initials avatars
85
+ function nameHue(s: string): number {
86
+ let h = 0;
87
+ for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xffff;
88
+ return h % 360;
89
+ }
90
+ const activeColor = $derived(colorFn ? colorFn(value) : undefined);
91
+
92
+ const filteredGroups = $derived.by(() => {
93
+ if (!searchQuery.trim()) return groups;
94
+ const q = searchQuery.toLowerCase();
95
+ const result: SearchDropdownGroup[] = [];
96
+ for (const group of groups) {
97
+ const filtered = group.options.filter(o =>
98
+ o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q) || (o.icon && o.icon.toLowerCase().includes(q))
99
+ );
100
+ if (filtered.length > 0) result.push({ label: group.label, options: filtered });
101
+ }
102
+ return result;
103
+ });
104
+
105
+ // Flat ordered list of visible options for keyboard navigation
106
+ const flatOptions = $derived.by(() => {
107
+ const opts: SearchDropdownOption[] = [];
108
+ for (const group of filteredGroups) {
109
+ for (const opt of group.options) opts.push(opt);
110
+ }
111
+ return opts;
112
+ });
113
+
114
+ // When search changes, reset highlight to first option (or to current value's position)
115
+ $effect(() => {
116
+ void filteredGroups;
117
+ if (!open && !embedded) return;
118
+ const idx = flatOptions.findIndex(o => o.value === value);
119
+ highlightedIndex = idx >= 0 ? idx : 0;
120
+ });
121
+
122
+ function select(optionValue: string) {
123
+ open = false;
124
+ searchQuery = '';
125
+ onChange(optionValue);
126
+ }
127
+
128
+ function scrollHighlightedIntoView() {
129
+ tick().then(() => {
130
+ const el = optionListRef?.querySelector<HTMLElement>('[data-highlighted="true"]');
131
+ el?.scrollIntoView({ block: 'center' });
132
+ });
133
+ }
134
+
135
+ function handleSearchKeydown(e: KeyboardEvent) {
136
+ if (e.key === 'Escape') {
137
+ e.stopPropagation();
138
+ open = false;
139
+ searchQuery = '';
140
+ } else if (e.key === 'ArrowDown') {
141
+ e.preventDefault();
142
+ highlightedIndex = Math.min(highlightedIndex + 1, flatOptions.length - 1);
143
+ scrollHighlightedIntoView();
144
+ } else if (e.key === 'ArrowUp') {
145
+ e.preventDefault();
146
+ highlightedIndex = Math.max(highlightedIndex - 1, 0);
147
+ scrollHighlightedIntoView();
148
+ } else if (e.key === 'Enter') {
149
+ e.preventDefault();
150
+ e.stopPropagation();
151
+ if (highlightedIndex >= 0 && highlightedIndex < flatOptions.length) {
152
+ select(flatOptions[highlightedIndex].value);
153
+ }
154
+ }
155
+ }
156
+
157
+ function handleClickOutside(e: MouseEvent) {
158
+ if (containerRef && !containerRef.contains(e.target as Node)) {
159
+ open = false;
160
+ searchQuery = '';
161
+ }
162
+ }
163
+
164
+ $effect(() => {
165
+ if (embedded) {
166
+ tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
167
+ return;
168
+ }
169
+ if (open) {
170
+ if (appendToBody && triggerRef) {
171
+ const rect = triggerRef.getBoundingClientRect();
172
+ const width = Math.max(rect.width, 192);
173
+ // Viewport-aware flip: if there isn't room below the trigger but there
174
+ // is above, anchor the panel to the trigger's top and grow upward. This
175
+ // keeps the panel on-screen for rows near the bottom of a scroll area
176
+ // (e.g. suggested-task rows in the completed-session overlay).
177
+ const estPanelHeight = 300;
178
+ const spaceBelow = window.innerHeight - rect.bottom;
179
+ const spaceAbove = rect.top;
180
+ const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
181
+ // Clamp left so a right-edge trigger doesn't push the panel off-screen.
182
+ const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
183
+ const vertical = flipUp
184
+ ? `bottom: ${window.innerHeight - rect.top + 4}px;`
185
+ : `top: ${rect.bottom + 4}px;`;
186
+ panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; margin-top: 0;`;
187
+ }
188
+ document.addEventListener('mousedown', handleClickOutside);
189
+ tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
190
+ return () => document.removeEventListener('mousedown', handleClickOutside);
191
+ }
192
+ });
193
+
194
+ export function openDropdown() {
195
+ open = true;
196
+ }
197
+ </script>
198
+
199
+ {#snippet panelBody()}
200
+ <!-- Search input -->
201
+ <div class="sd-search">
202
+ <div class="sd-search-inner">
203
+ <svg class="sd-search-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
204
+ <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" />
205
+ </svg>
206
+ <input
207
+ bind:this={searchInput}
208
+ bind:value={searchQuery}
209
+ onkeydown={handleSearchKeydown}
210
+ type="text"
211
+ {placeholder}
212
+ class="sd-search-input"
213
+ autocomplete="off"
214
+ />
215
+ {#if searchQuery}
216
+ <button type="button" aria-label="Clear search" onclick={() => { searchQuery = ''; searchInput?.focus(); }} class="sd-search-clear">
217
+ <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
218
+ </button>
219
+ {/if}
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Options list -->
224
+ <ul class="sd-list" bind:this={optionListRef}>
225
+ {#if filteredGroups.length > 0}
226
+ {#each filteredGroups as group, _gi}
227
+ <li class="sd-group-label">
228
+ <span>{group.label}</span>
229
+ </li>
230
+ {#each group.options as option}
231
+ {@const flatIdx = flatOptions.indexOf(option)}
232
+ <li>
233
+ <button
234
+ type="button"
235
+ onclick={() => select(option.value)}
236
+ class="sd-option"
237
+ class:sd-option-selected={value === option.value}
238
+ class:sd-option-highlighted={flatIdx === highlightedIndex}
239
+ data-highlighted={flatIdx === highlightedIndex ? 'true' : undefined}
240
+ style={colorFn && colorFn(option.value)
241
+ ? variant === 'chip'
242
+ ? `--sd-color: ${colorFn(option.value)};`
243
+ : `border-left-color: ${colorFn(option.value)}; color: ${colorFn(option.value)};`
244
+ : ''}
245
+ >
246
+ {#if option.image}
247
+ <img src={option.image} alt="" class="sd-avatar" />
248
+ {:else if option.icon}
249
+ <span class="sd-option-icon">{option.icon}</span>
250
+ {:else if variant !== 'chip' && option.value}
251
+ <span class="sd-avatar sd-avatar-initials" style="background:oklch(0.55 0.15 {nameHue(option.label)})">
252
+ {option.label.charAt(0).toUpperCase()}
253
+ </span>
254
+ {/if}
255
+ {#if variant === 'chip' && colorFn && colorFn(option.value)}
256
+ <span class="sd-option-chip">{option.label}</span>
257
+ {:else}
258
+ <span class="truncate">{option.label}</span>
259
+ {/if}
260
+ {#if value === option.value}
261
+ <svg class="sd-check" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
262
+ {/if}
263
+ </button>
264
+ </li>
265
+ {/each}
266
+ {/each}
267
+ {:else}
268
+ <li class="sd-empty">No matches for "{searchQuery}"</li>
269
+ {/if}
270
+ </ul>
271
+ {#if footer}
272
+ <div class="sd-footer">
273
+ {@render footer()}
274
+ </div>
275
+ {/if}
276
+ {/snippet}
277
+
278
+ {#if embedded}
279
+ <div class="sd-embedded" class:sd-panel-sm={size === 'sm'}>
280
+ {@render panelBody()}
281
+ </div>
282
+ {:else}
283
+ <div class="search-dropdown" class:sd-chip-wrap={variant === 'chip'} bind:this={containerRef}>
284
+ <button
285
+ type="button"
286
+ class="sd-trigger"
287
+ class:sd-disabled={disabled}
288
+ class:sd-chip={variant === 'chip'}
289
+ class:sd-sm={size === 'sm'}
290
+ style={activeColor
291
+ ? variant === 'chip'
292
+ ? `--sd-color: ${activeColor};`
293
+ : `border-left-color: ${activeColor}; border-left-width: 3px; color: ${activeColor};`
294
+ : ''}
295
+ bind:this={triggerRef}
296
+ onclick={() => { if (!disabled) open = !open; }}
297
+ {disabled}
298
+ {...triggerAttrs}
299
+ >
300
+ <span class="sd-trigger-label">
301
+ {#if variant !== 'chip' && triggerImage}
302
+ <img src={triggerImage} alt="" class="sd-avatar" />
303
+ {:else if triggerIcon}
304
+ <span class="sd-trigger-icon">{triggerIcon}</span>
305
+ {:else if variant !== 'chip' && selectedOption && selectedOption.value}
306
+ <span class="sd-avatar sd-avatar-initials" style="background:oklch(0.55 0.15 {nameHue(selectedOption.label)})">
307
+ {selectedOption.label.charAt(0).toUpperCase()}
308
+ </span>
309
+ {/if}
310
+ <span class="truncate">{triggerLabel}</span>
311
+ </span>
312
+ {#if clearable && value !== ''}
313
+ <button
314
+ type="button"
315
+ class="sd-clear"
316
+ aria-label="Clear filter"
317
+ onclick={(e) => { e.stopPropagation(); onChange(''); }}
318
+ >
319
+ <svg class="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
320
+ </button>
321
+ {/if}
322
+ <svg class="sd-chevron" class:sd-chevron-open={open} fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
323
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
324
+ </svg>
325
+ </button>
326
+
327
+ {#if open}
328
+ <div
329
+ class="sd-panel"
330
+ class:sd-panel-sm={size === 'sm'}
331
+ class:sd-panel-dropup={dropup && !appendToBody}
332
+ style={appendToBody ? panelStyle : undefined}
333
+ transition:slide={{ duration: 120 }}
334
+ >
335
+ {@render panelBody()}
336
+ </div>
337
+ {/if}
338
+ </div>
339
+ {/if}
340
+
341
+ <style>
342
+ .search-dropdown {
343
+ position: relative;
344
+ }
345
+
346
+ /* Trigger button */
347
+ .sd-trigger {
348
+ width: 100%;
349
+ padding: 0.25rem 0.5rem;
350
+ border-radius: 0.5rem;
351
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
352
+ font-size: 0.8125rem;
353
+ text-align: left;
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ gap: 0.375rem;
358
+ transition: background 0.15s, border-color 0.15s;
359
+ min-height: 2rem;
360
+ cursor: pointer;
361
+ background: oklch(0.16 0.01 250);
362
+ border: 1px solid oklch(0.25 0.02 250);
363
+ color: oklch(0.85 0.02 250);
364
+ }
365
+ .sd-trigger:hover:not(:disabled) {
366
+ background: oklch(0.18 0.01 250);
367
+ border-color: oklch(0.30 0.02 250);
368
+ }
369
+ .sd-trigger:disabled, .sd-disabled {
370
+ opacity: 0.5;
371
+ cursor: not-allowed;
372
+ }
373
+
374
+ /* Small size variant — matches DaisyUI select-sm / input-sm */
375
+ .sd-sm {
376
+ font-size: 0.75rem;
377
+ min-height: 2rem;
378
+ padding: 0 0.75rem;
379
+ border-radius: var(--rounded-btn, 0.5rem);
380
+ background: var(--b2, oklch(0.16 0.01 250));
381
+ border: 1px solid oklch(from var(--bc, oklch(0.85 0.02 250)) l c h / 0.3);
382
+ color: var(--bc, oklch(0.85 0.02 250));
383
+ }
384
+ .sd-sm .sd-chevron {
385
+ width: 0.625rem;
386
+ height: 0.625rem;
387
+ }
388
+ .sd-panel-sm {
389
+ font-size: 0.75rem;
390
+ }
391
+ .sd-panel-sm .sd-search {
392
+ padding: 0.25rem 0.375rem;
393
+ }
394
+ .sd-panel-sm .sd-search-input {
395
+ font-size: 0.6875rem;
396
+ }
397
+ .sd-panel-sm .sd-option {
398
+ padding: 0.375rem 0.625rem;
399
+ font-size: 0.75rem;
400
+ }
401
+ .sd-panel-sm .sd-group-label {
402
+ font-size: 0.625rem;
403
+ padding: 0.25rem 0.625rem 0.125rem;
404
+ }
405
+
406
+ /* Chip variant — TopBar fav-chip style: full border, color-tinted background */
407
+ .sd-chip-wrap {
408
+ display: inline-flex;
409
+ }
410
+ /* Right-align the panel for chip variant so it doesn't spill past the container edge */
411
+ .sd-chip-wrap .sd-panel {
412
+ right: 0;
413
+ left: auto;
414
+ }
415
+ .sd-chip {
416
+ width: auto;
417
+ max-width: 10rem;
418
+ min-height: 1.5rem;
419
+ padding: 0.15rem 0.4rem;
420
+ border-radius: 0.375rem;
421
+ font-size: 0.6875rem;
422
+ font-weight: 600;
423
+ text-transform: uppercase;
424
+ letter-spacing: 0.025em;
425
+ background: transparent;
426
+ border: 1px solid color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 35%, transparent);
427
+ color: color-mix(in oklch, var(--sd-color, oklch(0.7 0 0)) 85%, oklch(0.80 0 0));
428
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s, color 0.15s;
429
+ }
430
+ .sd-chip:hover:not(:disabled) {
431
+ background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
432
+ border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
433
+ color: var(--sd-color, oklch(0.80 0 0));
434
+ box-shadow: 0 0 6px color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
435
+ }
436
+
437
+ .sd-trigger-label {
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 0.375rem;
441
+ min-width: 0;
442
+ }
443
+ .sd-avatar {
444
+ width: 1.25rem;
445
+ height: 1.25rem;
446
+ border-radius: 50%;
447
+ object-fit: cover;
448
+ flex-shrink: 0;
449
+ }
450
+ .sd-avatar-initials {
451
+ display: inline-flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ font-size: 0.6rem;
455
+ font-weight: 700;
456
+ color: oklch(1 0 0);
457
+ line-height: 1;
458
+ }
459
+ .sd-trigger-icon {
460
+ flex-shrink: 0;
461
+ font-size: 0.75rem;
462
+ }
463
+
464
+ .sd-clear {
465
+ display: flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ flex-shrink: 0;
469
+ padding: 0.1rem;
470
+ border-radius: 0.25rem;
471
+ background: none;
472
+ border: none;
473
+ cursor: pointer;
474
+ color: oklch(0.50 0.02 250);
475
+ transition: color 0.1s, background 0.1s;
476
+ }
477
+ .sd-clear:hover {
478
+ color: oklch(0.75 0.15 30);
479
+ background: oklch(0.75 0.15 30 / 0.12);
480
+ }
481
+
482
+ .sd-chevron {
483
+ width: 0.75rem;
484
+ height: 0.75rem;
485
+ flex-shrink: 0;
486
+ transition: transform 0.15s;
487
+ color: oklch(0.50 0.02 250);
488
+ }
489
+ .sd-chevron-open {
490
+ transform: rotate(180deg);
491
+ }
492
+
493
+ /* Embedded (inline) mode — panel content with no trigger or positioning */
494
+ .sd-embedded {
495
+ width: 100%;
496
+ }
497
+ .sd-embedded .sd-search {
498
+ background: transparent;
499
+ border-bottom: 1px solid oklch(0.22 0.02 250);
500
+ }
501
+ .sd-embedded .sd-list {
502
+ max-height: none;
503
+ }
504
+
505
+ /* Dropdown panel */
506
+ .sd-panel {
507
+ position: absolute;
508
+ z-index: 9999;
509
+ top: 100%;
510
+ margin-top: 0.25rem;
511
+ width: max-content;
512
+ min-width: max(100%, 14rem);
513
+ border-radius: 0.5rem;
514
+ overflow: hidden;
515
+ box-shadow: 0 4px 24px oklch(0 0 0 / 0.4);
516
+ background: oklch(0.16 0.01 250);
517
+ border: 1px solid oklch(0.25 0.02 250);
518
+ }
519
+
520
+ .sd-panel-dropup {
521
+ top: auto;
522
+ bottom: 100%;
523
+ margin-top: 0;
524
+ margin-bottom: 0.25rem;
525
+ box-shadow: 0 -4px 24px oklch(0 0 0 / 0.4);
526
+ }
527
+
528
+ /* Search section */
529
+ .sd-search {
530
+ padding: 0.375rem 0.5rem;
531
+ border-bottom: 1px solid oklch(0.25 0.02 250);
532
+ background: oklch(0.13 0.01 250);
533
+ }
534
+ .sd-search-inner {
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 0.375rem;
538
+ background: oklch(0.19 0.01 250);
539
+ border: 1px solid oklch(0.28 0.02 250);
540
+ border-radius: 0.25rem;
541
+ padding: 0.2rem 0.4rem;
542
+ }
543
+ .sd-search-icon {
544
+ width: 0.75rem;
545
+ height: 0.75rem;
546
+ flex-shrink: 0;
547
+ color: oklch(0.60 0.04 250);
548
+ }
549
+ .sd-search-input {
550
+ width: 100%;
551
+ background: transparent;
552
+ font-size: 0.6875rem;
553
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
554
+ color: oklch(0.85 0.02 250);
555
+ border: none;
556
+ outline: none;
557
+ }
558
+ .sd-search-input::placeholder {
559
+ color: oklch(0.52 0.03 250);
560
+ }
561
+ .sd-search-clear {
562
+ color: oklch(0.50 0.02 250);
563
+ background: none;
564
+ border: none;
565
+ cursor: pointer;
566
+ padding: 0;
567
+ display: flex;
568
+ }
569
+ .sd-search-clear:hover {
570
+ color: oklch(0.75 0.15 30);
571
+ }
572
+
573
+ /* Options list */
574
+ .sd-list {
575
+ padding: 0.125rem 0;
576
+ max-height: 280px;
577
+ overflow-y: auto;
578
+ list-style: none;
579
+ margin: 0;
580
+ }
581
+
582
+ .sd-group-label {
583
+ padding: 0.375rem 0.75rem 0.125rem;
584
+ }
585
+ .sd-group-label span {
586
+ font-size: 0.5625rem;
587
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
588
+ font-weight: 600;
589
+ text-transform: uppercase;
590
+ letter-spacing: 0.05em;
591
+ color: oklch(0.50 0.10 250);
592
+ }
593
+
594
+ .sd-option {
595
+ width: 100%;
596
+ padding: 0.375rem 0.75rem;
597
+ display: flex;
598
+ align-items: center;
599
+ gap: 0.5rem;
600
+ text-align: left;
601
+ font-size: 0.6875rem;
602
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
603
+ transition: background 0.1s;
604
+ cursor: pointer;
605
+ background: transparent;
606
+ border: none;
607
+ border-left: 2px solid transparent;
608
+ color: oklch(0.80 0.02 250);
609
+ }
610
+ .sd-option:hover {
611
+ background: oklch(0.19 0.01 250);
612
+ }
613
+ .sd-option-selected {
614
+ background: oklch(0.20 0.02 250);
615
+ border-left-color: oklch(0.65 0.15 250);
616
+ }
617
+ .sd-option-highlighted {
618
+ background: oklch(0.22 0.02 250);
619
+ outline: 1px solid oklch(0.45 0.10 250 / 0.5);
620
+ outline-offset: -1px;
621
+ }
622
+ .sd-option-highlighted.sd-option-selected {
623
+ background: oklch(0.24 0.03 250);
624
+ }
625
+
626
+ /* Chip variant — options render as inline chip pills */
627
+ .sd-chip-wrap .sd-option {
628
+ border-left: none;
629
+ padding: 0.25rem 0.625rem;
630
+ }
631
+ .sd-chip-wrap .sd-option-selected {
632
+ background: oklch(0.19 0.01 250);
633
+ border-left: none;
634
+ }
635
+ .sd-option-chip {
636
+ display: inline-flex;
637
+ align-items: center;
638
+ padding: 0.1rem 0.45rem;
639
+ border-radius: 0.3rem;
640
+ font-size: 0.6875rem;
641
+ font-weight: 600;
642
+ text-transform: uppercase;
643
+ letter-spacing: 0.025em;
644
+ border: 1px solid color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 35%, transparent);
645
+ color: color-mix(in oklch, var(--sd-color, oklch(0.7 0 0)) 85%, oklch(0.80 0 0));
646
+ background: transparent;
647
+ transition: background 0.1s, border-color 0.1s, color 0.1s;
648
+ }
649
+ .sd-chip-wrap .sd-option:hover .sd-option-chip {
650
+ background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 12%, transparent);
651
+ border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
652
+ color: var(--sd-color, oklch(0.80 0 0));
653
+ }
654
+ .sd-chip-wrap .sd-option-selected .sd-option-chip {
655
+ background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
656
+ border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
657
+ color: var(--sd-color, oklch(0.80 0 0));
658
+ }
659
+
660
+ .sd-option-icon {
661
+ flex-shrink: 0;
662
+ width: 1.25rem;
663
+ text-align: center;
664
+ font-size: 0.75rem;
665
+ }
666
+
667
+ .sd-check {
668
+ width: 0.75rem;
669
+ height: 0.75rem;
670
+ flex-shrink: 0;
671
+ margin-left: auto;
672
+ color: oklch(0.70 0.15 145);
673
+ }
674
+
675
+ .sd-footer {
676
+ border-top: 1px solid oklch(0.22 0.02 250);
677
+ }
678
+
679
+ .sd-empty {
680
+ padding: 0.75rem;
681
+ text-align: center;
682
+ font-size: 0.625rem;
683
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
684
+ color: oklch(0.45 0.02 250);
685
+ }
686
+ </style>