@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,9 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
3
  import { buildTestId } from '../../helper/testid.js';
4
+ import { fly } from 'svelte/transition';
5
+ import { quintOut } from 'svelte/easing';
6
+ import { onMount } from 'svelte';
4
7
  import type { PopoverProps, PopoverPlacement } from './popover-types.js';
5
8
 
6
9
  let {
@@ -25,13 +28,25 @@
25
28
  let showTimer: ReturnType<typeof setTimeout> | undefined;
26
29
  let hideTimer: ReturnType<typeof setTimeout> | undefined;
27
30
 
28
- // Panel position in viewport coordinates — updated on open and on
29
- // scroll/resize so the panel tracks the trigger. Kept reactive so the
30
- // style attribute re-renders when these change.
31
31
  let panelTop = $state(0);
32
32
  let panelLeft = $state(0);
33
33
  let panelTransform = $state('');
34
34
 
35
+ // Mobile detection — below `sm` (640px), click/manual popovers
36
+ // render as a bottom sheet instead of a positioned dropdown.
37
+ // Hover popovers are skipped on mobile (no hover).
38
+ let isMobile = $state(
39
+ typeof window !== 'undefined' && window.matchMedia('(max-width: 639.98px)').matches
40
+ );
41
+ onMount(() => {
42
+ const mql = window.matchMedia('(max-width: 639.98px)');
43
+ const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
44
+ mql.addEventListener('change', handler);
45
+ return () => mql.removeEventListener('change', handler);
46
+ });
47
+
48
+ const useSheet = $derived(isMobile && trigger !== 'hover');
49
+
35
50
  function show() {
36
51
  if (disabled) return;
37
52
  clearTimeout(showTimer);
@@ -65,7 +80,6 @@
65
80
  open = false;
66
81
  }
67
82
 
68
- // Clean up any in-flight timers on unmount.
69
83
  $effect(() => {
70
84
  return () => {
71
85
  clearTimeout(showTimer);
@@ -79,23 +93,12 @@
79
93
 
80
94
  function handleWindowClick(e: MouseEvent) {
81
95
  if (!closeOnOutsideClick || !open) return;
96
+ if (useSheet) return;
82
97
  const target = e.target as Node;
83
- // The panel lives outside the trigger wrapper in the DOM, so check both.
84
98
  if (wrapper?.contains(target) || panelEl?.contains(target)) return;
85
99
  close();
86
100
  }
87
101
 
88
- /**
89
- * Compute panel position from the trigger's bounding rect. Uses
90
- * `position: fixed` + a very high z-index so the panel sits above
91
- * sibling content (Storybook sidebars, sticky headers, etc.) instead
92
- * of being clipped / layered under it.
93
- *
94
- * The computed position is then clamped to the viewport with an 8px
95
- * gutter — this keeps panels on-screen near viewport edges on narrow
96
- * devices. We measure the panel's own rect so the clamp accounts for
97
- * the `translate(-50%, 0)` etc. transforms that re-anchor the panel.
98
- */
99
102
  function updatePosition() {
100
103
  if (!wrapper || !panelEl) return;
101
104
  const r = wrapper.getBoundingClientRect();
@@ -124,10 +127,6 @@
124
127
  break;
125
128
  }
126
129
 
127
- // Clamp to viewport. We re-measure the panel after applying the
128
- // transform (next frame) because its true on-screen rect depends
129
- // on `panelTransform`. Adjust `panelLeft`/`panelTop` so the rect
130
- // ends up inside [VIEWPORT_GUTTER, viewport - VIEWPORT_GUTTER].
131
130
  requestAnimationFrame(() => {
132
131
  if (!panelEl) return;
133
132
  const pr = panelEl.getBoundingClientRect();
@@ -144,13 +143,8 @@
144
143
  });
145
144
  }
146
145
 
147
- /**
148
- * Re-measure on every open, and while open, on scroll/resize so the
149
- * panel follows its trigger when the page scrolls.
150
- */
151
146
  $effect(() => {
152
- if (!open) return;
153
- // Initial placement (defer one frame so panelEl has mounted).
147
+ if (!open || useSheet) return;
154
148
  requestAnimationFrame(updatePosition);
155
149
 
156
150
  const handler = () => updatePosition();
@@ -162,6 +156,17 @@
162
156
  };
163
157
  });
164
158
 
159
+ $effect(() => {
160
+ if (!panelEl) return;
161
+ const el = panelEl;
162
+ document.body.appendChild(el);
163
+ return () => {
164
+ if (el.parentNode === document.body) {
165
+ document.body.removeChild(el);
166
+ }
167
+ };
168
+ });
169
+
165
170
  const arrowClass = $derived(
166
171
  {
167
172
  top: 'top-full left-1/2 -translate-x-1/2 border-t-white border-l-transparent border-r-transparent border-b-transparent',
@@ -182,14 +187,6 @@
182
187
  data-testid={buildTestId('popover', undefined, testId)}
183
188
  >
184
189
  {#if trigger === 'click'}
185
- <!--
186
- Click-mode: forward clicks to toggle. Do NOT add role/tabindex here
187
- — consumers pass an interactive child (Button, <a>, etc.) that
188
- handles its own focus/keyboard; adding them here would nest
189
- interactive elements (invalid HTML + a11y issue). Keyboard support
190
- comes via Enter/Space on the inner button dispatching a click,
191
- which bubbles to this handler.
192
- -->
193
190
  <!-- svelte-ignore a11y_click_events_have_key_events -->
194
191
  <!-- svelte-ignore a11y_no_static_element_interactions -->
195
192
  <span class="inline-flex" aria-haspopup="dialog" aria-expanded={open} onclick={toggle}>
@@ -207,12 +204,12 @@
207
204
  {@render children()}
208
205
  </span>
209
206
  {:else}
210
- <!-- manual — consumer drives `open` -->
211
207
  <span class="inline-flex">{@render children()}</span>
212
208
  {/if}
213
209
  </span>
214
210
 
215
- {#if open}
211
+ <!-- Desktop: positioned panel -->
212
+ {#if open && !useSheet}
216
213
  <div
217
214
  bind:this={panelEl}
218
215
  role="dialog"
@@ -232,3 +229,29 @@
232
229
  {@render content({ close })}
233
230
  </div>
234
231
  {/if}
232
+
233
+ <!-- Mobile: bottom sheet -->
234
+ {#if open && useSheet}
235
+ <button
236
+ type="button"
237
+ class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
238
+ aria-label="Close"
239
+ onclick={close}
240
+ ></button>
241
+ <div
242
+ role="dialog"
243
+ tabindex="-1"
244
+ class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
245
+ transition:fly={{ y: 300, duration: 200, easing: quintOut }}
246
+ data-testid={buildTestId('popover', 'sheet', testId)}
247
+ >
248
+ <!-- Drag handle -->
249
+ <div class="flex cursor-pointer justify-center py-2">
250
+ <div class="bg-default-300 h-1 w-8 rounded-full"></div>
251
+ </div>
252
+ <!-- Content -->
253
+ <div class="flex-1 cursor-pointer overflow-y-auto p-2">
254
+ {@render content({ close })}
255
+ </div>
256
+ </div>
257
+ {/if}
@@ -314,7 +314,7 @@
314
314
  type="button"
315
315
  onclick={() => handleSelect(group, tab.value)}
316
316
  class={cn(
317
- 'rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap',
317
+ 'cursor-pointer rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap',
318
318
  active
319
319
  ? 'bg-primary-50 text-primary-700 border-primary-200'
320
320
  : 'border-default-200 text-default-700 hover:bg-default-50'
@@ -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