@stll/ui 0.25.2 → 0.26.1

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.
@@ -1,16 +1,16 @@
1
+ import { Result } from "better-result";
2
+ import { Temporal } from "temporal-polyfill/full";
1
3
  //#region src/calendar/resource-calendar.logic.ts
2
4
  const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
3
- const DAY_IN_MS = 864e5;
4
- const toUTCDate = (value) => {
5
+ const toPlainDate = (value) => {
5
6
  if (!ISO_DATE_PATTERN.test(value)) return null;
6
- const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
7
- return date.toISOString().slice(0, 10) === value ? date : null;
7
+ return Result.try(() => Temporal.PlainDate.from(value)).unwrapOr(null);
8
8
  };
9
9
  const differenceInCalendarDays = (later, earlier) => {
10
- const laterDate = toUTCDate(later);
11
- const earlierDate = toUTCDate(earlier);
10
+ const laterDate = toPlainDate(later);
11
+ const earlierDate = toPlainDate(earlier);
12
12
  if (laterDate === null || earlierDate === null) throw new RangeError("Calendar dates must use normalized YYYY-MM-DD values");
13
- return (laterDate.getTime() - earlierDate.getTime()) / DAY_IN_MS;
13
+ return laterDate.since(earlierDate, { largestUnit: "days" }).days;
14
14
  };
15
15
  const getResourceCalendarPlacement = ({ entry, visibleRange }) => {
16
16
  const visibleDayCount = differenceInCalendarDays(visibleRange.endDateExclusive, visibleRange.startDate);
@@ -27,7 +27,7 @@ const getResourceCalendarPlacement = ({ entry, visibleRange }) => {
27
27
  const assertConsecutiveCalendarDates = (dates) => {
28
28
  if (dates.length === 0) throw new RangeError("A resource calendar needs at least one date column");
29
29
  const first = dates.at(0);
30
- if (first === void 0 || toUTCDate(first) === null) throw new RangeError("Resource calendar date columns must be consecutive normalized dates");
30
+ if (first === void 0 || toPlainDate(first) === null) throw new RangeError("Resource calendar date columns must be consecutive normalized dates");
31
31
  for (let index = 1; index < dates.length; index += 1) {
32
32
  const previous = dates.at(index - 1);
33
33
  const current = dates.at(index);
@@ -35,11 +35,10 @@ const assertConsecutiveCalendarDates = (dates) => {
35
35
  }
36
36
  };
37
37
  const nextCalendarDate = (value) => {
38
- const date = toUTCDate(value);
38
+ const date = toPlainDate(value);
39
39
  if (date === null) throw new RangeError("Calendar dates must use normalized YYYY-MM-DD values");
40
- date.setUTCDate(date.getUTCDate() + 1);
41
- const nextDate = date.toISOString().slice(0, 10);
42
- if (toUTCDate(nextDate) === null) throw new RangeError("Calendar dates must have a following normalized YYYY-MM-DD value");
40
+ const nextDate = date.add({ days: 1 }).toString();
41
+ if (toPlainDate(nextDate) === null) throw new RangeError("Calendar dates must have a following normalized YYYY-MM-DD value");
43
42
  return nextDate;
44
43
  };
45
44
  const layoutResourceCalendarEntries = (entries, visibleRange) => {
@@ -10,7 +10,7 @@
10
10
  */
11
11
  declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
12
12
  declare const buttonVariants: (props?: ({
13
- size?: "default" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "sm" | "xl" | "xs" | null | undefined;
13
+ size?: "sm" | "default" | "lg" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "xl" | "xs" | null | undefined;
14
14
  variant?: "link" | "default" | "destructive" | "destructive-outline" | "ghost" | "outline" | "secondary" | null | undefined;
15
15
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
16
  //#endregion
@@ -31,17 +31,27 @@ type ColorPickerProps = {
31
31
  align?: "start" | "center" | "end";
32
32
  className?: string;
33
33
  };
34
- type ColorPickerContentProps = {
34
+ type ColorPickerContentBaseProps = {
35
35
  value?: string | undefined;
36
36
  onSelect?: ((value: string) => void) | undefined;
37
- onClear?: (() => void) | undefined;
38
37
  presets: ColorPreset[];
39
- columns: number;
40
- defaultExpanded: boolean;
41
38
  moreLabel: string;
42
39
  };
40
+ type ColorPickerContentProps = ColorPickerContentBaseProps & ({
41
+ columns: number;
42
+ defaultExpanded: boolean;
43
+ onClear?: (() => void) | undefined;
44
+ /** Popover content closes on preset selection. */
45
+ presentation?: "popover";
46
+ } | {
47
+ columns?: never;
48
+ defaultExpanded?: never;
49
+ onClear?: never;
50
+ /** Inline content stays mounted and reserves its popup for custom color. */
51
+ presentation: "inline";
52
+ });
43
53
  declare const DEFAULT_PRESETS: ColorPreset[];
44
- declare const ColorPickerContent: ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel }: ColorPickerContentProps) => React$1.JSX.Element;
54
+ declare const ColorPickerContent: ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel, presentation }: ColorPickerContentProps) => React$1.JSX.Element;
45
55
  declare const ColorPicker: ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel, children, side, align, className }: ColorPickerProps) => React$1.JSX.Element;
46
56
  //#endregion
47
57
  export { ColorPicker, ColorPickerContent, type ColorPickerContentProps, type ColorPickerProps, type ColorPreset, DEFAULT_PRESETS };
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
+ import { OVERLAY_LAYER_CLASS_NAMES } from "../lib/overlay-layer.js";
3
4
  import { CheckIcon, ChevronDownIcon } from "lucide-react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { Suspense, lazy, useState } from "react";
6
7
  import { Popover } from "@base-ui/react/popover";
7
8
  //#region src/components/color-picker.tsx
@@ -112,25 +113,97 @@ const isLightHex = (hex) => {
112
113
  const b = Number.parseInt(hex.slice(4, 6), 16);
113
114
  return (r * 299 + g * 587 + b * 114) / 1e3 > 220;
114
115
  };
116
+ const checkIconColorClassNames = {
117
+ dark: "text-(--color-white)",
118
+ light: "text-(--color-black)"
119
+ };
115
120
  /** Check if a value looks like a 6-char hex (no CSS vars, no named colors). */
116
121
  const looksLikeHex = (v) => /^[0-9A-Fa-f]{6}$/u.test(v);
117
- const ColorSwatch = ({ cssColor, selected, label, isLight, onClick }) => /* @__PURE__ */ jsx(Popover.Close, {
118
- render: /* @__PURE__ */ jsx("button", {
122
+ const ColorSwatch = ({ cssColor, selected, label, isLight, onClick, presentation }) => {
123
+ let selectionClassName = "border-border/40";
124
+ if (selected) selectionClassName = presentation === "inline" ? "border-transparent ring-2 ring-ring" : "border-foreground ring-ring/24 ring-1";
125
+ const swatch = /* @__PURE__ */ jsx("button", {
119
126
  "aria-label": label,
120
- className: cn("hover:border-foreground relative flex size-6 items-center justify-center rounded-md border transition-[transform,border-color] hover:scale-115 sm:size-5", selected ? "border-foreground ring-ring/24 ring-1" : "border-border/40", isLight && !selected && "border-border"),
127
+ "aria-pressed": selected,
128
+ className: cn(presentation === "inline" ? "ring-offset-popover relative grid size-11 shrink-0 place-items-center rounded-full border ring-offset-2 transition-transform outline-none hover:scale-105 focus-visible:ring-2" : "hover:border-foreground relative flex size-6 items-center justify-center rounded-md border transition-[transform,border-color] hover:scale-115 sm:size-5", selectionClassName, isLight && !selected && "border-border"),
121
129
  onClick,
122
130
  style: { backgroundColor: cssColor },
123
- type: "button"
131
+ type: "button",
132
+ children: selected && /* @__PURE__ */ jsx(CheckIcon, { className: cn("pointer-events-none", presentation === "inline" ? "bg-background/88 text-foreground size-5 rounded-full p-0.5 shadow-sm" : "size-3 sm:size-2.5", presentation === "popover" && checkIconColorClassNames[isLight ? "light" : "dark"]) })
133
+ });
134
+ return presentation === "inline" ? swatch : /* @__PURE__ */ jsx(Popover.Close, { render: swatch });
135
+ };
136
+ const CustomColorControls = ({ handleInputChange, handlePickerChange, inputHex, pickerHex }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Suspense, {
137
+ fallback: /* @__PURE__ */ jsx("div", {
138
+ "aria-hidden": true,
139
+ className: "ring-border/30 h-[140px] w-full rounded-lg ring-1"
124
140
  }),
125
- children: selected && /* @__PURE__ */ jsx(CheckIcon, {
126
- className: "pointer-events-none size-3 sm:size-2.5",
127
- style: { color: isLight ? "#000" : "#fff" }
141
+ children: /* @__PURE__ */ jsx(HexColorPicker, {
142
+ className: "ring-border/30 !h-[140px] !w-full overflow-hidden rounded-lg ring-1",
143
+ color: pickerHex,
144
+ onChange: (hex) => handlePickerChange(hex.replace("#", ""))
128
145
  })
129
- });
130
- const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel }) => {
146
+ }), /* @__PURE__ */ jsxs("div", {
147
+ className: "flex items-center gap-1.5",
148
+ children: [
149
+ /* @__PURE__ */ jsx("span", {
150
+ className: "text-muted-foreground text-[11px]",
151
+ children: "#"
152
+ }),
153
+ /* @__PURE__ */ jsx("input", {
154
+ "aria-label": "Custom hex color",
155
+ className: "border-input bg-background text-foreground h-6 flex-1 rounded border px-1.5 font-mono text-[11px] outline-none",
156
+ dir: "ltr",
157
+ maxLength: 6,
158
+ onChange: (event) => handleInputChange(event.target.value),
159
+ onKeyDown: (event) => {
160
+ if (event.key !== "Escape") event.stopPropagation();
161
+ },
162
+ onMouseDown: (event) => event.stopPropagation(),
163
+ onPointerDown: (event) => event.stopPropagation(),
164
+ placeholder: "FF0000",
165
+ value: inputHex
166
+ }),
167
+ isValidHex(inputHex) && /* @__PURE__ */ jsx("span", {
168
+ className: "border-border size-6 shrink-0 rounded border",
169
+ style: { backgroundColor: `#${inputHex}` }
170
+ })
171
+ ]
172
+ })] });
173
+ const InlineCustomColor = ({ customSelected, handleInputChange, handlePickerChange, inputHex, label, pickerHex, presets, value }) => {
174
+ const customColor = customSelected ? `#${value}` : void 0;
175
+ const customGradient = `conic-gradient(${presets.map((preset) => swatchColor(preset)).join(", ")})`;
176
+ return /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, {
177
+ render: /* @__PURE__ */ jsx("button", {
178
+ "aria-label": label,
179
+ "aria-pressed": customSelected,
180
+ className: cn("ring-offset-popover relative grid size-11 shrink-0 place-items-center rounded-full border border-transparent ring-offset-2 transition-transform outline-none hover:scale-105 focus-visible:ring-2", customSelected && "ring-ring ring-2"),
181
+ style: customColor ? { backgroundColor: customColor } : { backgroundImage: customGradient },
182
+ type: "button"
183
+ }),
184
+ children: customSelected ? /* @__PURE__ */ jsx(CheckIcon, { className: "bg-background/88 text-foreground pointer-events-none size-5 rounded-full p-0.5 shadow-sm" }) : null
185
+ }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
186
+ align: "end",
187
+ className: OVERLAY_LAYER_CLASS_NAMES.popup,
188
+ side: "bottom",
189
+ sideOffset: 4,
190
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
191
+ className: "bg-popover text-popover-foreground flex w-56 flex-col gap-2 rounded-lg border p-2 shadow-lg/5",
192
+ "data-slot": "color-picker-custom-popup",
193
+ children: /* @__PURE__ */ jsx(CustomColorControls, {
194
+ handleInputChange,
195
+ handlePickerChange,
196
+ inputHex,
197
+ pickerHex
198
+ })
199
+ })
200
+ }) })] });
201
+ };
202
+ const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel, presentation = "popover" }) => {
131
203
  const [expanded, setExpanded] = useState(defaultExpanded);
132
204
  const [pickerHex, setPickerHex] = useState(() => (looksLikeHex(value ?? "") ? value : "000000") ?? "000000");
133
205
  const [inputHex, setInputHex] = useState("");
206
+ const customSelected = !presets.some((preset) => preset.value === value) && looksLikeHex(value ?? "");
134
207
  /** Called when the visual picker (SB square / hue strip) emits a color. */
135
208
  const handlePickerChange = (hex) => {
136
209
  const normalized = normalizeHex(hex);
@@ -147,6 +220,27 @@ const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaul
147
220
  onSelect?.(cleaned);
148
221
  }
149
222
  };
223
+ if (presentation === "inline") return /* @__PURE__ */ jsxs("div", {
224
+ className: "flex items-center gap-1",
225
+ "data-slot": "color-picker",
226
+ children: [presets.map((preset) => /* @__PURE__ */ jsx(ColorSwatch, {
227
+ cssColor: swatchColor(preset),
228
+ isLight: looksLikeHex(preset.value) && isLightHex(preset.value),
229
+ label: preset.label,
230
+ onClick: () => onSelect?.(preset.value),
231
+ presentation: "inline",
232
+ selected: value === preset.value
233
+ }, preset.value)), /* @__PURE__ */ jsx(InlineCustomColor, {
234
+ customSelected,
235
+ handleInputChange,
236
+ handlePickerChange,
237
+ inputHex,
238
+ label: moreLabel,
239
+ pickerHex,
240
+ presets,
241
+ value
242
+ })]
243
+ });
150
244
  return /* @__PURE__ */ jsxs("div", {
151
245
  className: "flex flex-col gap-1.5",
152
246
  "data-slot": "color-picker",
@@ -172,6 +266,7 @@ const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaul
172
266
  isLight: looksLikeHex(preset.value) && isLightHex(preset.value),
173
267
  label: preset.label,
174
268
  onClick: () => onSelect?.(preset.value),
269
+ presentation: "popover",
175
270
  selected: value === preset.value
176
271
  }, preset.value))
177
272
  }),
@@ -180,43 +275,14 @@ const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaul
180
275
  onClick: () => setExpanded(true),
181
276
  type: "button",
182
277
  children: [moreLabel, /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3" })]
183
- }) : /* @__PURE__ */ jsxs("div", {
278
+ }) : /* @__PURE__ */ jsx("div", {
184
279
  className: "border-border flex flex-col gap-2 border-t pt-2",
185
- children: [/* @__PURE__ */ jsx(Suspense, {
186
- fallback: /* @__PURE__ */ jsx("div", {
187
- "aria-hidden": true,
188
- className: "ring-border/30 h-[140px] w-full rounded-lg ring-1"
189
- }),
190
- children: /* @__PURE__ */ jsx(HexColorPicker, {
191
- className: "ring-border/30 !h-[140px] !w-full overflow-hidden rounded-lg ring-1",
192
- color: pickerHex,
193
- onChange: (hex) => handlePickerChange(hex.replace("#", ""))
194
- })
195
- }), /* @__PURE__ */ jsxs("div", {
196
- className: "flex items-center gap-1.5",
197
- children: [
198
- /* @__PURE__ */ jsx("span", {
199
- className: "text-muted-foreground text-[11px]",
200
- children: "#"
201
- }),
202
- /* @__PURE__ */ jsx("input", {
203
- "aria-label": "Custom hex color",
204
- className: "border-input bg-background text-foreground h-6 flex-1 rounded border px-1.5 font-mono text-[11px] outline-none",
205
- dir: "ltr",
206
- maxLength: 6,
207
- onChange: (e) => handleInputChange(e.target.value),
208
- onKeyDown: (e) => e.stopPropagation(),
209
- onMouseDown: (e) => e.stopPropagation(),
210
- onPointerDown: (e) => e.stopPropagation(),
211
- placeholder: "FF0000",
212
- value: inputHex
213
- }),
214
- isValidHex(inputHex) && /* @__PURE__ */ jsx("span", {
215
- className: "border-border size-6 shrink-0 rounded border",
216
- style: { backgroundColor: `#${inputHex}` }
217
- })
218
- ]
219
- })]
280
+ children: /* @__PURE__ */ jsx(CustomColorControls, {
281
+ handleInputChange,
282
+ handlePickerChange,
283
+ inputHex,
284
+ pickerHex
285
+ })
220
286
  })
221
287
  ]
222
288
  });
@@ -228,7 +294,7 @@ const ColorPicker = ({ value, onSelect, onClear, presets = DEFAULT_PRESETS, colu
228
294
  children
229
295
  }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
230
296
  align,
231
- className: "z-50",
297
+ className: OVERLAY_LAYER_CLASS_NAMES.popup,
232
298
  side,
233
299
  sideOffset: 4,
234
300
  children: /* @__PURE__ */ jsx(Popover.Popup, {
@@ -240,6 +306,7 @@ const ColorPicker = ({ value, onSelect, onClear, presets = DEFAULT_PRESETS, colu
240
306
  moreLabel,
241
307
  onClear,
242
308
  onSelect,
309
+ presentation: "popover",
243
310
  presets,
244
311
  value
245
312
  })
@@ -3,17 +3,22 @@ import { cn } from "../lib/utils.js";
3
3
  import { Button } from "./button.js";
4
4
  import { DirectionalIcon } from "./directional-icon.js";
5
5
  import { getLocaleWeekInfo, getWeekendDays } from "../lib/week.js";
6
- import { localDateFromTimestamp, millisecondsUntilNextLocalDate, resolveCalendarViewMonth } from "./date-picker-popover.logic.js";
6
+ import { localDateFromTimestamp, millisecondsUntilNextLocalDate, resolveCalendarViewMonth, shiftCalendarDate } from "./date-picker-popover.logic.js";
7
7
  import { Popover, PopoverContent as PopoverPopup, PopoverTrigger } from "./popover.js";
8
8
  import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { useCallback, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
11
+ import { Temporal } from "temporal-polyfill/full";
11
12
  //#region src/components/date-picker-popover.tsx
12
- const toISODate = (date) => date.toISOString().slice(0, 10);
13
+ const toISODate = (date) => date.toString();
14
+ const toUTCDateTime = (date) => date.toZonedDateTime({
15
+ plainTime: Temporal.PlainTime.from("00:00"),
16
+ timeZone: "UTC"
17
+ }).epochMilliseconds;
13
18
  const HYDRATION_DATE = "1970-01-01";
14
19
  const HYDRATION_LOCALE = "en";
15
20
  const noopSubscribe = (_onStoreChange) => () => void 0;
16
- const getLocalToday = () => localDateFromTimestamp(Date.now());
21
+ const getLocalToday = () => localDateFromTimestamp(Temporal.Now.instant().epochMilliseconds);
17
22
  const localDateListeners = /* @__PURE__ */ new Set();
18
23
  let localDateTimeoutId;
19
24
  const notifyLocalDateListeners = () => {
@@ -24,7 +29,7 @@ const scheduleNextLocalDate = () => {
24
29
  localDateTimeoutId = setTimeout(() => {
25
30
  notifyLocalDateListeners();
26
31
  if (localDateListeners.size > 0) scheduleNextLocalDate();
27
- }, millisecondsUntilNextLocalDate(Date.now()));
32
+ }, millisecondsUntilNextLocalDate(Temporal.Now.instant().epochMilliseconds));
28
33
  };
29
34
  const refreshLocalDateEnvironment = () => {
30
35
  notifyLocalDateListeners();
@@ -58,19 +63,21 @@ const getFirstDayOfWeek = (locale) => {
58
63
  };
59
64
  const getMonthDays = (year, month, firstDow, weekendDays, today) => {
60
65
  const days = [];
61
- const first = new Date(Date.UTC(year, month, 1));
62
- const startOffset = ((first.getUTCDay() + 6) % 7 - firstDow + 7) % 7;
63
- const start = new Date(first);
64
- start.setUTCDate(start.getUTCDate() - startOffset);
66
+ const first = Temporal.PlainDate.from({
67
+ year,
68
+ month: month + 1,
69
+ day: 1
70
+ });
71
+ const startOffset = (first.dayOfWeek - 1 - firstDow + 7) % 7;
72
+ const start = first.subtract({ days: startOffset });
65
73
  for (let i = 0; i < 42; i++) {
66
- const d = new Date(start);
67
- d.setUTCDate(d.getUTCDate() + i);
74
+ const d = start.add({ days: i });
68
75
  const iso = toISODate(d);
69
76
  days.push({
70
77
  date: iso,
71
- isCurrentMonth: d.getUTCMonth() === month,
78
+ isCurrentMonth: d.month === month + 1,
72
79
  isToday: iso === today,
73
- isWeekend: weekendDays.has(d.getUTCDay())
80
+ isWeekend: weekendDays.has(d.dayOfWeek % 7)
74
81
  });
75
82
  }
76
83
  return days;
@@ -87,14 +94,21 @@ const getWeekdayFormatter = (locale) => {
87
94
  const getWeekdayLabels = (locale, firstDow, weekendDays) => {
88
95
  const fmt = getWeekdayFormatter(locale);
89
96
  return Array.from({ length: 7 }, (_, i) => {
90
- const d = new Date(Date.UTC(2024, 0, 1 + (i + firstDow) % 7));
97
+ const d = Temporal.PlainDate.from("2024-01-01").add({ days: (i + firstDow) % 7 });
91
98
  return {
92
- isWeekend: weekendDays.has(d.getUTCDay()),
93
- label: fmt.format(d)
99
+ isWeekend: weekendDays.has(d.dayOfWeek % 7),
100
+ label: fmt.format(toUTCDateTime(d))
94
101
  };
95
102
  });
96
103
  };
97
104
  const monthFormatters = /* @__PURE__ */ new Map();
105
+ const dateFormatters = /* @__PURE__ */ new Map();
106
+ const getDateFormatter = (locale, options) => {
107
+ const key = `${locale}:${JSON.stringify(options)}`;
108
+ const formatter = dateFormatters.get(key) ?? new Intl.DateTimeFormat(locale, options);
109
+ dateFormatters.set(key, formatter);
110
+ return formatter;
111
+ };
98
112
  const getMonthFormatter = (locale, format) => {
99
113
  const key = `${locale}:${format}`;
100
114
  const fmt = monthFormatters.get(key) ?? new Intl.DateTimeFormat(locale, {
@@ -107,7 +121,11 @@ const getMonthFormatter = (locale, format) => {
107
121
  };
108
122
  const getMonthLabels = (locale, format = "long") => {
109
123
  const fmt = getMonthFormatter(locale, format);
110
- return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(Date.UTC(2024, i, 1))));
124
+ return Array.from({ length: 12 }, (_, i) => fmt.format(toUTCDateTime(Temporal.PlainDate.from({
125
+ year: 2024,
126
+ month: i + 1,
127
+ day: 1
128
+ }))));
111
129
  };
112
130
  const monthYearFormatters = /* @__PURE__ */ new Map();
113
131
  const getMonthYearFormatter = (locale) => {
@@ -120,7 +138,11 @@ const getMonthYearFormatter = (locale) => {
120
138
  monthYearFormatters.set(locale, fmt);
121
139
  return fmt;
122
140
  };
123
- const formatMonthYear = (locale, year, month) => getMonthYearFormatter(locale).format(new Date(Date.UTC(year, month, 1)));
141
+ const formatMonthYear = (locale, year, month) => getMonthYearFormatter(locale).format(toUTCDateTime(Temporal.PlainDate.from({
142
+ year,
143
+ month: month + 1,
144
+ day: 1
145
+ })));
124
146
  const relativeTimeFormatters = /* @__PURE__ */ new Map();
125
147
  const getRelativeTimeFormatter = (locale) => {
126
148
  const fmt = relativeTimeFormatters.get(locale) ?? new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
@@ -134,14 +156,10 @@ const deriveTodayLabel = (locale) => {
134
156
  };
135
157
  const normalizeDate = (v) => {
136
158
  if (v === null || v === void 0) return "";
137
- if (v instanceof Date) return v.toISOString().slice(0, 10);
159
+ if (v instanceof Date) return Temporal.Instant.fromEpochMilliseconds(v.getTime()).toZonedDateTimeISO("UTC").toPlainDate().toString();
138
160
  return v.length >= 10 ? v.slice(0, 10) : v;
139
161
  };
140
- const addDays = (iso, n) => {
141
- const d = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
142
- d.setUTCDate(d.getUTCDate() + n);
143
- return toISODate(d);
144
- };
162
+ const addDays = (iso, n) => Temporal.PlainDate.from(iso).add({ days: n }).toString();
145
163
  const isBefore = (a, b) => a < b;
146
164
  const isAfter = (a, b) => a > b;
147
165
  /** Round down to the start of a decade (e.g. 2026 → 2020). */
@@ -186,22 +204,22 @@ const DatePickerPopoverContent = ({ id, labelledBy, value: rawValue, onChange, l
186
204
  maxDate,
187
205
  isDateDisabled
188
206
  ]);
189
- const displayLabel = value ? (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toLocaleDateString(locale, {
207
+ const displayLabel = value ? getDateFormatter(locale, {
190
208
  month: "short",
191
209
  day: "numeric",
192
210
  year: "numeric",
193
211
  calendar: "gregory",
194
212
  timeZone: "UTC"
195
- }) : placeholderLabel ?? "—";
196
- const formatDayLabel = useCallback((iso) => (/* @__PURE__ */ new Date(`${iso}T00:00:00Z`)).toLocaleDateString(locale, {
213
+ }).format(toUTCDateTime(Temporal.PlainDate.from(value))) : placeholderLabel ?? "—";
214
+ const formatDayLabel = useCallback((iso) => getDateFormatter(locale, {
197
215
  weekday: "long",
198
216
  month: "long",
199
217
  day: "numeric",
200
218
  year: "numeric",
201
219
  calendar: "gregory",
202
220
  timeZone: "UTC"
203
- }), [locale]);
204
- const handleGridKeyDown = useCallback((e) => {
221
+ }).format(toUTCDateTime(Temporal.PlainDate.from(iso))), [locale]);
222
+ const handleGridKeyDown = (e) => {
205
223
  const firstDay = days.at(0);
206
224
  if (!firstDay) return;
207
225
  const current = focusedDate || value || firstDay.date;
@@ -212,22 +230,14 @@ const DatePickerPopoverContent = ({ id, labelledBy, value: rawValue, onChange, l
212
230
  else if (e.key === "ArrowDown") next = addDays(current, 7);
213
231
  else if (e.key === "ArrowUp") next = addDays(current, -7);
214
232
  else if (e.key === "Home") {
215
- const offset = (((/* @__PURE__ */ new Date(`${current}T00:00:00Z`)).getUTCDay() + 6) % 7 - firstDow + 7) % 7;
233
+ const offset = (Temporal.PlainDate.from(current).dayOfWeek - 1 - firstDow + 7) % 7;
216
234
  next = addDays(current, -offset);
217
235
  } else if (e.key === "End") {
218
- const offset = (((/* @__PURE__ */ new Date(`${current}T00:00:00Z`)).getUTCDay() + 6) % 7 - firstDow + 7) % 7;
236
+ const offset = (Temporal.PlainDate.from(current).dayOfWeek - 1 - firstDow + 7) % 7;
219
237
  next = addDays(current, 6 - offset);
220
- } else if (e.key === "PageUp") {
221
- const d = /* @__PURE__ */ new Date(`${current}T00:00:00Z`);
222
- if (e.shiftKey) d.setUTCFullYear(d.getUTCFullYear() - 1);
223
- else d.setUTCMonth(d.getUTCMonth() - 1);
224
- next = toISODate(d);
225
- } else if (e.key === "PageDown") {
226
- const d = /* @__PURE__ */ new Date(`${current}T00:00:00Z`);
227
- if (e.shiftKey) d.setUTCFullYear(d.getUTCFullYear() + 1);
228
- else d.setUTCMonth(d.getUTCMonth() + 1);
229
- next = toISODate(d);
230
- } else {
238
+ } else if (e.key === "PageUp") next = shiftCalendarDate(current, e.shiftKey ? { years: -1 } : { months: -1 });
239
+ else if (e.key === "PageDown") next = shiftCalendarDate(current, e.shiftKey ? { years: 1 } : { months: 1 });
240
+ else {
231
241
  if (e.key === "Enter" || e.key === " ") {
232
242
  e.preventDefault();
233
243
  if (!isDayDisabled(current)) onChange(current);
@@ -237,9 +247,9 @@ const DatePickerPopoverContent = ({ id, labelledBy, value: rawValue, onChange, l
237
247
  e.preventDefault();
238
248
  if (next) {
239
249
  setFocusedDate(next);
240
- const nextDate = /* @__PURE__ */ new Date(`${next}T00:00:00Z`);
241
- const nextMonth = nextDate.getUTCMonth();
242
- const nextYear = nextDate.getUTCFullYear();
250
+ const nextDate = Temporal.PlainDate.from(next);
251
+ const nextMonth = nextDate.month - 1;
252
+ const nextYear = nextDate.year;
243
253
  if (nextMonth !== viewMonth || nextYear !== viewYear) setViewMonthOverride({
244
254
  month: nextMonth,
245
255
  year: nextYear
@@ -248,17 +258,7 @@ const DatePickerPopoverContent = ({ id, labelledBy, value: rawValue, onChange, l
248
258
  (gridRef.current?.querySelector(`[data-date="${next}"]`))?.focus();
249
259
  });
250
260
  }
251
- }, [
252
- focusedDate,
253
- value,
254
- days,
255
- firstDow,
256
- viewMonth,
257
- viewYear,
258
- isDayDisabled,
259
- onChange,
260
- setViewMonthOverride
261
- ]);
261
+ };
262
262
  const handlePrev = () => {
263
263
  if (view === "days") if (viewMonth === 0) setViewMonthOverride({
264
264
  month: 11,
@@ -316,8 +316,8 @@ const DatePickerPopoverContent = ({ id, labelledBy, value: rawValue, onChange, l
316
316
  setDecadeBaseOverride(decadeStart(year));
317
317
  setView("months");
318
318
  };
319
- const selectedYear = value ? (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).getUTCFullYear() : null;
320
- const selectedMonth = value ? (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).getUTCMonth() : null;
319
+ const selectedYear = value ? Temporal.PlainDate.from(value).year : null;
320
+ const selectedMonth = value ? Temporal.PlainDate.from(value).month - 1 : null;
321
321
  const handleOpenChange = (open) => {
322
322
  onOpenChange?.(open);
323
323
  if (!open) {
@@ -451,10 +451,10 @@ const DatePickerPopoverContent = ({ id, labelledBy, value: rawValue, onChange, l
451
451
  children: [/* @__PURE__ */ jsx(Button, {
452
452
  className: "flex-1",
453
453
  onClick: () => {
454
- const todayDate = /* @__PURE__ */ new Date(`${today}T00:00:00Z`);
454
+ const todayDate = Temporal.PlainDate.from(today);
455
455
  setViewMonthOverride({
456
- month: todayDate.getUTCMonth(),
457
- year: todayDate.getUTCFullYear()
456
+ month: todayDate.month - 1,
457
+ year: todayDate.year
458
458
  });
459
459
  setView("days");
460
460
  },
@@ -487,9 +487,9 @@ const DatePickerPopover = (props) => {
487
487
  const MONTHS_PER_ROW = 3;
488
488
  const MonthGrid = ({ locale, viewYear, currentMonth, currentYear, onSelect, today }) => {
489
489
  const labels = useMemo(() => getMonthLabels(locale, "short"), [locale]);
490
- const now = /* @__PURE__ */ new Date(`${today}T00:00:00Z`);
491
- const todayMonth = now.getUTCMonth();
492
- const todayYear = now.getUTCFullYear();
490
+ const now = Temporal.PlainDate.from(today);
491
+ const todayMonth = now.month - 1;
492
+ const todayYear = now.year;
493
493
  const rows = [];
494
494
  for (let r = 0; r < 12; r += MONTHS_PER_ROW) rows.push(Array.from({ length: MONTHS_PER_ROW }, (_, c) => r + c));
495
495
  return /* @__PURE__ */ jsx("div", {
@@ -518,7 +518,7 @@ const MonthGrid = ({ locale, viewYear, currentMonth, currentYear, onSelect, toda
518
518
  };
519
519
  const YEARS_PER_ROW = 3;
520
520
  const YearGrid = ({ decadeBase, currentYear, onSelect, today }) => {
521
- const todayYear = (/* @__PURE__ */ new Date(`${today}T00:00:00Z`)).getUTCFullYear();
521
+ const todayYear = Temporal.PlainDate.from(today).year;
522
522
  const startYear = decadeBase - 1;
523
523
  const rows = [];
524
524
  for (let r = 0; r < DECADE_SIZE; r += YEARS_PER_ROW) rows.push(Array.from({ length: Math.min(YEARS_PER_ROW, DECADE_SIZE - r) }, (_, c) => r + c));
@@ -9,6 +9,10 @@ declare const resolveCalendarViewMonth: ({ override, today, value }: {
9
9
  today: string;
10
10
  value: string;
11
11
  }) => CalendarMonth;
12
+ declare const shiftCalendarDate: (date: string, options: {
13
+ months?: number;
14
+ years?: number;
15
+ }) => string;
12
16
  declare const millisecondsUntilNextLocalDate: (timestamp: number) => number;
13
17
  //#endregion
14
- export { CalendarMonth, localDateFromTimestamp, millisecondsUntilNextLocalDate, resolveCalendarViewMonth };
18
+ export { CalendarMonth, localDateFromTimestamp, millisecondsUntilNextLocalDate, resolveCalendarViewMonth, shiftCalendarDate };
@@ -1,26 +1,31 @@
1
+ import { Temporal } from "temporal-polyfill/full";
1
2
  //#region src/components/date-picker-popover.logic.ts
2
3
  const DATE_ROLLOVER_EPSILON_MS = 50;
3
4
  const padDatePart = (value) => value.toString().padStart(2, "0");
4
5
  const localDateFromTimestamp = (timestamp) => {
5
- const current = new Date(timestamp);
6
+ const current = Temporal.Instant.fromEpochMilliseconds(timestamp).toZonedDateTimeISO(Temporal.Now.timeZoneId());
6
7
  return [
7
- current.getFullYear(),
8
- padDatePart(current.getMonth() + 1),
9
- padDatePart(current.getDate())
8
+ current.year,
9
+ padDatePart(current.month),
10
+ padDatePart(current.day)
10
11
  ].join("-");
11
12
  };
12
13
  const calendarMonthFromDate = (date) => {
13
- const current = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
14
+ const current = Temporal.PlainDate.from(date);
14
15
  return {
15
- month: current.getUTCMonth(),
16
- year: current.getUTCFullYear()
16
+ month: current.month - 1,
17
+ year: current.year
17
18
  };
18
19
  };
19
20
  const resolveCalendarViewMonth = ({ override, today, value }) => override ?? calendarMonthFromDate(value || today);
21
+ const shiftCalendarDate = (date, options) => Temporal.PlainDate.from(date).add(options).toString();
20
22
  const millisecondsUntilNextLocalDate = (timestamp) => {
21
- const current = new Date(timestamp);
22
- const nextLocalDate = new Date(current.getFullYear(), current.getMonth(), current.getDate() + 1);
23
- return Math.max(0, nextLocalDate.getTime() - timestamp) + DATE_ROLLOVER_EPSILON_MS;
23
+ const current = Temporal.Instant.fromEpochMilliseconds(timestamp).toZonedDateTimeISO(Temporal.Now.timeZoneId());
24
+ const nextLocalDate = current.toPlainDate().add({ days: 1 }).toZonedDateTime({
25
+ plainTime: Temporal.PlainTime.from("00:00"),
26
+ timeZone: current.timeZoneId
27
+ });
28
+ return Math.max(0, nextLocalDate.epochMilliseconds - timestamp) + DATE_ROLLOVER_EPSILON_MS;
24
29
  };
25
30
  //#endregion
26
- export { localDateFromTimestamp, millisecondsUntilNextLocalDate, resolveCalendarViewMonth };
31
+ export { localDateFromTimestamp, millisecondsUntilNextLocalDate, resolveCalendarViewMonth, shiftCalendarDate };
@@ -3,6 +3,7 @@ import { cn } from "../lib/utils.js";
3
3
  import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { Temporal } from "temporal-polyfill/full";
6
7
  //#region src/components/outline-rail.tsx
7
8
  /**
8
9
  * Outline rail — the shared right-edge navigation rail.
@@ -153,7 +154,7 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
153
154
  if (!container || items.length === 0) return;
154
155
  let raf = 0;
155
156
  const compute = () => {
156
- if (Date.now() < manualLockUntil.current || container.scrollHeight <= 0) return;
157
+ if (Temporal.Now.instant().epochMilliseconds < manualLockUntil.current || container.scrollHeight <= 0) return;
157
158
  const centrePct = (container.scrollTop + container.clientHeight / 2) / container.scrollHeight * 100;
158
159
  let next = null;
159
160
  for (const item of items) {
@@ -197,7 +198,7 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
197
198
  if (!container) return;
198
199
  if (activeId === void 0) {
199
200
  setDerivedActive(id);
200
- manualLockUntil.current = Date.now() + 900;
201
+ manualLockUntil.current = Temporal.Now.instant().epochMilliseconds + 900;
201
202
  }
202
203
  onJumpRef.current(id, container);
203
204
  }, [activeId, scrollContainerRef]);
@@ -30,7 +30,7 @@ declare function useSidebar(): SidebarContextProps;
30
30
  * left for the content column. Mobile reports 0: there the sidebar is an
31
31
  * overlay sheet and occupies no layout width.
32
32
  */
33
- declare function useSidebarInlineSize(): 0 | 256 | 48;
33
+ declare function useSidebarInlineSize(): 0 | 48 | 256;
34
34
  declare const SidebarProvider: ({ defaultOpen, forceCollapsed, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }: React.ComponentProps<"div"> & {
35
35
  defaultOpen?: boolean;
36
36
  forceCollapsed?: boolean;
@@ -83,7 +83,7 @@ declare const SidebarMenu: ({ className, ...props }: React.ComponentProps<"ul">)
83
83
  declare const SidebarMenuItem: ({ className, ...props }: React.ComponentProps<"li">) => import("react").JSX.Element;
84
84
  declare const sidebarMenuButtonVariants: (props?: ({
85
85
  variant?: "default" | "outline" | null | undefined;
86
- size?: "default" | "lg" | "sm" | "rail" | null | undefined;
86
+ size?: "sm" | "default" | "lg" | "rail" | null | undefined;
87
87
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
88
88
  declare const SidebarMenuButton: ({ asChild, isActive, variant, size, tooltip, className, ...props }: React.ComponentProps<"button"> & {
89
89
  asChild?: boolean;
@@ -8,7 +8,7 @@ import { Tabs } from "@base-ui/react/tabs";
8
8
  */
9
9
  declare const InspectorTabs: ({ className, ...props }: Omit<Tabs.Root.Props, "orientation">) => React$1.JSX.Element;
10
10
  declare const INSPECTOR_RAIL_MEDIA_QUERY: "(min-width: 48rem)";
11
- declare const resolveInspectorTabOrientation: (isRailLayout: boolean) => "vertical" | "horizontal";
11
+ declare const resolveInspectorTabOrientation: (isRailLayout: boolean) => "horizontal" | "vertical";
12
12
  declare const InspectorTabList: ({ className, ...props }: Tabs.List.Props) => React$1.JSX.Element;
13
13
  declare const InspectorTab: ({ className, ...props }: Tabs.Tab.Props) => React$1.JSX.Element;
14
14
  declare const InspectorTabPanel: ({ className, ...props }: Tabs.Panel.Props) => React$1.JSX.Element;
@@ -5,6 +5,6 @@ declare const CONTROL_SIZE: Readonly<{
5
5
  readonly lg: "lg";
6
6
  }>;
7
7
  type ControlSize = (typeof CONTROL_SIZE)[keyof typeof CONTROL_SIZE];
8
- declare const CONTROL_SIZES: readonly ("default" | "lg" | "sm")[];
8
+ declare const CONTROL_SIZES: readonly ("sm" | "default" | "lg")[];
9
9
  //#endregion
10
10
  export { CONTROL_SIZE, CONTROL_SIZES, type ControlSize };
@@ -10,13 +10,13 @@
10
10
  */
11
11
  declare const getLocaleWeekInfo: (locale: string) => Intl.WeekInfo | undefined;
12
12
  /**
13
- * First weekday as a `Date.getDay()` value (0 = Sunday … 6 = Saturday): Monday
13
+ * First weekday as a day-of-week value (0 = Sunday … 6 = Saturday): Monday
14
14
  * across most of Europe, Sunday in the US, Saturday across much of the Gulf.
15
15
  * Falls back to Monday when the runtime lacks week info.
16
16
  */
17
17
  declare const getFirstWeekday: (locale: string) => number;
18
18
  /**
19
- * Weekend weekdays as `Date.getDay()` values (0 = Sunday … 6 = Saturday):
19
+ * Weekend weekdays as day-of-week values (0 = Sunday … 6 = Saturday):
20
20
  * Saturday/Sunday across the West, Friday/Saturday across much of the Gulf.
21
21
  * Falls back to Saturday/Sunday when the runtime lacks week info.
22
22
  */
package/dist/lib/week.js CHANGED
@@ -18,7 +18,7 @@ const getLocaleWeekInfo = (locale) => {
18
18
  }
19
19
  };
20
20
  /**
21
- * First weekday as a `Date.getDay()` value (0 = Sunday … 6 = Saturday): Monday
21
+ * First weekday as a day-of-week value (0 = Sunday … 6 = Saturday): Monday
22
22
  * across most of Europe, Sunday in the US, Saturday across much of the Gulf.
23
23
  * Falls back to Monday when the runtime lacks week info.
24
24
  */
@@ -27,7 +27,7 @@ const getFirstWeekday = (locale) => {
27
27
  return typeof firstDay === "number" ? firstDay % 7 : 1;
28
28
  };
29
29
  /**
30
- * Weekend weekdays as `Date.getDay()` values (0 = Sunday … 6 = Saturday):
30
+ * Weekend weekdays as day-of-week values (0 = Sunday … 6 = Saturday):
31
31
  * Saturday/Sunday across the West, Friday/Saturday across much of the Gulf.
32
32
  * Falls back to Saturday/Sunday when the runtime lacks week info.
33
33
  */
@@ -4,6 +4,7 @@ import { BidiText } from "../components/bidi-text.js";
4
4
  import { ReviewAuthorAvatar } from "./review-author-avatar.js";
5
5
  import { CheckIcon, RotateCcwIcon, Trash2Icon } from "lucide-react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Temporal } from "temporal-polyfill/full";
7
8
  //#region src/review/review-comment-card.tsx
8
9
  /** One comment on a reviewed surface: who wrote it, when, what it says, what
9
10
  * it points at, and the two things a reader can do to it. */
@@ -70,7 +71,7 @@ const ReviewCommentCard = ({ author, timestamp, formattedTime, body, anchorText,
70
71
  * yields no attribute rather than throwing on `toISOString`. */
71
72
  const toIsoInstant = (timestamp) => {
72
73
  if (typeof timestamp === "string") return timestamp;
73
- return Number.isNaN(timestamp.getTime()) ? void 0 : timestamp.toISOString();
74
+ return Number.isNaN(timestamp.getTime()) ? void 0 : Temporal.Instant.fromEpochMilliseconds(timestamp.getTime()).toString({ fractionalSecondDigits: 3 });
74
75
  };
75
76
  //#endregion
76
77
  export { ReviewCommentCard };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/ui",
3
- "version": "0.25.2",
3
+ "version": "0.26.1",
4
4
  "description": "Stella's design system: bidi-aware React primitives built on Base UI, the dockable inspector pane, and the Tailwind v4 theme they are styled with.",
5
5
  "keywords": [
6
6
  "base-ui",
@@ -594,7 +594,8 @@
594
594
  "clsx": "^2.1.1",
595
595
  "input-otp": "^1.5.0",
596
596
  "lucide-react": "1.39.0",
597
- "tailwind-merge": "^3.6.0"
597
+ "tailwind-merge": "^3.6.0",
598
+ "temporal-polyfill": "1.0.4"
598
599
  },
599
600
  "devDependencies": {
600
601
  "@atlaskit/pragmatic-drag-and-drop": "^3.1.0",