@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
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
3
  import { buildTestId } from '../../helper/testid.js';
4
+ import { Size } from '../../variants.js';
5
+ import type { VariantSizes } from '../../index.js';
4
6
  import type { CalendarProps } from './calendar-types.js';
5
7
 
6
8
  let {
@@ -14,11 +16,91 @@
14
16
  weekStartsOn = 1,
15
17
  hideHeader = false,
16
18
  disabled = false,
19
+ size = Size.MD,
17
20
  class: className = '',
18
21
  onselect,
19
22
  testId
20
23
  }: CalendarProps = $props();
21
24
 
25
+ // Calendar dimensions don't map cleanly onto form-size tokens — a
26
+ // date grid's "size" means cell + panel width, not input height. So
27
+ // we maintain a dedicated ladder here, tuned so a Calendar at the
28
+ // same `size` as its enclosing DatePicker feels proportional.
29
+ type CalendarDensity = {
30
+ panel: string;
31
+ padding: string;
32
+ cell: string;
33
+ navBtn: string;
34
+ navIcon: string;
35
+ monthText: string;
36
+ dayHeaderText: string;
37
+ cellText: string;
38
+ };
39
+ const calendarSize: Record<VariantSizes, CalendarDensity> = {
40
+ [Size.XS]: {
41
+ panel: 'w-48',
42
+ padding: 'p-2',
43
+ cell: 'size-6',
44
+ navBtn: 'size-5',
45
+ navIcon: 'size-3',
46
+ monthText: 'text-xs',
47
+ dayHeaderText: 'text-[9px]',
48
+ cellText: 'text-[10px]'
49
+ },
50
+ [Size.SM]: {
51
+ panel: 'w-56',
52
+ padding: 'p-2.5',
53
+ cell: 'size-7',
54
+ navBtn: 'size-6',
55
+ navIcon: 'size-3.5',
56
+ monthText: 'text-xs',
57
+ dayHeaderText: 'text-[10px]',
58
+ cellText: 'text-xs'
59
+ },
60
+ [Size.MD]: {
61
+ panel: 'w-64',
62
+ padding: 'p-3',
63
+ cell: 'size-8',
64
+ navBtn: 'size-7',
65
+ navIcon: 'size-4',
66
+ monthText: 'text-sm',
67
+ dayHeaderText: 'text-[10px]',
68
+ cellText: 'text-xs'
69
+ },
70
+ [Size.LG]: {
71
+ panel: 'w-72',
72
+ padding: 'p-3.5',
73
+ cell: 'size-9',
74
+ navBtn: 'size-8',
75
+ navIcon: 'size-4',
76
+ monthText: 'text-base',
77
+ dayHeaderText: 'text-xs',
78
+ cellText: 'text-sm'
79
+ },
80
+ [Size.XL]: {
81
+ panel: 'w-80',
82
+ padding: 'p-4',
83
+ cell: 'size-10',
84
+ navBtn: 'size-9',
85
+ navIcon: 'size-5',
86
+ monthText: 'text-lg',
87
+ dayHeaderText: 'text-xs',
88
+ cellText: 'text-sm'
89
+ },
90
+ // Form controls cap at xl — see `form-size.ts`.
91
+ [Size.XXL]: {
92
+ panel: 'w-80',
93
+ padding: 'p-4',
94
+ cell: 'size-10',
95
+ navBtn: 'size-9',
96
+ navIcon: 'size-5',
97
+ monthText: 'text-lg',
98
+ dayHeaderText: 'text-xs',
99
+ cellText: 'text-sm'
100
+ }
101
+ };
102
+ const density = $derived(calendarSize[size]);
103
+
22
104
  const anchor = $derived(initialMonth ?? value ?? valueStart ?? new Date());
23
105
 
24
106
  let viewYear = $state(anchor.getFullYear());
@@ -146,7 +228,11 @@
146
228
 
147
229
  <div
148
230
  class={cn(
149
- 'border-default-200 inline-block w-64 rounded-lg border bg-white p-3 shadow-xs select-none',
231
+ 'inline-block bg-white select-none',
232
+ 'border-default-200 rounded-lg border shadow-xs',
233
+ 'max-sm:w-full max-sm:rounded-none max-sm:border-0 max-sm:shadow-none',
234
+ density.panel,
235
+ density.padding,
150
236
  className
151
237
  )}
152
238
  data-testid={buildTestId('calendar', undefined, testId)}
@@ -156,11 +242,14 @@
156
242
  <button
157
243
  type="button"
158
244
  onclick={prevMonth}
159
- class="text-default-500 hover:bg-default-100 hover:text-default-800 flex size-7 cursor-pointer items-center justify-center rounded"
245
+ class={cn(
246
+ 'text-default-500 hover:bg-default-100 hover:text-default-800 flex cursor-pointer items-center justify-center rounded',
247
+ density.navBtn
248
+ )}
160
249
  aria-label="Previous month"
161
250
  {disabled}
162
251
  >
163
- <svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
252
+ <svg class={density.navIcon} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
164
253
  <path
165
254
  fill-rule="evenodd"
166
255
  d="M12.78 5.22a.75.75 0 0 1 0 1.06L9.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0z"
@@ -168,15 +257,18 @@
168
257
  />
169
258
  </svg>
170
259
  </button>
171
- <span class="text-default-800 text-sm font-semibold">{monthLabel}</span>
260
+ <span class={cn('text-default-800 font-semibold', density.monthText)}>{monthLabel}</span>
172
261
  <button
173
262
  type="button"
174
263
  onclick={nextMonth}
175
- class="text-default-500 hover:bg-default-100 hover:text-default-800 flex size-7 cursor-pointer items-center justify-center rounded"
264
+ class={cn(
265
+ 'text-default-500 hover:bg-default-100 hover:text-default-800 flex cursor-pointer items-center justify-center rounded',
266
+ density.navBtn
267
+ )}
176
268
  aria-label="Next month"
177
269
  {disabled}
178
270
  >
179
- <svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
271
+ <svg class={density.navIcon} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
180
272
  <path
181
273
  fill-rule="evenodd"
182
274
  d="M7.22 14.78a.75.75 0 0 1 0-1.06L10.94 10 7.22 6.28a.75.75 0 0 1 1.06-1.06l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0z"
@@ -187,7 +279,12 @@
187
279
  </div>
188
280
  {/if}
189
281
 
190
- <div class="text-default-400 mb-1 grid grid-cols-7 gap-0.5 text-center text-[10px] font-medium">
282
+ <div
283
+ class={cn(
284
+ 'text-default-400 mb-1 grid grid-cols-7 gap-0.5 text-center font-medium',
285
+ density.dayHeaderText
286
+ )}
287
+ >
191
288
  {#each dayHeaders() as d (d)}
192
289
  <div>{d}</div>
193
290
  {/each}
@@ -202,7 +299,9 @@
202
299
  aria-pressed={cell.isSelected}
203
300
  aria-label={cell.date.toLocaleDateString()}
204
301
  class={cn(
205
- 'relative flex size-8 items-center justify-center rounded text-xs transition-colors',
302
+ 'relative flex items-center justify-center rounded transition-colors',
303
+ density.cell,
304
+ density.cellText,
206
305
  !cell.inMonth && 'text-default-300',
207
306
  cell.inMonth && !cell.disabled && 'text-default-700 hover:bg-default-100 cursor-pointer',
208
307
  cell.disabled && 'text-default-200 cursor-not-allowed',
@@ -1,4 +1,5 @@
1
1
  import type { ClassValue } from 'tailwind-variants';
2
+ import type { VariantSizes } from '../../index.js';
2
3
  /**
3
4
  * Calendar selection mode.
4
5
  * - `'single'`: one date. Bind to `value`.
@@ -31,6 +32,13 @@ export type CalendarProps = {
31
32
  hideHeader?: boolean;
32
33
  /** Disable all interaction. */
33
34
  disabled?: boolean;
35
+ /**
36
+ * Density preset. Scales day cells, headers, and overall panel width
37
+ * to match the tight form-size ladder so a `size="sm"` Calendar fits
38
+ * inside a `size="sm"` DatePicker / DateRange popover without feeling
39
+ * oversized. `2xl` aliases `xl`. @default 'md'
40
+ */
41
+ size?: VariantSizes;
34
42
  /** Wrapper class. */
35
43
  class?: ClassValue;
36
44
  /**
@@ -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 Popover from '../../elements/popover/Popover.svelte';
6
7
  import Calendar from '../calendar/Calendar.svelte';
7
8
  import type { DatePickerProps } from './date-picker-types.js';
@@ -38,16 +39,7 @@
38
39
  const display = $derived(value ? formatDate(value) : '');
39
40
  const hasErrors = $derived(errors.length > 0);
40
41
 
41
- const sizeClass = $derived(
42
- {
43
- [Size.XS]: 'h-7 text-xs',
44
- [Size.SM]: 'h-8 text-sm',
45
- [Size.MD]: 'h-10 text-sm',
46
- [Size.LG]: 'h-12 text-base',
47
- [Size.XL]: 'h-14 text-lg',
48
- [Size.XXL]: 'h-16 text-lg'
49
- }[size]
50
- );
42
+ const tokens = $derived(formSizeTokens[size]);
51
43
 
52
44
  function clear(e: MouseEvent) {
53
45
  e.stopPropagation();
@@ -83,9 +75,14 @@
83
75
  aria-expanded={open}
84
76
  aria-invalid={hasErrors}
85
77
  class={cn(
86
- 'flex w-full items-center justify-between gap-2 rounded-lg border bg-white px-3 transition-colors',
78
+ 'flex w-full items-center justify-between border bg-white transition-colors',
87
79
  'focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none',
88
- sizeClass,
80
+ tokens.height,
81
+ tokens.padX,
82
+ tokens.text,
83
+ tokens.gap,
84
+ tokens.radius,
85
+ tokens.shadow,
89
86
  hasErrors
90
87
  ? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
91
88
  : 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
@@ -115,7 +112,7 @@
115
112
  </button>
116
113
  {:else}
117
114
  <svg
118
- class="text-default-400 size-4"
115
+ class={cn('text-default-400', tokens.iconSize)}
119
116
  viewBox="0 0 20 20"
120
117
  fill="currentColor"
121
118
  aria-hidden="true"
@@ -130,7 +127,7 @@
130
127
  </button>
131
128
 
132
129
  {#snippet content()}
133
- <Calendar {value} {minDate} {maxDate} onselect={(d) => handleSelect(d as Date)} />
130
+ <Calendar {value} {minDate} {maxDate} {size} onselect={(d) => handleSelect(d as Date)} />
134
131
  {/snippet}
135
132
  </Popover>
136
133
 
@@ -0,0 +1,37 @@
1
+ import type { VariantSizes } from '../index.js';
2
+ /**
3
+ * Canonical size tokens used by every form control — `Input`, `Textarea`,
4
+ * `NumberInput`, `Tags`, `Checkbox`, and the select-based controls.
5
+ *
6
+ * The goal is that a form using `size="sm"` across mixed controls looks
7
+ * vertically aligned and typographically coordinated. Before this map,
8
+ * each component had its own inline size map that disagreed with the
9
+ * others (e.g. `Input` only supported SM/MD/LG while `Textarea` supported
10
+ * all six, and `MD` meant `text-base` in `Input` but `text-sm` in
11
+ * `Textarea`).
12
+ *
13
+ * Multi-line controls (Textarea) read only `padX` / `padY` / `text` —
14
+ * height is content-driven. Single-line controls also read `height`.
15
+ */
16
+ export type FormSizeTokens = {
17
+ /** Control height for single-line inputs (e.g. `h-10`). */
18
+ height: string;
19
+ /** Horizontal padding token (e.g. `px-3`). */
20
+ padX: string;
21
+ /** Vertical padding, used by multi-line controls and chip rows. */
22
+ padY: string;
23
+ /** Font size token (e.g. `text-base`). */
24
+ text: string;
25
+ /** Inline gap between leading icon and text (e.g. `gap-2`). */
26
+ gap: string;
27
+ /** Inline icon dimension (e.g. `size-4`). */
28
+ iconSize: string;
29
+ /** Rounded corner radius that scales with the control (e.g. `rounded-lg`). */
30
+ radius: string;
31
+ /**
32
+ * Drop shadow. `shadow-none` at the smallest sizes so `xs` / inline
33
+ * table cells don't stand out against their container.
34
+ */
35
+ shadow: string;
36
+ };
37
+ export declare const formSizeTokens: Record<VariantSizes, FormSizeTokens>;
@@ -0,0 +1,67 @@
1
+ import { Size } from '../variants.js';
2
+ // Tight ladder by design — we prefer compact, focused form controls
3
+ // over bulky ones. `md` (the default) sits at `h-7` / `text-xs` so a
4
+ // stock form reads dense; consumers who want roomier controls opt up to
5
+ // `lg` / `xl`. `xs` is unmistakably inline-text-in-a-table-cell — 20px
6
+ // tall, no shadow, barely-there rounded corners.
7
+ //
8
+ // `2xl` (`Size.XXL`) intentionally aliases `xl` for form controls: form
9
+ // fields don't need a sixth, jumbo tier — anything larger reads as a
10
+ // display element rather than an input. We still accept the token (the
11
+ // `Size` enum exposes it library-wide) and quietly fall back to `xl`
12
+ // rather than requiring every component to exclude it from its size type.
13
+ const xl = {
14
+ height: 'h-11',
15
+ padX: 'px-3.5',
16
+ padY: 'py-2.5',
17
+ text: 'text-base',
18
+ gap: 'gap-2.5',
19
+ iconSize: 'size-5',
20
+ radius: 'rounded-lg',
21
+ shadow: 'shadow-xs'
22
+ };
23
+ export const formSizeTokens = {
24
+ [Size.XS]: {
25
+ height: 'h-5',
26
+ padX: 'px-1',
27
+ padY: 'py-0',
28
+ text: 'text-xs',
29
+ gap: 'gap-1',
30
+ iconSize: 'size-3',
31
+ radius: 'rounded-sm',
32
+ shadow: 'shadow-none'
33
+ },
34
+ [Size.SM]: {
35
+ height: 'h-6',
36
+ padX: 'px-1.5',
37
+ padY: 'py-0.5',
38
+ text: 'text-xs',
39
+ gap: 'gap-1',
40
+ iconSize: 'size-3',
41
+ radius: 'rounded',
42
+ shadow: 'shadow-xs'
43
+ },
44
+ [Size.MD]: {
45
+ height: 'h-7',
46
+ padX: 'px-2',
47
+ padY: 'py-1',
48
+ text: 'text-xs',
49
+ gap: 'gap-1.5',
50
+ iconSize: 'size-3.5',
51
+ radius: 'rounded-md',
52
+ shadow: 'shadow-xs'
53
+ },
54
+ [Size.LG]: {
55
+ height: 'h-9',
56
+ padX: 'px-2.5',
57
+ padY: 'py-1.5',
58
+ text: 'text-sm',
59
+ gap: 'gap-2',
60
+ iconSize: 'size-4',
61
+ radius: 'rounded-md',
62
+ shadow: 'shadow-xs'
63
+ },
64
+ [Size.XL]: xl,
65
+ // Form controls cap at xl visually — see comment above.
66
+ [Size.XXL]: xl
67
+ };
@@ -236,6 +236,24 @@ export type NumberInputProps = {
236
236
  placeholder?: string;
237
237
  size?: VariantSizes;
238
238
  class?: ClassValue;
239
+ /**
240
+ * Leading icon rendered inside the field. Pass a Svelte component
241
+ * (e.g. an icon from your icon library) to fully customise, or use
242
+ * `iconPreset` for built-in options. When both are set, `icon` wins.
243
+ */
244
+ icon?: Component;
245
+ /**
246
+ * Built-in leading icon preset. Saves importing a separate icon
247
+ * component for common use cases.
248
+ * - `'currency'` — banknotes
249
+ * - `'quantity'` — hash / #
250
+ * - `'percentage'` — %
251
+ * - `'weight'` — scale
252
+ * - `'temperature'` — thermometer
253
+ *
254
+ * Omit (or pass `undefined`) for no icon. Overridden by `icon` prop.
255
+ */
256
+ iconPreset?: 'currency' | 'quantity' | 'percentage' | 'weight' | 'temperature';
239
257
  /**
240
258
  * Currently selected unit. When `units` is provided, this doubles as
241
259
  * the controlled value of the unit dropdown.
@@ -252,6 +270,16 @@ export type NumberInputProps = {
252
270
  dropdownIcon?: Component;
253
271
  /** Fires when the user picks a different unit. */
254
272
  onunitchange?: (prevUnit: string, newUnit: string) => void;
273
+ /**
274
+ * Format large numbers with thousands separators for readability
275
+ * (e.g. `1,232,312`). Formatting is applied on blur; while focused,
276
+ * the raw number is shown so typing stays predictable. @default true
277
+ */
278
+ formatThousands?: boolean;
279
+ /**
280
+ * BCP 47 locale used for thousands-separator formatting. @default 'en-US'
281
+ */
282
+ locale?: string;
255
283
  testId?: string;
256
284
  };
257
285
  /**
@@ -291,6 +319,11 @@ export interface DateRangeProps {
291
319
  */
292
320
  format?: string;
293
321
  errors?: string[];
322
+ /**
323
+ * Trigger control size — shares the form-size ladder with `Input`,
324
+ * `Select`, etc. so a row of mixed controls lines up. @default 'md'
325
+ */
326
+ size?: VariantSizes;
294
327
  /**
295
328
  * Fires when the user completes a selection. Both dates may be
296
329
  * undefined (cleared).