@makolabs/ripple 3.0.0 → 3.0.2

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 (34) hide show
  1. package/dist/elements/accordion/Accordion.svelte +1 -1
  2. package/dist/elements/combobox/ComboBox.svelte +59 -29
  3. package/dist/elements/dropdown/Select.svelte +98 -62
  4. package/dist/elements/dropdown/select.d.ts +3 -108
  5. package/dist/elements/dropdown/select.js +37 -46
  6. package/dist/elements/popover/Popover.svelte +59 -36
  7. package/dist/filters/CompactFilters.svelte +1 -1
  8. package/dist/forms/Checkbox.svelte +24 -9
  9. package/dist/forms/DateRange.svelte +236 -204
  10. package/dist/forms/Input.svelte +18 -18
  11. package/dist/forms/MarketSelector.svelte +1 -1
  12. package/dist/forms/NumberInput.svelte +160 -55
  13. package/dist/forms/RadioGroup.svelte +6 -2
  14. package/dist/forms/SegmentedControl.svelte +1 -1
  15. package/dist/forms/Tags.svelte +32 -11
  16. package/dist/forms/Textarea.svelte +8 -13
  17. package/dist/forms/Toggle.svelte +22 -14
  18. package/dist/forms/calendar/Calendar.svelte +107 -8
  19. package/dist/forms/calendar/calendar-types.d.ts +8 -0
  20. package/dist/forms/date-picker/DatePicker.svelte +11 -14
  21. package/dist/forms/form-size.d.ts +37 -0
  22. package/dist/forms/form-size.js +67 -0
  23. package/dist/forms/form-types.d.ts +33 -0
  24. package/dist/forms/month-picker/MonthPicker.svelte +299 -0
  25. package/dist/forms/month-picker/MonthPicker.svelte.d.ts +4 -0
  26. package/dist/forms/month-picker/month-picker-types.d.ts +22 -0
  27. package/dist/forms/month-picker/month-picker-types.js +1 -0
  28. package/dist/forms/segmented-control.d.ts +2 -2
  29. package/dist/forms/segmented-control.js +18 -15
  30. package/dist/forms/slider.js +35 -28
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/layout/activity-list/ActivityList.svelte +1 -1
  34. package/package.json +1 -1
@@ -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 {
@@ -12,32 +13,63 @@
12
13
  placeholder = 'Enter a number',
13
14
  size = Size.MD,
14
15
  class: className = '',
16
+ icon: LeadingIcon,
17
+ iconPreset,
15
18
  units = [],
16
19
  errors,
17
20
  disabled = false,
18
21
  dropdownIcon: DropdownIcon,
19
22
  onunitchange: onUnitChange,
23
+ formatThousands = true,
24
+ locale = 'en-US',
20
25
  testId,
21
26
  ...restProps
22
27
  }: NumberInputProps = $props();
23
28
 
29
+ const showPresetIcon = $derived(!LeadingIcon && iconPreset);
30
+
24
31
  let showUnitDropdown = $state(false);
25
32
  let containerRef = $state<HTMLDivElement | null>(null);
33
+ let inputFocused = $state(false);
26
34
 
27
35
  const selectedOption = $derived(units.find((u) => u.value === unit));
28
36
 
37
+ // Only render the dropdown trigger when there's something to switch
38
+ // between. A single-unit field looks like a static currency label
39
+ // rather than an editable selector — no chevron, no click target.
40
+ const hasMultipleUnits = $derived(units.length > 1);
41
+
42
+ // While focused, show the raw number so typing stays predictable
43
+ // (typed commas would otherwise fight the formatter). On blur we
44
+ // reformat with locale thousands separators for readability.
45
+ const displayValue = $derived(
46
+ value == null || Number.isNaN(value)
47
+ ? ''
48
+ : inputFocused || !formatThousands
49
+ ? String(value)
50
+ : value.toLocaleString(locale)
51
+ );
52
+
53
+ function handleInput(e: Event) {
54
+ const raw = (e.currentTarget as HTMLInputElement).value.replace(/[^0-9.-]/g, '');
55
+ if (raw === '' || raw === '-') {
56
+ value = 0;
57
+ return;
58
+ }
59
+ const num = Number(raw);
60
+ if (!Number.isNaN(num)) value = num;
61
+ }
62
+
63
+ const tokens = $derived(formSizeTokens[size]);
64
+
29
65
  const containerClass = $derived(
30
66
  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.MD,
39
- 'h-12': size === Size.LG
40
- },
67
+ 'relative flex items-center gap-1 border bg-white',
68
+ tokens.radius,
69
+ tokens.shadow,
70
+ tokens.height,
71
+ errors?.length && 'border-danger-300',
72
+ disabled && 'cursor-not-allowed opacity-50',
41
73
  'border-default-300 focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',
42
74
  className
43
75
  )
@@ -45,12 +77,9 @@
45
77
 
46
78
  const inputClass = $derived(
47
79
  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.MD,
52
- 'text-lg': size === Size.LG
53
- }
80
+ 'w-full bg-transparent outline-none disabled:cursor-not-allowed placeholder:text-default-400',
81
+ tokens.padX,
82
+ tokens.text
54
83
  )
55
84
  );
56
85
 
@@ -91,27 +120,27 @@
91
120
  >
92
121
  {/if}
93
122
  <div class={containerClass} bind:this={containerRef}>
94
- <svg
95
- xmlns="http://www.w3.org/2000/svg"
96
- width="24"
97
- height="24"
98
- viewBox="0 0 24 24"
99
- class="text-default-500 ml-3 size-4 flex-shrink-0"
100
- >
101
- <path
102
- fill="none"
103
- stroke="currentColor"
104
- stroke-linecap="round"
105
- stroke-linejoin="round"
106
- stroke-width="2"
107
- d="M17 9V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2m2 4h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2m7-5a2 2 0 1 1-4 0a2 2 0 0 1 4 0"
108
- />
109
- </svg>
123
+ {#if LeadingIcon}
124
+ <span class={cn('text-default-500 flex shrink-0 items-center', tokens.padX)}>
125
+ <LeadingIcon class={tokens.iconSize} />
126
+ </span>
127
+ {:else if showPresetIcon}
128
+ <span
129
+ class={cn('text-default-500 flex shrink-0 items-center', tokens.padX)}
130
+ aria-hidden="true"
131
+ >
132
+ {@render presetIconSvg(iconPreset)}
133
+ </span>
134
+ {/if}
110
135
  <input
111
136
  {name}
112
137
  id={name}
113
- bind:value
114
- type="number"
138
+ type="text"
139
+ inputmode="decimal"
140
+ value={displayValue}
141
+ oninput={handleInput}
142
+ onfocus={() => (inputFocused = true)}
143
+ onblur={() => (inputFocused = false)}
115
144
  {placeholder}
116
145
  {disabled}
117
146
  class={inputClass}
@@ -119,27 +148,41 @@
119
148
  {...restProps}
120
149
  />
121
150
 
122
- <button
123
- type="button"
124
- class="hover:bg-default-100 flex items-center gap-1 rounded px-1"
125
- onclick={handleUnitToggle}
126
- {disabled}
127
- >
128
- {#if selectedOption?.icon}
129
- {@const Icon = selectedOption.icon}
130
- <Icon />
131
- {/if}
132
- <span class="text-sm">{unit}</span>
133
- {#if DropdownIcon}
134
- <DropdownIcon class={iconClass} />
135
- {:else}
136
- <svg class={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
137
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
138
- </svg>
139
- {/if}
140
- </button>
141
-
142
- {#if showUnitDropdown}
151
+ {#if hasMultipleUnits}
152
+ <!-- Clickable selector: renders only when there's more than one
153
+ unit to pick from. Otherwise the unit is static text below. -->
154
+ <button
155
+ type="button"
156
+ class="hover:bg-default-100 flex items-center gap-1 rounded px-1"
157
+ onclick={handleUnitToggle}
158
+ {disabled}
159
+ >
160
+ {#if selectedOption?.icon}
161
+ {@const Icon = selectedOption.icon}
162
+ <Icon />
163
+ {/if}
164
+ <span class={cn(tokens.text)}>{unit}</span>
165
+ {#if DropdownIcon}
166
+ <DropdownIcon class={iconClass} />
167
+ {:else}
168
+ <svg class={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
169
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
170
+ </svg>
171
+ {/if}
172
+ </button>
173
+ {:else if unit}
174
+ <!-- Static unit label — no chevron, not clickable. Matches the
175
+ field's text size so it sits inline. -->
176
+ <span class={cn('text-default-500 flex items-center gap-1 pr-2', tokens.text)}>
177
+ {#if selectedOption?.icon}
178
+ {@const Icon = selectedOption.icon}
179
+ <Icon />
180
+ {/if}
181
+ {unit}
182
+ </span>
183
+ {/if}
184
+
185
+ {#if showUnitDropdown && hasMultipleUnits}
143
186
  <div class={dropdownClass}>
144
187
  {#each units as unitOption (unitOption.value)}
145
188
  <button
@@ -167,3 +210,65 @@
167
210
  {/each}
168
211
  {/if}
169
212
  </div>
213
+
214
+ {#snippet presetIconSvg(preset: string | undefined)}
215
+ {#if preset === 'currency'}
216
+ <svg
217
+ class={tokens.iconSize}
218
+ viewBox="0 0 24 24"
219
+ fill="none"
220
+ stroke="currentColor"
221
+ stroke-width="2"
222
+ stroke-linecap="round"
223
+ stroke-linejoin="round"
224
+ >
225
+ <path
226
+ d="M17 9V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2m2 4h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2m7-5a2 2 0 1 1-4 0a2 2 0 0 1 4 0"
227
+ />
228
+ </svg>
229
+ {:else if preset === 'quantity'}
230
+ <span class={cn(tokens.iconSize, 'flex items-center justify-center font-bold')}>#</span>
231
+ {:else if preset === 'percentage'}
232
+ <svg
233
+ class={tokens.iconSize}
234
+ viewBox="0 0 24 24"
235
+ fill="none"
236
+ stroke="currentColor"
237
+ stroke-width="2"
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"
240
+ >
241
+ <path d="M19 5 5 19" /><circle cx="6.5" cy="6.5" r="2.5" /><circle
242
+ cx="17.5"
243
+ cy="17.5"
244
+ r="2.5"
245
+ />
246
+ </svg>
247
+ {:else if preset === 'weight'}
248
+ <svg
249
+ class={tokens.iconSize}
250
+ viewBox="0 0 24 24"
251
+ fill="none"
252
+ stroke="currentColor"
253
+ stroke-width="2"
254
+ stroke-linecap="round"
255
+ stroke-linejoin="round"
256
+ >
257
+ <path
258
+ d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
259
+ /><path d="M3.27 6.96 12 12.01l8.73-5.05M12 22.08V12" />
260
+ </svg>
261
+ {:else if preset === 'temperature'}
262
+ <svg
263
+ class={tokens.iconSize}
264
+ viewBox="0 0 24 24"
265
+ fill="none"
266
+ stroke="currentColor"
267
+ stroke-width="2"
268
+ stroke-linecap="round"
269
+ stroke-linejoin="round"
270
+ >
271
+ <path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z" />
272
+ </svg>
273
+ {/if}
274
+ {/snippet}
@@ -2,6 +2,7 @@
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { buildTestId } from '../helper/testid.js';
4
4
  import { Color, Size } from '../variants.js';
5
+ import { formSizeTokens } from './form-size.js';
5
6
  import type { RadioGroupProps } from '../index.js';
6
7
 
7
8
  let {
@@ -21,7 +22,10 @@
21
22
  }: RadioGroupProps = $props();
22
23
 
23
24
  const hasErrors = $derived(errors.length > 0);
25
+ const tokens = $derived(formSizeTokens[size]);
24
26
 
27
+ // Radio circle scales with the form ladder — `size-3` at xs through
28
+ // `size-6` at xl. 2xl aliases xl.
25
29
  const dotSize = $derived(
26
30
  {
27
31
  [Size.XS]: 'size-3',
@@ -29,7 +33,7 @@
29
33
  [Size.MD]: 'size-4',
30
34
  [Size.LG]: 'size-5',
31
35
  [Size.XL]: 'size-6',
32
- [Size.XXL]: 'size-7'
36
+ [Size.XXL]: 'size-6'
33
37
  }[size]
34
38
  );
35
39
 
@@ -104,7 +108,7 @@
104
108
  {/if}
105
109
  </span>
106
110
  <span class="flex flex-col">
107
- <span class="text-default-800 text-sm">{option.label}</span>
111
+ <span class={cn('text-default-800', tokens.text)}>{option.label}</span>
108
112
  {#if option.description}
109
113
  <span class="text-default-500 text-xs">{option.description}</span>
110
114
  {/if}
@@ -41,7 +41,7 @@
41
41
 
42
42
  const rootClass = $derived(
43
43
  cn(
44
- orientation === 'auto' ? '@container w-full' : 'w-fit',
44
+ orientation === 'auto' ? '@container w-full' : 'w-full sm:w-fit',
45
45
  labelLayout === 'inline' ? 'flex flex-row items-center gap-2' : 'flex flex-col gap-2',
46
46
  orientation === 'auto' &&
47
47
  labelLayout === 'inline' &&
@@ -2,6 +2,7 @@
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 { formSizeTokens } from './form-size.js';
5
6
  import { fade } from 'svelte/transition';
6
7
  import { flip } from 'svelte/animate';
7
8
  import { quintOut } from 'svelte/easing';
@@ -34,6 +35,25 @@
34
35
  showSuggestions ? suggestions.filter(isUnselected).filter(matchesInput).slice(0, 5) : []
35
36
  );
36
37
 
38
+ const tokens = $derived(formSizeTokens[size]);
39
+
40
+ // Chip size shifted one tier down from the Tags container so chip
41
+ // text matches the container text (e.g. Tags `md` uses `text-xs`, so
42
+ // Badge `sm` — which is also `text-xs` — lines up, instead of Badge
43
+ // `md` which jumps to `text-sm`).
44
+ const chipSize = $derived(
45
+ (
46
+ {
47
+ [Size.XS]: Size.XS,
48
+ [Size.SM]: Size.XS,
49
+ [Size.MD]: Size.SM,
50
+ [Size.LG]: Size.MD,
51
+ [Size.XL]: Size.LG,
52
+ [Size.XXL]: Size.LG
53
+ } as const
54
+ )[size]
55
+ );
56
+
37
57
  function handleKeydown(event: KeyboardEvent) {
38
58
  if (event.key === 'Enter') {
39
59
  event.preventDefault();
@@ -108,12 +128,14 @@
108
128
 
109
129
  const containerClass = $derived(
110
130
  cn(
111
- 'relative flex flex-wrap gap-2 rounded-lg border bg-white shadow-xs px-3 py-2',
131
+ 'relative flex flex-wrap gap-2 border bg-white',
132
+ tokens.radius,
133
+ tokens.shadow,
134
+ tokens.padX,
135
+ tokens.padY,
112
136
  'border-default-300 focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',
113
- {
114
- 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500':
115
- errors?.length
116
- },
137
+ errors?.length &&
138
+ 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500',
117
139
  className
118
140
  )
119
141
  );
@@ -143,7 +165,7 @@
143
165
  transition:fade={{ duration: 250, easing: quintOut }}
144
166
  animate:flip={{ duration: 300, easing: quintOut }}
145
167
  >
146
- <Badge {size} color="info" onclose={() => handleTagRemoval(tag)} class="shadow-xs">
168
+ <Badge size={chipSize} color="info" onclose={() => handleTagRemoval(tag)} class="shadow-xs">
147
169
  {tag}
148
170
  </Badge>
149
171
  </div>
@@ -154,11 +176,10 @@
154
176
  {name}
155
177
  id={name}
156
178
  {placeholder}
157
- class={cn('placeholder:text-default-400 min-w-[120px] flex-1 bg-transparent outline-none', {
158
- 'text-sm': size === Size.SM,
159
- 'text-base': size === Size.MD,
160
- 'text-lg': size === Size.LG
161
- })}
179
+ class={cn(
180
+ 'placeholder:text-default-400 min-w-[120px] flex-1 bg-transparent outline-none',
181
+ tokens.text
182
+ )}
162
183
  type="text"
163
184
  autocomplete="off"
164
185
  onkeydown={handleKeydown}
@@ -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 { TextareaProps } from '../index.js';
6
7
 
7
8
  let {
@@ -27,25 +28,19 @@
27
28
 
28
29
  let el = $state<HTMLTextAreaElement | undefined>();
29
30
 
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
-
31
+ const tokens = $derived(formSizeTokens[size]);
41
32
  const hasErrors = $derived(errors.length > 0);
42
33
 
43
34
  const textareaClasses = $derived(
44
35
  cn(
45
- 'w-full rounded-lg border bg-white px-3 py-2 shadow-xs transition-colors',
36
+ 'w-full border bg-white transition-colors',
46
37
  'placeholder:text-default-400',
47
38
  'focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2',
48
- sizeClass,
39
+ tokens.radius,
40
+ tokens.shadow,
41
+ tokens.padX,
42
+ tokens.padY,
43
+ tokens.text,
49
44
  hasErrors
50
45
  ? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
51
46
  : 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
@@ -2,6 +2,7 @@
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { Color, Size } from '../variants.js';
4
4
  import { buildTestId } from '../helper/testid.js';
5
+ import { formSizeTokens } from './form-size.js';
5
6
  import type { ToggleProps, VariantColors, VariantSizes } from '../index.js';
6
7
 
7
8
  let {
@@ -35,15 +36,21 @@
35
36
  )[color]
36
37
  );
37
38
 
39
+ const tokens = $derived(formSizeTokens[size]);
40
+
41
+ // Track + thumb + on-state offset ladder. Chosen to roughly match the
42
+ // height of an Input at the same `size` (20/24/28/36/44px) so a row
43
+ // with a Toggle and an Input reads evenly. 2xl aliases xl — form
44
+ // controls cap at xl.
38
45
  const toggleSize = $derived(
39
46
  (
40
47
  {
41
- [Size.XS]: 'w-8 h-4',
48
+ [Size.XS]: 'w-7 h-3.5',
42
49
  [Size.SM]: 'w-8 h-4',
43
50
  [Size.MD]: 'w-10 h-5',
44
51
  [Size.LG]: 'w-12 h-6',
45
- [Size.XL]: 'w-12 h-6',
46
- [Size.XXL]: 'w-12 h-6'
52
+ [Size.XL]: 'w-14 h-7',
53
+ [Size.XXL]: 'w-14 h-7'
47
54
  } satisfies Record<VariantSizes, string>
48
55
  )[size]
49
56
  );
@@ -51,12 +58,12 @@
51
58
  const thumbSize = $derived(
52
59
  (
53
60
  {
54
- [Size.XS]: 'h-3 w-3',
61
+ [Size.XS]: 'h-2.5 w-2.5',
55
62
  [Size.SM]: 'h-3 w-3',
56
63
  [Size.MD]: 'h-4 w-4',
57
64
  [Size.LG]: 'h-5 w-5',
58
- [Size.XL]: 'h-5 w-5',
59
- [Size.XXL]: 'h-5 w-5'
65
+ [Size.XL]: 'h-6 w-6',
66
+ [Size.XXL]: 'h-6 w-6'
60
67
  } satisfies Record<VariantSizes, string>
61
68
  )[size]
62
69
  );
@@ -64,12 +71,12 @@
64
71
  const thumbPosition = $derived(
65
72
  (
66
73
  {
67
- [Size.XS]: value ? 'translate-x-4' : 'translate-x-0.5',
74
+ [Size.XS]: value ? 'translate-x-3.5' : 'translate-x-0.5',
68
75
  [Size.SM]: value ? 'translate-x-4' : 'translate-x-0.5',
69
76
  [Size.MD]: value ? 'translate-x-5' : 'translate-x-0.5',
70
77
  [Size.LG]: value ? 'translate-x-6' : 'translate-x-0.5',
71
- [Size.XL]: value ? 'translate-x-6' : 'translate-x-0.5',
72
- [Size.XXL]: value ? 'translate-x-6' : 'translate-x-0.5'
78
+ [Size.XL]: value ? 'translate-x-7' : 'translate-x-0.5',
79
+ [Size.XXL]: value ? 'translate-x-7' : 'translate-x-0.5'
73
80
  } satisfies Record<VariantSizes, string>
74
81
  )[size]
75
82
  );
@@ -112,11 +119,12 @@
112
119
  );
113
120
 
114
121
  const labelClasses = $derived(
115
- cn('text-sm font-medium', {
116
- 'text-default-700': !errors.length,
117
- 'text-danger-600': errors.length,
118
- 'opacity-50': disabled
119
- })
122
+ cn(
123
+ 'font-medium',
124
+ tokens.text,
125
+ errors.length ? 'text-danger-600' : 'text-default-700',
126
+ disabled && 'opacity-50'
127
+ )
120
128
  );
121
129
 
122
130
  function handleKeyDown(event: KeyboardEvent) {