@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.
- package/README.md +403 -497
- package/dist/adapters/storage/S3Adapter.d.ts +49 -1
- package/dist/adapters/storage/S3Adapter.js +38 -1
- package/dist/adapters/storage/types.d.ts +20 -0
- package/dist/ai/AIChatInterface.svelte +2 -1
- package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
- package/dist/ai/CodeRenderer.svelte +7 -2
- package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
- package/dist/ai/ComposeDropdown.svelte +1 -1
- package/dist/ai/MessageBox.svelte +3 -3
- package/dist/ai/MessageBox.svelte.d.ts +3 -2
- package/dist/ai/ThinkingDisplay.svelte +4 -3
- package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
- package/dist/ai/ai-types.d.ts +55 -1
- package/dist/button/Button.svelte +5 -5
- package/dist/button/button-types.d.ts +49 -4
- package/dist/button/button.d.ts +9 -9
- package/dist/button/button.js +6 -6
- package/dist/charts/Chart.svelte +8 -16
- package/dist/charts/chart-types.d.ts +78 -1
- package/dist/drawer/Drawer.svelte +6 -26
- package/dist/drawer/drawer-types.d.ts +33 -12
- package/dist/drawer/drawer.d.ts +3 -3
- package/dist/drawer/drawer.js +1 -1
- package/dist/elements/accordion/Accordion.svelte +6 -17
- package/dist/elements/accordion/accordion-types.d.ts +53 -6
- package/dist/elements/alert/Alert.svelte +3 -0
- package/dist/elements/badge/Badge.svelte +1 -1
- package/dist/elements/badge/badge-types.d.ts +22 -0
- package/dist/elements/badge/badge.d.ts +3 -3
- package/dist/elements/badge/badge.js +1 -1
- package/dist/elements/combobox/ComboBox.svelte +244 -0
- package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
- package/dist/elements/combobox/combobox-types.d.ts +41 -0
- package/dist/elements/combobox/combobox-types.js +1 -0
- package/dist/elements/context-menu/ContextMenu.svelte +137 -0
- package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
- package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
- package/dist/elements/context-menu/context-menu-types.js +1 -0
- package/dist/elements/dropdown/Dropdown.svelte +1 -1
- package/dist/elements/dropdown/Select.svelte +4 -1
- package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
- package/dist/elements/dropdown/dropdown.d.ts +3 -3
- package/dist/elements/dropdown/dropdown.js +2 -2
- package/dist/elements/dropdown/select.d.ts +3 -108
- package/dist/elements/dropdown/select.js +38 -47
- package/dist/elements/empty-state/EmptyState.svelte +1 -1
- package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
- package/dist/elements/empty-state/empty-state.d.ts +3 -3
- package/dist/elements/empty-state/empty-state.js +2 -2
- package/dist/elements/file-upload/FileUpload.svelte +5 -0
- package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
- package/dist/elements/pagination/Pagination.svelte +53 -21
- package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
- package/dist/elements/popover/Popover.svelte +254 -0
- package/dist/elements/popover/Popover.svelte.d.ts +4 -0
- package/dist/elements/popover/index.d.ts +2 -0
- package/dist/elements/popover/index.js +1 -0
- package/dist/elements/popover/popover-types.d.ts +60 -0
- package/dist/elements/popover/popover-types.js +1 -0
- package/dist/elements/progress/Progress.svelte +32 -7
- package/dist/elements/progress/progress-types.d.ts +48 -1
- package/dist/elements/skeleton/Skeleton.svelte +56 -0
- package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
- package/dist/elements/skeleton/index.d.ts +2 -0
- package/dist/elements/skeleton/index.js +1 -0
- package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
- package/dist/elements/skeleton/skeleton-types.js +1 -0
- package/dist/elements/spinner/Spinner.svelte +1 -1
- package/dist/elements/spinner/spinner-types.d.ts +20 -0
- package/dist/elements/spinner/spinner.d.ts +3 -3
- package/dist/elements/spinner/spinner.js +2 -2
- package/dist/elements/tooltip/Tooltip.svelte +108 -11
- package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
- package/dist/file-browser/FileBrowser.svelte +21 -12
- package/dist/filters/CompactFilters.svelte +221 -33
- package/dist/filters/CompactFilters.svelte.d.ts +1 -1
- package/dist/filters/FilterBar.svelte +184 -0
- package/dist/filters/FilterBar.svelte.d.ts +4 -0
- package/dist/filters/FilterPopover.svelte +346 -0
- package/dist/filters/FilterPopover.svelte.d.ts +4 -0
- package/dist/filters/date-presets.d.ts +15 -0
- package/dist/filters/date-presets.js +107 -0
- package/dist/filters/filter-types.d.ts +69 -3
- package/dist/filters/index.d.ts +5 -0
- package/dist/filters/index.js +4 -0
- package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
- package/dist/filters/sync-filters-to-url.svelte.js +114 -0
- package/dist/forms/Checkbox.svelte +24 -9
- package/dist/forms/DateRange.svelte +23 -6
- package/dist/forms/Input.svelte +19 -19
- package/dist/forms/MarketSelector.svelte +9 -4
- package/dist/forms/NumberInput.svelte +14 -18
- package/dist/forms/RadioGroup.svelte +127 -0
- package/dist/forms/RadioGroup.svelte.d.ts +4 -0
- package/dist/forms/SegmentedControl.svelte +11 -4
- package/dist/forms/Slider.svelte +72 -3
- package/dist/forms/Tags.svelte +44 -14
- package/dist/forms/Textarea.svelte +121 -0
- package/dist/forms/Textarea.svelte.d.ts +4 -0
- package/dist/forms/Toggle.svelte +30 -22
- package/dist/forms/calendar/Calendar.svelte +315 -0
- package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/forms/calendar/calendar-types.d.ts +54 -0
- package/dist/forms/calendar/calendar-types.js +1 -0
- package/dist/forms/calendar/index.d.ts +2 -0
- package/dist/forms/calendar/index.js +1 -0
- package/dist/forms/date-picker/DatePicker.svelte +141 -0
- package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
- package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
- package/dist/forms/date-picker/date-picker-types.js +1 -0
- package/dist/forms/form-size.d.ts +37 -0
- package/dist/forms/form-size.js +67 -0
- package/dist/forms/form-types.d.ts +430 -6
- package/dist/forms/market/market-selector-types.d.ts +52 -1
- package/dist/forms/segmented-control.d.ts +5 -2
- package/dist/forms/segmented-control.js +25 -13
- package/dist/forms/slider.d.ts +3 -3
- package/dist/forms/slider.js +37 -30
- package/dist/funcs/user-management.remote.js +1 -1
- package/dist/header/Breadcrumbs.svelte +4 -20
- package/dist/header/PageHeader.svelte +6 -14
- package/dist/header/breadcrumbs.d.ts +3 -11
- package/dist/header/breadcrumbs.js +10 -5
- package/dist/header/header-types.d.ts +62 -11
- package/dist/index.d.ts +35 -9
- package/dist/index.js +24 -4
- package/dist/layout/activity-list/ActivityList.svelte +13 -7
- package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
- package/dist/layout/card/Card.svelte +12 -15
- package/dist/layout/card/MetricCard.svelte +50 -32
- package/dist/layout/card/card-types.d.ts +114 -4
- package/dist/layout/navbar/navbar-types.d.ts +48 -0
- package/dist/layout/navbar/navbar.d.ts +3 -3
- package/dist/layout/navbar/navbar.js +2 -2
- package/dist/layout/sidebar/Sidebar.svelte +87 -11
- package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
- package/dist/layout/stepper/Stepper.svelte +288 -0
- package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
- package/dist/layout/stepper/stepper-types.d.ts +80 -0
- package/dist/layout/stepper/stepper-types.js +1 -0
- package/dist/layout/table/Table.svelte +91 -85
- package/dist/layout/table/table-types.d.ts +148 -24
- package/dist/layout/table/table.d.ts +3 -3
- package/dist/layout/table/table.js +2 -2
- package/dist/layout/tabs/Tab.svelte +6 -2
- package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
- package/dist/layout/tabs/TabGroup.svelte +9 -2
- package/dist/layout/tabs/tabs-types.d.ts +63 -0
- package/dist/layout/tabs/tabs.d.ts +3 -3
- package/dist/layout/tabs/tabs.js +12 -6
- package/dist/modal/ConfirmDialog.svelte +65 -0
- package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
- package/dist/modal/Modal.svelte +6 -26
- package/dist/modal/confirm-dialog-types.d.ts +39 -0
- package/dist/modal/confirm-dialog-types.js +1 -0
- package/dist/modal/modal-types.d.ts +51 -12
- package/dist/modal/modal.d.ts +3 -3
- package/dist/modal/modal.js +3 -3
- package/dist/pipeline/Pipeline.svelte +8 -3
- package/dist/pipeline/pipeline-types.d.ts +55 -3
- package/dist/pipeline/pipeline.d.ts +18 -3
- package/dist/pipeline/pipeline.js +7 -2
- package/dist/server/s3.d.ts +35 -3
- package/dist/sonner/Toaster.svelte +29 -0
- package/dist/sonner/Toaster.svelte.d.ts +4 -0
- package/dist/sonner/index.d.ts +21 -0
- package/dist/sonner/index.js +20 -0
- package/dist/user-management/UserManagement.svelte +22 -16
- package/dist/user-management/UserModal.svelte +10 -7
- package/dist/user-management/UserTable.svelte +16 -17
- package/dist/user-management/UserViewModal.svelte +11 -11
- package/dist/user-management/user-management-types.d.ts +118 -31
- package/dist/variants.d.ts +1 -1
- package/dist/variants.js +1 -1
- package/package.json +7 -4
- package/dist/config/ai.d.ts +0 -13
- package/dist/config/ai.js +0 -44
- package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
- package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
- package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
- package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
- package/dist/helper/deprecation.d.ts +0 -14
- package/dist/helper/deprecation.js +0 -24
- package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
- 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
|
|
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
|
-
|
|
45
|
+
onpagechange?: (page: number) => void;
|
|
18
46
|
/** Callback when page size changes */
|
|
19
|
-
|
|
47
|
+
onpagesizechange?: (pageSize: number) => void;
|
|
20
48
|
/** Callback when first page is clicked */
|
|
21
|
-
|
|
49
|
+
onfirstpage?: () => void;
|
|
22
50
|
/** Callback when last page is clicked */
|
|
23
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
41
|
+
onpagechange?: (page: number) => void;
|
|
14
42
|
/** Callback when page size changes */
|
|
15
|
-
|
|
43
|
+
onpagesizechange?: (pageSize: number) => void;
|
|
16
44
|
/** Callback when first page is clicked */
|
|
17
|
-
|
|
45
|
+
onfirstpage?: () => void;
|
|
18
46
|
/** Callback when last page is clicked */
|
|
19
|
-
|
|
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 @@
|
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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: {
|
|
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: {
|
|
142
|
+
<div class={fillClass} style="width: {tween.current}%"></div>
|
|
118
143
|
{/if}
|
|
119
144
|
</div>
|
|
120
145
|
|