compote-ui 0.79.1 → 0.80.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 (69) hide show
  1. package/dist/components/data-table-v9/column-filter-editing.svelte.d.ts +41 -0
  2. package/dist/components/data-table-v9/column-filter-editing.svelte.js +88 -0
  3. package/dist/components/data-table-v9/column-filter-kind.d.ts +10 -0
  4. package/dist/components/data-table-v9/column-filter-kind.js +25 -0
  5. package/dist/components/data-table-v9/create-table.svelte.js +49 -26
  6. package/dist/components/data-table-v9/data-table-head.svelte +230 -201
  7. package/dist/components/data-table-v9/data-table-head.svelte.d.ts +2 -0
  8. package/dist/components/data-table-v9/data-table-utils.d.ts +7 -0
  9. package/dist/components/data-table-v9/data-table-utils.js +4 -0
  10. package/dist/components/data-table-v9/data-table.svelte +154 -151
  11. package/dist/components/data-table-v9/data-table.svelte.d.ts +2 -0
  12. package/dist/components/data-table-v9/date-cell.d.ts +13 -0
  13. package/dist/components/data-table-v9/date-cell.js +38 -0
  14. package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte +45 -0
  15. package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte.d.ts +7 -0
  16. package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte +33 -0
  17. package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte.d.ts +30 -0
  18. package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte +52 -0
  19. package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte.d.ts +28 -0
  20. package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte +51 -0
  21. package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte.d.ts +30 -0
  22. package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte +59 -0
  23. package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte.d.ts +28 -0
  24. package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte +20 -0
  25. package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte.d.ts +8 -0
  26. package/dist/components/data-table-v9/filter-revisions.svelte.d.ts +17 -0
  27. package/dist/components/data-table-v9/filter-revisions.svelte.js +42 -0
  28. package/dist/components/data-table-v9/header/column-header-filter.svelte +66 -0
  29. package/dist/components/data-table-v9/header/column-header-filter.svelte.d.ts +30 -0
  30. package/dist/components/data-table-v9/table-view-state.svelte.d.ts +1 -0
  31. package/dist/components/data-table-v9/table-view-state.svelte.js +3 -0
  32. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +203 -433
  33. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +1 -1
  34. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +82 -79
  35. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +2 -0
  36. package/dist/components/dialog/alert-dialog.svelte +73 -71
  37. package/dist/components/dialog/dialog-root.svelte +45 -43
  38. package/dist/components/drawer/drawer-content.svelte +213 -211
  39. package/dist/components/number-input/number-input.svelte +2 -6
  40. package/dist/components/popover/popover-content.svelte +97 -71
  41. package/dist/components/popover/popover-content.svelte.d.ts +2 -1
  42. package/dist/components/popover/popover-description.svelte +19 -15
  43. package/dist/components/popover/popover-labels.svelte.d.ts +18 -0
  44. package/dist/components/popover/popover-labels.svelte.js +35 -0
  45. package/dist/components/popover/popover-root.svelte +27 -24
  46. package/dist/components/popover/popover-title.svelte +19 -15
  47. package/dist/icons/PhFunnelFill.svelte +19 -0
  48. package/dist/icons/PhFunnelFill.svelte.d.ts +5 -0
  49. package/dist/icons/index.d.ts +1 -0
  50. package/dist/icons/index.js +1 -0
  51. package/dist/utils/date-filter.d.ts +31 -0
  52. package/dist/utils/date-filter.js +58 -0
  53. package/dist/utils/date.d.ts +19 -1
  54. package/dist/utils/date.js +48 -1
  55. package/dist/utils/debounced-filter-edit.svelte.d.ts +16 -0
  56. package/dist/utils/debounced-filter-edit.svelte.js +56 -0
  57. package/dist/utils/number-input-value.d.ts +10 -0
  58. package/dist/utils/number-input-value.js +15 -0
  59. package/dist/utils/range-filter.d.ts +19 -0
  60. package/dist/utils/range-filter.js +23 -0
  61. package/dist/utils/release-focus-on-exit.d.ts +22 -0
  62. package/dist/utils/release-focus-on-exit.js +26 -0
  63. package/package.json +1 -1
  64. package/skills/component-usage/SKILL.md +1 -1
  65. package/skills/component-usage/references/forms-and-inputs.md +3 -0
  66. package/skills/component-usage/references/overlays-and-floating-ui.md +278 -273
  67. package/skills/data-table/SKILL.md +314 -283
  68. package/skills/data-table/references/column-options.md +175 -150
  69. package/skills/data-table/references/table-state-and-reactivity.md +65 -45
@@ -1,15 +1,19 @@
1
- <script lang="ts">
2
- import { Popover } from '@ark-ui/svelte/popover';
3
- import type { PopoverDescriptionBaseProps } from '@ark-ui/svelte/popover';
4
- import type { ClassValue } from 'svelte/elements';
5
-
6
- interface Props extends PopoverDescriptionBaseProps {
7
- class?: ClassValue;
8
- }
9
-
10
- let { class: className, children, ...rest }: Props = $props();
11
- </script>
12
-
13
- <Popover.Description {...rest} class={className ?? 'mt-1 text-sm text-ink-dim'}>
14
- {@render children?.()}
15
- </Popover.Description>
1
+ <script lang="ts">
2
+ import { Popover } from '@ark-ui/svelte/popover';
3
+ import type { PopoverDescriptionBaseProps } from '@ark-ui/svelte/popover';
4
+ import type { ClassValue } from 'svelte/elements';
5
+ import { getPopoverLabels } from './popover-labels.svelte';
6
+
7
+ interface Props extends PopoverDescriptionBaseProps {
8
+ class?: ClassValue;
9
+ }
10
+
11
+ let { class: className, children, ...rest }: Props = $props();
12
+
13
+ const labels = getPopoverLabels();
14
+ $effect(() => labels?.registerDescription());
15
+ </script>
16
+
17
+ <Popover.Description {...rest} class={className ?? 'mt-1 text-sm text-ink-dim'}>
18
+ {@render children?.()}
19
+ </Popover.Description>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which labelling parts a popover currently renders. Zag checks for a title/description only
3
+ * when its machine starts — before lazily mounted content exists — so it never sets
4
+ * `aria-labelledby` / `aria-describedby` on the content. Title and Description register here
5
+ * instead, and Content links them by the ids zag gives them.
6
+ */
7
+ export declare class PopoverLabels {
8
+ #private;
9
+ get hasTitle(): boolean;
10
+ get hasDescription(): boolean;
11
+ /** Registers a rendered title; returns the unregister function. */
12
+ registerTitle(): () => void;
13
+ /** Registers a rendered description; returns the unregister function. */
14
+ registerDescription(): () => void;
15
+ }
16
+ export declare function setPopoverLabels(): PopoverLabels;
17
+ /** Undefined when the part is rendered outside compote's `Popover.Root`. */
18
+ export declare function getPopoverLabels(): PopoverLabels | undefined;
@@ -0,0 +1,35 @@
1
+ import { getContext, setContext, untrack } from 'svelte';
2
+ const key = Symbol('compote-popover-labels');
3
+ /**
4
+ * Which labelling parts a popover currently renders. Zag checks for a title/description only
5
+ * when its machine starts — before lazily mounted content exists — so it never sets
6
+ * `aria-labelledby` / `aria-describedby` on the content. Title and Description register here
7
+ * instead, and Content links them by the ids zag gives them.
8
+ */
9
+ export class PopoverLabels {
10
+ #titles = $state(0);
11
+ #descriptions = $state(0);
12
+ get hasTitle() {
13
+ return this.#titles > 0;
14
+ }
15
+ get hasDescription() {
16
+ return this.#descriptions > 0;
17
+ }
18
+ /** Registers a rendered title; returns the unregister function. */
19
+ registerTitle() {
20
+ untrack(() => this.#titles++);
21
+ return () => untrack(() => this.#titles--);
22
+ }
23
+ /** Registers a rendered description; returns the unregister function. */
24
+ registerDescription() {
25
+ untrack(() => this.#descriptions++);
26
+ return () => untrack(() => this.#descriptions--);
27
+ }
28
+ }
29
+ export function setPopoverLabels() {
30
+ return setContext(key, new PopoverLabels());
31
+ }
32
+ /** Undefined when the part is rendered outside compote's `Popover.Root`. */
33
+ export function getPopoverLabels() {
34
+ return getContext(key);
35
+ }
@@ -1,24 +1,27 @@
1
- <script lang="ts">
2
- import { Popover } from '@ark-ui/svelte/popover';
3
- import type { PopoverRootBaseProps } from '@ark-ui/svelte/popover';
4
- import type { Snippet } from 'svelte';
5
-
6
- interface Props extends PopoverRootBaseProps {
7
- open?: boolean;
8
- children: Snippet;
9
- lazyMount?: boolean;
10
- unmountOnExit?: boolean;
11
- }
12
-
13
- let {
14
- open = $bindable(),
15
- children,
16
- lazyMount = true,
17
- unmountOnExit = true,
18
- ...restProps
19
- }: Props = $props();
20
- </script>
21
-
22
- <Popover.Root bind:open {lazyMount} {unmountOnExit} {...restProps}>
23
- {@render children()}
24
- </Popover.Root>
1
+ <script lang="ts">
2
+ import { Popover } from '@ark-ui/svelte/popover';
3
+ import type { PopoverRootBaseProps } from '@ark-ui/svelte/popover';
4
+ import type { Snippet } from 'svelte';
5
+ import { setPopoverLabels } from './popover-labels.svelte';
6
+
7
+ interface Props extends PopoverRootBaseProps {
8
+ open?: boolean;
9
+ children: Snippet;
10
+ lazyMount?: boolean;
11
+ unmountOnExit?: boolean;
12
+ }
13
+
14
+ let {
15
+ open = $bindable(),
16
+ children,
17
+ lazyMount = true,
18
+ unmountOnExit = true,
19
+ ...restProps
20
+ }: Props = $props();
21
+
22
+ setPopoverLabels();
23
+ </script>
24
+
25
+ <Popover.Root bind:open {lazyMount} {unmountOnExit} {...restProps}>
26
+ {@render children()}
27
+ </Popover.Root>
@@ -1,15 +1,19 @@
1
- <script lang="ts">
2
- import { Popover } from '@ark-ui/svelte/popover';
3
- import type { PopoverTitleBaseProps } from '@ark-ui/svelte/popover';
4
- import type { ClassValue } from 'svelte/elements';
5
-
6
- interface Props extends PopoverTitleBaseProps {
7
- class?: ClassValue;
8
- }
9
-
10
- let { class: className, children, ...rest }: Props = $props();
11
- </script>
12
-
13
- <Popover.Title {...rest} class={className ?? 'text-sm font-semibold'}>
14
- {@render children?.()}
15
- </Popover.Title>
1
+ <script lang="ts">
2
+ import { Popover } from '@ark-ui/svelte/popover';
3
+ import type { PopoverTitleBaseProps } from '@ark-ui/svelte/popover';
4
+ import type { ClassValue } from 'svelte/elements';
5
+ import { getPopoverLabels } from './popover-labels.svelte';
6
+
7
+ interface Props extends PopoverTitleBaseProps {
8
+ class?: ClassValue;
9
+ }
10
+
11
+ let { class: className, children, ...rest }: Props = $props();
12
+
13
+ const labels = getPopoverLabels();
14
+ $effect(() => labels?.registerTitle());
15
+ </script>
16
+
17
+ <Popover.Title {...rest} class={className ?? 'text-sm font-semibold'}>
18
+ {@render children?.()}
19
+ </Popover.Title>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ let { class: className = '', ...restProps } = $props();
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="0 0 256 256"
10
+ class={className}
11
+ {...restProps}
12
+ >
13
+ <path
14
+ fill="currentColor"
15
+ d="m227.81 66.76l-.08.09L160 139.17v55.49a16 16 0 0 1-7.13 13.34l-32 21.34A16 16 0 0 1 96 216v-76.83L28.27 66.85l-.08-.09A16 16 0 0 1 40 40h176a16 16 0 0 1 11.84 26.76Z"
16
+ />
17
+ </svg>
18
+
19
+ <!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
@@ -0,0 +1,5 @@
1
+ declare const PhFunnelFill: import("svelte").Component<{
2
+ class?: string;
3
+ } & Record<string, any>, {}, "">;
4
+ type PhFunnelFill = ReturnType<typeof PhFunnelFill>;
5
+ export default PhFunnelFill;
@@ -16,6 +16,7 @@ export { default as PhFile } from './PhFile.svelte';
16
16
  export { default as PhFileArchive } from './PhFileArchive.svelte';
17
17
  export { default as PhFileText } from './PhFileText.svelte';
18
18
  export { default as PhFunnel } from './PhFunnel.svelte';
19
+ export { default as PhFunnelFill } from './PhFunnelFill.svelte';
19
20
  export { default as PhHeadphones } from './PhHeadphones.svelte';
20
21
  export { default as PhImage } from './PhImage.svelte';
21
22
  export { default as PhListMagnifyingGlass } from './PhListMagnifyingGlass.svelte';
@@ -16,6 +16,7 @@ export { default as PhFile } from './PhFile.svelte';
16
16
  export { default as PhFileArchive } from './PhFileArchive.svelte';
17
17
  export { default as PhFileText } from './PhFileText.svelte';
18
18
  export { default as PhFunnel } from './PhFunnel.svelte';
19
+ export { default as PhFunnelFill } from './PhFunnelFill.svelte';
19
20
  export { default as PhHeadphones } from './PhHeadphones.svelte';
20
21
  export { default as PhImage } from './PhImage.svelte';
21
22
  export { default as PhListMagnifyingGlass } from './PhListMagnifyingGlass.svelte';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Calendar-day range filters for date and date-time columns.
3
+ *
4
+ * The filter value is `[from?, to?]`, each an ISO calendar date (`YYYY-MM-DD`) or empty — no
5
+ * time, no zone, so it serializes cleanly. Both bounds are inclusive days. A cell matches
6
+ * when the day it shows in the display zone (see `readDateInZone`) falls inside the range.
7
+ * For an instant that is the half-open interval [start of `from`, start of the day after
8
+ * `to`) in that zone, with day starts taken from the calendar — so 23- and 25-hour DST days
9
+ * need no special handling.
10
+ */
11
+ import { CalendarDate } from '@internationalized/date';
12
+ /** A date range filter value as stored in table state. */
13
+ export type DateRangeFilterValue = [from?: string, to?: string];
14
+ /** A filter value read into calendar dates; an absent bound leaves that end open. */
15
+ export type DateRange = {
16
+ from?: CalendarDate;
17
+ to?: CalendarDate;
18
+ };
19
+ /**
20
+ * The range a filter value stands for. Invalid bounds are dropped and a reversed range is
21
+ * swapped (as TanStack's `inNumberRange` does). Always returns a range — never undefined —
22
+ * because the table falls back to the raw value when `resolveFilterValue` returns nullish.
23
+ */
24
+ export declare function resolveDateRange(filterValue: unknown): DateRange;
25
+ /** Whether a filter value has no usable bound, so the filter should be removed. */
26
+ export declare function isEmptyDateRange(filterValue: unknown): boolean;
27
+ /**
28
+ * Whether a cell value falls in `range`, judged by the calendar day it shows in `timeZone`.
29
+ * Empty or unparseable cell values never match while the filter is active.
30
+ */
31
+ export declare function matchesDateRange(value: unknown, range: DateRange, timeZone: string): boolean;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Calendar-day range filters for date and date-time columns.
3
+ *
4
+ * The filter value is `[from?, to?]`, each an ISO calendar date (`YYYY-MM-DD`) or empty — no
5
+ * time, no zone, so it serializes cleanly. Both bounds are inclusive days. A cell matches
6
+ * when the day it shows in the display zone (see `readDateInZone`) falls inside the range.
7
+ * For an instant that is the half-open interval [start of `from`, start of the day after
8
+ * `to`) in that zone, with day starts taken from the calendar — so 23- and 25-hour DST days
9
+ * need no special handling.
10
+ */
11
+ import { CalendarDate, parseDate, toCalendarDate } from '@internationalized/date';
12
+ import { readDateInZone } from './date';
13
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
14
+ /** A bound as a calendar date; anything but a valid `YYYY-MM-DD` reads as no bound. */
15
+ function readBound(value) {
16
+ if (typeof value !== 'string' || !DATE_ONLY.test(value))
17
+ return undefined;
18
+ try {
19
+ return parseDate(value);
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ /**
26
+ * The range a filter value stands for. Invalid bounds are dropped and a reversed range is
27
+ * swapped (as TanStack's `inNumberRange` does). Always returns a range — never undefined —
28
+ * because the table falls back to the raw value when `resolveFilterValue` returns nullish.
29
+ */
30
+ export function resolveDateRange(filterValue) {
31
+ if (!Array.isArray(filterValue))
32
+ return {};
33
+ const from = readBound(filterValue[0]);
34
+ const to = readBound(filterValue[1]);
35
+ if (from && to && from.compare(to) > 0)
36
+ return { from: to, to: from };
37
+ return { from, to };
38
+ }
39
+ /** Whether a filter value has no usable bound, so the filter should be removed. */
40
+ export function isEmptyDateRange(filterValue) {
41
+ const { from, to } = resolveDateRange(filterValue);
42
+ return !from && !to;
43
+ }
44
+ /**
45
+ * Whether a cell value falls in `range`, judged by the calendar day it shows in `timeZone`.
46
+ * Empty or unparseable cell values never match while the filter is active.
47
+ */
48
+ export function matchesDateRange(value, range, timeZone) {
49
+ const date = readDateInZone(value, timeZone);
50
+ if (!date)
51
+ return false;
52
+ const day = date instanceof CalendarDate ? date : toCalendarDate(date);
53
+ if (range.from && day.compare(range.from) < 0)
54
+ return false;
55
+ if (range.to && day.compare(range.to) > 0)
56
+ return false;
57
+ return true;
58
+ }
@@ -1,4 +1,4 @@
1
- import { type DateValue } from '@internationalized/date';
1
+ import { CalendarDate, ZonedDateTime, type DateValue } from '@internationalized/date';
2
2
  export type { DateValue };
3
3
  /**
4
4
  * Anything the date components accept for a single value: an
@@ -24,3 +24,21 @@ export declare function dateValueToString(value: DateValue): string;
24
24
  export declare function dateValueToDate(value: DateValue, timeZone?: string): Date;
25
25
  /** Emit a `DateValue` back in the original `shape` it was bound as. */
26
26
  export declare function fromDateValue(value: DateValue | null, shape: DateValueShape, timeZone?: string): DateValue | string | Date | null;
27
+ /**
28
+ * A value read for display in `timeZone`: a calendar date (a day, with no instant behind it)
29
+ * or an instant converted into `timeZone`.
30
+ */
31
+ export type ZonedOrCalendarDate = CalendarDate | ZonedDateTime;
32
+ /**
33
+ * Read any date-ish value — a `DateValue`, ISO/DB string, native `Date` or epoch-millisecond
34
+ * number — the way it should be shown in `timeZone`. Never throws: anything unparseable
35
+ * (including an out-of-range date like `2024-02-30`) is `null`.
36
+ *
37
+ * - `"2024-03-15"` and `CalendarDate` stay calendar dates. They are never turned into an
38
+ * instant, which would put them on the previous day in zones west of UTC.
39
+ * - `Date`, epoch numbers, offset strings (`Z`, `+02:00`) and `ZonedDateTime` are instants,
40
+ * converted into `timeZone`.
41
+ * - A datetime without an offset (`"2024-03-15T10:30"`, `CalendarDateTime`) is a wall-clock
42
+ * time in `timeZone`.
43
+ */
44
+ export declare function readDateInZone(value: unknown, timeZone: string): ZonedOrCalendarDate | null;
@@ -1,4 +1,4 @@
1
- import { parseDate, parseDateTime, parseAbsolute, parseAbsoluteToLocal, fromDate, getLocalTimeZone } from '@internationalized/date';
1
+ import { CalendarDate, CalendarDateTime, ZonedDateTime, parseDate, parseDateTime, parseAbsolute, parseAbsoluteToLocal, fromAbsolute, fromDate, getLocalTimeZone, toTimeZone, toZoned } from '@internationalized/date';
2
2
  const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
3
3
  // Trailing `Z`, or a `+HH`, `+HH:MM`, `+HHMM` offset.
4
4
  const HAS_OFFSET = /(?:[zZ]|[+-]\d{2}(?::?\d{2})?)$/;
@@ -62,3 +62,50 @@ export function fromDateValue(value, shape, timeZone) {
62
62
  return dateValueToDate(value, timeZone);
63
63
  return value;
64
64
  }
65
+ /**
66
+ * Read any date-ish value — a `DateValue`, ISO/DB string, native `Date` or epoch-millisecond
67
+ * number — the way it should be shown in `timeZone`. Never throws: anything unparseable
68
+ * (including an out-of-range date like `2024-02-30`) is `null`.
69
+ *
70
+ * - `"2024-03-15"` and `CalendarDate` stay calendar dates. They are never turned into an
71
+ * instant, which would put them on the previous day in zones west of UTC.
72
+ * - `Date`, epoch numbers, offset strings (`Z`, `+02:00`) and `ZonedDateTime` are instants,
73
+ * converted into `timeZone`.
74
+ * - A datetime without an offset (`"2024-03-15T10:30"`, `CalendarDateTime`) is a wall-clock
75
+ * time in `timeZone`.
76
+ */
77
+ export function readDateInZone(value, timeZone) {
78
+ try {
79
+ return readDate(value, timeZone);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ function readDate(value, timeZone) {
86
+ if (value == null)
87
+ return null;
88
+ if (value instanceof Date) {
89
+ return Number.isNaN(value.getTime()) ? null : fromDate(value, timeZone);
90
+ }
91
+ if (typeof value === 'number') {
92
+ return Number.isFinite(value) ? fromAbsolute(value, timeZone) : null;
93
+ }
94
+ if (value instanceof ZonedDateTime)
95
+ return toTimeZone(value, timeZone);
96
+ if (value instanceof CalendarDateTime)
97
+ return toZoned(value, timeZone);
98
+ if (value instanceof CalendarDate)
99
+ return value;
100
+ if (typeof value !== 'string')
101
+ return null;
102
+ const raw = value.trim();
103
+ if (!raw)
104
+ return null;
105
+ if (DATE_ONLY.test(raw))
106
+ return parseDate(raw);
107
+ const iso = normalizeIso(raw);
108
+ return HAS_OFFSET.test(iso)
109
+ ? parseAbsolute(iso, timeZone)
110
+ : toZoned(parseDateTime(iso), timeZone);
111
+ }
@@ -0,0 +1,16 @@
1
+ export declare class DebouncedFilterEdits<T> {
2
+ #private;
3
+ constructor(delay?: number);
4
+ /** The pending edit for `id`, or undefined when there is none or it went stale. */
5
+ get(id: string, current: unknown): T | undefined;
6
+ /**
7
+ * Records an edit against the filter's `current` value: `update` receives the live
8
+ * pending edit (undefined when there is none or it went stale) and returns the new one.
9
+ * After the delay, `commit` runs with it — unless `read()` shows the filter changed.
10
+ */
11
+ edit(id: string, current: unknown, update: (pending: T | undefined) => T, read: () => unknown, commit: (value: T) => void): void;
12
+ /** Drops the pending edit for `id` without committing it. */
13
+ cancel(id: string): void;
14
+ /** Drops every pending edit without committing. */
15
+ cancelAll(): void;
16
+ }
@@ -0,0 +1,56 @@
1
+ export class DebouncedFilterEdits {
2
+ // Raw, not deep: `base` must stay the exact filter value it is compared against — a deep
3
+ // $state would proxy an array base (a range) and `Object.is` would never match it again.
4
+ #pending = $state.raw({});
5
+ // Timer handles are bookkeeping, never rendered — a reactive map would only add overhead.
6
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
7
+ #timers = new Map();
8
+ #delay;
9
+ constructor(delay = 300) {
10
+ this.#delay = delay;
11
+ }
12
+ /** The pending edit for `id`, or undefined when there is none or it went stale. */
13
+ get(id, current) {
14
+ const pending = this.#pending[id];
15
+ return pending && Object.is(pending.base, current) ? pending.value : undefined;
16
+ }
17
+ /**
18
+ * Records an edit against the filter's `current` value: `update` receives the live
19
+ * pending edit (undefined when there is none or it went stale) and returns the new one.
20
+ * After the delay, `commit` runs with it — unless `read()` shows the filter changed.
21
+ */
22
+ edit(id, current, update, read, commit) {
23
+ this.#pending = {
24
+ ...this.#pending,
25
+ [id]: { value: update(this.get(id, current)), base: current }
26
+ };
27
+ clearTimeout(this.#timers.get(id));
28
+ this.#timers.set(id, setTimeout(() => {
29
+ this.#timers.delete(id);
30
+ const pending = this.#pending[id];
31
+ this.#remove(id);
32
+ if (pending && Object.is(read(), pending.base))
33
+ commit(pending.value);
34
+ }, this.#delay));
35
+ }
36
+ /** Drops the pending edit for `id` without committing it. */
37
+ cancel(id) {
38
+ clearTimeout(this.#timers.get(id));
39
+ this.#timers.delete(id);
40
+ this.#remove(id);
41
+ }
42
+ /** Drops every pending edit without committing. */
43
+ cancelAll() {
44
+ for (const timer of this.#timers.values())
45
+ clearTimeout(timer);
46
+ this.#timers.clear();
47
+ this.#pending = {};
48
+ }
49
+ #remove(id) {
50
+ if (!(id in this.#pending))
51
+ return;
52
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- dropped from the record
53
+ const { [id]: _removed, ...rest } = this.#pending;
54
+ this.#pending = rest;
55
+ }
56
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The string value NumberInput hands to Ark's NumberInput.Root.
3
+ *
4
+ * An empty value (`null`, `undefined` or NaN) becomes `''`, a controlled empty value. Passing
5
+ * `undefined` instead would switch Ark to its internal state, which keeps showing the last
6
+ * number — so clearing a bound from outside (e.g. a filter going from [10, 100] to
7
+ * [undefined, 100]) would leave the 10 on screen. The one exception: an `undefined` value
8
+ * with a `defaultValue` set stays `undefined`, so uncontrolled use keeps working.
9
+ */
10
+ export declare function toNumberInputValue(value: number | null | undefined, locale: string, hasDefaultValue?: boolean): string | undefined;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The string value NumberInput hands to Ark's NumberInput.Root.
3
+ *
4
+ * An empty value (`null`, `undefined` or NaN) becomes `''`, a controlled empty value. Passing
5
+ * `undefined` instead would switch Ark to its internal state, which keeps showing the last
6
+ * number — so clearing a bound from outside (e.g. a filter going from [10, 100] to
7
+ * [undefined, 100]) would leave the 10 on screen. The one exception: an `undefined` value
8
+ * with a `defaultValue` set stays `undefined`, so uncontrolled use keeps working.
9
+ */
10
+ export function toNumberInputValue(value, locale, hasDefaultValue = false) {
11
+ if (value == null || Number.isNaN(value)) {
12
+ return value === undefined && hasDefaultValue ? undefined : '';
13
+ }
14
+ return new Intl.NumberFormat(locale, { useGrouping: false, maximumFractionDigits: 20 }).format(value);
15
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Value handling for numeric range filters (`inNumberRange`): the filter value is a
3
+ * `[min, max]` tuple where either bound may be `undefined`.
4
+ */
5
+ export type RangeFilterValue = [number | undefined, number | undefined];
6
+ /** Bounds edited in the UI but not committed yet; `null` means the bound was cleared. */
7
+ export type PendingRange = {
8
+ min?: number | null;
9
+ max?: number | null;
10
+ };
11
+ /** The `[min, max]` bounds of a range filter value, or both undefined when there is none. */
12
+ export declare function rangeBounds(filterValue: unknown): RangeFilterValue;
13
+ /**
14
+ * The filter value after committing `pending` edits onto the current filter value. A bound
15
+ * that wasn't edited keeps its current (live) value, so a stale bound can't come back after
16
+ * the filter was changed elsewhere. Returns `undefined` when both bounds are empty, which
17
+ * removes the filter.
18
+ */
19
+ export declare function mergeRangeFilter(current: unknown, pending: PendingRange): RangeFilterValue | undefined;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Value handling for numeric range filters (`inNumberRange`): the filter value is a
3
+ * `[min, max]` tuple where either bound may be `undefined`.
4
+ */
5
+ /** The `[min, max]` bounds of a range filter value, or both undefined when there is none. */
6
+ export function rangeBounds(filterValue) {
7
+ if (!Array.isArray(filterValue))
8
+ return [undefined, undefined];
9
+ const [min, max] = filterValue;
10
+ return [typeof min === 'number' ? min : undefined, typeof max === 'number' ? max : undefined];
11
+ }
12
+ /**
13
+ * The filter value after committing `pending` edits onto the current filter value. A bound
14
+ * that wasn't edited keeps its current (live) value, so a stale bound can't come back after
15
+ * the filter was changed elsewhere. Returns `undefined` when both bounds are empty, which
16
+ * removes the filter.
17
+ */
18
+ export function mergeRangeFilter(current, pending) {
19
+ const [currentMin, currentMax] = rangeBounds(current);
20
+ const min = 'min' in pending ? (pending.min ?? undefined) : currentMin;
21
+ const max = 'max' in pending ? (pending.max ?? undefined) : currentMax;
22
+ return min === undefined && max === undefined ? undefined : [min, max];
23
+ }
@@ -0,0 +1,22 @@
1
+ declare function releaseFocusOnExit(event: Event): void;
2
+ /**
3
+ * Spread onto Ark overlay content (popover, dialog, drawer) so a field focused inside it blurs
4
+ * before the content unmounts, not during the unmount.
5
+ *
6
+ * Zag's presence can unmount content from inside a Svelte effect (it does when the document is
7
+ * hidden). Svelte then removes the content mid-flush; a field still focused in there blurs during
8
+ * that removal, and zag handles some blurs with `flushSync` (DatePicker's machine, for one). That
9
+ * nested `flushSync` breaks the outer flush: "Cannot read properties of null (reading 'clear')"
10
+ * in Svelte's `flush_queued_effects` (still the case in svelte 5.57.2).
11
+ *
12
+ * Zag dispatches `exitcomplete` on the content node once presence decides to unmount, before
13
+ * Svelte removes it. Blurring there lets the field commit on blur as usual, at a point where a
14
+ * nested `flushSync` is safe. Usually focus is already back on the trigger by then (exit
15
+ * animations outlast the focus restore), and this does nothing.
16
+ *
17
+ * A spread because Ark's content prop types don't list the `exitcomplete` event.
18
+ */
19
+ export declare const releaseFocusOnExitProps: {
20
+ onexitcomplete: typeof releaseFocusOnExit;
21
+ };
22
+ export {};
@@ -0,0 +1,26 @@
1
+ function releaseFocusOnExit(event) {
2
+ const content = event.currentTarget;
3
+ if (!(content instanceof HTMLElement))
4
+ return;
5
+ const active = content.ownerDocument.activeElement;
6
+ if (active instanceof HTMLElement && content.contains(active))
7
+ active.blur();
8
+ }
9
+ /**
10
+ * Spread onto Ark overlay content (popover, dialog, drawer) so a field focused inside it blurs
11
+ * before the content unmounts, not during the unmount.
12
+ *
13
+ * Zag's presence can unmount content from inside a Svelte effect (it does when the document is
14
+ * hidden). Svelte then removes the content mid-flush; a field still focused in there blurs during
15
+ * that removal, and zag handles some blurs with `flushSync` (DatePicker's machine, for one). That
16
+ * nested `flushSync` breaks the outer flush: "Cannot read properties of null (reading 'clear')"
17
+ * in Svelte's `flush_queued_effects` (still the case in svelte 5.57.2).
18
+ *
19
+ * Zag dispatches `exitcomplete` on the content node once presence decides to unmount, before
20
+ * Svelte removes it. Blurring there lets the field commit on blur as usual, at a point where a
21
+ * nested `flushSync` is safe. Usually focus is already back on the trigger by then (exit
22
+ * animations outlast the focus restore), and this does nothing.
23
+ *
24
+ * A spread because Ark's content prop types don't list the `exitcomplete` event.
25
+ */
26
+ export const releaseFocusOnExitProps = { onexitcomplete: releaseFocusOnExit };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.79.1",
3
+ "version": "0.80.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@ description: >
8
8
  metadata:
9
9
  type: core
10
10
  library: compote-ui
11
- library_version: '0.74.2'
11
+ library_version: '0.79.2'
12
12
  sources:
13
13
  - src/lib/index.ts
14
14
  - src/lib/utils/collections.ts
@@ -172,6 +172,9 @@ If selection must be restricted to a fixed set (pick-only, no free text), use `C
172
172
  Props: `value?: number | null` (bindable), `label?`, `readonly?`, `layout?: 'vertical' | 'horizontal'`,
173
173
  plus Ark UI NumberInput props (`min`, `max`, `step`, `formatOptions`, `locale`).
174
174
 
175
+ `value` is controlled: setting it to `null` from outside clears the input. Use `defaultValue`
176
+ (a string) only for uncontrolled use, without passing `value`.
177
+
175
178
  ## PasswordInput
176
179
 
177
180
  Password field with a built-in show/hide toggle. The underlying `<input>` is a real DOM input —