@makolabs/ripple 2.5.9 → 3.0.1

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 (186) 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 +244 -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 -108
  46. package/dist/elements/dropdown/select.js +38 -47
  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 +254 -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/Checkbox.svelte +24 -9
  90. package/dist/forms/DateRange.svelte +23 -6
  91. package/dist/forms/Input.svelte +19 -19
  92. package/dist/forms/MarketSelector.svelte +9 -4
  93. package/dist/forms/NumberInput.svelte +14 -18
  94. package/dist/forms/RadioGroup.svelte +127 -0
  95. package/dist/forms/RadioGroup.svelte.d.ts +4 -0
  96. package/dist/forms/SegmentedControl.svelte +11 -4
  97. package/dist/forms/Slider.svelte +72 -3
  98. package/dist/forms/Tags.svelte +44 -14
  99. package/dist/forms/Textarea.svelte +121 -0
  100. package/dist/forms/Textarea.svelte.d.ts +4 -0
  101. package/dist/forms/Toggle.svelte +30 -22
  102. package/dist/forms/calendar/Calendar.svelte +315 -0
  103. package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
  104. package/dist/forms/calendar/calendar-types.d.ts +54 -0
  105. package/dist/forms/calendar/calendar-types.js +1 -0
  106. package/dist/forms/calendar/index.d.ts +2 -0
  107. package/dist/forms/calendar/index.js +1 -0
  108. package/dist/forms/date-picker/DatePicker.svelte +141 -0
  109. package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
  110. package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
  111. package/dist/forms/date-picker/date-picker-types.js +1 -0
  112. package/dist/forms/form-size.d.ts +37 -0
  113. package/dist/forms/form-size.js +67 -0
  114. package/dist/forms/form-types.d.ts +430 -6
  115. package/dist/forms/market/market-selector-types.d.ts +52 -1
  116. package/dist/forms/segmented-control.d.ts +5 -2
  117. package/dist/forms/segmented-control.js +25 -13
  118. package/dist/forms/slider.d.ts +3 -3
  119. package/dist/forms/slider.js +37 -30
  120. package/dist/funcs/user-management.remote.js +1 -1
  121. package/dist/header/Breadcrumbs.svelte +4 -20
  122. package/dist/header/PageHeader.svelte +6 -14
  123. package/dist/header/breadcrumbs.d.ts +3 -11
  124. package/dist/header/breadcrumbs.js +10 -5
  125. package/dist/header/header-types.d.ts +62 -11
  126. package/dist/index.d.ts +35 -9
  127. package/dist/index.js +24 -4
  128. package/dist/layout/activity-list/ActivityList.svelte +13 -7
  129. package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
  130. package/dist/layout/card/Card.svelte +12 -15
  131. package/dist/layout/card/MetricCard.svelte +50 -32
  132. package/dist/layout/card/card-types.d.ts +114 -4
  133. package/dist/layout/navbar/navbar-types.d.ts +48 -0
  134. package/dist/layout/navbar/navbar.d.ts +3 -3
  135. package/dist/layout/navbar/navbar.js +2 -2
  136. package/dist/layout/sidebar/Sidebar.svelte +87 -11
  137. package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
  138. package/dist/layout/stepper/Stepper.svelte +288 -0
  139. package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
  140. package/dist/layout/stepper/stepper-types.d.ts +80 -0
  141. package/dist/layout/stepper/stepper-types.js +1 -0
  142. package/dist/layout/table/Table.svelte +91 -85
  143. package/dist/layout/table/table-types.d.ts +148 -24
  144. package/dist/layout/table/table.d.ts +3 -3
  145. package/dist/layout/table/table.js +2 -2
  146. package/dist/layout/tabs/Tab.svelte +6 -2
  147. package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
  148. package/dist/layout/tabs/TabGroup.svelte +9 -2
  149. package/dist/layout/tabs/tabs-types.d.ts +63 -0
  150. package/dist/layout/tabs/tabs.d.ts +3 -3
  151. package/dist/layout/tabs/tabs.js +12 -6
  152. package/dist/modal/ConfirmDialog.svelte +65 -0
  153. package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
  154. package/dist/modal/Modal.svelte +6 -26
  155. package/dist/modal/confirm-dialog-types.d.ts +39 -0
  156. package/dist/modal/confirm-dialog-types.js +1 -0
  157. package/dist/modal/modal-types.d.ts +51 -12
  158. package/dist/modal/modal.d.ts +3 -3
  159. package/dist/modal/modal.js +3 -3
  160. package/dist/pipeline/Pipeline.svelte +8 -3
  161. package/dist/pipeline/pipeline-types.d.ts +55 -3
  162. package/dist/pipeline/pipeline.d.ts +18 -3
  163. package/dist/pipeline/pipeline.js +7 -2
  164. package/dist/server/s3.d.ts +35 -3
  165. package/dist/sonner/Toaster.svelte +29 -0
  166. package/dist/sonner/Toaster.svelte.d.ts +4 -0
  167. package/dist/sonner/index.d.ts +21 -0
  168. package/dist/sonner/index.js +20 -0
  169. package/dist/user-management/UserManagement.svelte +22 -16
  170. package/dist/user-management/UserModal.svelte +10 -7
  171. package/dist/user-management/UserTable.svelte +16 -17
  172. package/dist/user-management/UserViewModal.svelte +11 -11
  173. package/dist/user-management/user-management-types.d.ts +118 -31
  174. package/dist/variants.d.ts +1 -1
  175. package/dist/variants.js +1 -1
  176. package/package.json +7 -4
  177. package/dist/config/ai.d.ts +0 -13
  178. package/dist/config/ai.js +0 -44
  179. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
  180. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
  181. package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
  182. package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
  183. package/dist/helper/deprecation.d.ts +0 -14
  184. package/dist/helper/deprecation.js +0 -24
  185. package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
  186. package/dist/modal/ModalFooterTestWrapper.svelte.d.ts +0 -8
@@ -4,7 +4,35 @@
4
4
  import type { ClassValue } from 'tailwind-variants';
5
5
 
6
6
  /**
7
- * Pagination component props
7
+ * Props for `<Pagination>` page navigation controls. Used standalone
8
+ * for client-driven lists, or automatically inside `<Table>` when
9
+ * `pagination` is on.
10
+ *
11
+ * `currentPage` is a regular (non-bindable) prop — pass it in and
12
+ * update via `onpagechange`, or set `internalState={true}` and omit
13
+ * `currentPage` to let the component track pages itself. Most apps
14
+ * prefer external state so the URL or a store can drive it.
15
+ *
16
+ * @example
17
+ * ```svelte
18
+ * <Pagination
19
+ * currentPage={page}
20
+ * totalItems={42}
21
+ * pageSize={10}
22
+ * onpagechange={(p) => goto(`?page=${p}`)}
23
+ * />
24
+ * ```
25
+ *
26
+ * @example
27
+ * ```svelte
28
+ * <!-- Compact "Page 3 of 7" mode for tight footers -->
29
+ * <Pagination
30
+ * {totalItems}
31
+ * pageSize={20}
32
+ * template="compact"
33
+ * showFirstLast={false}
34
+ * />
35
+ * ```
8
36
  */
9
37
  export interface PaginationProps {
10
38
  /** Current page number (1-indexed). If not provided, component manages state internally. */
@@ -14,13 +42,13 @@
14
42
  /** Number of items per page */
15
43
  pageSize?: number;
16
44
  /** Callback when page changes */
17
- onPageChange?: (page: number) => void;
45
+ onpagechange?: (page: number) => void;
18
46
  /** Callback when page size changes */
19
- onPageSizeChange?: (pageSize: number) => void;
47
+ onpagesizechange?: (pageSize: number) => void;
20
48
  /** Callback when first page is clicked */
21
- onFirstPage?: () => void;
49
+ onfirstpage?: () => void;
22
50
  /** Callback when last page is clicked */
23
- onLastPage?: () => void;
51
+ onlastpage?: () => void;
24
52
  /** Whether pagination controls are disabled */
25
53
  disabled?: boolean;
26
54
  /** Show page size selector */
@@ -62,10 +90,10 @@
62
90
  currentPage: externalCurrentPage,
63
91
  totalItems,
64
92
  pageSize: externalPageSize,
65
- onPageChange,
66
- onPageSizeChange,
67
- onFirstPage,
68
- onLastPage,
93
+ onpagechange,
94
+ onpagesizechange,
95
+ onfirstpage,
96
+ onlastpage,
69
97
  disabled = false,
70
98
  showPageSize = false,
71
99
  pageSizeOptions = [5, 10, 25, 50, 100],
@@ -140,7 +168,7 @@
140
168
  if (internalState && externalCurrentPage === undefined) {
141
169
  internalCurrentPage = page;
142
170
  }
143
- onPageChange?.(page);
171
+ onpagechange?.(page);
144
172
  }
145
173
  }
146
174
 
@@ -150,8 +178,8 @@
150
178
  if (internalState && externalCurrentPage === undefined) {
151
179
  internalCurrentPage = targetPage;
152
180
  }
153
- onPageChange?.(targetPage);
154
- onFirstPage?.();
181
+ onpagechange?.(targetPage);
182
+ onfirstpage?.();
155
183
  }
156
184
  }
157
185
 
@@ -161,8 +189,8 @@
161
189
  if (internalState && externalCurrentPage === undefined) {
162
190
  internalCurrentPage = targetPage;
163
191
  }
164
- onPageChange?.(targetPage);
165
- onLastPage?.();
192
+ onpagechange?.(targetPage);
193
+ onlastpage?.();
166
194
  }
167
195
  }
168
196
 
@@ -172,7 +200,7 @@
172
200
  if (internalState && externalCurrentPage === undefined) {
173
201
  internalCurrentPage = targetPage;
174
202
  }
175
- onPageChange?.(targetPage);
203
+ onpagechange?.(targetPage);
176
204
  }
177
205
  }
178
206
 
@@ -182,7 +210,7 @@
182
210
  if (internalState && externalCurrentPage === undefined) {
183
211
  internalCurrentPage = targetPage;
184
212
  }
185
- onPageChange?.(targetPage);
213
+ onpagechange?.(targetPage);
186
214
  }
187
215
  }
188
216
 
@@ -203,14 +231,14 @@
203
231
  if (internalState && externalCurrentPage === undefined) {
204
232
  internalCurrentPage = adjustedPage;
205
233
  }
206
- onPageChange?.(adjustedPage);
234
+ onpagechange?.(adjustedPage);
207
235
  }
208
236
 
209
237
  // Update page size
210
238
  if (internalState && externalPageSize === undefined) {
211
239
  internalPageSize = newPageSize;
212
240
  }
213
- onPageSizeChange?.(newPageSize);
241
+ onpagesizechange?.(newPageSize);
214
242
  }
215
243
 
216
244
  /**
@@ -246,8 +274,10 @@
246
274
  const shouldShowPagination = $derived(!hideWhenNoItems || validTotalItems > 0);
247
275
  const shouldShowControls = $derived(!hideWhenSinglePage || totalPages > 1);
248
276
 
249
- // Default classes using ripple-ui design system
250
- const defaultWrapperClass = 'flex items-center justify-between p-4';
277
+ // Default classes using ripple-ui design system.
278
+ // `flex-wrap` + `gap-y-3` stacks the info row and button row on narrow
279
+ // viewports instead of squeezing them onto one line.
280
+ const defaultWrapperClass = 'flex flex-wrap items-center justify-between gap-y-3 p-4';
251
281
  const defaultInfoClass = 'text-default-500 text-sm';
252
282
  const defaultButtonClass =
253
283
  'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium';
@@ -293,7 +323,9 @@
293
323
  {/if}
294
324
 
295
325
  {#if shouldShowControls && showNavigation}
296
- <div class="flex items-center gap-1">
326
+ <!-- `flex-wrap` lets page-number buttons wrap to a second row
327
+ rather than overflowing the container on narrow viewports. -->
328
+ <div class="flex flex-wrap items-center gap-1">
297
329
  {#if showFirstLast}
298
330
  <button
299
331
  onclick={firstPage}
@@ -1,6 +1,34 @@
1
1
  import type { ClassValue } from 'tailwind-variants';
2
2
  /**
3
- * Pagination component props
3
+ * Props for `<Pagination>` page navigation controls. Used standalone
4
+ * for client-driven lists, or automatically inside `<Table>` when
5
+ * `pagination` is on.
6
+ *
7
+ * `currentPage` is a regular (non-bindable) prop — pass it in and
8
+ * update via `onpagechange`, or set `internalState={true}` and omit
9
+ * `currentPage` to let the component track pages itself. Most apps
10
+ * prefer external state so the URL or a store can drive it.
11
+ *
12
+ * @example
13
+ * ```svelte
14
+ * <Pagination
15
+ * currentPage={page}
16
+ * totalItems={42}
17
+ * pageSize={10}
18
+ * onpagechange={(p) => goto(`?page=${p}`)}
19
+ * />
20
+ * ```
21
+ *
22
+ * @example
23
+ * ```svelte
24
+ * <!-- Compact "Page 3 of 7" mode for tight footers -->
25
+ * <Pagination
26
+ * {totalItems}
27
+ * pageSize={20}
28
+ * template="compact"
29
+ * showFirstLast={false}
30
+ * />
31
+ * ```
4
32
  */
5
33
  export interface PaginationProps {
6
34
  /** Current page number (1-indexed). If not provided, component manages state internally. */
@@ -10,13 +38,13 @@ export interface PaginationProps {
10
38
  /** Number of items per page */
11
39
  pageSize?: number;
12
40
  /** Callback when page changes */
13
- onPageChange?: (page: number) => void;
41
+ onpagechange?: (page: number) => void;
14
42
  /** Callback when page size changes */
15
- onPageSizeChange?: (pageSize: number) => void;
43
+ onpagesizechange?: (pageSize: number) => void;
16
44
  /** Callback when first page is clicked */
17
- onFirstPage?: () => void;
45
+ onfirstpage?: () => void;
18
46
  /** Callback when last page is clicked */
19
- onLastPage?: () => void;
47
+ onlastpage?: () => void;
20
48
  /** Whether pagination controls are disabled */
21
49
  disabled?: boolean;
22
50
  /** Show page size selector */
@@ -0,0 +1,254 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+ import { buildTestId } from '../../helper/testid.js';
4
+ import type { PopoverProps, PopoverPlacement } from './popover-types.js';
5
+
6
+ let {
7
+ open = $bindable(false),
8
+ placement = 'bottom',
9
+ trigger = 'click',
10
+ arrow = false,
11
+ delay = 100,
12
+ hideDelay = 0,
13
+ closeOnEscape = true,
14
+ closeOnOutsideClick = true,
15
+ disabled = false,
16
+ class: className = '',
17
+ panelClass = '',
18
+ children,
19
+ content,
20
+ testId
21
+ }: PopoverProps = $props();
22
+
23
+ let wrapper = $state<HTMLSpanElement | undefined>();
24
+ let panelEl = $state<HTMLDivElement | undefined>();
25
+ let showTimer: ReturnType<typeof setTimeout> | undefined;
26
+ let hideTimer: ReturnType<typeof setTimeout> | undefined;
27
+
28
+ // Panel position in viewport coordinates — updated on open and on
29
+ // scroll/resize so the panel tracks the trigger. Kept reactive so the
30
+ // style attribute re-renders when these change.
31
+ let panelTop = $state(0);
32
+ let panelLeft = $state(0);
33
+ let panelTransform = $state('');
34
+
35
+ function show() {
36
+ if (disabled) return;
37
+ clearTimeout(showTimer);
38
+ clearTimeout(hideTimer);
39
+ if (delay > 0 && trigger === 'hover') {
40
+ showTimer = setTimeout(() => (open = true), delay);
41
+ } else {
42
+ open = true;
43
+ }
44
+ }
45
+
46
+ function hide() {
47
+ clearTimeout(showTimer);
48
+ clearTimeout(hideTimer);
49
+ if (hideDelay > 0 && trigger === 'hover') {
50
+ hideTimer = setTimeout(() => (open = false), hideDelay);
51
+ } else {
52
+ open = false;
53
+ }
54
+ }
55
+
56
+ function toggle() {
57
+ if (disabled) return;
58
+ if (open) hide();
59
+ else show();
60
+ }
61
+
62
+ function close() {
63
+ clearTimeout(showTimer);
64
+ clearTimeout(hideTimer);
65
+ open = false;
66
+ }
67
+
68
+ // Clean up any in-flight timers on unmount.
69
+ $effect(() => {
70
+ return () => {
71
+ clearTimeout(showTimer);
72
+ clearTimeout(hideTimer);
73
+ };
74
+ });
75
+
76
+ function handleWindowKey(e: KeyboardEvent) {
77
+ if (closeOnEscape && open && e.key === 'Escape') close();
78
+ }
79
+
80
+ function handleWindowClick(e: MouseEvent) {
81
+ if (!closeOnOutsideClick || !open) return;
82
+ const target = e.target as Node;
83
+ // The panel lives outside the trigger wrapper in the DOM, so check both.
84
+ if (wrapper?.contains(target) || panelEl?.contains(target)) return;
85
+ close();
86
+ }
87
+
88
+ /**
89
+ * Compute panel position from the trigger's bounding rect. Uses
90
+ * `position: fixed` + a very high z-index so the panel sits above
91
+ * sibling content (Storybook sidebars, sticky headers, etc.) instead
92
+ * of being clipped / layered under it.
93
+ *
94
+ * The computed position is then clamped to the viewport with an 8px
95
+ * gutter — this keeps panels on-screen near viewport edges on narrow
96
+ * devices. We measure the panel's own rect so the clamp accounts for
97
+ * the `translate(-50%, 0)` etc. transforms that re-anchor the panel.
98
+ */
99
+ function updatePosition() {
100
+ if (!wrapper || !panelEl) return;
101
+ const r = wrapper.getBoundingClientRect();
102
+ const GAP = 8;
103
+ const VIEWPORT_GUTTER = 8;
104
+ switch (placement as PopoverPlacement) {
105
+ case 'top':
106
+ panelTop = r.top - GAP;
107
+ panelLeft = r.left + r.width / 2;
108
+ panelTransform = 'translate(-50%, -100%)';
109
+ break;
110
+ case 'bottom':
111
+ panelTop = r.bottom + GAP;
112
+ panelLeft = r.left + r.width / 2;
113
+ panelTransform = 'translate(-50%, 0)';
114
+ break;
115
+ case 'left':
116
+ panelTop = r.top + r.height / 2;
117
+ panelLeft = r.left - GAP;
118
+ panelTransform = 'translate(-100%, -50%)';
119
+ break;
120
+ case 'right':
121
+ panelTop = r.top + r.height / 2;
122
+ panelLeft = r.right + GAP;
123
+ panelTransform = 'translate(0, -50%)';
124
+ break;
125
+ }
126
+
127
+ // Clamp to viewport. We re-measure the panel after applying the
128
+ // transform (next frame) because its true on-screen rect depends
129
+ // on `panelTransform`. Adjust `panelLeft`/`panelTop` so the rect
130
+ // ends up inside [VIEWPORT_GUTTER, viewport - VIEWPORT_GUTTER].
131
+ requestAnimationFrame(() => {
132
+ if (!panelEl) return;
133
+ const pr = panelEl.getBoundingClientRect();
134
+ const vw = window.innerWidth;
135
+ const vh = window.innerHeight;
136
+ let dx = 0;
137
+ let dy = 0;
138
+ if (pr.left < VIEWPORT_GUTTER) dx = VIEWPORT_GUTTER - pr.left;
139
+ else if (pr.right > vw - VIEWPORT_GUTTER) dx = vw - VIEWPORT_GUTTER - pr.right;
140
+ if (pr.top < VIEWPORT_GUTTER) dy = VIEWPORT_GUTTER - pr.top;
141
+ else if (pr.bottom > vh - VIEWPORT_GUTTER) dy = vh - VIEWPORT_GUTTER - pr.bottom;
142
+ if (dx !== 0) panelLeft += dx;
143
+ if (dy !== 0) panelTop += dy;
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Re-measure on every open, and while open, on scroll/resize so the
149
+ * panel follows its trigger when the page scrolls.
150
+ */
151
+ $effect(() => {
152
+ if (!open) return;
153
+ // Initial placement (defer one frame so panelEl has mounted).
154
+ requestAnimationFrame(updatePosition);
155
+
156
+ const handler = () => updatePosition();
157
+ window.addEventListener('scroll', handler, true);
158
+ window.addEventListener('resize', handler);
159
+ return () => {
160
+ window.removeEventListener('scroll', handler, true);
161
+ window.removeEventListener('resize', handler);
162
+ };
163
+ });
164
+
165
+ /**
166
+ * Portal the panel element to `document.body` once it mounts, so any
167
+ * ancestor with `transform` / `filter` / `will-change` (Storybook's
168
+ * docs container, a scaled preview, a card with transform-based
169
+ * hover, etc.) doesn't re-parent our `position: fixed` panel and
170
+ * clip / mis-position it. On unmount, we simply remove the node —
171
+ * Svelte's own `{#if}` teardown will still work because it tracks
172
+ * the element by reference, not by DOM path.
173
+ */
174
+ $effect(() => {
175
+ if (!panelEl) return;
176
+ const el = panelEl;
177
+ document.body.appendChild(el);
178
+ return () => {
179
+ if (el.parentNode === document.body) {
180
+ document.body.removeChild(el);
181
+ }
182
+ };
183
+ });
184
+
185
+ const arrowClass = $derived(
186
+ {
187
+ top: 'top-full left-1/2 -translate-x-1/2 border-t-white border-l-transparent border-r-transparent border-b-transparent',
188
+ bottom:
189
+ 'bottom-full left-1/2 -translate-x-1/2 border-b-white border-l-transparent border-r-transparent border-t-transparent',
190
+ left: 'left-full top-1/2 -translate-y-1/2 border-l-white border-t-transparent border-b-transparent border-r-transparent',
191
+ right:
192
+ 'right-full top-1/2 -translate-y-1/2 border-r-white border-t-transparent border-b-transparent border-l-transparent'
193
+ }[placement]
194
+ );
195
+ </script>
196
+
197
+ <svelte:window onkeydown={handleWindowKey} onmousedown={handleWindowClick} />
198
+
199
+ <span
200
+ bind:this={wrapper}
201
+ class={cn('relative inline-flex', className)}
202
+ data-testid={buildTestId('popover', undefined, testId)}
203
+ >
204
+ {#if trigger === 'click'}
205
+ <!--
206
+ Click-mode: forward clicks to toggle. Do NOT add role/tabindex here
207
+ — consumers pass an interactive child (Button, <a>, etc.) that
208
+ handles its own focus/keyboard; adding them here would nest
209
+ interactive elements (invalid HTML + a11y issue). Keyboard support
210
+ comes via Enter/Space on the inner button dispatching a click,
211
+ which bubbles to this handler.
212
+ -->
213
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
214
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
215
+ <span class="inline-flex" aria-haspopup="dialog" aria-expanded={open} onclick={toggle}>
216
+ {@render children()}
217
+ </span>
218
+ {:else if trigger === 'hover'}
219
+ <span
220
+ class="inline-flex"
221
+ role="group"
222
+ onmouseenter={show}
223
+ onmouseleave={hide}
224
+ onfocusin={show}
225
+ onfocusout={hide}
226
+ >
227
+ {@render children()}
228
+ </span>
229
+ {:else}
230
+ <!-- manual — consumer drives `open` -->
231
+ <span class="inline-flex">{@render children()}</span>
232
+ {/if}
233
+ </span>
234
+
235
+ {#if open}
236
+ <div
237
+ bind:this={panelEl}
238
+ role="dialog"
239
+ tabindex="-1"
240
+ class={cn(
241
+ 'border-default-200 fixed z-[9999] min-w-[10rem] rounded-lg border bg-white p-2 shadow-lg',
242
+ panelClass
243
+ )}
244
+ style="top: {panelTop}px; left: {panelLeft}px; transform: {panelTransform};"
245
+ onmouseenter={trigger === 'hover' && hideDelay > 0 ? () => clearTimeout(hideTimer) : undefined}
246
+ onmouseleave={trigger === 'hover' ? hide : undefined}
247
+ data-testid={buildTestId('popover', 'panel', testId)}
248
+ >
249
+ {#if arrow}
250
+ <span class={cn('absolute size-0 border-[5px]', arrowClass)} aria-hidden="true"></span>
251
+ {/if}
252
+ {@render content({ close })}
253
+ </div>
254
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { PopoverProps } from './popover-types.js';
2
+ declare const Popover: import("svelte").Component<PopoverProps, {}, "open">;
3
+ type Popover = ReturnType<typeof Popover>;
4
+ export default Popover;
@@ -0,0 +1,2 @@
1
+ export { default as Popover } from './Popover.svelte';
2
+ export type { PopoverProps, PopoverPlacement, PopoverTrigger } from './popover-types.js';
@@ -0,0 +1 @@
1
+ export { default as Popover } from './Popover.svelte';
@@ -0,0 +1,60 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { Snippet } from 'svelte';
3
+ /**
4
+ * Where the Popover panel appears relative to its trigger. Popover always
5
+ * centers along the cross-axis of the placement.
6
+ */
7
+ export type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right';
8
+ /**
9
+ * How the Popover opens:
10
+ * - `'click'` — clicking the trigger toggles open/close (default, most common)
11
+ * - `'hover'` — opens on mouseenter/focusin. Use `hideDelay` to let users
12
+ * cross from trigger into panel without it closing.
13
+ * - `'manual'` — you drive `open` yourself via `bind:open`. Trigger
14
+ * children are just visual (no event handlers attached by Popover).
15
+ */
16
+ export type PopoverTrigger = 'click' | 'hover' | 'manual';
17
+ export type PopoverProps = {
18
+ /** Bindable open state. Defaults to false. */
19
+ open?: boolean;
20
+ /** Placement relative to the trigger. @default 'bottom' */
21
+ placement?: PopoverPlacement;
22
+ /** How the trigger opens the panel. @default 'click' */
23
+ trigger?: PopoverTrigger;
24
+ /** Show an arrow pointing at the trigger. @default false */
25
+ arrow?: boolean;
26
+ /**
27
+ * When `trigger='hover'`, delay before showing (ms). Ignored for
28
+ * click/manual. @default 100
29
+ */
30
+ delay?: number;
31
+ /**
32
+ * When `trigger='hover'`, delay before hiding after mouseleave (ms).
33
+ * Non-zero lets users move into the panel. Ignored for click/manual.
34
+ * @default 0
35
+ */
36
+ hideDelay?: number;
37
+ /** Close when the Escape key is pressed while panel is open. @default true */
38
+ closeOnEscape?: boolean;
39
+ /** Close when user clicks outside the panel and trigger. @default true */
40
+ closeOnOutsideClick?: boolean;
41
+ /** Disable the trigger and prevent opening. */
42
+ disabled?: boolean;
43
+ /** Wrapper class (around trigger). */
44
+ class?: ClassValue;
45
+ /** Panel class (the floating element). */
46
+ panelClass?: ClassValue;
47
+ /**
48
+ * The trigger element. Rendered inline — the Popover wraps it in an
49
+ * inline-flex container so it sits where you put the <Popover>.
50
+ */
51
+ children: Snippet;
52
+ /**
53
+ * The panel content. Receives `close()` so you can wire buttons inside
54
+ * the panel that dismiss it.
55
+ */
56
+ content: Snippet<[{
57
+ close: () => void;
58
+ }]>;
59
+ testId?: string;
60
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
3
  import { Color, Size } from '../../variants.js';
4
+ import { Tween } from 'svelte/motion';
5
+ import { quintOut } from 'svelte/easing';
4
6
  import type { ProgressProps, VariantColors } from '../../index.js';
5
7
 
6
8
  let {
7
9
  value,
8
10
  max = 100,
9
- size = Size.BASE,
11
+ size = Size.MD,
10
12
  color = Color.PRIMARY,
11
13
  showLabel = true,
12
14
  labelPosition = 'right',
@@ -38,12 +40,12 @@
38
40
 
39
41
  function getSizeTextClass(size: string): string {
40
42
  if (size === Size.XS || size === Size.SM) return 'text-xs';
41
- if (size === Size.BASE) return 'text-sm';
43
+ if (size === Size.MD) return 'text-sm';
42
44
  return 'text-base'; // For Size.LG, Size.XL
43
45
  }
44
46
 
45
47
  // Derived values
46
- const percentage = $derived(segments ? 100 : calculatePercentage(value, max));
48
+ const percentage = $derived(segments ? 100 : calculatePercentage(value ?? 0, max));
47
49
  const segmentPercentages = $derived(
48
50
  segments?.map((segment) => ({
49
51
  ...segment,
@@ -51,6 +53,29 @@
51
53
  })) || []
52
54
  );
53
55
 
56
+ // Animate from 0 → target on mount (and on subsequent value changes)
57
+ // using a tween with quintOut easing for a smooth, non-jittery feel.
58
+ const tween = new Tween(0, { duration: 800, easing: quintOut });
59
+
60
+ // Derive tween instances from the current segment count so that
61
+ // dynamically-added/removed segments get their own tweens instead of
62
+ // being stuck at 0. The $derived runs whenever `segments` identity
63
+ // changes — consumers should pass a stable array unless they truly
64
+ // want a rebuild.
65
+ const segmentTweens = $derived(
66
+ (segments ?? []).map(() => new Tween(0, { duration: 800, easing: quintOut }))
67
+ );
68
+
69
+ $effect(() => {
70
+ tween.target = percentage;
71
+ });
72
+
73
+ $effect(() => {
74
+ segmentPercentages.forEach((s, i) => {
75
+ if (segmentTweens[i]) segmentTweens[i].target = s.percentage;
76
+ });
77
+ });
78
+
54
79
  // Class compositions
55
80
  const containerClass = $derived(
56
81
  cn(
@@ -68,7 +93,7 @@
68
93
  cn('w-full rounded-full bg-default-200', {
69
94
  'h-1.5': size === Size.XS,
70
95
  'h-2': size === Size.SM,
71
- 'h-2.5': size === Size.BASE,
96
+ 'h-2.5': size === Size.MD,
72
97
  'h-3': size === Size.LG,
73
98
  'h-4': size === Size.XL,
74
99
  'order-2': !segments && labelPosition === 'top',
@@ -78,7 +103,7 @@
78
103
  );
79
104
 
80
105
  const fillClass = $derived(
81
- cn('h-full rounded-full transition-all', segments ? '' : getColorClass(color), barClass)
106
+ cn('h-full rounded-full', segments ? '' : getColorClass(color), barClass)
82
107
  );
83
108
 
84
109
  const labelTextClass = $derived(
@@ -108,13 +133,13 @@
108
133
  {#if segment.percentage > 0}
109
134
  <div
110
135
  class={cn(getColorClass(segment.color), barClass)}
111
- style="width: {segment.percentage}%"
136
+ style="width: {segmentTweens[index]?.current ?? 0}%"
112
137
  title={segment.label || `${segment.value} (${segment.percentage}%)`}
113
138
  ></div>
114
139
  {/if}
115
140
  {/each}
116
141
  {:else}
117
- <div class={fillClass} style="width: {percentage}%"></div>
142
+ <div class={fillClass} style="width: {tween.current}%"></div>
118
143
  {/if}
119
144
  </div>
120
145