@proyecto-viviana/solid-stately 0.3.0 → 0.4.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 (63) hide show
  1. package/dist/calendar/IncompleteDate.d.ts +61 -0
  2. package/dist/calendar/createCalendarState.d.ts +20 -10
  3. package/dist/calendar/createDateFieldState.d.ts +10 -3
  4. package/dist/calendar/createRangeCalendarState.d.ts +10 -2
  5. package/dist/calendar/index.d.ts +1 -1
  6. package/dist/collections/createListState.d.ts +4 -1
  7. package/dist/collections/createSelectionState.d.ts +17 -5
  8. package/dist/collections/getChildNodes.d.ts +11 -0
  9. package/dist/collections/index.d.ts +2 -1
  10. package/dist/combobox/createComboBoxState.d.ts +7 -0
  11. package/dist/dnd/index.d.ts +1 -1
  12. package/dist/dnd/types.d.ts +3 -1
  13. package/dist/grid/types.d.ts +17 -3
  14. package/dist/index.d.ts +6 -6
  15. package/dist/index.js +1570 -425
  16. package/dist/index.js.map +1 -1
  17. package/dist/select/createSelectState.d.ts +6 -0
  18. package/dist/selection/Selection.d.ts +16 -0
  19. package/dist/selection/SelectionManager.d.ts +110 -0
  20. package/dist/selection/createMultipleSelectionState.d.ts +59 -0
  21. package/dist/selection/index.d.ts +1 -0
  22. package/dist/table/TableCollection.d.ts +30 -0
  23. package/dist/table/createTreeGridState.d.ts +15 -0
  24. package/dist/table/index.d.ts +2 -1
  25. package/dist/table/types.d.ts +58 -0
  26. package/dist/toast/createToastState.d.ts +16 -0
  27. package/dist/toast/index.d.ts +1 -1
  28. package/dist/tree/types.d.ts +14 -6
  29. package/package.json +1 -1
  30. package/src/calendar/IncompleteDate.ts +430 -0
  31. package/src/calendar/createCalendarState.ts +104 -37
  32. package/src/calendar/createDateFieldState.ts +262 -261
  33. package/src/calendar/createRangeCalendarState.ts +131 -19
  34. package/src/calendar/index.ts +2 -0
  35. package/src/collections/createListState.ts +50 -19
  36. package/src/collections/createMenuState.ts +3 -0
  37. package/src/collections/createSelectionState.ts +38 -28
  38. package/src/collections/getChildNodes.ts +98 -0
  39. package/src/collections/index.ts +9 -0
  40. package/src/combobox/createComboBoxState.ts +8 -0
  41. package/src/dnd/index.d.ts +1 -0
  42. package/src/dnd/index.ts +1 -0
  43. package/src/dnd/types.d.ts +3 -1
  44. package/src/dnd/types.ts +4 -1
  45. package/src/grid/createGridState.ts +36 -2
  46. package/src/grid/types.ts +17 -3
  47. package/src/index.d.ts +1 -0
  48. package/src/index.ts +11 -0
  49. package/src/select/createSelectState.ts +15 -2
  50. package/src/selection/Selection.ts +28 -0
  51. package/src/selection/SelectionManager.ts +526 -0
  52. package/src/selection/createMultipleSelectionState.ts +247 -0
  53. package/src/selection/index.ts +6 -0
  54. package/src/table/TableCollection.ts +213 -43
  55. package/src/table/createTableState.ts +20 -0
  56. package/src/table/createTreeGridState.ts +146 -0
  57. package/src/table/index.ts +3 -0
  58. package/src/table/types.ts +66 -0
  59. package/src/tabs/createTabListState.ts +11 -9
  60. package/src/toast/createToastState.ts +43 -10
  61. package/src/toast/index.ts +1 -0
  62. package/src/tree/createTreeState.ts +37 -6
  63. package/src/tree/types.ts +14 -6
@@ -3,22 +3,30 @@
3
3
  *
4
4
  * Provides state management for date field components with segment-based editing.
5
5
  * Based on @react-stately/datepicker useDateFieldState
6
+ *
7
+ * Editing is modeled with an {@link IncompleteDate} display value so that invalid
8
+ * intermediate dates (e.g. February 31st, produced by editing the day before the
9
+ * month) are held as-typed while the field is focused and only constrained on blur
10
+ * via `confirmPlaceholder`, matching upstream behavior. Out-of-range values are
11
+ * never snapped to `minValue`/`maxValue`; they are kept and reported as invalid.
6
12
  */
7
13
 
8
- import { createSignal, createMemo, type Accessor } from "solid-js";
14
+ import { createSignal, createMemo, createEffect, type Accessor } from "solid-js";
9
15
  import {
10
- CalendarDate,
16
+ type AnyDateTime,
17
+ type Calendar,
11
18
  type CalendarDateTime,
12
19
  type DateValue,
20
+ GregorianCalendar,
13
21
  today,
14
22
  getLocalTimeZone,
15
23
  DateFormatter,
16
24
  toCalendarDate as intlToCalendarDate,
17
25
  toCalendarDateTime,
18
- endOfMonth,
19
26
  } from "@internationalized/date";
20
27
  import { access, type MaybeAccessor } from "../utils";
21
28
  import type { ValidationState } from "./createCalendarState";
29
+ import { IncompleteDate } from "./IncompleteDate";
22
30
  import {
23
31
  createFormValidationState,
24
32
  DEFAULT_VALIDATION_RESULT,
@@ -47,7 +55,7 @@ export interface DateSegment {
47
55
  /** The text content of the segment. */
48
56
  text: string;
49
57
  /** The numeric value of the segment (if applicable). */
50
- value?: number;
58
+ value?: number | null;
51
59
  /** The minimum value for the segment. */
52
60
  minValue?: number;
53
61
  /** The maximum value for the segment. */
@@ -160,62 +168,53 @@ export interface DateFieldState<T extends DateValue = DateValue> {
160
168
  defaultValue: T | null;
161
169
  }
162
170
 
171
+ type HourCycle = "h11" | "h12" | "h23" | "h24";
172
+
163
173
  /**
164
174
  * Provides state management for a date field component.
165
175
  */
166
- export function createDateFieldState<T extends DateValue = CalendarDate>(
176
+ export function createDateFieldState<T extends DateValue = DateValue>(
167
177
  props: DateFieldStateProps<T> = {},
168
178
  ): DateFieldState<T> {
169
179
  const timeZone = getLocalTimeZone();
170
180
  const locale = props.locale ?? "en-US";
171
181
  const granularity = props.granularity ?? "day";
182
+ const calendar: Calendar = new GregorianCalendar();
183
+ const hourCycle = resolveHourCycle(locale, props.hourCycle);
184
+ const displaySegments = computeDisplaySegments(granularity, hourCycle);
185
+
186
+ const getPlaceholderValue = (): DateValue => {
187
+ const base = props.placeholderValue ?? today(timeZone);
188
+ return granularity === "day" ? intlToCalendarDate(base) : toCalendarDateTime(base);
189
+ };
190
+
191
+ const toSource = (v: DateValue | null): Partial<Omit<AnyDateTime, "copy">> | null =>
192
+ (v as Partial<Omit<AnyDateTime, "copy">> | null) ?? null;
172
193
 
173
194
  // State signals
174
195
  const initialValue = props.defaultValue ?? null;
175
196
  const [internalValue, setInternalValue] = createSignal<T | null>(initialValue);
176
197
 
177
- // Track partial values during editing
178
- const [placeholderDate, setPlaceholderDate] = createSignal<Partial<DateParts>>({});
179
-
180
- // Controlled vs uncontrolled value
198
+ // Controlled vs uncontrolled committed value
181
199
  const value = createMemo<T | null>(() => {
182
200
  const controlled = access(props.value);
183
201
  return controlled !== undefined ? controlled : internalValue();
184
202
  });
185
203
 
186
- // The effective date value
187
- const dateValue = createMemo<DateValue | null>(() => {
188
- const v = value();
189
- if (v) return v;
190
-
191
- // Build from placeholder parts
192
- const parts = placeholderDate();
193
- const placeholder = props.placeholderValue ?? today(timeZone);
194
-
195
- if (Object.keys(parts).length === 0) return null;
196
-
197
- // Create a date from the parts
198
- const year = parts.year ?? placeholder.year;
199
- const month = parts.month ?? placeholder.month;
200
- const day = parts.day ?? placeholder.day;
204
+ // The display override: holds the (possibly invalid/incomplete) value being edited.
205
+ const [displayValue, setDisplayValue] = createSignal<IncompleteDate>(
206
+ new IncompleteDate(calendar, hourCycle, toSource(value())),
207
+ );
201
208
 
202
- if (granularity === "day") {
203
- return intlToCalendarDate(placeholder).set({ year, month, day });
209
+ // Reset the display override whenever the committed value changes from the outside
210
+ // (e.g. a controlled `value` prop update).
211
+ let lastSyncedValue = value();
212
+ createEffect(() => {
213
+ const v = value();
214
+ if (v !== lastSyncedValue) {
215
+ lastSyncedValue = v;
216
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(v)));
204
217
  }
205
-
206
- // For time granularities
207
- const hour = parts.hour ?? 0;
208
- const minute = parts.minute ?? 0;
209
- const second = parts.second ?? 0;
210
-
211
- return toCalendarDateTime(placeholder).set({
212
- year,
213
- month,
214
- day,
215
- hour,
216
- minute,
217
- second,
218
- }) as unknown as T;
219
218
  });
220
219
 
221
220
  // Derived states
@@ -284,187 +283,184 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
284
283
  () => explicitValidationState() ?? (isInvalid() ? "invalid" : undefined),
285
284
  );
286
285
 
287
- // Generate segments based on granularity and locale
288
- const segments = createMemo<DateSegment[]>(() => {
286
+ // The effective date value, derived from the display override.
287
+ const dateValue = createMemo<DateValue | null>(() => {
289
288
  const v = value();
290
- const placeholder = props.placeholderValue ?? today(timeZone);
291
- const parts = placeholderDate();
289
+ if (v) return v;
290
+ const dv = displayValue();
291
+ if (dv.isCleared(displaySegments)) return null;
292
+ return dv.toValue(getPlaceholderValue());
293
+ });
292
294
 
293
- const segs: DateSegment[] = [];
295
+ // Generate segments from the display override (so invalid intermediate values render as typed).
296
+ const segments = createMemo<DateSegment[]>(() => {
297
+ const dv = displayValue();
298
+ const base = value() ?? getPlaceholderValue();
299
+ const resolved = dv.toValue(base);
300
+ const jsDate = resolved.toDate(timeZone);
294
301
 
295
- // Determine format options based on granularity
296
302
  const shouldForceLeadingZeros = props.shouldForceLeadingZeros === true;
297
- const formatOptions: Intl.DateTimeFormatOptions = {
298
- year: "numeric",
299
- month: shouldForceLeadingZeros ? "2-digit" : "numeric",
300
- day: shouldForceLeadingZeros ? "2-digit" : "numeric",
301
- };
302
-
303
- if (granularity !== "day") {
304
- formatOptions.hour = "2-digit";
305
- formatOptions.minute = "2-digit";
306
- if (granularity === "second") {
307
- formatOptions.second = "2-digit";
308
- }
309
- if (props.hourCycle) {
310
- formatOptions.hourCycle = props.hourCycle === 12 ? "h12" : "h23";
311
- }
312
- }
313
-
303
+ const formatOptions = buildFormatOptions(granularity, shouldForceLeadingZeros, props.hourCycle);
314
304
  const formatter = new DateFormatter(locale, formatOptions);
315
- const dateToFormat = v ?? placeholder;
316
- const formattedParts = formatter.formatToParts(dateToFormat.toDate(timeZone));
305
+ const resolvedOptions = formatter.resolvedOptions() as unknown as Record<string, string>;
306
+ const parts = formatter.formatToParts(jsDate);
317
307
 
318
- for (const part of formattedParts) {
308
+ const numberFormatter = new Intl.NumberFormat(locale, { useGrouping: false });
309
+ const twoDigitFormatter = new Intl.NumberFormat(locale, {
310
+ useGrouping: false,
311
+ minimumIntegerDigits: 2,
312
+ });
313
+
314
+ const result: DateSegment[] = [];
315
+ for (const part of parts) {
319
316
  const type = mapPartType(part.type);
320
317
 
321
318
  if (type === "literal") {
322
- segs.push({
319
+ result.push({
323
320
  type: "literal",
324
321
  text: part.value,
325
322
  isEditable: false,
326
323
  isPlaceholder: false,
327
324
  placeholder: part.value,
328
325
  });
329
- } else if (type) {
330
- const segValue = getSegmentValue(v, type);
331
- const partValue = parts[type as keyof DateParts];
332
- const hasValue = v !== null || partValue !== undefined;
333
-
334
- segs.push({
335
- type,
336
- text: hasValue
337
- ? part.value
338
- : getPlaceholderText(type, locale, granularity, shouldForceLeadingZeros),
339
- value: segValue ?? partValue,
340
- minValue: getMinValue(type),
341
- maxValue: getMaxValue(type, v ?? placeholder),
342
- isEditable: !isDisabled() && !isReadOnly(),
343
- isPlaceholder: !hasValue,
344
- placeholder: getPlaceholderText(type, locale, granularity, shouldForceLeadingZeros),
345
- });
326
+ continue;
327
+ }
328
+ if (!type) continue;
329
+
330
+ const editable = isEditableSegment(type);
331
+ const rawValue = rawSegmentValue(dv, type);
332
+ const isPlaceholder = editable && rawValue == null;
333
+ const limits = dv.getSegmentLimits(type);
334
+ const placeholderText = getPlaceholderText(
335
+ type,
336
+ locale,
337
+ granularity,
338
+ shouldForceLeadingZeros,
339
+ );
340
+
341
+ // For numeric date/time segments, render the raw edited value (e.g. "31" even
342
+ // when the resolved date is Feb 28) using a NumberFormatter, mirroring upstream.
343
+ let text = part.value;
344
+ if (
345
+ (type === "year" || type === "month" || type === "day" || type === "hour") &&
346
+ typeof rawValue === "number"
347
+ ) {
348
+ const numeric = rawValue ?? 0;
349
+ text =
350
+ resolvedOptions[type] === "2-digit"
351
+ ? twoDigitFormatter.format(numeric)
352
+ : numberFormatter.format(numeric);
346
353
  }
354
+
355
+ result.push({
356
+ type,
357
+ text: isPlaceholder ? placeholderText : text,
358
+ value: limits?.value,
359
+ minValue: limits?.minValue,
360
+ maxValue: limits?.maxValue,
361
+ isEditable: editable && !isDisabled() && !isReadOnly(),
362
+ isPlaceholder,
363
+ placeholder: placeholderText,
364
+ });
347
365
  }
348
366
 
349
- return segs;
367
+ return result;
350
368
  });
351
369
 
352
- // Set value with onChange callback
353
- const setValue = (newValue: T | null) => {
354
- if (isDisabled() || isReadOnly()) return;
355
-
370
+ // Commit a concrete value (updates the controlled/uncontrolled value and fires onChange).
371
+ const commitDate = (newValue: T | null) => {
356
372
  const controlled = access(props.value);
357
373
  if (controlled === undefined) {
374
+ lastSyncedValue = newValue;
358
375
  setInternalValue(() => newValue);
359
376
  }
360
-
361
- if (props.onChange) {
362
- props.onChange(newValue);
363
- }
364
-
365
- // Clear placeholder parts
366
- setPlaceholderDate({});
377
+ props.onChange?.(newValue);
367
378
  };
368
379
 
369
- // Set a specific segment value
370
- const setSegment = (type: DateSegmentType, newValue: number) => {
380
+ // Set value with the upstream split: complete + valid commits eagerly; an incomplete
381
+ // or invalid (e.g. Feb 31) value is held as a display override until blur.
382
+ const setValue = (newValue: T | IncompleteDate | null) => {
371
383
  if (isDisabled() || isReadOnly()) return;
372
384
 
373
- const v = value();
374
-
375
- if (v) {
376
- // Update existing value
377
- const updated = updateDatePart(v, type, newValue);
378
- setValue(updated as T);
379
- } else {
380
- // Update placeholder parts
381
- setPlaceholderDate((prev) => ({
382
- ...prev,
383
- [type]: newValue,
384
- }));
385
+ if (
386
+ newValue == null ||
387
+ (newValue instanceof IncompleteDate && newValue.isCleared(displaySegments))
388
+ ) {
389
+ commitDate(null);
390
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, null));
391
+ return;
385
392
  }
386
- };
387
393
 
388
- // Increment a segment
389
- const incrementSegment = (type: DateSegmentType) => {
390
- if (isDisabled() || isReadOnly()) return;
394
+ if (!(newValue instanceof IncompleteDate)) {
395
+ commitDate(newValue);
396
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(newValue)));
397
+ return;
398
+ }
391
399
 
392
- const v = value();
393
- const current = v ? getSegmentValue(v, type) : placeholderDate()[type as keyof DateParts];
394
- const max = getMaxValue(type, v ?? props.placeholderValue ?? today(timeZone));
395
- const min = getMinValue(type);
400
+ // If the new value is complete and valid, trigger onChange eagerly. If it represents
401
+ // an incomplete or invalid value (e.g. February 30th), wait until blur to trigger onChange.
402
+ if (newValue.isComplete(displaySegments)) {
403
+ const resolved = newValue.toValue(value() ?? getPlaceholderValue());
404
+ if (newValue.validate(resolved, displaySegments)) {
405
+ const committed = value();
406
+ if (!committed || resolved.compare(committed) !== 0) {
407
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(resolved)));
408
+ commitDate(resolved as T);
409
+ return;
410
+ }
411
+ }
412
+ }
396
413
 
397
- const newValue = current !== undefined ? current + 1 : min;
398
- setSegment(type, newValue > max ? min : newValue);
414
+ // Incomplete/invalid value. Set temporary display override.
415
+ setDisplayValue(newValue);
399
416
  };
400
417
 
401
- // Decrement a segment
402
- const decrementSegment = (type: DateSegmentType) => {
418
+ const adjustSegment = (type: DateSegmentType, amount: number) => {
403
419
  if (isDisabled() || isReadOnly()) return;
420
+ setValue(displayValue().cycle(type, amount, getPlaceholderValue(), displaySegments));
421
+ };
404
422
 
405
- const v = value();
406
- const current = v ? getSegmentValue(v, type) : placeholderDate()[type as keyof DateParts];
407
- const max = getMaxValue(type, v ?? props.placeholderValue ?? today(timeZone));
408
- const min = getMinValue(type);
409
-
410
- const newValue = current !== undefined ? current - 1 : max;
411
- setSegment(type, newValue < min ? max : newValue);
423
+ // Set a specific segment value
424
+ const setSegment = (type: DateSegmentType, newValue: number) => {
425
+ if (isDisabled() || isReadOnly()) return;
426
+ setValue(displayValue().set(type, newValue, getPlaceholderValue()));
412
427
  };
413
428
 
414
- // Clear a segment
429
+ const incrementSegment = (type: DateSegmentType) => adjustSegment(type, 1);
430
+ const decrementSegment = (type: DateSegmentType) => adjustSegment(type, -1);
431
+
432
+ // Clear a segment, reverting it to its placeholder.
415
433
  const clearSegment = (type: DateSegmentType) => {
416
434
  if (isDisabled() || isReadOnly()) return;
417
- if (type === "literal") return;
418
-
419
- const v = value();
420
- if (v) {
421
- // Reset only the target segment to its minimum value instead of clearing the entire value
422
- const min = getMinValue(type);
423
- const updated = updateDatePart(v, type, min);
424
- setValue(updated as T);
425
- } else {
426
- setPlaceholderDate((prev) => {
427
- const next = { ...prev };
428
- delete next[type as keyof DateParts];
429
- return next;
430
- });
435
+ let dv = displayValue();
436
+ if (type !== "timeZoneName" && type !== "literal") {
437
+ dv = dv.clear(type);
431
438
  }
439
+ setValue(dv);
432
440
  };
433
441
 
434
- // Confirm placeholder value
442
+ // When the field is blurred, constrain a complete-but-invalid display value
443
+ // (e.g. February 31st -> February 28th) and emit onChange. Out-of-range values
444
+ // are left as-is and reported via validation rather than being snapped.
435
445
  const confirmPlaceholder = () => {
436
446
  if (isDisabled() || isReadOnly()) return;
437
-
438
- const minValue = access(props.minValue);
439
- const maxValue = access(props.maxValue);
440
- const constrain = (candidate: DateValue): DateValue => {
441
- if (minValue && candidate.compare(minValue) < 0) {
442
- return minValue;
443
- }
444
- if (maxValue && candidate.compare(maxValue) > 0) {
445
- return maxValue;
446
- }
447
- return candidate;
448
- };
449
-
450
- const parts = placeholderDate();
451
- if (Object.keys(parts).length > 0) {
452
- const dv = dateValue();
453
- if (dv) {
454
- const candidate = constrain(dv);
455
- // Commit even if unavailable so isInvalid can flag it; don't leave field stuck
456
- setValue(candidate as T);
457
- }
458
- return;
447
+ const dv = displayValue();
448
+ if (!dv.isComplete(displaySegments)) return;
449
+
450
+ const resolved = dv.toValue(value() ?? getPlaceholderValue());
451
+ const committed = value();
452
+ const needsCommit = !committed || resolved.compare(committed) !== 0;
453
+ if (needsCommit) {
454
+ commitDate(resolved as T);
459
455
  }
460
456
 
461
- const current = value();
462
- if (current) {
463
- const constrained = constrain(current);
464
- // Apply min/max constraints even if unavailable; isInvalid will flag unavailable dates
465
- if (constrained.compare(current) !== 0) {
466
- setValue(constrained as T);
467
- }
457
+ // Reset the display override so a constrained value (e.g. February 31st ->
458
+ // February 28th) renders. Only touch the signal when the displayed raw value
459
+ // diverged from the resolved value, or when we committed — otherwise leave it
460
+ // untouched so blur (which fires on every focus move) does not churn the
461
+ // segments and drop focus.
462
+ if (needsCommit || !dv.validate(resolved, displaySegments)) {
463
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(value() ?? resolved)));
468
464
  }
469
465
  };
470
466
 
@@ -519,99 +515,124 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
519
515
  };
520
516
  }
521
517
 
522
- interface DateParts {
523
- year?: number;
524
- month?: number;
525
- day?: number;
526
- hour?: number;
527
- minute?: number;
528
- second?: number;
518
+ const EDITABLE_SEGMENTS: Record<string, boolean> = {
519
+ year: true,
520
+ month: true,
521
+ day: true,
522
+ hour: true,
523
+ minute: true,
524
+ second: true,
525
+ dayPeriod: true,
526
+ era: true,
527
+ };
528
+
529
+ function isEditableSegment(type: DateSegmentType): boolean {
530
+ return EDITABLE_SEGMENTS[type] === true;
529
531
  }
530
532
 
531
- function mapPartType(type: Intl.DateTimeFormatPartTypes): DateSegmentType | null {
533
+ function rawSegmentValue(dv: IncompleteDate, type: DateSegmentType): number | string | null {
532
534
  switch (type) {
535
+ case "era":
536
+ return dv.era;
533
537
  case "year":
534
- return "year";
538
+ return dv.year;
535
539
  case "month":
536
- return "month";
540
+ return dv.month;
537
541
  case "day":
538
- return "day";
542
+ return dv.day;
539
543
  case "hour":
540
- return "hour";
544
+ return dv.hour;
541
545
  case "minute":
542
- return "minute";
546
+ return dv.minute;
543
547
  case "second":
544
- return "second";
548
+ return dv.second;
545
549
  case "dayPeriod":
546
- return "dayPeriod";
547
- case "era":
548
- return "era";
549
- case "timeZoneName":
550
- return "timeZoneName";
551
- case "literal":
552
- return "literal";
550
+ return dv.dayPeriod;
553
551
  default:
554
552
  return null;
555
553
  }
556
554
  }
557
555
 
558
- function getSegmentValue(date: DateValue | null, type: DateSegmentType): number | undefined {
559
- if (!date) return undefined;
556
+ function resolveHourCycle(locale: string, hourCycle?: 12 | 24): HourCycle {
557
+ const formatter = new DateFormatter(locale, {
558
+ hour: "numeric",
559
+ hour12: hourCycle != null ? hourCycle === 12 : undefined,
560
+ });
561
+ return (formatter.resolvedOptions().hourCycle as HourCycle) ?? "h23";
562
+ }
560
563
 
561
- switch (type) {
562
- case "year":
563
- return date.year;
564
- case "month":
565
- return date.month;
566
- case "day":
567
- return date.day;
568
- case "hour":
569
- return "hour" in date ? (date as CalendarDateTime).hour : undefined;
570
- case "minute":
571
- return "minute" in date ? (date as CalendarDateTime).minute : undefined;
572
- case "second":
573
- return "second" in date ? (date as CalendarDateTime).second : undefined;
574
- default:
575
- return undefined;
576
- }
564
+ function computeDisplaySegments(
565
+ granularity: "day" | "hour" | "minute" | "second",
566
+ hourCycle: HourCycle,
567
+ ): DateSegmentType[] {
568
+ const is12HourClock = hourCycle === "h11" || hourCycle === "h12";
569
+ const segments: DateSegmentType[] = [
570
+ "era",
571
+ "year",
572
+ "month",
573
+ "day",
574
+ "hour",
575
+ ...(is12HourClock ? (["dayPeriod"] as const) : []),
576
+ "minute",
577
+ "second",
578
+ ];
579
+ // We never expose era as an editable segment, so the maximum displayed unit is the year.
580
+ const minIndex = segments.indexOf("era");
581
+ const maxIndex = segments.indexOf(
582
+ granularity === "hour" && is12HourClock ? "dayPeriod" : granularity,
583
+ );
584
+ return segments.slice(minIndex, maxIndex + 1);
577
585
  }
578
586
 
579
- function getMinValue(type: DateSegmentType): number {
580
- switch (type) {
581
- case "year":
582
- return 1;
583
- case "month":
584
- return 1;
585
- case "day":
586
- return 1;
587
- case "hour":
588
- return 0;
589
- case "minute":
590
- return 0;
591
- case "second":
592
- return 0;
593
- default:
594
- return 0;
587
+ function buildFormatOptions(
588
+ granularity: "day" | "hour" | "minute" | "second",
589
+ shouldForceLeadingZeros: boolean,
590
+ hourCycle?: 12 | 24,
591
+ ): Intl.DateTimeFormatOptions {
592
+ const formatOptions: Intl.DateTimeFormatOptions = {
593
+ year: "numeric",
594
+ month: shouldForceLeadingZeros ? "2-digit" : "numeric",
595
+ day: shouldForceLeadingZeros ? "2-digit" : "numeric",
596
+ };
597
+
598
+ if (granularity !== "day") {
599
+ formatOptions.hour = "2-digit";
600
+ formatOptions.minute = "2-digit";
601
+ if (granularity === "second") {
602
+ formatOptions.second = "2-digit";
603
+ }
604
+ if (hourCycle) {
605
+ formatOptions.hourCycle = hourCycle === 12 ? "h12" : "h23";
606
+ }
595
607
  }
608
+
609
+ return formatOptions;
596
610
  }
597
611
 
598
- function getMaxValue(type: DateSegmentType, date: DateValue): number {
612
+ function mapPartType(type: Intl.DateTimeFormatPartTypes): DateSegmentType | null {
599
613
  switch (type) {
600
614
  case "year":
601
- return 9999;
615
+ return "year";
602
616
  case "month":
603
- return 12;
617
+ return "month";
604
618
  case "day":
605
- // Get days in month
606
- return endOfMonth(date).day;
619
+ return "day";
607
620
  case "hour":
608
- return 23;
621
+ return "hour";
609
622
  case "minute":
610
- return 59;
623
+ return "minute";
611
624
  case "second":
612
- return 59;
625
+ return "second";
626
+ case "dayPeriod":
627
+ return "dayPeriod";
628
+ case "era":
629
+ return "era";
630
+ case "timeZoneName":
631
+ return "timeZoneName";
632
+ case "literal":
633
+ return "literal";
613
634
  default:
614
- return 0;
635
+ return null;
615
636
  }
616
637
  }
617
638
 
@@ -646,7 +667,7 @@ function getPlaceholderText(
646
667
  for (const part of parts) {
647
668
  const mappedType = mapPartType(part.type);
648
669
  if (mappedType === type) {
649
- return part.value.replace(/\d/g, (d) => {
670
+ return part.value.replace(/\d/g, (_d) => {
650
671
  if (type === "year") return "y";
651
672
  if (type === "month") return "m";
652
673
  if (type === "day") return "d";
@@ -673,25 +694,5 @@ function getPlaceholderText(
673
694
  }
674
695
  }
675
696
 
676
- function updateDatePart(date: DateValue, type: DateSegmentType, value: number): DateValue {
677
- if (typeof (date as { set?: unknown }).set !== "function") {
678
- return date;
679
- }
680
-
681
- switch (type) {
682
- case "year":
683
- return date.set({ year: value });
684
- case "month":
685
- return date.set({ month: value });
686
- case "day":
687
- return date.set({ day: value });
688
- case "hour":
689
- return date.set({ hour: value });
690
- case "minute":
691
- return date.set({ minute: value });
692
- case "second":
693
- return date.set({ second: value });
694
- default:
695
- return date;
696
- }
697
- }
697
+ // Re-export so existing imports of CalendarDateTime keep resolving through this module.
698
+ export type { CalendarDateTime };