@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.
Files changed (183) hide show
  1. package/README.md +403 -497
  2. package/dist/adapters/storage/S3Adapter.d.ts +49 -1
  3. package/dist/adapters/storage/S3Adapter.js +38 -1
  4. package/dist/adapters/storage/types.d.ts +20 -0
  5. package/dist/ai/AIChatInterface.svelte +2 -1
  6. package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
  7. package/dist/ai/CodeRenderer.svelte +7 -2
  8. package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
  9. package/dist/ai/ComposeDropdown.svelte +1 -1
  10. package/dist/ai/MessageBox.svelte +3 -3
  11. package/dist/ai/MessageBox.svelte.d.ts +3 -2
  12. package/dist/ai/ThinkingDisplay.svelte +4 -3
  13. package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
  14. package/dist/ai/ai-types.d.ts +55 -1
  15. package/dist/button/Button.svelte +5 -5
  16. package/dist/button/button-types.d.ts +49 -4
  17. package/dist/button/button.d.ts +9 -9
  18. package/dist/button/button.js +6 -6
  19. package/dist/charts/Chart.svelte +8 -16
  20. package/dist/charts/chart-types.d.ts +78 -1
  21. package/dist/drawer/Drawer.svelte +6 -26
  22. package/dist/drawer/drawer-types.d.ts +33 -12
  23. package/dist/drawer/drawer.d.ts +3 -3
  24. package/dist/drawer/drawer.js +1 -1
  25. package/dist/elements/accordion/Accordion.svelte +6 -17
  26. package/dist/elements/accordion/accordion-types.d.ts +53 -6
  27. package/dist/elements/alert/Alert.svelte +3 -0
  28. package/dist/elements/badge/Badge.svelte +1 -1
  29. package/dist/elements/badge/badge-types.d.ts +22 -0
  30. package/dist/elements/badge/badge.d.ts +3 -3
  31. package/dist/elements/badge/badge.js +1 -1
  32. package/dist/elements/combobox/ComboBox.svelte +247 -0
  33. package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
  34. package/dist/elements/combobox/combobox-types.d.ts +41 -0
  35. package/dist/elements/combobox/combobox-types.js +1 -0
  36. package/dist/elements/context-menu/ContextMenu.svelte +137 -0
  37. package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
  38. package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
  39. package/dist/elements/context-menu/context-menu-types.js +1 -0
  40. package/dist/elements/dropdown/Dropdown.svelte +1 -1
  41. package/dist/elements/dropdown/Select.svelte +4 -1
  42. package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
  43. package/dist/elements/dropdown/dropdown.d.ts +3 -3
  44. package/dist/elements/dropdown/dropdown.js +2 -2
  45. package/dist/elements/dropdown/select.d.ts +3 -3
  46. package/dist/elements/dropdown/select.js +2 -2
  47. package/dist/elements/empty-state/EmptyState.svelte +1 -1
  48. package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
  49. package/dist/elements/empty-state/empty-state.d.ts +3 -3
  50. package/dist/elements/empty-state/empty-state.js +2 -2
  51. package/dist/elements/file-upload/FileUpload.svelte +5 -0
  52. package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
  53. package/dist/elements/pagination/Pagination.svelte +53 -21
  54. package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
  55. package/dist/elements/popover/Popover.svelte +234 -0
  56. package/dist/elements/popover/Popover.svelte.d.ts +4 -0
  57. package/dist/elements/popover/index.d.ts +2 -0
  58. package/dist/elements/popover/index.js +1 -0
  59. package/dist/elements/popover/popover-types.d.ts +60 -0
  60. package/dist/elements/popover/popover-types.js +1 -0
  61. package/dist/elements/progress/Progress.svelte +32 -7
  62. package/dist/elements/progress/progress-types.d.ts +48 -1
  63. package/dist/elements/skeleton/Skeleton.svelte +56 -0
  64. package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
  65. package/dist/elements/skeleton/index.d.ts +2 -0
  66. package/dist/elements/skeleton/index.js +1 -0
  67. package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
  68. package/dist/elements/skeleton/skeleton-types.js +1 -0
  69. package/dist/elements/spinner/Spinner.svelte +1 -1
  70. package/dist/elements/spinner/spinner-types.d.ts +20 -0
  71. package/dist/elements/spinner/spinner.d.ts +3 -3
  72. package/dist/elements/spinner/spinner.js +2 -2
  73. package/dist/elements/tooltip/Tooltip.svelte +108 -11
  74. package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
  75. package/dist/file-browser/FileBrowser.svelte +21 -12
  76. package/dist/filters/CompactFilters.svelte +221 -33
  77. package/dist/filters/CompactFilters.svelte.d.ts +1 -1
  78. package/dist/filters/FilterBar.svelte +184 -0
  79. package/dist/filters/FilterBar.svelte.d.ts +4 -0
  80. package/dist/filters/FilterPopover.svelte +346 -0
  81. package/dist/filters/FilterPopover.svelte.d.ts +4 -0
  82. package/dist/filters/date-presets.d.ts +15 -0
  83. package/dist/filters/date-presets.js +107 -0
  84. package/dist/filters/filter-types.d.ts +69 -3
  85. package/dist/filters/index.d.ts +5 -0
  86. package/dist/filters/index.js +4 -0
  87. package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
  88. package/dist/filters/sync-filters-to-url.svelte.js +114 -0
  89. package/dist/forms/DateRange.svelte +4 -2
  90. package/dist/forms/Input.svelte +2 -2
  91. package/dist/forms/MarketSelector.svelte +8 -3
  92. package/dist/forms/NumberInput.svelte +4 -4
  93. package/dist/forms/RadioGroup.svelte +123 -0
  94. package/dist/forms/RadioGroup.svelte.d.ts +4 -0
  95. package/dist/forms/SegmentedControl.svelte +11 -4
  96. package/dist/forms/Slider.svelte +72 -3
  97. package/dist/forms/Tags.svelte +14 -5
  98. package/dist/forms/Textarea.svelte +126 -0
  99. package/dist/forms/Textarea.svelte.d.ts +4 -0
  100. package/dist/forms/Toggle.svelte +8 -8
  101. package/dist/forms/calendar/Calendar.svelte +218 -0
  102. package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
  103. package/dist/forms/calendar/calendar-types.d.ts +46 -0
  104. package/dist/forms/calendar/calendar-types.js +1 -0
  105. package/dist/forms/calendar/index.d.ts +2 -0
  106. package/dist/forms/calendar/index.js +1 -0
  107. package/dist/forms/date-picker/DatePicker.svelte +144 -0
  108. package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
  109. package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
  110. package/dist/forms/date-picker/date-picker-types.js +1 -0
  111. package/dist/forms/form-types.d.ts +425 -6
  112. package/dist/forms/market/market-selector-types.d.ts +52 -1
  113. package/dist/forms/segmented-control.d.ts +5 -2
  114. package/dist/forms/segmented-control.js +16 -5
  115. package/dist/forms/slider.d.ts +3 -3
  116. package/dist/forms/slider.js +2 -2
  117. package/dist/funcs/user-management.remote.js +1 -1
  118. package/dist/header/Breadcrumbs.svelte +4 -20
  119. package/dist/header/PageHeader.svelte +6 -14
  120. package/dist/header/breadcrumbs.d.ts +3 -11
  121. package/dist/header/breadcrumbs.js +10 -5
  122. package/dist/header/header-types.d.ts +62 -11
  123. package/dist/index.d.ts +35 -9
  124. package/dist/index.js +24 -4
  125. package/dist/layout/activity-list/ActivityList.svelte +13 -7
  126. package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
  127. package/dist/layout/card/Card.svelte +12 -15
  128. package/dist/layout/card/MetricCard.svelte +50 -32
  129. package/dist/layout/card/card-types.d.ts +114 -4
  130. package/dist/layout/navbar/navbar-types.d.ts +48 -0
  131. package/dist/layout/navbar/navbar.d.ts +3 -3
  132. package/dist/layout/navbar/navbar.js +2 -2
  133. package/dist/layout/sidebar/Sidebar.svelte +87 -11
  134. package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
  135. package/dist/layout/stepper/Stepper.svelte +288 -0
  136. package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
  137. package/dist/layout/stepper/stepper-types.d.ts +80 -0
  138. package/dist/layout/stepper/stepper-types.js +1 -0
  139. package/dist/layout/table/Table.svelte +91 -85
  140. package/dist/layout/table/table-types.d.ts +148 -24
  141. package/dist/layout/table/table.d.ts +3 -3
  142. package/dist/layout/table/table.js +2 -2
  143. package/dist/layout/tabs/Tab.svelte +6 -2
  144. package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
  145. package/dist/layout/tabs/TabGroup.svelte +9 -2
  146. package/dist/layout/tabs/tabs-types.d.ts +63 -0
  147. package/dist/layout/tabs/tabs.d.ts +3 -3
  148. package/dist/layout/tabs/tabs.js +12 -6
  149. package/dist/modal/ConfirmDialog.svelte +65 -0
  150. package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
  151. package/dist/modal/Modal.svelte +6 -26
  152. package/dist/modal/confirm-dialog-types.d.ts +39 -0
  153. package/dist/modal/confirm-dialog-types.js +1 -0
  154. package/dist/modal/modal-types.d.ts +51 -12
  155. package/dist/modal/modal.d.ts +3 -3
  156. package/dist/modal/modal.js +3 -3
  157. package/dist/pipeline/Pipeline.svelte +8 -3
  158. package/dist/pipeline/pipeline-types.d.ts +55 -3
  159. package/dist/pipeline/pipeline.d.ts +18 -3
  160. package/dist/pipeline/pipeline.js +7 -2
  161. package/dist/server/s3.d.ts +35 -3
  162. package/dist/sonner/Toaster.svelte +29 -0
  163. package/dist/sonner/Toaster.svelte.d.ts +4 -0
  164. package/dist/sonner/index.d.ts +21 -0
  165. package/dist/sonner/index.js +20 -0
  166. package/dist/user-management/UserManagement.svelte +22 -16
  167. package/dist/user-management/UserModal.svelte +10 -7
  168. package/dist/user-management/UserTable.svelte +16 -17
  169. package/dist/user-management/UserViewModal.svelte +11 -11
  170. package/dist/user-management/user-management-types.d.ts +118 -31
  171. package/dist/variants.d.ts +1 -1
  172. package/dist/variants.js +1 -1
  173. package/package.json +7 -4
  174. package/dist/config/ai.d.ts +0 -13
  175. package/dist/config/ai.js +0 -44
  176. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
  177. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
  178. package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
  179. package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
  180. package/dist/helper/deprecation.d.ts +0 -14
  181. package/dist/helper/deprecation.js +0 -24
  182. package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
  183. 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)}` : ''}
@@ -12,7 +12,7 @@
12
12
  placeholder,
13
13
  disabled = false,
14
14
  class: className = '',
15
- size = Size.BASE,
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.BASE,
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 = 'horizontal',
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: showFlags ? countryCodeToFlagEmoji(code) : undefined
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
- {compact}
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.BASE,
13
+ size = Size.MD,
14
14
  class: className = '',
15
15
  units = [],
16
16
  errors,
17
17
  disabled = false,
18
- dropdownicon: DropdownIcon,
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.BASE,
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.BASE,
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>
@@ -0,0 +1,4 @@
1
+ import type { RadioGroupProps } from '../index.js';
2
+ declare const RadioGroup: import("svelte").Component<RadioGroupProps, {}, "value">;
3
+ type RadioGroup = ReturnType<typeof RadioGroup>;
4
+ export default RadioGroup;
@@ -17,7 +17,7 @@
17
17
  appearance = 'surface',
18
18
  orientation = 'horizontal',
19
19
  color = Color.PRIMARY,
20
- size = Size.BASE,
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
- const forward = orientation === 'horizontal' ? e.key === 'ArrowRight' : e.key === 'ArrowDown';
88
- const backward = orientation === 'horizontal' ? e.key === 'ArrowLeft' : e.key === 'ArrowUp';
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);
@@ -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.BASE,
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 baseClass = $derived(cn(base(), { 'mb-12': mode === 'enum' }, className));
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
@@ -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.BASE,
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
- <Badge {size} color="info" onclose={() => handleTagRemoval(tag)} class="shadow-xs">
139
- {tag}
140
- </Badge>
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.BASE,
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>