@makolabs/ripple 2.5.9 → 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.
- 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 +247 -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 -3
- package/dist/elements/dropdown/select.js +2 -2
- 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 +234 -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/DateRange.svelte +4 -2
- package/dist/forms/Input.svelte +2 -2
- package/dist/forms/MarketSelector.svelte +8 -3
- package/dist/forms/NumberInput.svelte +4 -4
- package/dist/forms/RadioGroup.svelte +123 -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 +14 -5
- package/dist/forms/Textarea.svelte +126 -0
- package/dist/forms/Textarea.svelte.d.ts +4 -0
- package/dist/forms/Toggle.svelte +8 -8
- package/dist/forms/calendar/Calendar.svelte +218 -0
- package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/forms/calendar/calendar-types.d.ts +46 -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 +144 -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-types.d.ts +425 -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 +16 -5
- package/dist/forms/slider.d.ts +3 -3
- package/dist/forms/slider.js +2 -2
- 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
|
@@ -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,234 @@
|
|
|
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
|
+
const arrowClass = $derived(
|
|
166
|
+
{
|
|
167
|
+
top: 'top-full left-1/2 -translate-x-1/2 border-t-white border-l-transparent border-r-transparent border-b-transparent',
|
|
168
|
+
bottom:
|
|
169
|
+
'bottom-full left-1/2 -translate-x-1/2 border-b-white border-l-transparent border-r-transparent border-t-transparent',
|
|
170
|
+
left: 'left-full top-1/2 -translate-y-1/2 border-l-white border-t-transparent border-b-transparent border-r-transparent',
|
|
171
|
+
right:
|
|
172
|
+
'right-full top-1/2 -translate-y-1/2 border-r-white border-t-transparent border-b-transparent border-l-transparent'
|
|
173
|
+
}[placement]
|
|
174
|
+
);
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<svelte:window onkeydown={handleWindowKey} onmousedown={handleWindowClick} />
|
|
178
|
+
|
|
179
|
+
<span
|
|
180
|
+
bind:this={wrapper}
|
|
181
|
+
class={cn('relative inline-flex', className)}
|
|
182
|
+
data-testid={buildTestId('popover', undefined, testId)}
|
|
183
|
+
>
|
|
184
|
+
{#if trigger === 'click'}
|
|
185
|
+
<!--
|
|
186
|
+
Click-mode: forward clicks to toggle. Do NOT add role/tabindex here
|
|
187
|
+
— consumers pass an interactive child (Button, <a>, etc.) that
|
|
188
|
+
handles its own focus/keyboard; adding them here would nest
|
|
189
|
+
interactive elements (invalid HTML + a11y issue). Keyboard support
|
|
190
|
+
comes via Enter/Space on the inner button dispatching a click,
|
|
191
|
+
which bubbles to this handler.
|
|
192
|
+
-->
|
|
193
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
194
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
195
|
+
<span class="inline-flex" aria-haspopup="dialog" aria-expanded={open} onclick={toggle}>
|
|
196
|
+
{@render children()}
|
|
197
|
+
</span>
|
|
198
|
+
{:else if trigger === 'hover'}
|
|
199
|
+
<span
|
|
200
|
+
class="inline-flex"
|
|
201
|
+
role="group"
|
|
202
|
+
onmouseenter={show}
|
|
203
|
+
onmouseleave={hide}
|
|
204
|
+
onfocusin={show}
|
|
205
|
+
onfocusout={hide}
|
|
206
|
+
>
|
|
207
|
+
{@render children()}
|
|
208
|
+
</span>
|
|
209
|
+
{:else}
|
|
210
|
+
<!-- manual — consumer drives `open` -->
|
|
211
|
+
<span class="inline-flex">{@render children()}</span>
|
|
212
|
+
{/if}
|
|
213
|
+
</span>
|
|
214
|
+
|
|
215
|
+
{#if open}
|
|
216
|
+
<div
|
|
217
|
+
bind:this={panelEl}
|
|
218
|
+
role="dialog"
|
|
219
|
+
tabindex="-1"
|
|
220
|
+
class={cn(
|
|
221
|
+
'border-default-200 fixed z-[9999] min-w-[10rem] rounded-lg border bg-white p-2 shadow-lg',
|
|
222
|
+
panelClass
|
|
223
|
+
)}
|
|
224
|
+
style="top: {panelTop}px; left: {panelLeft}px; transform: {panelTransform};"
|
|
225
|
+
onmouseenter={trigger === 'hover' && hideDelay > 0 ? () => clearTimeout(hideTimer) : undefined}
|
|
226
|
+
onmouseleave={trigger === 'hover' ? hide : undefined}
|
|
227
|
+
data-testid={buildTestId('popover', 'panel', testId)}
|
|
228
|
+
>
|
|
229
|
+
{#if arrow}
|
|
230
|
+
<span class={cn('absolute size-0 border-[5px]', arrowClass)} aria-hidden="true"></span>
|
|
231
|
+
{/if}
|
|
232
|
+
{@render content({ close })}
|
|
233
|
+
</div>
|
|
234
|
+
{/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
|
|
|
@@ -1,22 +1,69 @@
|
|
|
1
1
|
import type { ClassValue } from 'tailwind-variants';
|
|
2
2
|
import type { VariantColors, VariantSizes } from '../../index.js';
|
|
3
|
+
/** One segment of a multi-segment progress bar (`ProgressProps.segments`). */
|
|
3
4
|
export type ProgressSegment = {
|
|
5
|
+
/** Segment size. Treated as a share of `max` (so segment values should sum to ≤ `max`). */
|
|
4
6
|
value: number;
|
|
7
|
+
/** Segment fill color. */
|
|
5
8
|
color: VariantColors;
|
|
9
|
+
/** Optional label rendered inside the legend when `showLabels` is set. */
|
|
6
10
|
label?: string;
|
|
7
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Props for `<Progress>` — a horizontal progress bar. Supports two modes:
|
|
14
|
+
* 1. **Single value** — pass `value` (and optionally `max`). Renders one filled bar.
|
|
15
|
+
* 2. **Segmented** — pass `segments: ProgressSegment[]`. Renders a stacked bar with
|
|
16
|
+
* per-segment colors and an optional legend below.
|
|
17
|
+
*
|
|
18
|
+
* For KPI-style displays where the progress bar is part of a card,
|
|
19
|
+
* `<MetricCard>` accepts the same `percent`/`segments` props directly.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <!-- Simple -->
|
|
24
|
+
* <Progress value={68} showLabel />
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```svelte
|
|
29
|
+
* <!-- Multi-segment with legend -->
|
|
30
|
+
* <Progress
|
|
31
|
+
* max={100}
|
|
32
|
+
* segments={[
|
|
33
|
+
* { value: 50, color: 'success', label: 'Healthy' },
|
|
34
|
+
* { value: 30, color: 'warning', label: 'Warning' },
|
|
35
|
+
* { value: 20, color: 'danger', label: 'Critical' }
|
|
36
|
+
* ]}
|
|
37
|
+
* showLabels
|
|
38
|
+
* showValues
|
|
39
|
+
* />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
8
42
|
export type ProgressProps = {
|
|
9
|
-
value
|
|
43
|
+
/** Current value (single-mode). Ignored when `segments` is provided. @default 0 */
|
|
44
|
+
value?: number;
|
|
45
|
+
/** Max scale value. @default 100 */
|
|
10
46
|
max?: number;
|
|
11
47
|
size?: VariantSizes;
|
|
48
|
+
/** Fill color for single-mode. @default 'primary' */
|
|
12
49
|
color?: VariantColors;
|
|
50
|
+
/** Show "value / max" label next to the bar (single-mode). @default false */
|
|
13
51
|
showLabel?: boolean;
|
|
52
|
+
/** Where to render the label. @default 'top' */
|
|
14
53
|
labelPosition?: 'top' | 'bottom' | 'right';
|
|
54
|
+
/**
|
|
55
|
+
* Segmented bars — when provided, takes precedence over `value`.
|
|
56
|
+
* Each segment renders in its own color, and an optional legend
|
|
57
|
+
* appears below when `showLabels` or `showValues` is set.
|
|
58
|
+
*/
|
|
15
59
|
segments?: ProgressSegment[];
|
|
60
|
+
/** Show the legend under segmented bars. @default false */
|
|
16
61
|
showLabels?: boolean;
|
|
62
|
+
/** Show numeric values alongside labels in the legend. @default false */
|
|
17
63
|
showValues?: boolean;
|
|
18
64
|
class?: ClassValue;
|
|
19
65
|
labelClass?: ClassValue;
|
|
66
|
+
/** Classes on the inner bar element(s). */
|
|
20
67
|
barClass?: ClassValue;
|
|
21
68
|
testId?: string;
|
|
22
69
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import type { SkeletonProps } from './skeleton-types.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
class: className = '',
|
|
8
|
+
variant = 'pulse',
|
|
9
|
+
rounded = 'rounded-md',
|
|
10
|
+
ariaLabel = 'Loading',
|
|
11
|
+
style,
|
|
12
|
+
testId
|
|
13
|
+
}: SkeletonProps = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
class={cn(
|
|
18
|
+
'bg-default-200',
|
|
19
|
+
rounded,
|
|
20
|
+
variant === 'pulse' && 'animate-pulse',
|
|
21
|
+
variant === 'shimmer' && 'ripple-skeleton-shimmer overflow-hidden',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{style}
|
|
25
|
+
role="status"
|
|
26
|
+
aria-label={ariaLabel}
|
|
27
|
+
aria-busy="true"
|
|
28
|
+
data-testid={buildTestId('skeleton', undefined, testId)}
|
|
29
|
+
></div>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
/* Shimmer variant: gradient sweep using a CSS animation. Kept here (not Tailwind)
|
|
33
|
+
because moving gradients aren't expressible with Tailwind utilities. */
|
|
34
|
+
.ripple-skeleton-shimmer {
|
|
35
|
+
position: relative;
|
|
36
|
+
isolation: isolate;
|
|
37
|
+
}
|
|
38
|
+
.ripple-skeleton-shimmer::after {
|
|
39
|
+
content: '';
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset: 0;
|
|
42
|
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
|
|
43
|
+
transform: translateX(-100%);
|
|
44
|
+
animation: ripple-skeleton-shimmer 1.6s infinite;
|
|
45
|
+
}
|
|
46
|
+
@keyframes ripple-skeleton-shimmer {
|
|
47
|
+
100% {
|
|
48
|
+
transform: translateX(100%);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
@media (prefers-reduced-motion: reduce) {
|
|
52
|
+
.ripple-skeleton-shimmer::after {
|
|
53
|
+
animation: none;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Skeleton } from './Skeleton.svelte';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ClassValue } from 'tailwind-variants';
|
|
2
|
+
/**
|
|
3
|
+
* Animation style for `<Skeleton>`.
|
|
4
|
+
* - `'pulse'` — subtle opacity pulse (Tailwind's `animate-pulse`)
|
|
5
|
+
* - `'shimmer'` — gradient slides across the block
|
|
6
|
+
*/
|
|
7
|
+
export type SkeletonVariant = 'pulse' | 'shimmer';
|
|
8
|
+
/**
|
|
9
|
+
* Props for `<Skeleton>` — a placeholder block that mimics the shape of
|
|
10
|
+
* content while it loads. Use to preserve layout and avoid layout shift
|
|
11
|
+
* when the real content arrives.
|
|
12
|
+
*
|
|
13
|
+
* For wholesale "is my component loading?" UX, most components expose
|
|
14
|
+
* a `loading` prop that renders skeletons automatically (`<Card loading>`,
|
|
15
|
+
* `<Table loading>`, etc.). Use `<Skeleton>` directly when composing
|
|
16
|
+
* custom placeholder layouts.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <!-- Three-line text placeholder -->
|
|
21
|
+
* <div class="space-y-2">
|
|
22
|
+
* <Skeleton class="h-4 w-48" />
|
|
23
|
+
* <Skeleton class="h-4 w-64" />
|
|
24
|
+
* <Skeleton class="h-4 w-40" />
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <!-- Avatar placeholder -->
|
|
31
|
+
* <Skeleton class="size-10" rounded="rounded-full" variant="shimmer" />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface SkeletonProps {
|
|
35
|
+
/** Additional Tailwind classes — typically `w-*` and `h-*` to size the block. */
|
|
36
|
+
class?: ClassValue;
|
|
37
|
+
/**
|
|
38
|
+
* Animation style.
|
|
39
|
+
* - `pulse` (default): subtle opacity pulse via Tailwind's animate-pulse
|
|
40
|
+
* - `shimmer`: gradient slides across the block
|
|
41
|
+
*/
|
|
42
|
+
variant?: SkeletonVariant;
|
|
43
|
+
/** Override the rounded class. Pass `''` to suppress default rounding. @default 'rounded-md' */
|
|
44
|
+
rounded?: string;
|
|
45
|
+
/** Optional accessible label announced by screen readers. @default 'Loading' */
|
|
46
|
+
ariaLabel?: string;
|
|
47
|
+
/** Inline style — useful for dynamic widths/heights that Tailwind can't express. */
|
|
48
|
+
style?: string;
|
|
49
|
+
testId?: string;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|