@makolabs/ripple 2.5.8 → 3.0.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.
Files changed (184) hide show
  1. package/README.md +403 -497
  2. package/dist/adapters/storage/S3Adapter.d.ts +49 -1
  3. package/dist/adapters/storage/S3Adapter.js +38 -1
  4. package/dist/adapters/storage/types.d.ts +20 -0
  5. package/dist/ai/AIChatInterface.svelte +2 -1
  6. package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
  7. package/dist/ai/CodeRenderer.svelte +7 -2
  8. package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
  9. package/dist/ai/ComposeDropdown.svelte +1 -1
  10. package/dist/ai/MessageBox.svelte +3 -3
  11. package/dist/ai/MessageBox.svelte.d.ts +3 -2
  12. package/dist/ai/ThinkingDisplay.svelte +4 -3
  13. package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
  14. package/dist/ai/ai-types.d.ts +55 -1
  15. package/dist/button/Button.svelte +5 -5
  16. package/dist/button/button-types.d.ts +49 -4
  17. package/dist/button/button.d.ts +9 -9
  18. package/dist/button/button.js +6 -6
  19. package/dist/charts/Chart.svelte +8 -16
  20. package/dist/charts/chart-types.d.ts +78 -1
  21. package/dist/drawer/Drawer.svelte +6 -26
  22. package/dist/drawer/drawer-types.d.ts +33 -12
  23. package/dist/drawer/drawer.d.ts +3 -3
  24. package/dist/drawer/drawer.js +1 -1
  25. package/dist/elements/accordion/Accordion.svelte +6 -17
  26. package/dist/elements/accordion/accordion-types.d.ts +53 -6
  27. package/dist/elements/alert/Alert.svelte +3 -0
  28. package/dist/elements/badge/Badge.svelte +1 -1
  29. package/dist/elements/badge/badge-types.d.ts +22 -0
  30. package/dist/elements/badge/badge.d.ts +3 -3
  31. package/dist/elements/badge/badge.js +1 -1
  32. package/dist/elements/combobox/ComboBox.svelte +247 -0
  33. package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
  34. package/dist/elements/combobox/combobox-types.d.ts +41 -0
  35. package/dist/elements/combobox/combobox-types.js +1 -0
  36. package/dist/elements/context-menu/ContextMenu.svelte +137 -0
  37. package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
  38. package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
  39. package/dist/elements/context-menu/context-menu-types.js +1 -0
  40. package/dist/elements/dropdown/Dropdown.svelte +1 -1
  41. package/dist/elements/dropdown/Select.svelte +4 -1
  42. package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
  43. package/dist/elements/dropdown/dropdown.d.ts +3 -3
  44. package/dist/elements/dropdown/dropdown.js +2 -2
  45. package/dist/elements/dropdown/select.d.ts +3 -3
  46. package/dist/elements/dropdown/select.js +2 -2
  47. package/dist/elements/empty-state/EmptyState.svelte +1 -1
  48. package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
  49. package/dist/elements/empty-state/empty-state.d.ts +3 -3
  50. package/dist/elements/empty-state/empty-state.js +2 -2
  51. package/dist/elements/file-upload/FileUpload.svelte +5 -0
  52. package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
  53. package/dist/elements/pagination/Pagination.svelte +53 -21
  54. package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
  55. package/dist/elements/popover/Popover.svelte +234 -0
  56. package/dist/elements/popover/Popover.svelte.d.ts +4 -0
  57. package/dist/elements/popover/index.d.ts +2 -0
  58. package/dist/elements/popover/index.js +1 -0
  59. package/dist/elements/popover/popover-types.d.ts +60 -0
  60. package/dist/elements/popover/popover-types.js +1 -0
  61. package/dist/elements/progress/Progress.svelte +32 -7
  62. package/dist/elements/progress/progress-types.d.ts +48 -1
  63. package/dist/elements/skeleton/Skeleton.svelte +56 -0
  64. package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
  65. package/dist/elements/skeleton/index.d.ts +2 -0
  66. package/dist/elements/skeleton/index.js +1 -0
  67. package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
  68. package/dist/elements/skeleton/skeleton-types.js +1 -0
  69. package/dist/elements/spinner/Spinner.svelte +1 -1
  70. package/dist/elements/spinner/spinner-types.d.ts +20 -0
  71. package/dist/elements/spinner/spinner.d.ts +3 -3
  72. package/dist/elements/spinner/spinner.js +2 -2
  73. package/dist/elements/tooltip/Tooltip.svelte +108 -11
  74. package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
  75. package/dist/file-browser/FileBrowser.svelte +21 -12
  76. package/dist/filters/CompactFilters.svelte +221 -33
  77. package/dist/filters/CompactFilters.svelte.d.ts +1 -1
  78. package/dist/filters/FilterBar.svelte +184 -0
  79. package/dist/filters/FilterBar.svelte.d.ts +4 -0
  80. package/dist/filters/FilterPopover.svelte +346 -0
  81. package/dist/filters/FilterPopover.svelte.d.ts +4 -0
  82. package/dist/filters/date-presets.d.ts +15 -0
  83. package/dist/filters/date-presets.js +107 -0
  84. package/dist/filters/filter-types.d.ts +69 -3
  85. package/dist/filters/index.d.ts +5 -0
  86. package/dist/filters/index.js +4 -0
  87. package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
  88. package/dist/filters/sync-filters-to-url.svelte.js +114 -0
  89. package/dist/forms/DateRange.svelte +4 -2
  90. package/dist/forms/Input.svelte +2 -2
  91. package/dist/forms/MarketSelector.svelte +8 -3
  92. package/dist/forms/NumberInput.svelte +4 -4
  93. package/dist/forms/RadioGroup.svelte +123 -0
  94. package/dist/forms/RadioGroup.svelte.d.ts +4 -0
  95. package/dist/forms/SegmentedControl.svelte +11 -4
  96. package/dist/forms/Slider.svelte +72 -3
  97. package/dist/forms/Tags.svelte +14 -5
  98. package/dist/forms/Textarea.svelte +126 -0
  99. package/dist/forms/Textarea.svelte.d.ts +4 -0
  100. package/dist/forms/Toggle.svelte +8 -8
  101. package/dist/forms/calendar/Calendar.svelte +218 -0
  102. package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
  103. package/dist/forms/calendar/calendar-types.d.ts +46 -0
  104. package/dist/forms/calendar/calendar-types.js +1 -0
  105. package/dist/forms/calendar/index.d.ts +2 -0
  106. package/dist/forms/calendar/index.js +1 -0
  107. package/dist/forms/date-picker/DatePicker.svelte +144 -0
  108. package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
  109. package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
  110. package/dist/forms/date-picker/date-picker-types.js +1 -0
  111. package/dist/forms/form-types.d.ts +425 -6
  112. package/dist/forms/market/market-selector-types.d.ts +52 -1
  113. package/dist/forms/segmented-control.d.ts +5 -2
  114. package/dist/forms/segmented-control.js +16 -5
  115. package/dist/forms/slider.d.ts +3 -3
  116. package/dist/forms/slider.js +2 -2
  117. package/dist/funcs/user-management.remote.d.ts +1 -1
  118. package/dist/funcs/user-management.remote.js +2 -2
  119. package/dist/header/Breadcrumbs.svelte +4 -20
  120. package/dist/header/PageHeader.svelte +6 -14
  121. package/dist/header/breadcrumbs.d.ts +3 -11
  122. package/dist/header/breadcrumbs.js +10 -5
  123. package/dist/header/header-types.d.ts +62 -11
  124. package/dist/index.d.ts +35 -9
  125. package/dist/index.js +24 -4
  126. package/dist/layout/activity-list/ActivityList.svelte +13 -7
  127. package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
  128. package/dist/layout/card/Card.svelte +12 -15
  129. package/dist/layout/card/MetricCard.svelte +50 -32
  130. package/dist/layout/card/card-types.d.ts +114 -4
  131. package/dist/layout/navbar/navbar-types.d.ts +48 -0
  132. package/dist/layout/navbar/navbar.d.ts +3 -3
  133. package/dist/layout/navbar/navbar.js +2 -2
  134. package/dist/layout/sidebar/Sidebar.svelte +87 -11
  135. package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
  136. package/dist/layout/stepper/Stepper.svelte +288 -0
  137. package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
  138. package/dist/layout/stepper/stepper-types.d.ts +80 -0
  139. package/dist/layout/stepper/stepper-types.js +1 -0
  140. package/dist/layout/table/Table.svelte +91 -85
  141. package/dist/layout/table/table-types.d.ts +148 -24
  142. package/dist/layout/table/table.d.ts +3 -3
  143. package/dist/layout/table/table.js +2 -2
  144. package/dist/layout/tabs/Tab.svelte +6 -2
  145. package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
  146. package/dist/layout/tabs/TabGroup.svelte +9 -2
  147. package/dist/layout/tabs/tabs-types.d.ts +63 -0
  148. package/dist/layout/tabs/tabs.d.ts +3 -3
  149. package/dist/layout/tabs/tabs.js +12 -6
  150. package/dist/modal/ConfirmDialog.svelte +65 -0
  151. package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
  152. package/dist/modal/Modal.svelte +6 -26
  153. package/dist/modal/confirm-dialog-types.d.ts +39 -0
  154. package/dist/modal/confirm-dialog-types.js +1 -0
  155. package/dist/modal/modal-types.d.ts +51 -12
  156. package/dist/modal/modal.d.ts +3 -3
  157. package/dist/modal/modal.js +3 -3
  158. package/dist/pipeline/Pipeline.svelte +8 -3
  159. package/dist/pipeline/pipeline-types.d.ts +55 -3
  160. package/dist/pipeline/pipeline.d.ts +18 -3
  161. package/dist/pipeline/pipeline.js +7 -2
  162. package/dist/server/s3.d.ts +35 -3
  163. package/dist/sonner/Toaster.svelte +29 -0
  164. package/dist/sonner/Toaster.svelte.d.ts +4 -0
  165. package/dist/sonner/index.d.ts +21 -0
  166. package/dist/sonner/index.js +20 -0
  167. package/dist/user-management/UserManagement.svelte +22 -16
  168. package/dist/user-management/UserModal.svelte +10 -7
  169. package/dist/user-management/UserTable.svelte +16 -17
  170. package/dist/user-management/UserViewModal.svelte +11 -11
  171. package/dist/user-management/user-management-types.d.ts +118 -31
  172. package/dist/variants.d.ts +1 -1
  173. package/dist/variants.js +1 -1
  174. package/package.json +7 -4
  175. package/dist/config/ai.d.ts +0 -13
  176. package/dist/config/ai.js +0 -44
  177. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
  178. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
  179. package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
  180. package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
  181. package/dist/helper/deprecation.d.ts +0 -14
  182. package/dist/helper/deprecation.js +0 -24
  183. package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
  184. package/dist/modal/ModalFooterTestWrapper.svelte.d.ts +0 -8
@@ -0,0 +1,247 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+ import { buildTestId } from '../../helper/testid.js';
4
+ import { Size } from '../../variants.js';
5
+ import Popover from '../popover/Popover.svelte';
6
+ import type { ComboBoxProps, ComboBoxItem } from './combobox-types.js';
7
+
8
+ let {
9
+ name,
10
+ id = name,
11
+ label,
12
+ placeholder = 'Start typing…',
13
+ value = $bindable<string | null>(null),
14
+ items,
15
+ filter,
16
+ onsearch,
17
+ emptyLabel = 'No results',
18
+ clearable = true,
19
+ disabled = false,
20
+ required = false,
21
+ size = Size.MD,
22
+ errors = [],
23
+ class: className = '',
24
+ onchange,
25
+ testId
26
+ }: ComboBoxProps = $props();
27
+
28
+ let open = $state(false);
29
+ let query = $state('');
30
+ let highlightedIndex = $state(0);
31
+ let inputEl = $state<HTMLInputElement | undefined>();
32
+
33
+ const selectedItem = $derived(items.find((i) => i.value === value) ?? null);
34
+ const hasErrors = $derived(errors.length > 0);
35
+
36
+ /**
37
+ * When the field isn't open for editing, the text reflects the current
38
+ * selection. When open, the user's query takes over so they can narrow
39
+ * the list. Blur without selection reverts to the saved selection.
40
+ */
41
+ const displayValue = $derived(open ? query : (selectedItem?.label ?? ''));
42
+
43
+ const defaultFilter = (item: ComboBoxItem, q: string) => {
44
+ if (!q) return true;
45
+ return item.label.toLowerCase().includes(q.toLowerCase());
46
+ };
47
+
48
+ const filteredItems = $derived(
49
+ onsearch
50
+ ? items // when server-side, parent controls the filtered list
51
+ : items.filter((item) => (filter ?? defaultFilter)(item, query))
52
+ );
53
+
54
+ // Reset highlight when list shrinks
55
+ $effect(() => {
56
+ void filteredItems;
57
+ if (highlightedIndex >= filteredItems.length) highlightedIndex = 0;
58
+ });
59
+
60
+ const sizeClass = $derived(
61
+ {
62
+ [Size.XS]: 'h-7 text-xs',
63
+ [Size.SM]: 'h-8 text-sm',
64
+ [Size.MD]: 'h-10 text-sm',
65
+ [Size.LG]: 'h-12 text-base',
66
+ [Size.XL]: 'h-14 text-lg',
67
+ [Size.XXL]: 'h-16 text-lg'
68
+ }[size]
69
+ );
70
+
71
+ function openMenu() {
72
+ if (disabled) return;
73
+ open = true;
74
+ query = '';
75
+ }
76
+
77
+ function closeMenu() {
78
+ open = false;
79
+ query = '';
80
+ }
81
+
82
+ function pick(item: ComboBoxItem) {
83
+ if (item.disabled) return;
84
+ value = item.value;
85
+ onchange?.(item.value);
86
+ closeMenu();
87
+ }
88
+
89
+ function clear(e: MouseEvent) {
90
+ e.stopPropagation();
91
+ value = null;
92
+ onchange?.(null);
93
+ inputEl?.focus();
94
+ }
95
+
96
+ function handleInput(e: Event) {
97
+ const v = (e.currentTarget as HTMLInputElement).value;
98
+ query = v;
99
+ open = true;
100
+ highlightedIndex = 0;
101
+ onsearch?.(v);
102
+ }
103
+
104
+ function handleKey(e: KeyboardEvent) {
105
+ if (disabled) return;
106
+ if (e.key === 'ArrowDown') {
107
+ e.preventDefault();
108
+ if (!open) {
109
+ openMenu();
110
+ return;
111
+ }
112
+ highlightedIndex = Math.min(highlightedIndex + 1, filteredItems.length - 1);
113
+ } else if (e.key === 'ArrowUp') {
114
+ e.preventDefault();
115
+ highlightedIndex = Math.max(highlightedIndex - 1, 0);
116
+ } else if (e.key === 'Enter') {
117
+ e.preventDefault();
118
+ const item = filteredItems[highlightedIndex];
119
+ if (item) pick(item);
120
+ } else if (e.key === 'Escape') {
121
+ if (open) {
122
+ e.preventDefault();
123
+ closeMenu();
124
+ }
125
+ }
126
+ }
127
+ </script>
128
+
129
+ <div class={cn('w-full', className)} data-testid={buildTestId('combobox', 'wrapper', testId)}>
130
+ {#if label}
131
+ <label for={id} class="text-default-700 mb-1 block text-sm font-medium">
132
+ {label}
133
+ {#if required}<span class="text-danger-500" aria-hidden="true">*</span>{/if}
134
+ </label>
135
+ {/if}
136
+
137
+ <input type="hidden" {name} value={value ?? ''} {required} />
138
+
139
+ <Popover trigger="manual" bind:open panelClass="w-[var(--cbx-w,20rem)] p-0">
140
+ <div
141
+ class={cn(
142
+ 'flex w-full items-center gap-2 rounded-lg border bg-white px-3 transition-colors',
143
+ 'focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none',
144
+ sizeClass,
145
+ hasErrors
146
+ ? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
147
+ : 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
148
+ disabled && 'cursor-not-allowed opacity-50'
149
+ )}
150
+ >
151
+ <input
152
+ {id}
153
+ bind:this={inputEl}
154
+ type="text"
155
+ role="combobox"
156
+ aria-expanded={open}
157
+ aria-controls="{id}-listbox"
158
+ aria-autocomplete="list"
159
+ aria-invalid={hasErrors}
160
+ autocomplete="off"
161
+ {placeholder}
162
+ {disabled}
163
+ value={displayValue}
164
+ oninput={handleInput}
165
+ onfocus={openMenu}
166
+ onkeydown={handleKey}
167
+ class="flex-1 bg-transparent outline-none"
168
+ data-testid={buildTestId('combobox', undefined, testId)}
169
+ />
170
+ {#if clearable && value && !disabled}
171
+ <button
172
+ type="button"
173
+ onclick={clear}
174
+ aria-label="Clear selection"
175
+ class="text-default-400 hover:text-default-700 flex size-5 items-center justify-center rounded"
176
+ >
177
+ <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
178
+ <path
179
+ d="M3 3l6 6M9 3l-6 6"
180
+ stroke="currentColor"
181
+ stroke-width="1.5"
182
+ stroke-linecap="round"
183
+ />
184
+ </svg>
185
+ </button>
186
+ {/if}
187
+ <svg
188
+ class={cn('text-default-400 size-4 transition-transform', open && 'rotate-180')}
189
+ viewBox="0 0 20 20"
190
+ fill="currentColor"
191
+ aria-hidden="true"
192
+ >
193
+ <path
194
+ fill-rule="evenodd"
195
+ d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06z"
196
+ clip-rule="evenodd"
197
+ />
198
+ </svg>
199
+ </div>
200
+
201
+ {#snippet content()}
202
+ <ul
203
+ id="{id}-listbox"
204
+ role="listbox"
205
+ class="max-h-64 overflow-auto py-1"
206
+ data-testid={buildTestId('combobox', 'listbox', testId)}
207
+ >
208
+ {#each filteredItems as item, i (item.value)}
209
+ {@const selected = item.value === value}
210
+ {@const highlighted = i === highlightedIndex}
211
+ <li>
212
+ <button
213
+ type="button"
214
+ role="option"
215
+ aria-selected={selected}
216
+ disabled={item.disabled}
217
+ onmouseenter={() => (highlightedIndex = i)}
218
+ onclick={() => pick(item)}
219
+ class={cn(
220
+ 'flex w-full flex-col items-start gap-0.5 px-3 py-1.5 text-left text-sm',
221
+ highlighted && 'bg-primary-50 text-primary-700',
222
+ selected && !highlighted && 'text-primary-600 font-medium',
223
+ item.disabled && 'text-default-300 cursor-not-allowed',
224
+ !item.disabled && !highlighted && 'text-default-700 hover:bg-default-50'
225
+ )}
226
+ >
227
+ <span>{item.label}</span>
228
+ {#if item.description}
229
+ <span class="text-default-500 text-xs">{item.description}</span>
230
+ {/if}
231
+ </button>
232
+ </li>
233
+ {:else}
234
+ <li class="text-default-400 px-3 py-2 text-sm">{emptyLabel}</li>
235
+ {/each}
236
+ </ul>
237
+ {/snippet}
238
+ </Popover>
239
+
240
+ {#if hasErrors}
241
+ <ul class="mt-1 space-y-0.5" role="alert">
242
+ {#each errors as error (error)}
243
+ <li class="text-danger-600 text-xs">{error}</li>
244
+ {/each}
245
+ </ul>
246
+ {/if}
247
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { ComboBoxProps } from './combobox-types.js';
2
+ declare const ComboBox: import("svelte").Component<ComboBoxProps, {}, "value">;
3
+ type ComboBox = ReturnType<typeof ComboBox>;
4
+ export default ComboBox;
@@ -0,0 +1,41 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { VariantSizes } from '../../index.js';
3
+ export type ComboBoxItem = {
4
+ value: string;
5
+ label: string;
6
+ /** Optional helper text shown below the label in the dropdown. */
7
+ description?: string;
8
+ disabled?: boolean;
9
+ };
10
+ export type ComboBoxProps = {
11
+ name: string;
12
+ id?: string;
13
+ label?: string;
14
+ placeholder?: string;
15
+ /** Bindable selected value. `null` when unset. */
16
+ value?: string | null;
17
+ /** Full list of options. Filtered locally by the typed query. */
18
+ items: ComboBoxItem[];
19
+ /**
20
+ * Custom filter. If provided, replaces the default case-insensitive
21
+ * label substring match. Return true to include the item in the list.
22
+ */
23
+ filter?: (item: ComboBoxItem, query: string) => boolean;
24
+ /**
25
+ * Called on every keystroke with the current query. Use this to fetch
26
+ * items from a server (set `items` from the parent based on the query).
27
+ * When present, the local `filter` is bypassed for the fetched set.
28
+ */
29
+ onsearch?: (query: string) => void;
30
+ /** Message shown when filtering leaves no matches. @default 'No results' */
31
+ emptyLabel?: string;
32
+ /** Show a clear × when something is selected. @default true */
33
+ clearable?: boolean;
34
+ disabled?: boolean;
35
+ required?: boolean;
36
+ size?: VariantSizes;
37
+ errors?: string[];
38
+ class?: ClassValue;
39
+ onchange?: (value: string | null) => void;
40
+ testId?: string;
41
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+ import { buildTestId } from '../../helper/testid.js';
4
+ import { fly } from 'svelte/transition';
5
+ import { quintOut } from 'svelte/easing';
6
+ import { Color } from '../../variants.js';
7
+ import type { ContextMenuProps, ContextMenuItem } from './context-menu-types.js';
8
+
9
+ let {
10
+ items,
11
+ children,
12
+ trigger = 'rightclick',
13
+ class: className = '',
14
+ menuClass = '',
15
+ testId
16
+ }: ContextMenuProps = $props();
17
+
18
+ let open = $state(false);
19
+ let x = $state(0);
20
+ let y = $state(0);
21
+ let menuRef = $state<HTMLDivElement | undefined>();
22
+ let longPressTimer: ReturnType<typeof setTimeout> | undefined;
23
+
24
+ function openAt(clientX: number, clientY: number) {
25
+ x = clientX;
26
+ y = clientY;
27
+ open = true;
28
+ }
29
+
30
+ function close() {
31
+ open = false;
32
+ }
33
+
34
+ function handleContextMenu(e: MouseEvent) {
35
+ if (trigger !== 'rightclick') return;
36
+ e.preventDefault();
37
+ openAt(e.clientX, e.clientY);
38
+ }
39
+
40
+ function handleTouchStart(e: TouchEvent) {
41
+ if (trigger !== 'longpress') return;
42
+ const t = e.touches[0];
43
+ longPressTimer = setTimeout(() => openAt(t.clientX, t.clientY), 600);
44
+ }
45
+
46
+ function handleTouchEnd() {
47
+ clearTimeout(longPressTimer);
48
+ }
49
+
50
+ function handleWindowClick(e: MouseEvent) {
51
+ if (!open) return;
52
+ if (menuRef && !menuRef.contains(e.target as Node)) close();
53
+ }
54
+
55
+ function handleKey(e: KeyboardEvent) {
56
+ if (open && e.key === 'Escape') close();
57
+ }
58
+
59
+ function handleItem(item: Exclude<ContextMenuItem, 'separator'>) {
60
+ if (item.disabled) return;
61
+ item.onclick?.();
62
+ close();
63
+ }
64
+
65
+ function itemColorClass(color?: string): string {
66
+ if (color === Color.DANGER) return 'text-danger-600 hover:bg-danger-50';
67
+ return 'text-default-700 hover:bg-default-50';
68
+ }
69
+
70
+ // Clamp x/y into viewport after mount so the menu doesn't overflow
71
+ $effect(() => {
72
+ if (!open || !menuRef) return;
73
+ const rect = menuRef.getBoundingClientRect();
74
+ const maxX = window.innerWidth - rect.width - 4;
75
+ const maxY = window.innerHeight - rect.height - 4;
76
+ if (x > maxX) x = Math.max(4, maxX);
77
+ if (y > maxY) y = Math.max(4, maxY);
78
+ });
79
+ </script>
80
+
81
+ <svelte:window onmousedown={handleWindowClick} onkeydown={handleKey} />
82
+
83
+ <div
84
+ class={cn('inline-block', className)}
85
+ oncontextmenu={handleContextMenu}
86
+ ontouchstart={handleTouchStart}
87
+ ontouchend={handleTouchEnd}
88
+ ontouchcancel={handleTouchEnd}
89
+ role="presentation"
90
+ data-testid={buildTestId('context-menu', undefined, testId)}
91
+ >
92
+ {@render children()}
93
+ </div>
94
+
95
+ {#if open}
96
+ <div
97
+ bind:this={menuRef}
98
+ role="menu"
99
+ transition:fly={{ y: -4, duration: 140, easing: quintOut }}
100
+ class={cn(
101
+ 'border-default-200 fixed z-50 min-w-[11rem] rounded-lg border bg-white py-1 shadow-lg',
102
+ menuClass
103
+ )}
104
+ style="left: {x}px; top: {y}px;"
105
+ data-testid={buildTestId('context-menu', 'menu', testId)}
106
+ >
107
+ {#each items as item, i (i)}
108
+ {#if item === 'separator'}
109
+ <div class="bg-default-100 my-1 h-px" aria-hidden="true"></div>
110
+ {:else}
111
+ <button
112
+ type="button"
113
+ role="menuitem"
114
+ disabled={item.disabled}
115
+ onclick={() => handleItem(item)}
116
+ class={cn(
117
+ 'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
118
+ item.disabled
119
+ ? 'text-default-300 cursor-not-allowed'
120
+ : `cursor-pointer ${itemColorClass(item.color)}`
121
+ )}
122
+ >
123
+ {#if item.icon}
124
+ {@const Icon = item.icon}
125
+ <span class="flex size-4 shrink-0 items-center justify-center">
126
+ <Icon />
127
+ </span>
128
+ {/if}
129
+ <span class="flex-1">{item.label}</span>
130
+ {#if item.shortcut}
131
+ <span class="text-default-400 ml-4 text-xs">{item.shortcut}</span>
132
+ {/if}
133
+ </button>
134
+ {/if}
135
+ {/each}
136
+ </div>
137
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { ContextMenuProps } from './context-menu-types.js';
2
+ declare const ContextMenu: import("svelte").Component<ContextMenuProps, {}, "">;
3
+ type ContextMenu = ReturnType<typeof ContextMenu>;
4
+ export default ContextMenu;
@@ -0,0 +1,40 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { Component, Snippet } from 'svelte';
3
+ import type { VariantColors } from '../../index.js';
4
+ /**
5
+ * Single entry in a ContextMenu. A `'separator'` value renders a
6
+ * horizontal divider.
7
+ */
8
+ export type ContextMenuItem = {
9
+ label: string;
10
+ icon?: Component;
11
+ disabled?: boolean;
12
+ /**
13
+ * Semantic color for the item. `'danger'` is the conventional choice
14
+ * for destructive actions (Delete, Remove) and tints both the icon
15
+ * and the label.
16
+ */
17
+ color?: VariantColors;
18
+ /** Short text shown right-aligned (e.g. keyboard shortcut). */
19
+ shortcut?: string;
20
+ onclick?: () => void;
21
+ } | 'separator';
22
+ export type ContextMenuProps = {
23
+ /** Ordered list of items or `'separator'` strings. */
24
+ items: ContextMenuItem[];
25
+ /**
26
+ * Wrap this trigger in a right-click handler. Any child markup works —
27
+ * the consumer's interactive elements inside still receive their own
28
+ * events.
29
+ */
30
+ children: Snippet;
31
+ /**
32
+ * Alternative trigger gesture. `'rightclick'` is the web-standard
33
+ * `contextmenu` event. `'longpress'` is touch-friendly (600ms).
34
+ * @default 'rightclick'
35
+ */
36
+ trigger?: 'rightclick' | 'longpress';
37
+ class?: ClassValue;
38
+ menuClass?: ClassValue;
39
+ testId?: string;
40
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -16,7 +16,7 @@
16
16
  containerClass = '',
17
17
  itemClass = '',
18
18
  class: className = '',
19
- size = Size.BASE,
19
+ size = Size.MD,
20
20
  disabled = false,
21
21
  position = 'bottom-left',
22
22
  width = 'w-56',
@@ -3,6 +3,8 @@
3
3
  import { cn } from '../../helper/cls.js';
4
4
  import { buildTestId } from '../../helper/testid.js';
5
5
  import { selectTV } from './select.js';
6
+ import { fly } from 'svelte/transition';
7
+ import { quintOut } from 'svelte/easing';
6
8
  import type { SelectItem, SelectProps } from '../../index.js';
7
9
  import Badge from '../badge/Badge.svelte';
8
10
  import { Size } from '../../variants.js';
@@ -15,7 +17,7 @@
15
17
  placeholder = 'Select an option',
16
18
  searchable = false,
17
19
  disabled = false,
18
- size = Size.BASE,
20
+ size = Size.MD,
19
21
  class: className = '',
20
22
  containerClass = '',
21
23
  listClass = '',
@@ -407,6 +409,7 @@
407
409
  role="listbox"
408
410
  aria-labelledby="{selectId}-label"
409
411
  data-testid={buildTestId('select', 'list', testId)}
412
+ transition:fly={{ y: -8, duration: 300, easing: quintOut }}
410
413
  >
411
414
  {#if isSearchable}
412
415
  <div class={searchInputClass_}>