compote-ui 0.71.6 → 0.72.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.
@@ -4,7 +4,7 @@
4
4
  import { Field } from '@ark-ui/svelte/field';
5
5
  import { useLocaleContext } from '@ark-ui/svelte/locale';
6
6
  import { CalendarDateTime, getLocalTimeZone } from '@internationalized/date';
7
- import { toDateValue, fromDateValue, dateValueShape } from '../../utils/date';
7
+ import { toDateValue, fromDateValue, dateValueShape, dateValueToString } from '../../utils/date';
8
8
  import type { DateValueShape } from '../../utils/date';
9
9
  import { PhCalendarBlank } from '../../icons';
10
10
  import type { DateFieldProps } from './types';
@@ -97,6 +97,9 @@
97
97
  required,
98
98
  invalid,
99
99
  timeZone: tz,
100
+ // Submit a canonical ISO string rather than zag's locale-formatted display
101
+ // string. Only feeds the hidden input's value; segments are formatted separately.
102
+ format: (date) => dateValueToString(date),
100
103
  onValueChange(details) {
101
104
  datePicker().setValue(details.value);
102
105
  }
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { DateInput } from '@ark-ui/svelte/date-input';
3
3
  import { Field, useFieldContext } from '@ark-ui/svelte/field';
4
- import { toDateValue, fromDateValue, dateValueShape } from '../../utils/date';
4
+ import { toDateValue, fromDateValue, dateValueShape, dateValueToString } from '../../utils/date';
5
5
  import { getLocalTimeZone } from '@internationalized/date';
6
- import type { DateValueShape } from '../../utils/date';
6
+ import type { DateValue, DateValueShape } from '../../utils/date';
7
7
  import type { DateInputProps } from './types';
8
8
 
9
9
  let {
@@ -19,6 +19,9 @@
19
19
  readOnly,
20
20
  disabled,
21
21
  hideTimeZone = true,
22
+ // Submit a canonical ISO string rather than zag's locale-formatted display
23
+ // string. Only feeds the hidden input's value; segments are formatted separately.
24
+ format = (date: DateValue) => dateValueToString(date),
22
25
  ...restProps
23
26
  }: DateInputProps = $props();
24
27
 
@@ -50,6 +53,7 @@
50
53
  {...restProps}
51
54
  {granularity}
52
55
  {hourCycle}
56
+ {format}
53
57
  timeZone={tz}
54
58
  {hideTimeZone}
55
59
  invalid={isInvalid}
@@ -3,15 +3,22 @@
3
3
  import { DateInput, useDateInput } from '@ark-ui/svelte/date-input';
4
4
  import { Field } from '@ark-ui/svelte/field';
5
5
  import { useLocaleContext } from '@ark-ui/svelte/locale';
6
+ import { getLocalTimeZone } from '@internationalized/date';
7
+ import { toDateValue, fromDateValue, dateValueShape, dateValueToString } from '../../utils/date';
8
+ import type { DateValueShape } from '../../utils/date';
6
9
  import { PhCalendarBlank } from '../../icons';
7
10
  import type { DateRangeFieldProps } from './types';
8
11
  import DatePickerCalendar from '../date-picker/date-picker-calendar.svelte';
9
12
 
10
13
  let {
11
- value = $bindable([]),
12
- defaultValue,
14
+ start = $bindable(),
15
+ end = $bindable(),
16
+ defaultStart,
17
+ defaultEnd,
13
18
  label,
14
19
  name,
20
+ startName,
21
+ endName,
15
22
  min,
16
23
  max,
17
24
  disabled,
@@ -19,6 +26,7 @@
19
26
  required,
20
27
  invalid,
21
28
  timeZone,
29
+ granularity,
22
30
  hourCycle,
23
31
  onValueChange
24
32
  }: DateRangeFieldProps = $props();
@@ -26,21 +34,64 @@
26
34
  const locale = useLocaleContext();
27
35
  const id = $props.id();
28
36
 
37
+ // Default the display zone to the user's local zone. Without this Ark/zag
38
+ // formats segments in UTC, so a UTC value would show shifted hours.
39
+ const tz = $derived(timeZone ?? getLocalTimeZone());
40
+
41
+ // Remember the shape each end was bound as, so changes are emitted back in the
42
+ // same shape. Tracked per end so a half-filled range still round-trips; when a
43
+ // side starts null, default to string output since that is the common case
44
+ // binding to database columns.
45
+ let startShape: DateValueShape = 'string';
46
+ let endShape: DateValueShape = 'string';
47
+
48
+ const arkStart = $derived.by(() => {
49
+ const s = dateValueShape(start) ?? dateValueShape(defaultStart);
50
+ if (s) startShape = s;
51
+ return toDateValue(start ?? defaultStart, tz);
52
+ });
53
+
54
+ const arkEnd = $derived.by(() => {
55
+ const s = dateValueShape(end) ?? dateValueShape(defaultEnd);
56
+ if (s) endShape = s;
57
+ return toDateValue(end ?? defaultEnd, tz);
58
+ });
59
+
60
+ // zag reads the range positionally, so an end without a start cannot be
61
+ // represented — it would silently become the start. Drop it instead.
62
+ const arkValue = $derived(arkStart ? (arkEnd ? [arkStart, arkEnd] : [arkStart]) : []);
63
+
64
+ // Submit from `arkValue`, not from arkStart/arkEnd, so the hidden inputs can
65
+ // never disagree with what the picker actually holds.
66
+ const submittedStart = $derived(arkValue[0] ? dateValueToString(arkValue[0]) : '');
67
+ const submittedEnd = $derived(arkValue[1] ? dateValueToString(arkValue[1]) : '');
68
+
69
+ const resolvedStartName = $derived(startName ?? (name ? `${name}Start` : undefined));
70
+ const resolvedEndName = $derived(endName ?? (name ? `${name}End` : undefined));
71
+
29
72
  const datePicker = useDatePicker(() => ({
30
73
  id,
31
74
  locale: locale().locale,
32
75
  selectionMode: 'range',
33
- value,
34
- defaultValue,
76
+ value: arkValue,
35
77
  min,
36
78
  max,
37
79
  disabled,
38
80
  readOnly,
39
81
  required,
40
82
  invalid,
41
- timeZone,
83
+ timeZone: tz,
42
84
  onValueChange(details) {
43
- value = details.value;
85
+ const [nextStart = null, nextEnd = null] = details.value;
86
+ // Guard: skip if Ark UI echoes back the same range (prevents reactive loop)
87
+ if (
88
+ nextStart?.toString() === arkStart?.toString() &&
89
+ nextEnd?.toString() === arkEnd?.toString()
90
+ ) {
91
+ return;
92
+ }
93
+ start = fromDateValue(nextStart, startShape, tz);
94
+ end = fromDateValue(nextEnd, endShape, tz);
44
95
  onValueChange?.(details);
45
96
  }
46
97
  }));
@@ -49,6 +100,7 @@
49
100
  id,
50
101
  locale: locale().locale,
51
102
  selectionMode: 'range',
103
+ granularity: granularity ?? 'day',
52
104
  hourCycle,
53
105
  value: datePicker().value,
54
106
  min,
@@ -57,7 +109,7 @@
57
109
  readOnly,
58
110
  required,
59
111
  invalid,
60
- timeZone,
112
+ timeZone: tz,
61
113
  onValueChange(details) {
62
114
  datePicker().setValue(details.value);
63
115
  }
@@ -104,5 +156,13 @@
104
156
  <DatePickerCalendar />
105
157
  </DatePicker.RootProvider>
106
158
  </DateInput.Control>
107
- <DateInput.HiddenInput {name} />
159
+ <!-- Ark's HiddenInput would submit locale-formatted strings under names that
160
+ flip between `name` and `name[0]`/`name[1]` depending on how many dates are
161
+ selected. Plain inputs give stable names and canonical ISO values. -->
162
+ {#if resolvedStartName}
163
+ <input type="hidden" name={resolvedStartName} {disabled} value={submittedStart} />
164
+ {/if}
165
+ {#if resolvedEndName}
166
+ <input type="hidden" name={resolvedEndName} {disabled} value={submittedEnd} />
167
+ {/if}
108
168
  </DateInput.RootProvider>
@@ -1,4 +1,4 @@
1
1
  import type { DateRangeFieldProps } from './types';
2
- declare const DateRangeField: import("svelte").Component<DateRangeFieldProps, {}, "value">;
2
+ declare const DateRangeField: import("svelte").Component<DateRangeFieldProps, {}, "start" | "end">;
3
3
  type DateRangeField = ReturnType<typeof DateRangeField>;
4
4
  export default DateRangeField;
@@ -1,10 +1,29 @@
1
1
  import type { DatePickerRootBaseProps } from '@ark-ui/svelte/date-picker';
2
- import type { DateValue } from '@ark-ui/svelte/date-picker';
2
+ import type { DateValue, DateInputValue } from '../../utils/date';
3
+ export type { DateValue, DateInputValue };
3
4
  export interface DateRangeFieldProps extends Omit<DatePickerRootBaseProps, 'value' | 'defaultValue' | 'selectionMode'> {
4
- value?: DateValue[];
5
- defaultValue?: DateValue[];
5
+ /**
6
+ * Bound start of the range. Accepts a `DateValue`, an ISO/DB string, or a
7
+ * native `Date` — no manual conversion needed. Emitted back in the same shape
8
+ * it was provided (string in → string out, `Date` in → `Date` out); when it
9
+ * starts `null`, changes are emitted as a string.
10
+ *
11
+ * Bind strings when feeding SvelteKit remote functions: `query`/`command`
12
+ * arguments are serialized with devalue, which handles strings and `Date` but
13
+ * not `@internationalized/date` class instances.
14
+ */
15
+ start?: DateInputValue;
16
+ /** Bound end of the range. Ignored while {@link start} is empty. */
17
+ end?: DateInputValue;
18
+ defaultStart?: DateInputValue;
19
+ defaultEnd?: DateInputValue;
6
20
  label?: string;
21
+ /** Shorthand: names the hidden inputs `${name}Start` and `${name}End`. */
7
22
  name?: string;
23
+ /** Names the start hidden input outright. Overrides {@link name}. */
24
+ startName?: string;
25
+ /** Names the end hidden input outright. Overrides {@link name}. */
26
+ endName?: string;
8
27
  granularity?: 'day' | 'hour' | 'minute' | 'second';
9
28
  hourCycle?: 12 | 24;
10
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.71.6",
3
+ "version": "0.72.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev --open",
@@ -11,6 +11,8 @@
11
11
  "prepack": "svelte-kit sync && svelte-package && publint",
12
12
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
13
13
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
14
16
  "lint": "prettier --check . && eslint .",
15
17
  "format": "prettier --write .",
16
18
  "patch": "npm version patch && npm publish",
@@ -81,7 +83,8 @@
81
83
  "typescript": "^6.0.3",
82
84
  "typescript-eslint": "^8.68.0",
83
85
  "unplugin-icons": "^23.0.1",
84
- "vite": "^8.2.2"
86
+ "vite": "^8.2.2",
87
+ "vitest": "^5"
85
88
  },
86
89
  "keywords": [
87
90
  "svelte",