@makolabs/ripple 2.5.9 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +403 -497
  2. package/dist/adapters/storage/S3Adapter.d.ts +49 -1
  3. package/dist/adapters/storage/S3Adapter.js +38 -1
  4. package/dist/adapters/storage/types.d.ts +20 -0
  5. package/dist/ai/AIChatInterface.svelte +2 -1
  6. package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
  7. package/dist/ai/CodeRenderer.svelte +7 -2
  8. package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
  9. package/dist/ai/ComposeDropdown.svelte +1 -1
  10. package/dist/ai/MessageBox.svelte +3 -3
  11. package/dist/ai/MessageBox.svelte.d.ts +3 -2
  12. package/dist/ai/ThinkingDisplay.svelte +4 -3
  13. package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
  14. package/dist/ai/ai-types.d.ts +55 -1
  15. package/dist/button/Button.svelte +5 -5
  16. package/dist/button/button-types.d.ts +49 -4
  17. package/dist/button/button.d.ts +9 -9
  18. package/dist/button/button.js +6 -6
  19. package/dist/charts/Chart.svelte +8 -16
  20. package/dist/charts/chart-types.d.ts +78 -1
  21. package/dist/drawer/Drawer.svelte +6 -26
  22. package/dist/drawer/drawer-types.d.ts +33 -12
  23. package/dist/drawer/drawer.d.ts +3 -3
  24. package/dist/drawer/drawer.js +1 -1
  25. package/dist/elements/accordion/Accordion.svelte +6 -17
  26. package/dist/elements/accordion/accordion-types.d.ts +53 -6
  27. package/dist/elements/alert/Alert.svelte +3 -0
  28. package/dist/elements/badge/Badge.svelte +1 -1
  29. package/dist/elements/badge/badge-types.d.ts +22 -0
  30. package/dist/elements/badge/badge.d.ts +3 -3
  31. package/dist/elements/badge/badge.js +1 -1
  32. package/dist/elements/combobox/ComboBox.svelte +244 -0
  33. package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
  34. package/dist/elements/combobox/combobox-types.d.ts +41 -0
  35. package/dist/elements/combobox/combobox-types.js +1 -0
  36. package/dist/elements/context-menu/ContextMenu.svelte +137 -0
  37. package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
  38. package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
  39. package/dist/elements/context-menu/context-menu-types.js +1 -0
  40. package/dist/elements/dropdown/Dropdown.svelte +1 -1
  41. package/dist/elements/dropdown/Select.svelte +4 -1
  42. package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
  43. package/dist/elements/dropdown/dropdown.d.ts +3 -3
  44. package/dist/elements/dropdown/dropdown.js +2 -2
  45. package/dist/elements/dropdown/select.d.ts +3 -108
  46. package/dist/elements/dropdown/select.js +38 -47
  47. package/dist/elements/empty-state/EmptyState.svelte +1 -1
  48. package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
  49. package/dist/elements/empty-state/empty-state.d.ts +3 -3
  50. package/dist/elements/empty-state/empty-state.js +2 -2
  51. package/dist/elements/file-upload/FileUpload.svelte +5 -0
  52. package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
  53. package/dist/elements/pagination/Pagination.svelte +53 -21
  54. package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
  55. package/dist/elements/popover/Popover.svelte +254 -0
  56. package/dist/elements/popover/Popover.svelte.d.ts +4 -0
  57. package/dist/elements/popover/index.d.ts +2 -0
  58. package/dist/elements/popover/index.js +1 -0
  59. package/dist/elements/popover/popover-types.d.ts +60 -0
  60. package/dist/elements/popover/popover-types.js +1 -0
  61. package/dist/elements/progress/Progress.svelte +32 -7
  62. package/dist/elements/progress/progress-types.d.ts +48 -1
  63. package/dist/elements/skeleton/Skeleton.svelte +56 -0
  64. package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
  65. package/dist/elements/skeleton/index.d.ts +2 -0
  66. package/dist/elements/skeleton/index.js +1 -0
  67. package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
  68. package/dist/elements/skeleton/skeleton-types.js +1 -0
  69. package/dist/elements/spinner/Spinner.svelte +1 -1
  70. package/dist/elements/spinner/spinner-types.d.ts +20 -0
  71. package/dist/elements/spinner/spinner.d.ts +3 -3
  72. package/dist/elements/spinner/spinner.js +2 -2
  73. package/dist/elements/tooltip/Tooltip.svelte +108 -11
  74. package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
  75. package/dist/file-browser/FileBrowser.svelte +21 -12
  76. package/dist/filters/CompactFilters.svelte +221 -33
  77. package/dist/filters/CompactFilters.svelte.d.ts +1 -1
  78. package/dist/filters/FilterBar.svelte +184 -0
  79. package/dist/filters/FilterBar.svelte.d.ts +4 -0
  80. package/dist/filters/FilterPopover.svelte +346 -0
  81. package/dist/filters/FilterPopover.svelte.d.ts +4 -0
  82. package/dist/filters/date-presets.d.ts +15 -0
  83. package/dist/filters/date-presets.js +107 -0
  84. package/dist/filters/filter-types.d.ts +69 -3
  85. package/dist/filters/index.d.ts +5 -0
  86. package/dist/filters/index.js +4 -0
  87. package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
  88. package/dist/filters/sync-filters-to-url.svelte.js +114 -0
  89. package/dist/forms/Checkbox.svelte +24 -9
  90. package/dist/forms/DateRange.svelte +23 -6
  91. package/dist/forms/Input.svelte +19 -19
  92. package/dist/forms/MarketSelector.svelte +9 -4
  93. package/dist/forms/NumberInput.svelte +14 -18
  94. package/dist/forms/RadioGroup.svelte +127 -0
  95. package/dist/forms/RadioGroup.svelte.d.ts +4 -0
  96. package/dist/forms/SegmentedControl.svelte +11 -4
  97. package/dist/forms/Slider.svelte +72 -3
  98. package/dist/forms/Tags.svelte +44 -14
  99. package/dist/forms/Textarea.svelte +121 -0
  100. package/dist/forms/Textarea.svelte.d.ts +4 -0
  101. package/dist/forms/Toggle.svelte +30 -22
  102. package/dist/forms/calendar/Calendar.svelte +315 -0
  103. package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
  104. package/dist/forms/calendar/calendar-types.d.ts +54 -0
  105. package/dist/forms/calendar/calendar-types.js +1 -0
  106. package/dist/forms/calendar/index.d.ts +2 -0
  107. package/dist/forms/calendar/index.js +1 -0
  108. package/dist/forms/date-picker/DatePicker.svelte +141 -0
  109. package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
  110. package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
  111. package/dist/forms/date-picker/date-picker-types.js +1 -0
  112. package/dist/forms/form-size.d.ts +37 -0
  113. package/dist/forms/form-size.js +67 -0
  114. package/dist/forms/form-types.d.ts +430 -6
  115. package/dist/forms/market/market-selector-types.d.ts +52 -1
  116. package/dist/forms/segmented-control.d.ts +5 -2
  117. package/dist/forms/segmented-control.js +25 -13
  118. package/dist/forms/slider.d.ts +3 -3
  119. package/dist/forms/slider.js +37 -30
  120. package/dist/funcs/user-management.remote.js +1 -1
  121. package/dist/header/Breadcrumbs.svelte +4 -20
  122. package/dist/header/PageHeader.svelte +6 -14
  123. package/dist/header/breadcrumbs.d.ts +3 -11
  124. package/dist/header/breadcrumbs.js +10 -5
  125. package/dist/header/header-types.d.ts +62 -11
  126. package/dist/index.d.ts +35 -9
  127. package/dist/index.js +24 -4
  128. package/dist/layout/activity-list/ActivityList.svelte +13 -7
  129. package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
  130. package/dist/layout/card/Card.svelte +12 -15
  131. package/dist/layout/card/MetricCard.svelte +50 -32
  132. package/dist/layout/card/card-types.d.ts +114 -4
  133. package/dist/layout/navbar/navbar-types.d.ts +48 -0
  134. package/dist/layout/navbar/navbar.d.ts +3 -3
  135. package/dist/layout/navbar/navbar.js +2 -2
  136. package/dist/layout/sidebar/Sidebar.svelte +87 -11
  137. package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
  138. package/dist/layout/stepper/Stepper.svelte +288 -0
  139. package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
  140. package/dist/layout/stepper/stepper-types.d.ts +80 -0
  141. package/dist/layout/stepper/stepper-types.js +1 -0
  142. package/dist/layout/table/Table.svelte +91 -85
  143. package/dist/layout/table/table-types.d.ts +148 -24
  144. package/dist/layout/table/table.d.ts +3 -3
  145. package/dist/layout/table/table.js +2 -2
  146. package/dist/layout/tabs/Tab.svelte +6 -2
  147. package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
  148. package/dist/layout/tabs/TabGroup.svelte +9 -2
  149. package/dist/layout/tabs/tabs-types.d.ts +63 -0
  150. package/dist/layout/tabs/tabs.d.ts +3 -3
  151. package/dist/layout/tabs/tabs.js +12 -6
  152. package/dist/modal/ConfirmDialog.svelte +65 -0
  153. package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
  154. package/dist/modal/Modal.svelte +6 -26
  155. package/dist/modal/confirm-dialog-types.d.ts +39 -0
  156. package/dist/modal/confirm-dialog-types.js +1 -0
  157. package/dist/modal/modal-types.d.ts +51 -12
  158. package/dist/modal/modal.d.ts +3 -3
  159. package/dist/modal/modal.js +3 -3
  160. package/dist/pipeline/Pipeline.svelte +8 -3
  161. package/dist/pipeline/pipeline-types.d.ts +55 -3
  162. package/dist/pipeline/pipeline.d.ts +18 -3
  163. package/dist/pipeline/pipeline.js +7 -2
  164. package/dist/server/s3.d.ts +35 -3
  165. package/dist/sonner/Toaster.svelte +29 -0
  166. package/dist/sonner/Toaster.svelte.d.ts +4 -0
  167. package/dist/sonner/index.d.ts +21 -0
  168. package/dist/sonner/index.js +20 -0
  169. package/dist/user-management/UserManagement.svelte +22 -16
  170. package/dist/user-management/UserModal.svelte +10 -7
  171. package/dist/user-management/UserTable.svelte +16 -17
  172. package/dist/user-management/UserViewModal.svelte +11 -11
  173. package/dist/user-management/user-management-types.d.ts +118 -31
  174. package/dist/variants.d.ts +1 -1
  175. package/dist/variants.js +1 -1
  176. package/package.json +7 -4
  177. package/dist/config/ai.d.ts +0 -13
  178. package/dist/config/ai.js +0 -44
  179. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
  180. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
  181. package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
  182. package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
  183. package/dist/helper/deprecation.d.ts +0 -14
  184. package/dist/helper/deprecation.js +0 -24
  185. package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
  186. package/dist/modal/ModalFooterTestWrapper.svelte.d.ts +0 -8
@@ -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
+ }
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { buildTestId } from '../helper/testid.js';
4
- import type { CheckboxProps } from '../index.js';
4
+ import { Size } from '../variants.js';
5
+ import { formSizeTokens } from './form-size.js';
6
+ import type { CheckboxProps, VariantSizes } from '../index.js';
5
7
 
6
8
  let {
7
9
  name,
@@ -10,21 +12,34 @@
10
12
  disabled = false,
11
13
  errors = [],
12
14
  required = false,
15
+ size = Size.MD,
13
16
  testId
14
17
  }: CheckboxProps = $props();
15
18
 
19
+ // Checkbox box dimension uses Tailwind's `size-*` shorthand, scaling
20
+ // from size-3 (XS) up to size-7 (XXL) so it stays visually proportional
21
+ // to adjacent form controls at the same size.
22
+ const boxSize: Record<VariantSizes, string> = {
23
+ [Size.XS]: 'size-3',
24
+ [Size.SM]: 'size-3.5',
25
+ [Size.MD]: 'size-4',
26
+ [Size.LG]: 'size-5',
27
+ [Size.XL]: 'size-6',
28
+ [Size.XXL]: 'size-7'
29
+ };
30
+ const tokens = $derived(formSizeTokens[size]);
31
+
16
32
  const checkboxClass = $derived(
17
- cn('w-4 h-4 rounded text-primary-600 border-default-300 focus:ring-primary-500', {
18
- 'opacity-50 cursor-not-allowed': disabled,
19
- 'accent-danger-500': errors.length
20
- })
33
+ cn(
34
+ 'rounded text-primary-600 border-default-300 focus:ring-primary-500',
35
+ boxSize[size],
36
+ disabled && 'opacity-50 cursor-not-allowed',
37
+ errors.length && 'accent-danger-500'
38
+ )
21
39
  );
22
40
 
23
41
  const labelClass = $derived(
24
- cn('text-sm font-medium', {
25
- 'text-default-700': !errors.length,
26
- 'text-danger-600': errors.length
27
- })
42
+ cn('font-medium', tokens.text, errors.length ? 'text-danger-600' : 'text-default-700')
28
43
  );
29
44
  </script>
30
45
 
@@ -1,7 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../helper/cls.js';
3
+ import { Size } from '../variants.js';
4
+ import { formSizeTokens } from './form-size.js';
3
5
  import type { DateRangeProps } from '../index.js';
4
6
  import Portal from '../utils/Portal.svelte';
7
+ import { fly } from 'svelte/transition';
8
+ import { quintOut } from 'svelte/easing';
5
9
 
6
10
  let {
7
11
  startDate = $bindable(),
@@ -15,11 +19,14 @@
15
19
  endLabel = 'End date',
16
20
  format = 'MM/dd/yyyy',
17
21
  errors = [],
22
+ size = Size.MD,
18
23
  id,
19
24
  name,
20
25
  onselect
21
26
  }: DateRangeProps = $props();
22
27
 
28
+ const tokens = $derived(formSizeTokens[size]);
29
+
23
30
  let isOpen = $state(false);
24
31
  let hoveredDate = $state<Date | null>(null);
25
32
  let datePickerRef = $state<HTMLDivElement | null>(null);
@@ -237,7 +244,12 @@
237
244
  {id}
238
245
  type="button"
239
246
  class={cn(
240
- 'border-default-300 flex w-full items-center justify-between rounded-lg border bg-white px-3 py-2 text-sm shadow-xs',
247
+ 'border-default-300 flex w-full items-center justify-between border bg-white',
248
+ tokens.height,
249
+ tokens.padX,
250
+ tokens.text,
251
+ tokens.radius,
252
+ tokens.shadow,
241
253
  disabled
242
254
  ? 'bg-default-100 text-default-400 cursor-not-allowed opacity-50'
243
255
  : errors?.length
@@ -247,7 +259,6 @@
247
259
  onclick={toggleDatepicker}
248
260
  aria-haspopup="true"
249
261
  aria-expanded={isOpen}
250
- aria-invalid={errors?.length ? 'true' : undefined}
251
262
  aria-describedby={errors?.length ? `${id}-errors` : undefined}
252
263
  {disabled}
253
264
  >
@@ -258,7 +269,7 @@
258
269
  ? `${formatDate(startDate)} - Select end date`
259
270
  : placeholder}
260
271
  </span>
261
- <svg class="text-default-400 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
272
+ <svg class={cn('text-default-400', tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
262
273
  <path
263
274
  fill-rule="evenodd"
264
275
  d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
@@ -270,11 +281,16 @@
270
281
  {#if startDate || endDate}
271
282
  <button
272
283
  type="button"
273
- class="text-default-400 hover:text-default-500 absolute top-1/2 right-10 -translate-y-1/2"
284
+ class={cn(
285
+ 'text-default-400 hover:text-default-500 absolute top-1/2 -translate-y-1/2',
286
+ // Sit just left of the calendar icon; use the token gap
287
+ // so the clear button stays visually tied to the icon.
288
+ 'right-8'
289
+ )}
274
290
  onclick={clearDates}
275
291
  aria-label="Clear dates"
276
292
  >
277
- <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
293
+ <svg class={cn(tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
278
294
  <path
279
295
  fill-rule="evenodd"
280
296
  d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
@@ -298,6 +314,7 @@
298
314
  <div
299
315
  bind:this={calendarRef}
300
316
  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"
317
+ transition:fly={{ y: -8, duration: 300, easing: quintOut }}
301
318
  >
302
319
  <div class="mb-2 flex items-center justify-between">
303
320
  {#if viewMode === 'days'}
@@ -488,7 +505,7 @@
488
505
 
489
506
  {#if startDate || endDate}
490
507
  <div
491
- class="border-default-200 text-default-500 mt-4 flex justify-between border-t pt-3 text-xs"
508
+ 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
509
  >
493
510
  <div>
494
511
  {startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}
@@ -2,6 +2,7 @@
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { buildTestId } from '../helper/testid.js';
4
4
  import { Size } from '../variants.js';
5
+ import { formSizeTokens } from './form-size.js';
5
6
  import type { InputProps } from '../index.js';
6
7
 
7
8
  let {
@@ -12,7 +13,7 @@
12
13
  placeholder,
13
14
  disabled = false,
14
15
  class: className = '',
15
- size = Size.BASE,
16
+ size = Size.MD,
16
17
  value = $bindable(),
17
18
  errors = [],
18
19
  testId,
@@ -20,26 +21,25 @@
20
21
  }: InputProps = $props();
21
22
 
22
23
  const BASIC_TYPES = ['text', 'email', 'password', 'number', 'tel', 'url', 'date', 'textarea'];
24
+ const tokens = $derived(formSizeTokens[size]);
23
25
  const inputClasses = $derived(
24
26
  cn(
25
- 'transition-colors placeholder:text-default-400',
26
- {
27
- 'border rounded-lg shadow-xs w-full bg-white px-3 py-2 text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2':
28
- BASIC_TYPES.includes(type),
29
- 'w-full bg-white px-3 py-2 text-sm resize-y min-h-[100px]': type === 'textarea',
30
- 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500':
31
- errors.length,
32
- 'opacity-50 cursor-not-allowed': disabled,
33
- ...(BASIC_TYPES.includes(type)
34
- ? {
35
- 'h-8 text-sm': size === Size.SM,
36
- 'h-10 text-base': size === Size.BASE,
37
- 'h-12 text-lg': size === Size.LG
38
- }
39
- : {}),
40
- 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500':
41
- !errors.length
42
- },
27
+ 'w-full bg-white transition-colors placeholder:text-default-400',
28
+ tokens.padX,
29
+ tokens.text,
30
+ // All basic types (including textarea) get the bordered look.
31
+ BASIC_TYPES.includes(type) && [
32
+ 'border focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2',
33
+ tokens.radius,
34
+ tokens.shadow
35
+ ],
36
+ // Single-line types get a fixed height; textarea is content-driven.
37
+ type !== 'textarea' && tokens.height,
38
+ type === 'textarea' && ['resize-y min-h-[100px]', tokens.padY],
39
+ errors.length
40
+ ? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
41
+ : 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
42
+ disabled && 'opacity-50 cursor-not-allowed',
43
43
  className
44
44
  )
45
45
  );
@@ -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
- size = Size.SM,
22
+ size = Size.MD,
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}
@@ -2,6 +2,7 @@
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { buildTestId } from '../helper/testid.js';
4
4
  import { Size } from '../variants.js';
5
+ import { formSizeTokens } from './form-size.js';
5
6
  import type { NumberInputProps } from '../index.js';
6
7
 
7
8
  let {
@@ -10,12 +11,12 @@
10
11
  name,
11
12
  label,
12
13
  placeholder = 'Enter a number',
13
- size = Size.BASE,
14
+ size = Size.MD,
14
15
  class: className = '',
15
16
  units = [],
16
17
  errors,
17
18
  disabled = false,
18
- dropdownicon: DropdownIcon,
19
+ dropdownIcon: DropdownIcon,
19
20
  onunitchange: onUnitChange,
20
21
  testId,
21
22
  ...restProps
@@ -26,18 +27,16 @@
26
27
 
27
28
  const selectedOption = $derived(units.find((u) => u.value === unit));
28
29
 
30
+ const tokens = $derived(formSizeTokens[size]);
31
+
29
32
  const containerClass = $derived(
30
33
  cn(
31
- 'relative flex items-center gap-1 rounded-lg border bg-white shadow-xs',
32
- {
33
- 'border-danger-300': errors?.length,
34
- 'cursor-not-allowed opacity-50': disabled
35
- },
36
- {
37
- 'h-8': size === Size.SM,
38
- 'h-10': size === Size.BASE,
39
- 'h-12': size === Size.LG
40
- },
34
+ 'relative flex items-center gap-1 border bg-white',
35
+ tokens.radius,
36
+ tokens.shadow,
37
+ tokens.height,
38
+ errors?.length && 'border-danger-300',
39
+ disabled && 'cursor-not-allowed opacity-50',
41
40
  'border-default-300 focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',
42
41
  className
43
42
  )
@@ -45,12 +44,9 @@
45
44
 
46
45
  const inputClass = $derived(
47
46
  cn(
48
- 'w-full bg-transparent outline-none disabled:cursor-not-allowed px-3 placeholder:text-default-400',
49
- {
50
- 'text-sm': size === Size.SM,
51
- 'text-base': size === Size.BASE,
52
- 'text-lg': size === Size.LG
53
- }
47
+ 'w-full bg-transparent outline-none disabled:cursor-not-allowed placeholder:text-default-400',
48
+ tokens.padX,
49
+ tokens.text
54
50
  )
55
51
  );
56
52
 
@@ -0,0 +1,127 @@
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 { formSizeTokens } from './form-size.js';
6
+ import type { RadioGroupProps } from '../index.js';
7
+
8
+ let {
9
+ name,
10
+ label,
11
+ options,
12
+ value = $bindable<string | undefined>(undefined),
13
+ disabled = false,
14
+ required = false,
15
+ orientation = 'vertical',
16
+ size = Size.MD,
17
+ color = Color.PRIMARY,
18
+ errors = [],
19
+ class: className = '',
20
+ onchange,
21
+ testId
22
+ }: RadioGroupProps = $props();
23
+
24
+ const hasErrors = $derived(errors.length > 0);
25
+ const tokens = $derived(formSizeTokens[size]);
26
+
27
+ // Radio circle scales with the form ladder — `size-3` at xs through
28
+ // `size-6` at xl. 2xl aliases xl.
29
+ const dotSize = $derived(
30
+ {
31
+ [Size.XS]: 'size-3',
32
+ [Size.SM]: 'size-3.5',
33
+ [Size.MD]: 'size-4',
34
+ [Size.LG]: 'size-5',
35
+ [Size.XL]: 'size-6',
36
+ [Size.XXL]: 'size-6'
37
+ }[size]
38
+ );
39
+
40
+ const accentBg = $derived(
41
+ {
42
+ [Color.DEFAULT]: 'bg-default-600',
43
+ [Color.PRIMARY]: 'bg-primary-500',
44
+ [Color.SECONDARY]: 'bg-secondary-500',
45
+ [Color.INFO]: 'bg-info-500',
46
+ [Color.SUCCESS]: 'bg-success-500',
47
+ [Color.WARNING]: 'bg-warning-500',
48
+ [Color.DANGER]: 'bg-danger-500'
49
+ }[color]
50
+ );
51
+
52
+ function handlePick(next: string, optionDisabled: boolean) {
53
+ if (disabled || optionDisabled) return;
54
+ value = next;
55
+ onchange?.(next);
56
+ }
57
+ </script>
58
+
59
+ <fieldset
60
+ class={cn('w-full', className)}
61
+ {disabled}
62
+ aria-invalid={hasErrors}
63
+ data-testid={buildTestId('radio-group', undefined, testId)}
64
+ >
65
+ {#if label}
66
+ <legend class="text-default-700 mb-2 block text-sm font-medium">
67
+ {label}
68
+ {#if required}<span class="text-danger-500" aria-hidden="true">*</span>{/if}
69
+ </legend>
70
+ {/if}
71
+
72
+ <div
73
+ class={cn('flex gap-3', orientation === 'vertical' ? 'flex-col' : 'flex-wrap items-center')}
74
+ role="radiogroup"
75
+ aria-label={label}
76
+ >
77
+ {#each options as option (option.value)}
78
+ {@const checked = value === option.value}
79
+ {@const optionDisabled = disabled || option.disabled === true}
80
+ <label
81
+ class={cn(
82
+ 'group inline-flex cursor-pointer items-start gap-2 select-none',
83
+ optionDisabled && 'cursor-not-allowed opacity-60'
84
+ )}
85
+ >
86
+ <input
87
+ type="radio"
88
+ {name}
89
+ value={option.value}
90
+ {checked}
91
+ disabled={optionDisabled}
92
+ required={required && !value}
93
+ onchange={() => handlePick(option.value, optionDisabled)}
94
+ class="sr-only"
95
+ data-testid={buildTestId('radio-group', option.value, testId)}
96
+ />
97
+ <span
98
+ aria-hidden="true"
99
+ class={cn(
100
+ 'relative mt-0.5 flex shrink-0 items-center justify-center rounded-full border transition-colors',
101
+ dotSize,
102
+ checked ? `${accentBg} border-transparent` : 'border-default-300 bg-white',
103
+ hasErrors && !checked && 'border-danger-300'
104
+ )}
105
+ >
106
+ {#if checked}
107
+ <span class="size-1/3 rounded-full bg-white"></span>
108
+ {/if}
109
+ </span>
110
+ <span class="flex flex-col">
111
+ <span class={cn('text-default-800', tokens.text)}>{option.label}</span>
112
+ {#if option.description}
113
+ <span class="text-default-500 text-xs">{option.description}</span>
114
+ {/if}
115
+ </span>
116
+ </label>
117
+ {/each}
118
+ </div>
119
+
120
+ {#if hasErrors}
121
+ <ul class="mt-2 space-y-0.5" role="alert">
122
+ {#each errors as error (error)}
123
+ <li class="text-danger-600 text-xs">{error}</li>
124
+ {/each}
125
+ </ul>
126
+ {/if}
127
+ </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);