@makolabs/ripple 2.5.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.d.ts +1 -1
- package/dist/funcs/user-management.remote.js +2 -2
- 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function isDateRange(v) {
|
|
2
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Sync a reactive `selections` object to/from URL query params.
|
|
6
|
+
*
|
|
7
|
+
* - On first call: reads current URL params into the setter.
|
|
8
|
+
* - On every subsequent change to `getter()`: writes back to the URL.
|
|
9
|
+
*
|
|
10
|
+
* Designed for use inside a Svelte 5 component with `$effect` semantics — call
|
|
11
|
+
* it at the top level of `<script>` and it will register its own `$effect`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { CompactFilters, syncFiltersToUrl } from '@makolabs/ripple';
|
|
17
|
+
* let selections = $state({ status: 'all' });
|
|
18
|
+
* syncFiltersToUrl(() => selections, (v) => (selections = v));
|
|
19
|
+
* </script>
|
|
20
|
+
*
|
|
21
|
+
* <CompactFilters {filterGroups} bind:selections />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Environment:
|
|
25
|
+
* - Uses `window.location` and `history.replaceState` — no-ops on the server.
|
|
26
|
+
* - Multi-select values (`string[]`) are serialized as comma-joined strings.
|
|
27
|
+
* - Removes a key from the URL when its value is empty / empty array.
|
|
28
|
+
* - Leaves unrelated URL params untouched.
|
|
29
|
+
*/
|
|
30
|
+
export function syncFiltersToUrl(getter, setter, options = {}) {
|
|
31
|
+
if (typeof window === 'undefined')
|
|
32
|
+
return;
|
|
33
|
+
const { keys, debounceMs = 150, arrayKeys = [], dateRangeKeys = [] } = options;
|
|
34
|
+
// Initial read from URL — plain imperative parse, no reactivity needed.
|
|
35
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
36
|
+
const params = new URLSearchParams(window.location.search);
|
|
37
|
+
const initial = {};
|
|
38
|
+
const managedKeys = keys ?? Object.keys(getter());
|
|
39
|
+
let hasAny = false;
|
|
40
|
+
for (const key of managedKeys) {
|
|
41
|
+
if (dateRangeKeys.includes(key)) {
|
|
42
|
+
const from = params.get(`${key}_from`);
|
|
43
|
+
const to = params.get(`${key}_to`);
|
|
44
|
+
if (from && to) {
|
|
45
|
+
const preset = params.get(`${key}_preset`) ?? undefined;
|
|
46
|
+
initial[key] = preset ? { from, to, preset } : { from, to };
|
|
47
|
+
hasAny = true;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const raw = params.get(key);
|
|
52
|
+
if (raw !== null) {
|
|
53
|
+
initial[key] = arrayKeys.includes(key) ? raw.split(',').filter(Boolean) : raw;
|
|
54
|
+
hasAny = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (hasAny) {
|
|
58
|
+
setter({ ...getter(), ...initial });
|
|
59
|
+
}
|
|
60
|
+
// Write on change
|
|
61
|
+
let timer;
|
|
62
|
+
$effect(() => {
|
|
63
|
+
const current = getter();
|
|
64
|
+
// Touch each managed value to register reactive deps
|
|
65
|
+
for (const key of keys ?? Object.keys(current)) {
|
|
66
|
+
void current[key];
|
|
67
|
+
}
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
timer = setTimeout(() => {
|
|
70
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
71
|
+
const url = new URL(window.location.href);
|
|
72
|
+
const managed = keys ?? Object.keys(current);
|
|
73
|
+
for (const key of managed) {
|
|
74
|
+
const value = current[key];
|
|
75
|
+
if (dateRangeKeys.includes(key)) {
|
|
76
|
+
url.searchParams.delete(`${key}_from`);
|
|
77
|
+
url.searchParams.delete(`${key}_to`);
|
|
78
|
+
url.searchParams.delete(`${key}_preset`);
|
|
79
|
+
if (isDateRange(value)) {
|
|
80
|
+
url.searchParams.set(`${key}_from`, value.from);
|
|
81
|
+
url.searchParams.set(`${key}_to`, value.to);
|
|
82
|
+
if (value.preset)
|
|
83
|
+
url.searchParams.set(`${key}_preset`, value.preset);
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (value === undefined ||
|
|
88
|
+
value === null ||
|
|
89
|
+
value === '' ||
|
|
90
|
+
(Array.isArray(value) && value.length === 0)) {
|
|
91
|
+
url.searchParams.delete(key);
|
|
92
|
+
}
|
|
93
|
+
else if (Array.isArray(value)) {
|
|
94
|
+
url.searchParams.set(key, value.join(','));
|
|
95
|
+
}
|
|
96
|
+
else if (typeof value === 'string') {
|
|
97
|
+
url.searchParams.set(key, value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const next = url.pathname + (url.search ? url.search : '') + url.hash;
|
|
101
|
+
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
102
|
+
if (next !== currentPath) {
|
|
103
|
+
// Use SvelteKit's replaceState so the router stays in sync. Fall
|
|
104
|
+
// back to history.replaceState for non-SvelteKit consumers.
|
|
105
|
+
import('$app/navigation')
|
|
106
|
+
.then(({ replaceState }) => replaceState(next, {}))
|
|
107
|
+
.catch(() => {
|
|
108
|
+
window.history.replaceState({}, '', next);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}, debounceMs);
|
|
112
|
+
return () => clearTimeout(timer);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
3
|
import type { DateRangeProps } from '../index.js';
|
|
4
4
|
import Portal from '../utils/Portal.svelte';
|
|
5
|
+
import { fly } from 'svelte/transition';
|
|
6
|
+
import { quintOut } from 'svelte/easing';
|
|
5
7
|
|
|
6
8
|
let {
|
|
7
9
|
startDate = $bindable(),
|
|
@@ -247,7 +249,6 @@
|
|
|
247
249
|
onclick={toggleDatepicker}
|
|
248
250
|
aria-haspopup="true"
|
|
249
251
|
aria-expanded={isOpen}
|
|
250
|
-
aria-invalid={errors?.length ? 'true' : undefined}
|
|
251
252
|
aria-describedby={errors?.length ? `${id}-errors` : undefined}
|
|
252
253
|
{disabled}
|
|
253
254
|
>
|
|
@@ -298,6 +299,7 @@
|
|
|
298
299
|
<div
|
|
299
300
|
bind:this={calendarRef}
|
|
300
301
|
class="ring-opacity-5 ring-default-300 absolute z-10 mt-1 w-full origin-top-left rounded-md bg-white p-4 shadow-lg ring-1 focus:outline-none"
|
|
302
|
+
transition:fly={{ y: -8, duration: 300, easing: quintOut }}
|
|
301
303
|
>
|
|
302
304
|
<div class="mb-2 flex items-center justify-between">
|
|
303
305
|
{#if viewMode === 'days'}
|
|
@@ -488,7 +490,7 @@
|
|
|
488
490
|
|
|
489
491
|
{#if startDate || endDate}
|
|
490
492
|
<div
|
|
491
|
-
class="border-default-200 text-default-500 mt-4 flex justify-between border-t pt-3 text-xs"
|
|
493
|
+
class="border-default-200 text-default-500 mt-4 flex flex-wrap justify-between gap-x-4 gap-y-1 border-t pt-3 text-xs"
|
|
492
494
|
>
|
|
493
495
|
<div>
|
|
494
496
|
{startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}
|
package/dist/forms/Input.svelte
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
placeholder,
|
|
13
13
|
disabled = false,
|
|
14
14
|
class: className = '',
|
|
15
|
-
size = Size.
|
|
15
|
+
size = Size.MD,
|
|
16
16
|
value = $bindable(),
|
|
17
17
|
errors = [],
|
|
18
18
|
testId,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
...(BASIC_TYPES.includes(type)
|
|
34
34
|
? {
|
|
35
35
|
'h-8 text-sm': size === Size.SM,
|
|
36
|
-
'h-10 text-base': size === Size.
|
|
36
|
+
'h-10 text-base': size === Size.MD,
|
|
37
37
|
'h-12 text-lg': size === Size.LG
|
|
38
38
|
}
|
|
39
39
|
: {}),
|
|
@@ -15,24 +15,29 @@
|
|
|
15
15
|
showFlags = true,
|
|
16
16
|
label = '',
|
|
17
17
|
appearance = 'surface',
|
|
18
|
-
orientation = '
|
|
18
|
+
orientation = 'auto',
|
|
19
19
|
labelLayout = 'inline',
|
|
20
20
|
labelClass = undefined,
|
|
21
21
|
color = Color.PRIMARY,
|
|
22
22
|
size = Size.SM,
|
|
23
23
|
compact = false,
|
|
24
|
+
flagsOnly = false,
|
|
24
25
|
disabled = false,
|
|
25
26
|
class: className = '',
|
|
26
27
|
onchange = undefined,
|
|
27
28
|
testId
|
|
28
29
|
}: MarketSelectorProps = $props();
|
|
29
30
|
|
|
31
|
+
// flagsOnly implies showFlags + compact (which sr-onlys the label)
|
|
32
|
+
const effectiveShowFlags = $derived(showFlags || flagsOnly);
|
|
33
|
+
const effectiveCompact = $derived(compact || flagsOnly);
|
|
34
|
+
|
|
30
35
|
const options = $derived.by((): SegmentedOption[] => {
|
|
31
36
|
return markets.map((code) => ({
|
|
32
37
|
value: code,
|
|
33
38
|
label: code,
|
|
34
39
|
title: COUNTRY_NAMES[code],
|
|
35
|
-
prefix:
|
|
40
|
+
prefix: effectiveShowFlags ? countryCodeToFlagEmoji(code) : undefined
|
|
36
41
|
}));
|
|
37
42
|
});
|
|
38
43
|
|
|
@@ -63,7 +68,7 @@
|
|
|
63
68
|
{labelClass}
|
|
64
69
|
{color}
|
|
65
70
|
{size}
|
|
66
|
-
{
|
|
71
|
+
compact={effectiveCompact}
|
|
67
72
|
{disabled}
|
|
68
73
|
{testId}
|
|
69
74
|
onchange={handleChange}
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
name,
|
|
11
11
|
label,
|
|
12
12
|
placeholder = 'Enter a number',
|
|
13
|
-
size = Size.
|
|
13
|
+
size = Size.MD,
|
|
14
14
|
class: className = '',
|
|
15
15
|
units = [],
|
|
16
16
|
errors,
|
|
17
17
|
disabled = false,
|
|
18
|
-
|
|
18
|
+
dropdownIcon: DropdownIcon,
|
|
19
19
|
onunitchange: onUnitChange,
|
|
20
20
|
testId,
|
|
21
21
|
...restProps
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
'h-8': size === Size.SM,
|
|
38
|
-
'h-10': size === Size.
|
|
38
|
+
'h-10': size === Size.MD,
|
|
39
39
|
'h-12': size === Size.LG
|
|
40
40
|
},
|
|
41
41
|
'border-default-300 focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
'w-full bg-transparent outline-none disabled:cursor-not-allowed px-3 placeholder:text-default-400',
|
|
49
49
|
{
|
|
50
50
|
'text-sm': size === Size.SM,
|
|
51
|
-
'text-base': size === Size.
|
|
51
|
+
'text-base': size === Size.MD,
|
|
52
52
|
'text-lg': size === Size.LG
|
|
53
53
|
}
|
|
54
54
|
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../helper/testid.js';
|
|
4
|
+
import { Color, Size } from '../variants.js';
|
|
5
|
+
import type { RadioGroupProps } from '../index.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
name,
|
|
9
|
+
label,
|
|
10
|
+
options,
|
|
11
|
+
value = $bindable<string | undefined>(undefined),
|
|
12
|
+
disabled = false,
|
|
13
|
+
required = false,
|
|
14
|
+
orientation = 'vertical',
|
|
15
|
+
size = Size.MD,
|
|
16
|
+
color = Color.PRIMARY,
|
|
17
|
+
errors = [],
|
|
18
|
+
class: className = '',
|
|
19
|
+
onchange,
|
|
20
|
+
testId
|
|
21
|
+
}: RadioGroupProps = $props();
|
|
22
|
+
|
|
23
|
+
const hasErrors = $derived(errors.length > 0);
|
|
24
|
+
|
|
25
|
+
const dotSize = $derived(
|
|
26
|
+
{
|
|
27
|
+
[Size.XS]: 'size-3',
|
|
28
|
+
[Size.SM]: 'size-3.5',
|
|
29
|
+
[Size.MD]: 'size-4',
|
|
30
|
+
[Size.LG]: 'size-5',
|
|
31
|
+
[Size.XL]: 'size-6',
|
|
32
|
+
[Size.XXL]: 'size-7'
|
|
33
|
+
}[size]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const accentBg = $derived(
|
|
37
|
+
{
|
|
38
|
+
[Color.DEFAULT]: 'bg-default-600',
|
|
39
|
+
[Color.PRIMARY]: 'bg-primary-500',
|
|
40
|
+
[Color.SECONDARY]: 'bg-secondary-500',
|
|
41
|
+
[Color.INFO]: 'bg-info-500',
|
|
42
|
+
[Color.SUCCESS]: 'bg-success-500',
|
|
43
|
+
[Color.WARNING]: 'bg-warning-500',
|
|
44
|
+
[Color.DANGER]: 'bg-danger-500'
|
|
45
|
+
}[color]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function handlePick(next: string, optionDisabled: boolean) {
|
|
49
|
+
if (disabled || optionDisabled) return;
|
|
50
|
+
value = next;
|
|
51
|
+
onchange?.(next);
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<fieldset
|
|
56
|
+
class={cn('w-full', className)}
|
|
57
|
+
{disabled}
|
|
58
|
+
aria-invalid={hasErrors}
|
|
59
|
+
data-testid={buildTestId('radio-group', undefined, testId)}
|
|
60
|
+
>
|
|
61
|
+
{#if label}
|
|
62
|
+
<legend class="text-default-700 mb-2 block text-sm font-medium">
|
|
63
|
+
{label}
|
|
64
|
+
{#if required}<span class="text-danger-500" aria-hidden="true">*</span>{/if}
|
|
65
|
+
</legend>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
<div
|
|
69
|
+
class={cn('flex gap-3', orientation === 'vertical' ? 'flex-col' : 'flex-wrap items-center')}
|
|
70
|
+
role="radiogroup"
|
|
71
|
+
aria-label={label}
|
|
72
|
+
>
|
|
73
|
+
{#each options as option (option.value)}
|
|
74
|
+
{@const checked = value === option.value}
|
|
75
|
+
{@const optionDisabled = disabled || option.disabled === true}
|
|
76
|
+
<label
|
|
77
|
+
class={cn(
|
|
78
|
+
'group inline-flex cursor-pointer items-start gap-2 select-none',
|
|
79
|
+
optionDisabled && 'cursor-not-allowed opacity-60'
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<input
|
|
83
|
+
type="radio"
|
|
84
|
+
{name}
|
|
85
|
+
value={option.value}
|
|
86
|
+
{checked}
|
|
87
|
+
disabled={optionDisabled}
|
|
88
|
+
required={required && !value}
|
|
89
|
+
onchange={() => handlePick(option.value, optionDisabled)}
|
|
90
|
+
class="sr-only"
|
|
91
|
+
data-testid={buildTestId('radio-group', option.value, testId)}
|
|
92
|
+
/>
|
|
93
|
+
<span
|
|
94
|
+
aria-hidden="true"
|
|
95
|
+
class={cn(
|
|
96
|
+
'relative mt-0.5 flex shrink-0 items-center justify-center rounded-full border transition-colors',
|
|
97
|
+
dotSize,
|
|
98
|
+
checked ? `${accentBg} border-transparent` : 'border-default-300 bg-white',
|
|
99
|
+
hasErrors && !checked && 'border-danger-300'
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{#if checked}
|
|
103
|
+
<span class="size-1/3 rounded-full bg-white"></span>
|
|
104
|
+
{/if}
|
|
105
|
+
</span>
|
|
106
|
+
<span class="flex flex-col">
|
|
107
|
+
<span class="text-default-800 text-sm">{option.label}</span>
|
|
108
|
+
{#if option.description}
|
|
109
|
+
<span class="text-default-500 text-xs">{option.description}</span>
|
|
110
|
+
{/if}
|
|
111
|
+
</span>
|
|
112
|
+
</label>
|
|
113
|
+
{/each}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{#if hasErrors}
|
|
117
|
+
<ul class="mt-2 space-y-0.5" role="alert">
|
|
118
|
+
{#each errors as error (error)}
|
|
119
|
+
<li class="text-danger-600 text-xs">{error}</li>
|
|
120
|
+
{/each}
|
|
121
|
+
</ul>
|
|
122
|
+
{/if}
|
|
123
|
+
</fieldset>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
appearance = 'surface',
|
|
18
18
|
orientation = 'horizontal',
|
|
19
19
|
color = Color.PRIMARY,
|
|
20
|
-
size = Size.
|
|
20
|
+
size = Size.MD,
|
|
21
21
|
compact = false,
|
|
22
22
|
labelLayout = 'above',
|
|
23
23
|
labelClass = '',
|
|
@@ -41,8 +41,11 @@
|
|
|
41
41
|
|
|
42
42
|
const rootClass = $derived(
|
|
43
43
|
cn(
|
|
44
|
-
'w-fit',
|
|
44
|
+
orientation === 'auto' ? '@container w-full' : 'w-fit',
|
|
45
45
|
labelLayout === 'inline' ? 'flex flex-row items-center gap-2' : 'flex flex-col gap-2',
|
|
46
|
+
orientation === 'auto' &&
|
|
47
|
+
labelLayout === 'inline' &&
|
|
48
|
+
'@max-[250px]:flex-col @max-[250px]:items-stretch',
|
|
46
49
|
className
|
|
47
50
|
)
|
|
48
51
|
);
|
|
@@ -84,8 +87,12 @@
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
function handleSegmentKeydown(e: KeyboardEvent, index: number) {
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
// `'auto'` defaults to horizontal layout (flex-row) and only flips
|
|
91
|
+
// to vertical via a container query below 250px — treat it as
|
|
92
|
+
// horizontal for keyboard nav since that matches the default render.
|
|
93
|
+
const isHorizontal = orientation === 'horizontal' || orientation === 'auto';
|
|
94
|
+
const forward = isHorizontal ? e.key === 'ArrowRight' : e.key === 'ArrowDown';
|
|
95
|
+
const backward = isHorizontal ? e.key === 'ArrowLeft' : e.key === 'ArrowUp';
|
|
89
96
|
if (forward) {
|
|
90
97
|
e.preventDefault();
|
|
91
98
|
moveSelection(index, 1);
|
package/dist/forms/Slider.svelte
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { buildTestId } from '../helper/testid.js';
|
|
5
5
|
import { slider } from './slider.js';
|
|
6
6
|
import { Size } from '../variants.js';
|
|
7
|
-
import type { SliderProps } from '../index.js';
|
|
7
|
+
import type { SliderProps, SliderTick } from '../index.js';
|
|
8
8
|
|
|
9
9
|
interface EnumOption {
|
|
10
10
|
value: string | number;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
label,
|
|
21
21
|
mode = 'single' as SliderMode,
|
|
22
22
|
disabled = false,
|
|
23
|
-
size = Size.
|
|
23
|
+
size = Size.MD,
|
|
24
24
|
errors = [],
|
|
25
25
|
class: className = '',
|
|
26
26
|
min = 0,
|
|
@@ -38,9 +38,45 @@
|
|
|
38
38
|
maximumFractionDigits: 1,
|
|
39
39
|
minimumFractionDigits: 0
|
|
40
40
|
},
|
|
41
|
+
tickInterval,
|
|
42
|
+
ticks,
|
|
41
43
|
testId
|
|
42
44
|
}: SliderProps = $props();
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Resolved tick list for single/range modes. Explicit `ticks` wins;
|
|
48
|
+
* otherwise generated from `tickInterval`. Empty for enum mode.
|
|
49
|
+
*/
|
|
50
|
+
const resolvedTicks = $derived.by<SliderTick[]>(() => {
|
|
51
|
+
if (mode === 'enum') return [];
|
|
52
|
+
if (ticks && ticks.length > 0) {
|
|
53
|
+
return ticks
|
|
54
|
+
.map((t) => (typeof t === 'number' ? { value: t } : t))
|
|
55
|
+
.filter((t) => t.value >= min && t.value <= max);
|
|
56
|
+
}
|
|
57
|
+
if (tickInterval && tickInterval > 0) {
|
|
58
|
+
const out: SliderTick[] = [];
|
|
59
|
+
for (let v = min; v <= max; v += tickInterval) {
|
|
60
|
+
out.push({ value: v });
|
|
61
|
+
}
|
|
62
|
+
// Include max if rounding left it out
|
|
63
|
+
if (out[out.length - 1]?.value !== max) out.push({ value: max });
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function getTickPosition(tickValue: number): string {
|
|
70
|
+
return `${((tickValue - min) / (max - min)) * 100}%`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTickInRange(tickValue: number): boolean {
|
|
74
|
+
if (mode === 'range') return tickValue >= valueStart && tickValue <= valueEnd;
|
|
75
|
+
if (mode === 'single' && typeof value === 'number')
|
|
76
|
+
return tickValue >= min && tickValue <= value;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
$effect(() => {
|
|
45
81
|
if (mode === 'enum' && options.length > 0 && value === min) {
|
|
46
82
|
value = options[0].value;
|
|
@@ -63,7 +99,17 @@
|
|
|
63
99
|
})
|
|
64
100
|
);
|
|
65
101
|
|
|
66
|
-
const
|
|
102
|
+
const hasTickLabels = $derived(resolvedTicks.some((t) => t.label !== undefined));
|
|
103
|
+
const baseClass = $derived(
|
|
104
|
+
cn(
|
|
105
|
+
base(),
|
|
106
|
+
{
|
|
107
|
+
'mb-12': mode === 'enum' || hasTickLabels,
|
|
108
|
+
'mb-6': !hasTickLabels && resolvedTicks.length > 0 && mode !== 'enum'
|
|
109
|
+
},
|
|
110
|
+
className
|
|
111
|
+
)
|
|
112
|
+
);
|
|
67
113
|
const trackClass = $derived(cn(track()));
|
|
68
114
|
const rangeClass = $derived(cn(range()));
|
|
69
115
|
const thumbClass = $derived(cn(thumb()));
|
|
@@ -274,6 +320,29 @@
|
|
|
274
320
|
aria-label={label}
|
|
275
321
|
onclick={handleTrackClick}
|
|
276
322
|
>
|
|
323
|
+
{#each resolvedTicks as tick (tick.value)}
|
|
324
|
+
<div
|
|
325
|
+
class={cn(
|
|
326
|
+
'absolute top-1/2 h-2 w-px -translate-x-1/2 -translate-y-1/2',
|
|
327
|
+
isTickInRange(tick.value) ? 'bg-primary-400' : 'bg-default-300'
|
|
328
|
+
)}
|
|
329
|
+
style="left: {getTickPosition(tick.value)}"
|
|
330
|
+
aria-hidden="true"
|
|
331
|
+
></div>
|
|
332
|
+
{#if tick.label !== undefined}
|
|
333
|
+
<div
|
|
334
|
+
class={cn(
|
|
335
|
+
markClass,
|
|
336
|
+
'text-default-500 pointer-events-none top-4 text-[10px] whitespace-nowrap'
|
|
337
|
+
)}
|
|
338
|
+
style="left: {getTickPosition(tick.value)}"
|
|
339
|
+
aria-hidden="true"
|
|
340
|
+
>
|
|
341
|
+
{tick.label}
|
|
342
|
+
</div>
|
|
343
|
+
{/if}
|
|
344
|
+
{/each}
|
|
345
|
+
|
|
277
346
|
{#if mode === 'range'}
|
|
278
347
|
<div class={rangeClass} style="width: {getRangeWidth()}; left: {getRangeLeft()}"></div>
|
|
279
348
|
<div
|
package/dist/forms/Tags.svelte
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import Badge from '../elements/badge/Badge.svelte';
|
|
3
3
|
import { Size } from '../variants.js';
|
|
4
4
|
import { cn } from '../helper/cls.js';
|
|
5
|
+
import { fade } from 'svelte/transition';
|
|
6
|
+
import { flip } from 'svelte/animate';
|
|
7
|
+
import { quintOut } from 'svelte/easing';
|
|
5
8
|
import type { TagsProps } from '../index.js';
|
|
6
9
|
|
|
7
10
|
let {
|
|
@@ -10,7 +13,7 @@
|
|
|
10
13
|
label,
|
|
11
14
|
errors,
|
|
12
15
|
placeholder = 'Type and press enter to add tags...',
|
|
13
|
-
size = Size.
|
|
16
|
+
size = Size.MD,
|
|
14
17
|
class: className = '',
|
|
15
18
|
suggestions = [],
|
|
16
19
|
onaddtag: onAddTag,
|
|
@@ -135,9 +138,15 @@
|
|
|
135
138
|
{/if}
|
|
136
139
|
<div class={containerClass} onfocusout={handleFocusOut}>
|
|
137
140
|
{#each value as tag (tag)}
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
<div
|
|
142
|
+
class="inline-flex"
|
|
143
|
+
transition:fade={{ duration: 250, easing: quintOut }}
|
|
144
|
+
animate:flip={{ duration: 300, easing: quintOut }}
|
|
145
|
+
>
|
|
146
|
+
<Badge {size} color="info" onclose={() => handleTagRemoval(tag)} class="shadow-xs">
|
|
147
|
+
{tag}
|
|
148
|
+
</Badge>
|
|
149
|
+
</div>
|
|
141
150
|
{/each}
|
|
142
151
|
<input
|
|
143
152
|
bind:this={inputRef}
|
|
@@ -147,7 +156,7 @@
|
|
|
147
156
|
{placeholder}
|
|
148
157
|
class={cn('placeholder:text-default-400 min-w-[120px] flex-1 bg-transparent outline-none', {
|
|
149
158
|
'text-sm': size === Size.SM,
|
|
150
|
-
'text-base': size === Size.
|
|
159
|
+
'text-base': size === Size.MD,
|
|
151
160
|
'text-lg': size === Size.LG
|
|
152
161
|
})}
|
|
153
162
|
type="text"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../helper/testid.js';
|
|
4
|
+
import { Size } from '../variants.js';
|
|
5
|
+
import type { TextareaProps } from '../index.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
name,
|
|
9
|
+
id = name,
|
|
10
|
+
label,
|
|
11
|
+
placeholder,
|
|
12
|
+
value = $bindable(''),
|
|
13
|
+
disabled = false,
|
|
14
|
+
readonly = false,
|
|
15
|
+
rows = 3,
|
|
16
|
+
autoGrow = false,
|
|
17
|
+
maxRows = 10,
|
|
18
|
+
maxLength,
|
|
19
|
+
showCount = false,
|
|
20
|
+
size = Size.MD,
|
|
21
|
+
errors = [],
|
|
22
|
+
class: className = '',
|
|
23
|
+
oninput,
|
|
24
|
+
onblur,
|
|
25
|
+
testId
|
|
26
|
+
}: TextareaProps = $props();
|
|
27
|
+
|
|
28
|
+
let el = $state<HTMLTextAreaElement | undefined>();
|
|
29
|
+
|
|
30
|
+
const sizeClass = $derived(
|
|
31
|
+
{
|
|
32
|
+
[Size.XS]: 'text-xs',
|
|
33
|
+
[Size.SM]: 'text-sm',
|
|
34
|
+
[Size.MD]: 'text-sm',
|
|
35
|
+
[Size.LG]: 'text-base',
|
|
36
|
+
[Size.XL]: 'text-lg',
|
|
37
|
+
[Size.XXL]: 'text-lg'
|
|
38
|
+
}[size]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const hasErrors = $derived(errors.length > 0);
|
|
42
|
+
|
|
43
|
+
const textareaClasses = $derived(
|
|
44
|
+
cn(
|
|
45
|
+
'w-full rounded-lg border bg-white px-3 py-2 shadow-xs transition-colors',
|
|
46
|
+
'placeholder:text-default-400',
|
|
47
|
+
'focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2',
|
|
48
|
+
sizeClass,
|
|
49
|
+
hasErrors
|
|
50
|
+
? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
51
|
+
: 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
|
|
52
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
53
|
+
autoGrow ? 'resize-none overflow-hidden' : 'resize-y',
|
|
54
|
+
className
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When autoGrow is on, measure scrollHeight and clamp to maxRows.
|
|
60
|
+
* Runs after value changes so the textarea expands/shrinks with content.
|
|
61
|
+
*/
|
|
62
|
+
function resize() {
|
|
63
|
+
if (!autoGrow || !el) return;
|
|
64
|
+
el.style.height = 'auto';
|
|
65
|
+
const lineHeight = parseFloat(getComputedStyle(el).lineHeight || '20');
|
|
66
|
+
const padding =
|
|
67
|
+
parseFloat(getComputedStyle(el).paddingTop) + parseFloat(getComputedStyle(el).paddingBottom);
|
|
68
|
+
const maxHeight = lineHeight * maxRows + padding;
|
|
69
|
+
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
|
|
70
|
+
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
void value;
|
|
75
|
+
resize();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function handleInput(e: Event) {
|
|
79
|
+
const v = (e.currentTarget as HTMLTextAreaElement).value;
|
|
80
|
+
value = v;
|
|
81
|
+
oninput?.(v);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleBlur(e: FocusEvent) {
|
|
85
|
+
onblur?.((e.currentTarget as HTMLTextAreaElement).value);
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div class="w-full" data-testid={buildTestId('textarea', 'wrapper', testId)}>
|
|
90
|
+
{#if label}
|
|
91
|
+
<label for={id} class="text-default-700 mb-1 block text-sm font-medium">
|
|
92
|
+
{label}
|
|
93
|
+
</label>
|
|
94
|
+
{/if}
|
|
95
|
+
<textarea
|
|
96
|
+
bind:this={el}
|
|
97
|
+
{id}
|
|
98
|
+
{name}
|
|
99
|
+
{placeholder}
|
|
100
|
+
{disabled}
|
|
101
|
+
{readonly}
|
|
102
|
+
{rows}
|
|
103
|
+
maxlength={maxLength}
|
|
104
|
+
class={textareaClasses}
|
|
105
|
+
aria-invalid={hasErrors}
|
|
106
|
+
aria-describedby={hasErrors ? `${name}-error` : undefined}
|
|
107
|
+
data-testid={buildTestId('textarea', undefined, testId)}
|
|
108
|
+
{value}
|
|
109
|
+
oninput={handleInput}
|
|
110
|
+
onblur={handleBlur}
|
|
111
|
+
></textarea>
|
|
112
|
+
|
|
113
|
+
{#if showCount && maxLength !== undefined}
|
|
114
|
+
<div class="text-default-400 mt-1 text-right text-xs">
|
|
115
|
+
{value?.length ?? 0} / {maxLength}
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
|
|
119
|
+
{#if hasErrors}
|
|
120
|
+
<ul id="{name}-error" class="mt-1 space-y-0.5" role="alert">
|
|
121
|
+
{#each errors as error (error)}
|
|
122
|
+
<li class="text-danger-600 text-xs">{error}</li>
|
|
123
|
+
{/each}
|
|
124
|
+
</ul>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|