@pos-360/horizon 0.30.0 → 0.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
- import { cn, Label, Tooltip, Text, mergeRefs } from './chunk-EZDGMHS7.mjs';
1
+ import { cn, Label, Tooltip, Text, TextButton, mergeRefs } from './chunk-EZL7UCWW.mjs';
2
2
  import * as React10 from 'react';
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { Slot } from '@radix-ui/react-slot';
5
5
  import { cva } from 'class-variance-authority';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
8
- import { Check, X, ChevronRight, Circle, ChevronDown, ChevronUp, Search, Minus, PenLine, SlidersHorizontal, CalendarIcon, ChevronLeft, Clock } from 'lucide-react';
8
+ import { Check, X, ChevronRight, Circle, ChevronDown, ChevronUp, Search, Minus, PenLine, SlidersHorizontal, CalendarIcon, Clock, ChevronLeft } from 'lucide-react';
9
9
  import * as DialogPrimitive from '@radix-ui/react-dialog';
10
10
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
11
11
  import * as PopoverPrimitive from '@radix-ui/react-popover';
@@ -15,7 +15,7 @@ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
15
15
  import * as TabsPrimitive from '@radix-ui/react-tabs';
16
16
  import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
17
17
  import * as SwitchPrimitive from '@radix-ui/react-switch';
18
- import { endOfDay, startOfDay, endOfWeek, startOfWeek, endOfMonth, startOfMonth, subDays, subMonths, addMonths, subYears, isBefore, isSameDay, format, isSameMonth, eachDayOfInterval, isWithinInterval } from 'date-fns';
18
+ import { endOfDay, startOfDay, min, startOfWeek, startOfMonth, subDays, endOfWeek, endOfMonth, subMonths, addMonths, subYears, isBefore, isSameDay, format, isSameMonth, isAfter, eachDayOfInterval, isWithinInterval } from 'date-fns';
19
19
 
20
20
  var buttonVariants = cva(
21
21
  "group inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-hz-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -2099,15 +2099,16 @@ function to24Hour(hour12, period) {
2099
2099
  function pad(n) {
2100
2100
  return n.toString().padStart(2, "0");
2101
2101
  }
2102
- function clamp(val, min, max) {
2103
- return Math.max(min, Math.min(max, val));
2102
+ function clamp(val, min2, max) {
2103
+ return Math.max(min2, Math.min(max, val));
2104
2104
  }
2105
2105
  function TimeInput({
2106
2106
  value,
2107
- min,
2107
+ min: min2,
2108
2108
  max,
2109
2109
  onChange,
2110
2110
  onComplete,
2111
+ onDirty,
2111
2112
  inputRef,
2112
2113
  disabled = false,
2113
2114
  "aria-label": ariaLabel
@@ -2115,39 +2116,59 @@ function TimeInput({
2115
2116
  const [editValue, setEditValue] = React10.useState(null);
2116
2117
  const internalRef = React10.useRef(null);
2117
2118
  const committedRef = React10.useRef(false);
2119
+ const pendingDigitRef = React10.useRef(null);
2118
2120
  const setRefs = React10.useCallback(mergeRefs(internalRef, inputRef), [inputRef]);
2119
- const handleChange = (e) => {
2120
- const raw = e.target.value.replace(/\D/g, "").slice(0, 2);
2121
- if (raw === "") {
2122
- setEditValue(raw);
2123
- return;
2124
- }
2125
- if (raw.length === 1) {
2126
- setEditValue(raw);
2127
- return;
2128
- }
2121
+ const commitTwoDigits = (raw) => {
2129
2122
  const parsed = parseInt(raw, 10);
2130
- const clamped = clamp(parsed, min, max);
2123
+ const clamped = clamp(parsed, min2, max);
2124
+ pendingDigitRef.current = null;
2131
2125
  committedRef.current = true;
2132
2126
  onChange(clamped);
2133
2127
  setEditValue(null);
2134
2128
  onComplete?.();
2135
2129
  };
2130
+ const handleChange = (e) => {
2131
+ const allDigits = e.target.value.replace(/\D/g, "");
2132
+ if (allDigits === "") {
2133
+ setEditValue("");
2134
+ pendingDigitRef.current = null;
2135
+ return;
2136
+ }
2137
+ if (pendingDigitRef.current !== null) {
2138
+ if (allDigits.length > 2) {
2139
+ commitTwoDigits(pendingDigitRef.current + allDigits[allDigits.length - 1]);
2140
+ return;
2141
+ }
2142
+ pendingDigitRef.current = null;
2143
+ setEditValue("");
2144
+ return;
2145
+ }
2146
+ if (allDigits.length === 1) {
2147
+ pendingDigitRef.current = allDigits;
2148
+ setEditValue("0" + allDigits);
2149
+ onDirty?.();
2150
+ return;
2151
+ }
2152
+ commitTwoDigits(allDigits.slice(0, 2));
2153
+ };
2136
2154
  const commit = () => {
2137
2155
  if (committedRef.current) {
2138
2156
  committedRef.current = false;
2157
+ pendingDigitRef.current = null;
2139
2158
  setEditValue(null);
2140
2159
  return;
2141
2160
  }
2142
2161
  if (editValue === null) return;
2143
2162
  if (editValue === "") {
2163
+ pendingDigitRef.current = null;
2144
2164
  setEditValue(null);
2145
2165
  return;
2146
2166
  }
2147
2167
  const parsed = parseInt(editValue, 10);
2148
2168
  if (!isNaN(parsed)) {
2149
- onChange(clamp(parsed, min, max));
2169
+ onChange(clamp(parsed, min2, max));
2150
2170
  }
2171
+ pendingDigitRef.current = null;
2151
2172
  setEditValue(null);
2152
2173
  };
2153
2174
  const handleKeyDown = (e) => {
@@ -2155,18 +2176,21 @@ function TimeInput({
2155
2176
  commit();
2156
2177
  internalRef.current?.blur();
2157
2178
  } else if (e.key === "Escape") {
2179
+ pendingDigitRef.current = null;
2158
2180
  setEditValue(null);
2159
2181
  internalRef.current?.blur();
2160
2182
  } else if (e.key === "ArrowUp") {
2161
2183
  e.preventDefault();
2162
- const curr = value ?? min;
2163
- const next = curr >= max ? min : curr + 1;
2184
+ pendingDigitRef.current = null;
2185
+ const curr = value ?? min2;
2186
+ const next = curr >= max ? min2 : curr + 1;
2164
2187
  onChange(next);
2165
2188
  setEditValue(pad(next));
2166
2189
  } else if (e.key === "ArrowDown") {
2167
2190
  e.preventDefault();
2191
+ pendingDigitRef.current = null;
2168
2192
  const curr = value ?? max;
2169
- const next = curr <= min ? max : curr - 1;
2193
+ const next = curr <= min2 ? max : curr - 1;
2170
2194
  onChange(next);
2171
2195
  setEditValue(pad(next));
2172
2196
  }
@@ -2182,6 +2206,7 @@ function TimeInput({
2182
2206
  value: editValue ?? (value === null ? "--" : pad(value)),
2183
2207
  onChange: handleChange,
2184
2208
  onFocus: (e) => {
2209
+ pendingDigitRef.current = null;
2185
2210
  setEditValue("");
2186
2211
  requestAnimationFrame(() => e.target.select());
2187
2212
  },
@@ -2232,6 +2257,7 @@ function TimeField({
2232
2257
  hourRef,
2233
2258
  minuteRef,
2234
2259
  onMinuteComplete,
2260
+ onDirty,
2235
2261
  disabled = false
2236
2262
  }) {
2237
2263
  const { hour12, period } = to12Hour(value.hour);
@@ -2270,6 +2296,7 @@ function TimeField({
2270
2296
  max: 12,
2271
2297
  onChange: handleHourChange,
2272
2298
  onComplete: focusMinute,
2299
+ onDirty,
2273
2300
  inputRef: hourRef,
2274
2301
  disabled,
2275
2302
  "aria-label": `${label} hour`
@@ -2293,6 +2320,7 @@ function TimeField({
2293
2320
  max: 59,
2294
2321
  onChange: handleMinuteChange,
2295
2322
  onComplete: onMinuteComplete,
2323
+ onDirty,
2296
2324
  inputRef: mergedMinuteRef,
2297
2325
  disabled,
2298
2326
  "aria-label": `${label} minute`
@@ -2309,11 +2337,14 @@ function TimeField({
2309
2337
  ] })
2310
2338
  ] });
2311
2339
  }
2312
- function TimePickerColumn({ value, onChange, disabled = false }) {
2313
- const toHourRef = React10.useRef(null);
2340
+ function useTimeFieldState(value) {
2314
2341
  const fromSet = isTimeSet(value.from);
2315
2342
  const toSet = isTimeSet(value.to);
2316
2343
  const bothSet = fromSet && toSet;
2344
+ const orphanMinutes = value.from.hour === null && value.from.minute !== null || value.to.hour === null && value.to.minute !== null;
2345
+ const fromPartial = !fromSet && (value.from.hour !== null || value.from.minute !== null);
2346
+ const toPartial = !toSet && (value.to.hour !== null || value.to.minute !== null);
2347
+ const oneSideMissing = fromSet && !toSet && !toPartial || toSet && !fromSet && !fromPartial;
2317
2348
  const fromMinutes = (value.from.hour ?? 0) * 60 + (value.from.minute ?? 0);
2318
2349
  const toMinutes = (value.to.hour ?? 0) * 60 + (value.to.minute ?? 0);
2319
2350
  const bothEqual = fromMinutes === toMinutes;
@@ -2323,140 +2354,142 @@ function TimePickerColumn({ value, onChange, disabled = false }) {
2323
2354
  const durationRemaining = durationMinutes % 60;
2324
2355
  const showDuration = bothSet && !bothEqual;
2325
2356
  const durationLabel = durationRemaining > 0 ? `${durationHours}h ${durationRemaining}m window${isOvernight ? " (overnight)" : ""}` : `${durationHours}h window${isOvernight ? " (overnight)" : ""}`;
2326
- return /* @__PURE__ */ jsxs(
2327
- "div",
2328
- {
2329
- className: cn(
2330
- "flex flex-col px-2 pt-2 pb-2 gap-1",
2331
- "border-t border-gray-100 dark:border-neutral-700"
2357
+ const incompleteHint = orphanMinutes ? "Please set hours for the time to take effect" : fromPartial || toPartial ? "Please complete both hour and minute fields" : oneSideMissing ? `Please set the ${fromSet ? "to" : "from"} time` : null;
2358
+ return { showDuration, durationLabel, isOvernight, incompleteHint };
2359
+ }
2360
+ function TimePickerColumn({ value, onChange, disabled = false, onRemove, showRemove = false, onDirty }) {
2361
+ const toHourRef = React10.useRef(null);
2362
+ const { showDuration, durationLabel, isOvernight, incompleteHint } = useTimeFieldState(value);
2363
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col px-2 pt-2 pb-2 gap-1", children: [
2364
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2365
+ /* @__PURE__ */ jsx(
2366
+ Clock,
2367
+ {
2368
+ className: cn(
2369
+ "w-4 h-4 shrink-0",
2370
+ disabled ? "text-gray-300 dark:text-gray-600" : "text-gray-400 dark:text-gray-500"
2371
+ )
2372
+ }
2332
2373
  ),
2333
- children: [
2334
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2335
- /* @__PURE__ */ jsx(
2336
- Clock,
2337
- {
2338
- className: cn(
2339
- "w-4 h-4 shrink-0",
2340
- disabled ? "text-gray-300 dark:text-gray-600" : "text-gray-400 dark:text-gray-500"
2341
- )
2342
- }
2343
- ),
2344
- /* @__PURE__ */ jsx(
2345
- "span",
2346
- {
2347
- className: cn(
2348
- "text-xs font-semibold uppercase tracking-wider",
2349
- disabled ? "text-gray-300 dark:text-gray-600" : "text-gray-400 dark:text-gray-500"
2350
- ),
2351
- children: "Time"
2352
- }
2353
- ),
2354
- /* @__PURE__ */ jsx("span", { className: "text-[9px] font-medium text-gray-400 dark:text-gray-600 tracking-wide", children: "(Optional)" })
2355
- ] }),
2356
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pl-6", children: [
2357
- /* @__PURE__ */ jsx(
2358
- TimeField,
2359
- {
2360
- label: "From",
2361
- value: value.from,
2362
- onChange: (from) => onChange({ ...value, from }),
2363
- onMinuteComplete: () => toHourRef.current?.focus(),
2364
- disabled
2365
- }
2366
- ),
2367
- /* @__PURE__ */ jsx(
2368
- TimeField,
2369
- {
2370
- label: "To",
2371
- value: value.to,
2372
- onChange: (to) => onChange({ ...value, to }),
2373
- hourRef: toHourRef,
2374
- disabled
2375
- }
2374
+ /* @__PURE__ */ jsx(
2375
+ "span",
2376
+ {
2377
+ className: cn(
2378
+ "text-xs font-semibold uppercase tracking-wider",
2379
+ disabled ? "text-gray-300 dark:text-gray-600" : "text-gray-400 dark:text-gray-500"
2376
2380
  ),
2377
- showDuration && /* @__PURE__ */ jsx("span", { className: cn(
2378
- "text-[10px] font-medium",
2379
- isOvernight ? "text-amber-500 dark:text-amber-400" : "text-gray-400 dark:text-gray-500"
2380
- ), children: durationLabel })
2381
- ] })
2382
- ]
2383
- }
2384
- );
2381
+ children: "Time"
2382
+ }
2383
+ ),
2384
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] font-medium text-gray-400 dark:text-gray-600 tracking-wide", children: "(Optional)" }),
2385
+ showRemove && onRemove && /* @__PURE__ */ jsx("div", { className: "ml-auto", children: /* @__PURE__ */ jsx(
2386
+ TextButton,
2387
+ {
2388
+ variant: "danger",
2389
+ onClick: onRemove,
2390
+ size: "sm",
2391
+ className: "!text-[0.625rem]",
2392
+ children: "Remove"
2393
+ }
2394
+ ) })
2395
+ ] }),
2396
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pl-6", children: [
2397
+ /* @__PURE__ */ jsx(
2398
+ TimeField,
2399
+ {
2400
+ label: "From",
2401
+ value: value.from,
2402
+ onChange: (from) => onChange({ ...value, from }),
2403
+ onMinuteComplete: () => toHourRef.current?.focus(),
2404
+ onDirty,
2405
+ disabled
2406
+ }
2407
+ ),
2408
+ /* @__PURE__ */ jsx(
2409
+ TimeField,
2410
+ {
2411
+ label: "To",
2412
+ value: value.to,
2413
+ onChange: (to) => onChange({ ...value, to }),
2414
+ hourRef: toHourRef,
2415
+ onDirty,
2416
+ disabled
2417
+ }
2418
+ ),
2419
+ showDuration && /* @__PURE__ */ jsx("span", { className: cn(
2420
+ "text-[10px] font-medium",
2421
+ isOvernight ? "text-amber-500 dark:text-amber-400" : "text-gray-400 dark:text-gray-500"
2422
+ ), children: durationLabel }),
2423
+ incompleteHint && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium text-amber-500 dark:text-amber-400", children: incompleteHint })
2424
+ ] })
2425
+ ] });
2385
2426
  }
2386
- function TimePickerRow({ value, onChange, disabled = false }) {
2427
+ function TimePickerRow({ value, onChange, disabled = false, onRemove, showRemove = false, onDirty }) {
2387
2428
  const toHourRef = React10.useRef(null);
2388
- const fromSet = isTimeSet(value.from);
2389
- const toSet = isTimeSet(value.to);
2390
- const bothSet = fromSet && toSet;
2391
- const fromMinutes = (value.from.hour ?? 0) * 60 + (value.from.minute ?? 0);
2392
- const toMinutes = (value.to.hour ?? 0) * 60 + (value.to.minute ?? 0);
2393
- const bothEqual = fromMinutes === toMinutes;
2394
- const isOvernight = bothSet && toMinutes < fromMinutes;
2395
- const durationMinutes = bothSet && !bothEqual ? isOvernight ? 24 * 60 - fromMinutes + toMinutes : toMinutes - fromMinutes : 0;
2396
- const durationHours = Math.floor(durationMinutes / 60);
2397
- const durationRemaining = durationMinutes % 60;
2398
- const showDuration = bothSet && !bothEqual;
2399
- const durationLabel = durationRemaining > 0 ? `${durationHours}h ${durationRemaining}m window${isOvernight ? " (overnight)" : ""}` : `${durationHours}h window${isOvernight ? " (overnight)" : ""}`;
2400
- return /* @__PURE__ */ jsx(
2401
- "div",
2402
- {
2403
- className: cn(
2404
- "flex flex-col px-4 pt-2 pb-3 gap-1",
2405
- "border-t border-gray-100 dark:border-neutral-700"
2429
+ const { showDuration, durationLabel, isOvernight, incompleteHint } = useTimeFieldState(value);
2430
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col px-4 pt-2 pb-3 gap-1", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
2431
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
2432
+ /* @__PURE__ */ jsx(
2433
+ Clock,
2434
+ {
2435
+ className: cn(
2436
+ "w-4 h-4 shrink-0",
2437
+ disabled ? "text-gray-300 dark:text-gray-600" : "text-gray-400 dark:text-gray-500"
2438
+ )
2439
+ }
2406
2440
  ),
2407
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
2408
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
2409
- /* @__PURE__ */ jsx(
2410
- Clock,
2411
- {
2412
- className: cn(
2413
- "w-4 h-4 shrink-0",
2414
- disabled ? "text-gray-300 dark:text-gray-600" : "text-gray-400 dark:text-gray-500"
2415
- )
2416
- }
2417
- ),
2418
- /* @__PURE__ */ jsx("span", { className: "text-[9px] font-medium text-gray-300 dark:text-gray-600 tracking-wide", children: "OPT" })
2419
- ] }),
2420
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 flex-1", children: [
2421
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-5", children: [
2422
- /* @__PURE__ */ jsx(
2423
- TimeField,
2424
- {
2425
- label: "From",
2426
- value: value.from,
2427
- onChange: (from) => onChange({ ...value, from }),
2428
- onMinuteComplete: () => toHourRef.current?.focus(),
2429
- disabled
2430
- }
2431
- ),
2432
- /* @__PURE__ */ jsx(
2433
- "div",
2434
- {
2435
- className: cn(
2436
- "w-4 h-px",
2437
- disabled ? "bg-gray-200 dark:bg-neutral-700" : "bg-gray-300 dark:bg-neutral-600"
2438
- )
2439
- }
2440
- ),
2441
- /* @__PURE__ */ jsx(
2442
- TimeField,
2443
- {
2444
- label: "To",
2445
- value: value.to,
2446
- onChange: (to) => onChange({ ...value, to }),
2447
- hourRef: toHourRef,
2448
- disabled
2449
- }
2441
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] font-medium text-gray-300 dark:text-gray-600 tracking-wide", children: "OPT" })
2442
+ ] }),
2443
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 flex-1", children: [
2444
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2445
+ /* @__PURE__ */ jsx(
2446
+ TimeField,
2447
+ {
2448
+ label: "From",
2449
+ value: value.from,
2450
+ onChange: (from) => onChange({ ...value, from }),
2451
+ onMinuteComplete: () => toHourRef.current?.focus(),
2452
+ onDirty,
2453
+ disabled
2454
+ }
2455
+ ),
2456
+ /* @__PURE__ */ jsx(
2457
+ "div",
2458
+ {
2459
+ className: cn(
2460
+ "w-4 h-px",
2461
+ disabled ? "bg-gray-200 dark:bg-neutral-700" : "bg-gray-300 dark:bg-neutral-600"
2450
2462
  )
2451
- ] }),
2452
- showDuration && /* @__PURE__ */ jsx("span", { className: cn(
2453
- "text-[10px] font-medium pl-0.5",
2454
- isOvernight ? "text-amber-500 dark:text-amber-400" : "text-gray-400 dark:text-gray-500"
2455
- ), children: durationLabel })
2456
- ] })
2457
- ] })
2458
- }
2459
- );
2463
+ }
2464
+ ),
2465
+ /* @__PURE__ */ jsx(
2466
+ TimeField,
2467
+ {
2468
+ label: "To",
2469
+ value: value.to,
2470
+ onChange: (to) => onChange({ ...value, to }),
2471
+ hourRef: toHourRef,
2472
+ onDirty,
2473
+ disabled
2474
+ }
2475
+ ),
2476
+ showRemove && onRemove && /* @__PURE__ */ jsx(
2477
+ Button,
2478
+ {
2479
+ variant: "outline",
2480
+ onClick: onRemove,
2481
+ className: "text-[0.625rem] border border-red-100 dark:border-red-700 rounded-md px-2 py-1 h-8 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 transition-colors",
2482
+ children: "Remove"
2483
+ }
2484
+ )
2485
+ ] }),
2486
+ showDuration && /* @__PURE__ */ jsx("span", { className: cn(
2487
+ "text-[10px] font-medium pl-0.5",
2488
+ isOvernight ? "text-amber-500 dark:text-amber-400" : "text-gray-400 dark:text-gray-500"
2489
+ ), children: durationLabel }),
2490
+ incompleteHint && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium pl-0.5 text-amber-500 dark:text-amber-400", children: incompleteHint })
2491
+ ] })
2492
+ ] }) });
2460
2493
  }
2461
2494
  var WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
2462
2495
  function getCalendarDays(month) {
@@ -2539,9 +2572,14 @@ function CalendarMonth({
2539
2572
  onMouseLeave: () => onDayHover(void 0),
2540
2573
  children: days.map((day, idx) => {
2541
2574
  const outside = !isSameMonth(day, month);
2542
- const inRange = !outside && isInRange(day);
2543
- const rangeStart = !outside && isRangeStart(day);
2544
- const rangeEnd = !outside && isRangeEnd(day);
2575
+ if (outside) {
2576
+ return /* @__PURE__ */ jsx("div", { className: "h-9" }, idx);
2577
+ }
2578
+ const isFuture = isAfter(startOfDay(day), startOfDay(today));
2579
+ const disabled = isFuture;
2580
+ const inRange = !disabled && isInRange(day);
2581
+ const rangeStart = !disabled && isRangeStart(day);
2582
+ const rangeEnd = !disabled && isRangeEnd(day);
2545
2583
  const isToday = isSameDay(day, today);
2546
2584
  return /* @__PURE__ */ jsx(
2547
2585
  "div",
@@ -2556,16 +2594,17 @@ function CalendarMonth({
2556
2594
  children: /* @__PURE__ */ jsx(
2557
2595
  "button",
2558
2596
  {
2559
- onClick: () => !outside && onDayClick(day),
2560
- onMouseEnter: () => !outside && onDayHover(day),
2561
- tabIndex: outside ? -1 : 0,
2597
+ onClick: () => !disabled && onDayClick(day),
2598
+ onMouseEnter: () => !disabled && onDayHover(day),
2599
+ tabIndex: disabled ? -1 : 0,
2600
+ disabled,
2562
2601
  className: cn(
2563
2602
  "w-8 h-8 rounded-full text-sm flex items-center justify-center transition-colors",
2564
- outside && "text-gray-300 dark:text-gray-600 pointer-events-none",
2565
- !outside && !rangeStart && !rangeEnd && "text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-neutral-700",
2566
- !outside && inRange && !rangeStart && !rangeEnd && "text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50",
2603
+ isFuture && "text-gray-300 dark:text-gray-600 pointer-events-none cursor-not-allowed",
2604
+ !disabled && !rangeStart && !rangeEnd && "text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-neutral-700",
2605
+ !disabled && inRange && !rangeStart && !rangeEnd && "text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50",
2567
2606
  (rangeStart || rangeEnd) && "bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-500",
2568
- isToday && !rangeStart && !rangeEnd && !outside && "font-bold"
2607
+ isToday && !rangeStart && !rangeEnd && !disabled && "font-bold"
2569
2608
  ),
2570
2609
  children: format(day, "d")
2571
2610
  }
@@ -2578,6 +2617,167 @@ function CalendarMonth({
2578
2617
  )
2579
2618
  ] });
2580
2619
  }
2620
+ function useDateRangePicker({
2621
+ value,
2622
+ onChange,
2623
+ showTimePicker = false,
2624
+ timeValue,
2625
+ onTimeChange
2626
+ }) {
2627
+ const [open, setOpen] = React10.useState(false);
2628
+ const [internalRange, setInternalRange] = React10.useState({
2629
+ from: void 0,
2630
+ to: void 0
2631
+ });
2632
+ const [draft, setDraft] = React10.useState({
2633
+ from: void 0,
2634
+ to: void 0
2635
+ });
2636
+ const [hoverDate, setHoverDate] = React10.useState();
2637
+ const [viewMonth, setViewMonth] = React10.useState(
2638
+ () => startOfMonth(value?.from ?? /* @__PURE__ */ new Date())
2639
+ );
2640
+ const [activePreset, setActivePreset] = React10.useState();
2641
+ const [committedInternalTime, setCommittedInternalTime] = React10.useState(DEFAULT_TIME_RANGE);
2642
+ const [draftTime, setDraftTime] = React10.useState(DEFAULT_TIME_RANGE);
2643
+ const [timeExpanded, setTimeExpanded] = React10.useState(false);
2644
+ const [timeDirty, setTimeDirty] = React10.useState(false);
2645
+ const committedRange = value ?? internalRange;
2646
+ const committedTime = timeValue ?? committedInternalTime;
2647
+ const timeVisible = !!(draft.from && draft.to);
2648
+ const hasAnyTimeInput = timeDirty || draftTime.from.hour !== null || draftTime.from.minute !== null || draftTime.to.hour !== null || draftTime.to.minute !== null;
2649
+ const isTimeComplete2 = draftTime.from.hour !== null && draftTime.from.minute !== null && draftTime.to.hour !== null && draftTime.to.minute !== null;
2650
+ const hasPartialTime = showTimePicker && timeExpanded && !isTimeComplete2;
2651
+ const canClear = !!(draft.from || committedRange.from);
2652
+ const canApply = !(draft.from && !draft.to) && !!(draft.from || committedRange.from) && !hasPartialTime;
2653
+ const resetTime = React10.useCallback(() => {
2654
+ setDraftTime(DEFAULT_TIME_RANGE);
2655
+ setTimeExpanded(false);
2656
+ setTimeDirty(false);
2657
+ }, []);
2658
+ const handleTimeChange = React10.useCallback((newTime) => {
2659
+ setDraftTime(newTime);
2660
+ }, []);
2661
+ const handleOpenChange = React10.useCallback(
2662
+ (newOpen) => {
2663
+ if (newOpen) {
2664
+ setDraft(committedRange);
2665
+ if (committedRange.from)
2666
+ setViewMonth(startOfMonth(committedRange.from));
2667
+ setDraftTime(committedTime);
2668
+ const hasExistingTime = committedTime.from.hour !== null || committedTime.from.minute !== null || committedTime.to.hour !== null || committedTime.to.minute !== null;
2669
+ setTimeExpanded(hasExistingTime);
2670
+ setTimeDirty(false);
2671
+ }
2672
+ setOpen(newOpen);
2673
+ },
2674
+ [committedRange, committedTime]
2675
+ );
2676
+ const handleDayClick = React10.useCallback(
2677
+ (date) => {
2678
+ const { from, to } = draft;
2679
+ if (!from || from && to) {
2680
+ setDraft({ from: date, to: void 0 });
2681
+ setActivePreset(void 0);
2682
+ if (showTimePicker && timeExpanded) {
2683
+ resetTime();
2684
+ }
2685
+ return;
2686
+ }
2687
+ const [start, end] = isBefore(from, date) ? [from, date] : [date, from];
2688
+ setDraft({ from: start, to: end });
2689
+ setHoverDate(void 0);
2690
+ },
2691
+ [draft, showTimePicker, timeExpanded, resetTime]
2692
+ );
2693
+ const handlePreset = React10.useCallback(
2694
+ (preset) => {
2695
+ const newRange = preset.getRange();
2696
+ setDraft(newRange);
2697
+ setActivePreset(preset.label);
2698
+ if (newRange.from) setViewMonth(startOfMonth(newRange.from));
2699
+ if (showTimePicker) resetTime();
2700
+ },
2701
+ [showTimePicker, resetTime]
2702
+ );
2703
+ const handleApply = React10.useCallback(() => {
2704
+ if (draft.from && !draft.to) return;
2705
+ const newRange = draft.from && draft.to ? draft : void 0;
2706
+ const rangeChanged = newRange?.from?.getTime() !== committedRange.from?.getTime() || newRange?.to?.getTime() !== committedRange.to?.getTime();
2707
+ const timeChanged = draftTime.from.hour !== committedTime.from.hour || draftTime.from.minute !== committedTime.from.minute || draftTime.to.hour !== committedTime.to.hour || draftTime.to.minute !== committedTime.to.minute;
2708
+ if (rangeChanged) {
2709
+ if (onChange) onChange(newRange);
2710
+ else setInternalRange(newRange ?? { from: void 0, to: void 0 });
2711
+ }
2712
+ if (timeChanged) {
2713
+ if (onTimeChange) onTimeChange(draftTime);
2714
+ else setCommittedInternalTime(draftTime);
2715
+ }
2716
+ setOpen(false);
2717
+ }, [draft, onChange, committedRange, draftTime, committedTime, onTimeChange]);
2718
+ const handleClear = React10.useCallback(() => {
2719
+ setDraft({ from: void 0, to: void 0 });
2720
+ setActivePreset(void 0);
2721
+ if (showTimePicker) resetTime();
2722
+ }, [showTimePicker, resetTime]);
2723
+ const expandTime = React10.useCallback(() => {
2724
+ setTimeExpanded(true);
2725
+ }, []);
2726
+ const markTimeDirty = React10.useCallback(() => {
2727
+ setTimeDirty(true);
2728
+ }, []);
2729
+ return {
2730
+ open,
2731
+ draft,
2732
+ hoverDate,
2733
+ viewMonth,
2734
+ activePreset,
2735
+ committedRange,
2736
+ committedTime,
2737
+ draftTime,
2738
+ timeExpanded,
2739
+ timeVisible,
2740
+ hasAnyTimeInput,
2741
+ canClear,
2742
+ canApply,
2743
+ showTimePicker,
2744
+ setHoverDate,
2745
+ setViewMonth,
2746
+ handleTimeChange,
2747
+ handleOpenChange,
2748
+ handleDayClick,
2749
+ handlePreset,
2750
+ handleApply,
2751
+ handleClear,
2752
+ resetTime,
2753
+ expandTime,
2754
+ markTimeDirty
2755
+ };
2756
+ }
2757
+ function formatTime(hour, minute) {
2758
+ const period = hour >= 12 ? "PM" : "AM";
2759
+ const h = hour % 12 || 12;
2760
+ const m = minute.toString().padStart(2, "0");
2761
+ return `${h}:${m} ${period}`;
2762
+ }
2763
+ function isTimeComplete(time) {
2764
+ return time.from.hour !== null && time.from.minute !== null && time.to.hour !== null && time.to.minute !== null;
2765
+ }
2766
+ function formatDateRange(range, placeholder, time) {
2767
+ if (!range?.from) return placeholder;
2768
+ const hasTime = time && isTimeComplete(time);
2769
+ if (!range.to || isSameDay(range.from, range.to)) {
2770
+ const dateStr = format(range.from, "MMM d, yyyy");
2771
+ return hasTime ? `${dateStr} ${formatTime(time.from.hour, time.from.minute)} \u2013 ${formatTime(time.to.hour, time.to.minute)}` : dateStr;
2772
+ }
2773
+ const sameYear = range.from.getFullYear() === range.to.getFullYear();
2774
+ const fromDate = format(range.from, sameYear ? "MMM d" : "MMM d, yyyy");
2775
+ const toDate = format(range.to, "MMM d, yyyy");
2776
+ if (hasTime) {
2777
+ return `${fromDate} ${formatTime(time.from.hour, time.from.minute)} \u2013 ${toDate} ${formatTime(time.to.hour, time.to.minute)}`;
2778
+ }
2779
+ return `${fromDate} \u2013 ${toDate}`;
2780
+ }
2581
2781
  var DEFAULT_PRESETS = [
2582
2782
  {
2583
2783
  label: "Today",
@@ -2597,7 +2797,7 @@ var DEFAULT_PRESETS = [
2597
2797
  label: "This Week",
2598
2798
  getRange: () => ({
2599
2799
  from: startOfWeek(/* @__PURE__ */ new Date(), { weekStartsOn: 0 }),
2600
- to: endOfWeek(/* @__PURE__ */ new Date(), { weekStartsOn: 0 })
2800
+ to: min([endOfWeek(/* @__PURE__ */ new Date(), { weekStartsOn: 0 }), endOfDay(/* @__PURE__ */ new Date())])
2601
2801
  })
2602
2802
  },
2603
2803
  {
@@ -2614,7 +2814,7 @@ var DEFAULT_PRESETS = [
2614
2814
  label: "This Month",
2615
2815
  getRange: () => ({
2616
2816
  from: startOfMonth(/* @__PURE__ */ new Date()),
2617
- to: endOfMonth(/* @__PURE__ */ new Date())
2817
+ to: min([endOfMonth(/* @__PURE__ */ new Date()), endOfDay(/* @__PURE__ */ new Date())])
2618
2818
  })
2619
2819
  },
2620
2820
  {
@@ -2625,26 +2825,6 @@ var DEFAULT_PRESETS = [
2625
2825
  }
2626
2826
  }
2627
2827
  ];
2628
- function formatTime(tv) {
2629
- if (tv.hour === null || tv.minute === null) return "--:--";
2630
- const period = tv.hour >= 12 ? "PM" : "AM";
2631
- const h = tv.hour % 12 || 12;
2632
- const m = tv.minute.toString().padStart(2, "0");
2633
- return `${h}:${m} ${period}`;
2634
- }
2635
- function formatDateRange(range, placeholder, time) {
2636
- if (!range?.from) return placeholder;
2637
- const timeActive = time && time.from.hour !== null && time.to.hour !== null;
2638
- const timeSuffix = timeActive ? ` ${formatTime(time.from)} \u2013 ${formatTime(time.to)}` : "";
2639
- if (!range.to || isSameDay(range.from, range.to)) {
2640
- return format(range.from, "MMM d, yyyy") + timeSuffix;
2641
- }
2642
- const sameYear = range.from.getFullYear() === range.to.getFullYear();
2643
- if (sameYear) {
2644
- return `${format(range.from, "MMM d")} \u2013 ${format(range.to, "MMM d, yyyy")}${timeSuffix}`;
2645
- }
2646
- return `${format(range.from, "MMM d, yyyy")} \u2013 ${format(range.to, "MMM d, yyyy")}${timeSuffix}`;
2647
- }
2648
2828
  function DateRangePicker({
2649
2829
  value,
2650
2830
  onChange,
@@ -2658,77 +2838,15 @@ function DateRangePicker({
2658
2838
  timeValue,
2659
2839
  onTimeChange
2660
2840
  }) {
2661
- const [open, setOpen] = React10.useState(false);
2662
- const [internalRange, setInternalRange] = React10.useState({
2663
- from: void 0,
2664
- to: void 0
2841
+ const picker = useDateRangePicker({
2842
+ value,
2843
+ onChange,
2844
+ showTimePicker,
2845
+ timeValue,
2846
+ onTimeChange
2665
2847
  });
2666
- const [draft, setDraft] = React10.useState({
2667
- from: void 0,
2668
- to: void 0
2669
- });
2670
- const [hoverDate, setHoverDate] = React10.useState();
2671
- const [leftMonth, setLeftMonth] = React10.useState(
2672
- () => startOfMonth(value?.from ?? /* @__PURE__ */ new Date())
2673
- );
2674
- const [activePreset, setActivePreset] = React10.useState();
2675
- const [internalTime, setInternalTime] = React10.useState(DEFAULT_TIME_RANGE);
2676
- const committedRange = value ?? internalRange;
2677
- const currentTime = timeValue ?? internalTime;
2678
- const handleTimeChange = (newTime) => {
2679
- if (onTimeChange) onTimeChange(newTime);
2680
- else setInternalTime(newTime);
2681
- };
2682
- const timeVisible = !!(draft.from && draft.to);
2683
- const handleOpenChange = (newOpen) => {
2684
- if (newOpen) {
2685
- setDraft(committedRange);
2686
- if (committedRange.from) setLeftMonth(startOfMonth(committedRange.from));
2687
- if (!onTimeChange) setInternalTime(currentTime);
2688
- }
2689
- setOpen(newOpen);
2690
- };
2691
- const handleDayClick = (date) => {
2692
- const { from, to } = draft;
2693
- if (!from || from && to) {
2694
- setDraft({ from: date, to: void 0 });
2695
- setActivePreset(void 0);
2696
- return;
2697
- }
2698
- const [start, end] = isBefore(from, date) ? [from, date] : [date, from];
2699
- setDraft({ from: start, to: end });
2700
- setHoverDate(void 0);
2701
- };
2702
- const handlePreset = (preset) => {
2703
- const newRange = preset.getRange();
2704
- if (showTimePicker) {
2705
- setDraft(newRange);
2706
- setActivePreset(preset.label);
2707
- if (newRange.from) setLeftMonth(startOfMonth(newRange.from));
2708
- } else {
2709
- if (onChange) onChange(newRange);
2710
- else setInternalRange(newRange);
2711
- setActivePreset(preset.label);
2712
- if (newRange.from) setLeftMonth(startOfMonth(newRange.from));
2713
- setOpen(false);
2714
- }
2715
- };
2716
- const handleApply = () => {
2717
- if (draft.from && !draft.to) return;
2718
- const newRange = draft.from && draft.to ? draft : void 0;
2719
- if (onChange) onChange(newRange);
2720
- else setInternalRange(newRange ?? { from: void 0, to: void 0 });
2721
- setOpen(false);
2722
- };
2723
- const handleClear = () => {
2724
- setDraft({ from: void 0, to: void 0 });
2725
- setActivePreset(void 0);
2726
- if (showTimePicker) handleTimeChange(DEFAULT_TIME_RANGE);
2727
- };
2728
- const canClear = !!(draft.from || committedRange.from);
2729
- const canApply = !(draft.from && !draft.to) && !!(draft.from || committedRange.from);
2730
- const rightMonth = addMonths(leftMonth, 1);
2731
- return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
2848
+ const rightMonth = addMonths(picker.viewMonth, 1);
2849
+ return /* @__PURE__ */ jsxs(Popover, { open: picker.open, onOpenChange: picker.handleOpenChange, children: [
2732
2850
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2733
2851
  "button",
2734
2852
  {
@@ -2738,15 +2856,15 @@ function DateRangePicker({
2738
2856
  "hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
2739
2857
  "dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700",
2740
2858
  "disabled:pointer-events-none disabled:opacity-50",
2741
- committedRange.from ? "text-gray-900 dark:text-gray-100" : "text-gray-400 dark:text-gray-500",
2859
+ picker.committedRange.from ? "text-gray-900 dark:text-gray-100" : "text-gray-400 dark:text-gray-500",
2742
2860
  className
2743
2861
  ),
2744
2862
  children: [
2745
2863
  /* @__PURE__ */ jsx(CalendarIcon, { className: "w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" }),
2746
2864
  /* @__PURE__ */ jsx("span", { children: formatDateRange(
2747
- committedRange,
2865
+ picker.committedRange,
2748
2866
  placeholder,
2749
- showTimePicker ? currentTime : void 0
2867
+ showTimePicker ? picker.committedTime : void 0
2750
2868
  ) })
2751
2869
  ]
2752
2870
  }
@@ -2758,10 +2876,10 @@ function DateRangePicker({
2758
2876
  section.options.map((preset) => /* @__PURE__ */ jsx(
2759
2877
  "button",
2760
2878
  {
2761
- onClick: () => handlePreset(preset),
2879
+ onClick: () => picker.handlePreset(preset),
2762
2880
  className: cn(
2763
2881
  "w-full text-left px-2 py-1.5 rounded-md text-sm transition-colors",
2764
- activePreset === preset.label ? "bg-blue-50 text-blue-600 font-medium dark:bg-blue-950/50 dark:text-blue-400" : "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700"
2882
+ picker.activePreset === preset.label ? "bg-blue-50 text-blue-600 font-medium dark:bg-blue-950/50 dark:text-blue-400" : "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700"
2765
2883
  ),
2766
2884
  children: preset.label
2767
2885
  },
@@ -2773,12 +2891,12 @@ function DateRangePicker({
2773
2891
  /* @__PURE__ */ jsx(
2774
2892
  CalendarMonth,
2775
2893
  {
2776
- month: leftMonth,
2777
- range: draft,
2778
- hoverDate,
2779
- onDayClick: handleDayClick,
2780
- onDayHover: setHoverDate,
2781
- onPrevMonth: () => setLeftMonth(subMonths(leftMonth, 1)),
2894
+ month: picker.viewMonth,
2895
+ range: picker.draft,
2896
+ hoverDate: picker.hoverDate,
2897
+ onDayClick: picker.handleDayClick,
2898
+ onDayHover: picker.setHoverDate,
2899
+ onPrevMonth: () => picker.setViewMonth(subMonths(picker.viewMonth, 1)),
2782
2900
  showPrevNav: true,
2783
2901
  showNextNav: false
2784
2902
  }
@@ -2788,46 +2906,72 @@ function DateRangePicker({
2788
2906
  CalendarMonth,
2789
2907
  {
2790
2908
  month: rightMonth,
2791
- range: draft,
2792
- hoverDate,
2793
- onDayClick: handleDayClick,
2794
- onDayHover: setHoverDate,
2795
- onNextMonth: () => setLeftMonth(addMonths(leftMonth, 1)),
2909
+ range: picker.draft,
2910
+ hoverDate: picker.hoverDate,
2911
+ onDayClick: picker.handleDayClick,
2912
+ onDayHover: picker.setHoverDate,
2913
+ onNextMonth: () => picker.setViewMonth(addMonths(picker.viewMonth, 1)),
2796
2914
  showPrevNav: false,
2797
2915
  showNextNav: true
2798
2916
  }
2799
2917
  )
2800
2918
  ] }),
2801
- showTimePicker && /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: timeVisible && /* @__PURE__ */ jsx(
2919
+ showTimePicker && /* @__PURE__ */ jsx(
2802
2920
  motion.div,
2803
2921
  {
2804
- initial: { height: 0, opacity: 0 },
2805
- animate: { height: "auto", opacity: 1 },
2806
- exit: { height: 0, opacity: 0 },
2807
- transition: {
2808
- height: { type: "spring", stiffness: 400, damping: 30, mass: 0.8 },
2809
- opacity: { duration: 0.2 }
2810
- },
2811
- className: "overflow-hidden",
2812
- children: /* @__PURE__ */ jsx(
2813
- TimePickerRow,
2922
+ layout: true,
2923
+ transition: { layout: { duration: 0.2, ease: "easeInOut" } },
2924
+ className: "border-t border-gray-100 dark:border-neutral-700",
2925
+ children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: picker.timeVisible && !picker.timeExpanded ? /* @__PURE__ */ jsx(
2926
+ motion.div,
2814
2927
  {
2815
- value: currentTime,
2816
- onChange: handleTimeChange
2817
- }
2818
- )
2819
- },
2820
- "time-picker"
2821
- ) }),
2928
+ initial: { opacity: 0 },
2929
+ animate: { opacity: 1 },
2930
+ exit: { opacity: 0 },
2931
+ transition: { duration: 0.12 },
2932
+ children: /* @__PURE__ */ jsx("div", { className: "px-4 py-2 ", children: /* @__PURE__ */ jsx(
2933
+ Button,
2934
+ {
2935
+ variant: "outline",
2936
+ onClick: picker.expandTime,
2937
+ leadingDecorator: /* @__PURE__ */ jsx(Clock, { className: "w-3.5 h-3.5" }),
2938
+ className: "w-full rounded-md h-9",
2939
+ children: "Add time range"
2940
+ }
2941
+ ) })
2942
+ },
2943
+ "add-time-btn"
2944
+ ) : picker.timeVisible && picker.timeExpanded ? /* @__PURE__ */ jsx(
2945
+ motion.div,
2946
+ {
2947
+ initial: { opacity: 0 },
2948
+ animate: { opacity: 1 },
2949
+ exit: { opacity: 0 },
2950
+ transition: { duration: 0.12 },
2951
+ children: /* @__PURE__ */ jsx(
2952
+ TimePickerRow,
2953
+ {
2954
+ value: picker.draftTime,
2955
+ onChange: picker.handleTimeChange,
2956
+ onRemove: picker.resetTime,
2957
+ showRemove: picker.timeExpanded,
2958
+ onDirty: picker.markTimeDirty
2959
+ }
2960
+ )
2961
+ },
2962
+ "time-picker"
2963
+ ) : null })
2964
+ }
2965
+ ),
2822
2966
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 px-4 py-3 border-t border-gray-100 dark:border-neutral-700", children: [
2823
2967
  /* @__PURE__ */ jsx(
2824
2968
  "button",
2825
2969
  {
2826
- onClick: handleClear,
2827
- disabled: !canClear,
2970
+ onClick: picker.handleClear,
2971
+ disabled: !picker.canClear,
2828
2972
  className: cn(
2829
2973
  "px-3 py-1.5 rounded-md text-sm transition-colors",
2830
- canClear ? "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700" : "text-gray-300 dark:text-gray-600 cursor-not-allowed"
2974
+ picker.canClear ? "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700" : "text-gray-300 dark:text-gray-600 cursor-not-allowed"
2831
2975
  ),
2832
2976
  children: "Clear"
2833
2977
  }
@@ -2835,11 +2979,11 @@ function DateRangePicker({
2835
2979
  /* @__PURE__ */ jsx(
2836
2980
  "button",
2837
2981
  {
2838
- onClick: handleApply,
2839
- disabled: !canApply,
2982
+ onClick: picker.handleApply,
2983
+ disabled: !picker.canApply,
2840
2984
  className: cn(
2841
2985
  "px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
2842
- canApply ? "bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-500" : "bg-blue-100 text-blue-300 cursor-not-allowed dark:bg-blue-950/30 dark:text-blue-800"
2986
+ picker.canApply ? "bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-500" : "bg-blue-100 text-blue-300 cursor-not-allowed dark:bg-blue-950/30 dark:text-blue-800"
2843
2987
  ),
2844
2988
  children: "Apply"
2845
2989
  }
@@ -2849,30 +2993,6 @@ function DateRangePicker({
2849
2993
  ] }) })
2850
2994
  ] });
2851
2995
  }
2852
- function formatTime2(tv) {
2853
- if (tv.hour === null || tv.minute === null) return "";
2854
- const period = tv.hour >= 12 ? "PM" : "AM";
2855
- const h = tv.hour % 12 || 12;
2856
- const m = tv.minute.toString().padStart(2, "0");
2857
- return `${h}:${m} ${period}`;
2858
- }
2859
- function formatDateWithTime(date, dateFmt, time) {
2860
- const dateStr = format(date, dateFmt);
2861
- const timeStr = time ? formatTime2(time) : "";
2862
- return timeStr ? `${dateStr}, ${timeStr}` : dateStr;
2863
- }
2864
- function formatDateRange2(range, placeholder, time) {
2865
- if (!range?.from) return placeholder;
2866
- const hasTime = time && time.from.hour !== null && time.to.hour !== null;
2867
- if (!range.to || isSameDay(range.from, range.to)) {
2868
- return formatDateWithTime(range.from, "MMMM d, yyyy", hasTime ? time.from : void 0);
2869
- }
2870
- const sameYear = range.from.getFullYear() === range.to.getFullYear();
2871
- const fromFmt = sameYear ? "MMMM d" : "MMMM d, yyyy";
2872
- const fromStr = formatDateWithTime(range.from, fromFmt, hasTime ? time.from : void 0);
2873
- const toStr = formatDateWithTime(range.to, "MMMM d, yyyy", hasTime ? time.to : void 0);
2874
- return `${fromStr} \u2013 ${toStr}`;
2875
- }
2876
2996
  function DateRangePickerMobile({
2877
2997
  label,
2878
2998
  value,
@@ -2886,78 +3006,16 @@ function DateRangePickerMobile({
2886
3006
  timeValue,
2887
3007
  onTimeChange
2888
3008
  }) {
2889
- const [open, setOpen] = React10.useState(false);
2890
- const [internalRange, setInternalRange] = React10.useState({
2891
- from: void 0,
2892
- to: void 0
2893
- });
2894
- const [draft, setDraft] = React10.useState({
2895
- from: void 0,
2896
- to: void 0
3009
+ const picker = useDateRangePicker({
3010
+ value,
3011
+ onChange,
3012
+ showTimePicker,
3013
+ timeValue,
3014
+ onTimeChange
2897
3015
  });
2898
- const [hoverDate, setHoverDate] = React10.useState();
2899
- const [viewMonth, setViewMonth] = React10.useState(
2900
- () => startOfMonth(value?.from ?? /* @__PURE__ */ new Date())
2901
- );
2902
- const [activePreset, setActivePreset] = React10.useState();
2903
- const [internalTime, setInternalTime] = React10.useState(DEFAULT_TIME_RANGE);
2904
- const committedRange = value ?? internalRange;
2905
- const currentTime = timeValue ?? internalTime;
2906
- const handleTimeChange = (newTime) => {
2907
- if (onTimeChange) onTimeChange(newTime);
2908
- else setInternalTime(newTime);
2909
- };
2910
- const timeVisible = !!(draft.from && draft.to);
2911
- const handleOpenChange = (newOpen) => {
2912
- if (newOpen) {
2913
- setDraft(committedRange);
2914
- if (committedRange.from) setViewMonth(startOfMonth(committedRange.from));
2915
- if (!onTimeChange) setInternalTime(currentTime);
2916
- }
2917
- setOpen(newOpen);
2918
- };
2919
- const handleDayClick = (date) => {
2920
- const { from, to } = draft;
2921
- if (!from || from && to) {
2922
- setDraft({ from: date, to: void 0 });
2923
- setActivePreset(void 0);
2924
- return;
2925
- }
2926
- const [start, end] = isBefore(from, date) ? [from, date] : [date, from];
2927
- setDraft({ from: start, to: end });
2928
- setHoverDate(void 0);
2929
- };
2930
- const handlePreset = (preset) => {
2931
- const newRange = preset.getRange();
2932
- if (showTimePicker) {
2933
- setDraft(newRange);
2934
- setActivePreset(preset.label);
2935
- if (newRange.from) setViewMonth(startOfMonth(newRange.from));
2936
- } else {
2937
- if (onChange) onChange(newRange);
2938
- else setInternalRange(newRange);
2939
- setActivePreset(preset.label);
2940
- if (newRange.from) setViewMonth(startOfMonth(newRange.from));
2941
- setOpen(false);
2942
- }
2943
- };
2944
- const handleApply = () => {
2945
- if (draft.from && !draft.to) return;
2946
- const newRange = draft.from && draft.to ? draft : void 0;
2947
- if (onChange) onChange(newRange);
2948
- else setInternalRange(newRange ?? { from: void 0, to: void 0 });
2949
- setOpen(false);
2950
- };
2951
- const handleClear = () => {
2952
- setDraft({ from: void 0, to: void 0 });
2953
- setActivePreset(void 0);
2954
- if (showTimePicker) handleTimeChange(DEFAULT_TIME_RANGE);
2955
- };
2956
- const canClear = !!(draft.from || committedRange.from);
2957
- const canApply = !(draft.from && !draft.to) && !!(draft.from || committedRange.from);
2958
3016
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-1.5", className), children: [
2959
3017
  label && /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: label }),
2960
- /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: handleOpenChange, children: [
3018
+ /* @__PURE__ */ jsxs(Popover, { open: picker.open, onOpenChange: picker.handleOpenChange, children: [
2961
3019
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2962
3020
  "button",
2963
3021
  {
@@ -2967,14 +3025,14 @@ function DateRangePickerMobile({
2967
3025
  "hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
2968
3026
  "dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700",
2969
3027
  "disabled:pointer-events-none disabled:opacity-50",
2970
- committedRange.from ? "text-gray-900 dark:text-gray-100" : "text-gray-400 dark:text-gray-500"
3028
+ picker.committedRange.from ? "text-gray-900 dark:text-gray-100" : "text-gray-400 dark:text-gray-500"
2971
3029
  ),
2972
3030
  children: [
2973
3031
  /* @__PURE__ */ jsx(CalendarIcon, { className: "w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" }),
2974
- /* @__PURE__ */ jsx("span", { className: "truncate", children: formatDateRange2(
2975
- committedRange,
3032
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: formatDateRange(
3033
+ picker.committedRange,
2976
3034
  placeholder,
2977
- showTimePicker ? currentTime : void 0
3035
+ showTimePicker ? picker.committedTime : void 0
2978
3036
  ) })
2979
3037
  ]
2980
3038
  }
@@ -2987,10 +3045,10 @@ function DateRangePickerMobile({
2987
3045
  section.options.map((preset) => /* @__PURE__ */ jsx(
2988
3046
  "button",
2989
3047
  {
2990
- onClick: () => handlePreset(preset),
3048
+ onClick: () => picker.handlePreset(preset),
2991
3049
  className: cn(
2992
3050
  "w-full text-left px-2 py-1.5 rounded-md text-sm transition-colors",
2993
- activePreset === preset.label ? "bg-blue-50 text-blue-600 font-medium dark:bg-blue-950/50 dark:text-blue-400" : "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700"
3051
+ picker.activePreset === preset.label ? "bg-blue-50 text-blue-600 font-medium dark:bg-blue-950/50 dark:text-blue-400" : "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700"
2994
3052
  ),
2995
3053
  children: preset.label
2996
3054
  },
@@ -3002,52 +3060,73 @@ function DateRangePickerMobile({
3002
3060
  /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
3003
3061
  CalendarMonth,
3004
3062
  {
3005
- month: viewMonth,
3006
- range: draft,
3007
- hoverDate,
3008
- onDayClick: handleDayClick,
3009
- onDayHover: setHoverDate,
3010
- onPrevMonth: () => setViewMonth(subMonths(viewMonth, 1)),
3011
- onNextMonth: () => setViewMonth(addMonths(viewMonth, 1)),
3063
+ month: picker.viewMonth,
3064
+ range: picker.draft,
3065
+ hoverDate: picker.hoverDate,
3066
+ onDayClick: picker.handleDayClick,
3067
+ onDayHover: picker.setHoverDate,
3068
+ onPrevMonth: () => picker.setViewMonth(subMonths(picker.viewMonth, 1)),
3069
+ onNextMonth: () => picker.setViewMonth(addMonths(picker.viewMonth, 1)),
3012
3070
  showPrevNav: true,
3013
3071
  showNextNav: true
3014
3072
  }
3015
3073
  ) }),
3016
- showTimePicker && /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: timeVisible && /* @__PURE__ */ jsx(
3074
+ showTimePicker && /* @__PURE__ */ jsx(
3017
3075
  motion.div,
3018
3076
  {
3019
- initial: { height: 0, opacity: 0 },
3020
- animate: { height: "auto", opacity: 1 },
3021
- exit: { height: 0, opacity: 0 },
3022
- transition: {
3023
- height: {
3024
- type: "spring",
3025
- stiffness: 400,
3026
- damping: 30,
3027
- mass: 0.8
3077
+ layout: true,
3078
+ transition: { layout: { duration: 0.2, ease: "easeInOut" } },
3079
+ className: "border-t border-gray-100 dark:border-neutral-700",
3080
+ children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: picker.timeVisible && !picker.timeExpanded ? /* @__PURE__ */ jsx(
3081
+ motion.div,
3082
+ {
3083
+ initial: { opacity: 0 },
3084
+ animate: { opacity: 1 },
3085
+ exit: { opacity: 0 },
3086
+ transition: { duration: 0.12 },
3087
+ children: /* @__PURE__ */ jsx("div", { className: "px-2 py-2", children: /* @__PURE__ */ jsx(
3088
+ Button,
3089
+ {
3090
+ variant: "outline",
3091
+ onClick: picker.expandTime,
3092
+ leadingDecorator: /* @__PURE__ */ jsx(Clock, { className: "w-3.5 h-3.5" }),
3093
+ className: "w-full rounded-md h-9",
3094
+ children: "Add time range"
3095
+ }
3096
+ ) })
3028
3097
  },
3029
- opacity: { duration: 0.2 }
3030
- },
3031
- className: "overflow-hidden",
3032
- children: /* @__PURE__ */ jsx(
3033
- TimePickerColumn,
3098
+ "add-time-btn"
3099
+ ) : picker.timeVisible && picker.timeExpanded ? /* @__PURE__ */ jsx(
3100
+ motion.div,
3034
3101
  {
3035
- value: currentTime,
3036
- onChange: handleTimeChange
3037
- }
3038
- )
3039
- },
3040
- "time-picker"
3041
- ) }),
3102
+ initial: { opacity: 0 },
3103
+ animate: { opacity: 1 },
3104
+ exit: { opacity: 0 },
3105
+ transition: { duration: 0.12 },
3106
+ children: /* @__PURE__ */ jsx(
3107
+ TimePickerColumn,
3108
+ {
3109
+ value: picker.draftTime,
3110
+ onChange: picker.handleTimeChange,
3111
+ onRemove: picker.resetTime,
3112
+ showRemove: picker.timeExpanded,
3113
+ onDirty: picker.markTimeDirty
3114
+ }
3115
+ )
3116
+ },
3117
+ "time-picker"
3118
+ ) : null })
3119
+ }
3120
+ ),
3042
3121
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 px-2 py-3 border-t border-gray-100 dark:border-neutral-700", children: [
3043
3122
  /* @__PURE__ */ jsx(
3044
3123
  "button",
3045
3124
  {
3046
- onClick: handleClear,
3047
- disabled: !canClear,
3125
+ onClick: picker.handleClear,
3126
+ disabled: !picker.canClear,
3048
3127
  className: cn(
3049
3128
  "px-3 py-1.5 rounded-md text-sm transition-colors",
3050
- canClear ? "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700" : "text-gray-300 dark:text-gray-600 cursor-not-allowed"
3129
+ picker.canClear ? "text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-neutral-700" : "text-gray-300 dark:text-gray-600 cursor-not-allowed"
3051
3130
  ),
3052
3131
  children: "Clear"
3053
3132
  }
@@ -3055,11 +3134,11 @@ function DateRangePickerMobile({
3055
3134
  /* @__PURE__ */ jsx(
3056
3135
  "button",
3057
3136
  {
3058
- onClick: handleApply,
3059
- disabled: !canApply,
3137
+ onClick: picker.handleApply,
3138
+ disabled: !picker.canApply,
3060
3139
  className: cn(
3061
3140
  "px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
3062
- canApply ? "bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-500" : "bg-blue-100 text-blue-300 cursor-not-allowed dark:bg-blue-950/30 dark:text-blue-800"
3141
+ picker.canApply ? "bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-500" : "bg-blue-100 text-blue-300 cursor-not-allowed dark:bg-blue-950/30 dark:text-blue-800"
3063
3142
  ),
3064
3143
  children: "Apply"
3065
3144
  }
@@ -3319,5 +3398,5 @@ function PeriodComparisonSelector({
3319
3398
  }
3320
3399
 
3321
3400
  export { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, ColumnSelection, DEFAULT_COMPARISON_PERIODS, DEFAULT_PRESETS, DEFAULT_TIME_RANGE, DateRangePicker, DateRangePickerMobile, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Form, FormControl, FormDescription, FormField, FormLabel, FormMessage, MobileDataCard, PeriodComparisonSelector, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator3 as Separator, Skeleton, SkeletonAvatar, SkeletonBadge, SkeletonButton, SkeletonCard, SkeletonIcon, SkeletonInput, SkeletonSubtitle, SkeletonTableRow, SkeletonTableRows, SkeletonText, SkeletonTitle, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableRowCheckbox, TableSelectAll, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Toggle, buttonVariants, segmentedControlItemVariants, segmentedControlVariants, separatorVariants, switchLabelVariants, switchThumbVariants, switchTrackVariants, toggleGroupVariants, toggleItemVariants, useColumnVisibility, useFormContext, useFormFieldContext, useTableSelection };
3322
- //# sourceMappingURL=chunk-OWU2ABN4.mjs.map
3323
- //# sourceMappingURL=chunk-OWU2ABN4.mjs.map
3401
+ //# sourceMappingURL=chunk-HFDHLY24.mjs.map
3402
+ //# sourceMappingURL=chunk-HFDHLY24.mjs.map