@open-mercato/ui 0.6.1-develop.3256.1.fe3dec2464 → 0.6.1-develop.3276.1.1fcd145078

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/CrudForm.js +46 -129
  3. package/dist/backend/CrudForm.js.map +3 -3
  4. package/dist/backend/DataTable.js +10 -3
  5. package/dist/backend/DataTable.js.map +2 -2
  6. package/dist/backend/EmptyState.js +1 -47
  7. package/dist/backend/EmptyState.js.map +2 -2
  8. package/dist/backend/FilterOverlay.js +31 -26
  9. package/dist/backend/FilterOverlay.js.map +2 -2
  10. package/dist/backend/FlashMessages.js +23 -25
  11. package/dist/backend/FlashMessages.js.map +2 -2
  12. package/dist/backend/JsonBuilder.js +4 -2
  13. package/dist/backend/JsonBuilder.js.map +2 -2
  14. package/dist/backend/LogList.js +49 -0
  15. package/dist/backend/LogList.js.map +7 -0
  16. package/dist/backend/crud/CollapsibleGroup.js +13 -3
  17. package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
  18. package/dist/backend/detail/InlineEditors.js +80 -32
  19. package/dist/backend/detail/InlineEditors.js.map +2 -2
  20. package/dist/backend/inputs/ComboboxInput.js +60 -6
  21. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  22. package/dist/backend/inputs/DatePicker.js +6 -142
  23. package/dist/backend/inputs/DatePicker.js.map +2 -2
  24. package/dist/backend/inputs/DateTimePicker.js +6 -182
  25. package/dist/backend/inputs/DateTimePicker.js.map +2 -2
  26. package/dist/backend/inputs/TagsInput.js +16 -19
  27. package/dist/backend/inputs/TagsInput.js.map +2 -2
  28. package/dist/backend/inputs/TimeInput.js +1 -1
  29. package/dist/backend/inputs/TimeInput.js.map +2 -2
  30. package/dist/backend/inputs/TimePicker.js +83 -72
  31. package/dist/backend/inputs/TimePicker.js.map +2 -2
  32. package/dist/backend/utils/customFieldForms.js +2 -2
  33. package/dist/backend/utils/customFieldForms.js.map +1 -1
  34. package/dist/portal/components/PortalEmptyState.js +13 -9
  35. package/dist/portal/components/PortalEmptyState.js.map +2 -2
  36. package/dist/primitives/accordion.js +137 -0
  37. package/dist/primitives/accordion.js.map +7 -0
  38. package/dist/primitives/alert.js +180 -12
  39. package/dist/primitives/alert.js.map +2 -2
  40. package/dist/primitives/calendar.js +122 -32
  41. package/dist/primitives/calendar.js.map +2 -2
  42. package/dist/primitives/compact-select.js +47 -0
  43. package/dist/primitives/compact-select.js.map +7 -0
  44. package/dist/primitives/counter-input.js +174 -0
  45. package/dist/primitives/counter-input.js.map +7 -0
  46. package/dist/primitives/date-picker-helpers.js +79 -0
  47. package/dist/primitives/date-picker-helpers.js.map +7 -0
  48. package/dist/primitives/date-picker.js +216 -0
  49. package/dist/primitives/date-picker.js.map +7 -0
  50. package/dist/primitives/date-range-picker.js +239 -0
  51. package/dist/primitives/date-range-picker.js.map +7 -0
  52. package/dist/primitives/digit-input.js +157 -0
  53. package/dist/primitives/digit-input.js.map +7 -0
  54. package/dist/primitives/empty-state.js +93 -0
  55. package/dist/primitives/empty-state.js.map +7 -0
  56. package/dist/primitives/inline-input.js +29 -0
  57. package/dist/primitives/inline-input.js.map +7 -0
  58. package/dist/primitives/inline-select.js +41 -0
  59. package/dist/primitives/inline-select.js.map +7 -0
  60. package/dist/primitives/notification-stack.js +116 -0
  61. package/dist/primitives/notification-stack.js.map +7 -0
  62. package/dist/primitives/notification.js +62 -0
  63. package/dist/primitives/notification.js.map +7 -0
  64. package/dist/primitives/rich-editor.js +1230 -0
  65. package/dist/primitives/rich-editor.js.map +7 -0
  66. package/dist/primitives/select.js +1 -0
  67. package/dist/primitives/select.js.map +2 -2
  68. package/dist/primitives/skeleton.js +55 -0
  69. package/dist/primitives/skeleton.js.map +7 -0
  70. package/dist/primitives/tag-input.js +186 -0
  71. package/dist/primitives/tag-input.js.map +7 -0
  72. package/dist/primitives/tag.js +66 -13
  73. package/dist/primitives/tag.js.map +2 -2
  74. package/dist/primitives/time-picker.js +793 -0
  75. package/dist/primitives/time-picker.js.map +7 -0
  76. package/package.json +4 -3
  77. package/src/backend/CrudForm.tsx +61 -113
  78. package/src/backend/DataTable.tsx +17 -5
  79. package/src/backend/EmptyState.tsx +13 -63
  80. package/src/backend/FilterOverlay.tsx +38 -22
  81. package/src/backend/FlashMessages.tsx +23 -24
  82. package/src/backend/JsonBuilder.tsx +4 -2
  83. package/src/backend/LogList.tsx +109 -0
  84. package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
  85. package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
  86. package/src/backend/__tests__/LogList.test.tsx +163 -0
  87. package/src/backend/crud/CollapsibleGroup.tsx +17 -3
  88. package/src/backend/detail/InlineEditors.tsx +78 -25
  89. package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
  90. package/src/backend/inputs/ComboboxInput.tsx +81 -9
  91. package/src/backend/inputs/DatePicker.tsx +28 -155
  92. package/src/backend/inputs/DateTimePicker.tsx +25 -193
  93. package/src/backend/inputs/TagsInput.tsx +10 -13
  94. package/src/backend/inputs/TimeInput.tsx +2 -1
  95. package/src/backend/inputs/TimePicker.tsx +99 -70
  96. package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
  97. package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
  98. package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
  99. package/src/backend/utils/customFieldForms.ts +2 -2
  100. package/src/portal/components/PortalEmptyState.tsx +27 -22
  101. package/src/primitives/__tests__/accordion.test.tsx +197 -0
  102. package/src/primitives/__tests__/alert.test.tsx +204 -0
  103. package/src/primitives/__tests__/compact-select.test.tsx +98 -0
  104. package/src/primitives/__tests__/counter-input.test.tsx +165 -0
  105. package/src/primitives/__tests__/date-picker.test.tsx +221 -0
  106. package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
  107. package/src/primitives/__tests__/digit-input.test.tsx +168 -0
  108. package/src/primitives/__tests__/empty-state.test.tsx +154 -0
  109. package/src/primitives/__tests__/inline-input.test.tsx +100 -0
  110. package/src/primitives/__tests__/inline-select.test.tsx +103 -0
  111. package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
  112. package/src/primitives/__tests__/notification.test.tsx +119 -0
  113. package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
  114. package/src/primitives/__tests__/skeleton.test.tsx +76 -0
  115. package/src/primitives/__tests__/tag-input.test.tsx +198 -0
  116. package/src/primitives/__tests__/tag.test.tsx +123 -0
  117. package/src/primitives/__tests__/time-picker.test.tsx +392 -0
  118. package/src/primitives/accordion.tsx +225 -0
  119. package/src/primitives/alert.tsx +234 -24
  120. package/src/primitives/calendar.tsx +146 -38
  121. package/src/primitives/compact-select.tsx +59 -0
  122. package/src/primitives/counter-input.tsx +211 -0
  123. package/src/primitives/date-picker-helpers.ts +96 -0
  124. package/src/primitives/date-picker.tsx +290 -0
  125. package/src/primitives/date-range-picker.tsx +281 -0
  126. package/src/primitives/digit-input.tsx +208 -0
  127. package/src/primitives/empty-state.tsx +140 -0
  128. package/src/primitives/inline-input.tsx +55 -0
  129. package/src/primitives/inline-select.tsx +74 -0
  130. package/src/primitives/notification-stack.tsx +185 -0
  131. package/src/primitives/notification.tsx +136 -0
  132. package/src/primitives/rich-editor.tsx +1578 -0
  133. package/src/primitives/select.tsx +1 -0
  134. package/src/primitives/skeleton.tsx +70 -0
  135. package/src/primitives/tag-input.tsx +223 -0
  136. package/src/primitives/tag.tsx +69 -10
  137. package/src/primitives/time-picker.tsx +1059 -0
@@ -0,0 +1,793 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Check, ChevronLeft, ChevronRight, Clock, X } from "lucide-react";
5
+ import { cva } from "class-variance-authority";
6
+ import { cn } from "@open-mercato/shared/lib/utils";
7
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
8
+ import { Button } from "./button.js";
9
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
10
+ function padTwo(input) {
11
+ return String(input).padStart(2, "0");
12
+ }
13
+ function parseTime(value) {
14
+ if (!value) return null;
15
+ const parts = value.split(":");
16
+ if (parts.length < 2) return null;
17
+ const hour = parseInt(parts[0] ?? "0", 10);
18
+ const minute = parseInt(parts[1] ?? "0", 10);
19
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
20
+ return {
21
+ hour: Math.max(0, Math.min(23, hour)),
22
+ minute: Math.max(0, Math.min(59, minute))
23
+ };
24
+ }
25
+ function formatSlotDisplay(value, format) {
26
+ const parsed = parseTime(value);
27
+ if (!parsed) return { main: value, suffix: null };
28
+ if (format === "24h") {
29
+ return { main: `${padTwo(parsed.hour)}:${padTwo(parsed.minute)}`, suffix: null };
30
+ }
31
+ const hour12 = parsed.hour === 0 ? 12 : parsed.hour > 12 ? parsed.hour - 12 : parsed.hour;
32
+ const suffix = parsed.hour < 12 ? "AM" : "PM";
33
+ return { main: `${padTwo(hour12)}:${padTwo(parsed.minute)}`, suffix };
34
+ }
35
+ function generateSlots(startTime, endTime, intervalMinutes) {
36
+ const start = parseTime(startTime);
37
+ const end = parseTime(endTime);
38
+ if (!start || !end) return [];
39
+ const step = intervalMinutes > 0 ? intervalMinutes : 30;
40
+ const out = [];
41
+ const startTotal = start.hour * 60 + start.minute;
42
+ const endTotal = end.hour * 60 + end.minute;
43
+ if (endTotal < startTotal) return [];
44
+ for (let m = startTotal; m <= endTotal; m += step) {
45
+ const h = Math.floor(m / 60);
46
+ const min = m % 60;
47
+ out.push(`${padTwo(h)}:${padTwo(min)}`);
48
+ }
49
+ return out;
50
+ }
51
+ function formatTimePickerDisplay(value, format = "12h") {
52
+ return formatSlotDisplay(value, format);
53
+ }
54
+ function formatDuration(minutes, options) {
55
+ const short = options?.short ?? true;
56
+ if (!Number.isFinite(minutes) || minutes <= 0) return short ? "0 min" : "0 minutes";
57
+ if (minutes < 60) return short ? `${minutes} min` : `${minutes} minutes`;
58
+ if (minutes % (24 * 60) === 0) {
59
+ const days = minutes / (24 * 60);
60
+ return short ? days === 1 ? "1 day" : `${days} days` : days === 1 ? "1 day" : `${days} days`;
61
+ }
62
+ if (minutes % 60 === 0) {
63
+ const hours2 = minutes / 60;
64
+ return short ? hours2 === 1 ? "1 hour" : `${hours2} hours` : hours2 === 1 ? "1 hour" : `${hours2} hours`;
65
+ }
66
+ const hours = Math.floor(minutes / 60);
67
+ const rest = minutes - hours * 60;
68
+ return short ? `${hours}h ${rest}m` : `${hours} hour${hours === 1 ? "" : "s"} ${rest} minute${rest === 1 ? "" : "s"}`;
69
+ }
70
+ const defaultStatusLabels = {
71
+ available: "Available",
72
+ busy: "Busy",
73
+ "in-meeting": "In meeting",
74
+ offline: "Offline"
75
+ };
76
+ const statusColorMap = {
77
+ available: {
78
+ dot: "bg-status-success-icon",
79
+ bgSoft: "bg-status-success-bg",
80
+ border: "border-status-success-border",
81
+ text: "text-status-success-text"
82
+ },
83
+ busy: {
84
+ dot: "bg-status-error-icon",
85
+ bgSoft: "bg-status-error-bg",
86
+ border: "border-status-error-border",
87
+ text: "text-status-error-text"
88
+ },
89
+ "in-meeting": {
90
+ dot: "bg-status-warning-icon",
91
+ bgSoft: "bg-status-warning-bg",
92
+ border: "border-status-warning-border",
93
+ text: "text-status-warning-text"
94
+ },
95
+ offline: {
96
+ dot: "bg-muted-foreground",
97
+ bgSoft: "bg-muted",
98
+ border: "border-border",
99
+ text: "text-muted-foreground"
100
+ }
101
+ };
102
+ const slotVariants = cva(
103
+ "flex h-9 w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors text-left",
104
+ {
105
+ variants: {
106
+ state: {
107
+ default: "bg-background hover:bg-muted/50 text-muted-foreground cursor-pointer",
108
+ hover: "bg-muted/50 text-muted-foreground cursor-pointer",
109
+ active: "bg-brand-violet/10 text-brand-violet cursor-pointer",
110
+ disabled: "bg-background text-muted-foreground/40 cursor-not-allowed"
111
+ }
112
+ },
113
+ defaultVariants: { state: "default" }
114
+ }
115
+ );
116
+ const TimePickerSlot = React.forwardRef(
117
+ ({
118
+ value,
119
+ label,
120
+ rightText,
121
+ state,
122
+ selected,
123
+ disabled,
124
+ format = "12h",
125
+ onSelect,
126
+ className,
127
+ "aria-label": ariaLabel
128
+ }, ref) => {
129
+ const effectiveState = disabled ? "disabled" : selected ? "active" : state ?? "default";
130
+ const display = label != null ? { main: label, suffix: null } : formatSlotDisplay(value, format);
131
+ const right = rightText ? formatSlotDisplay(rightText, format) : null;
132
+ const isInteractive = effectiveState !== "disabled";
133
+ return /* @__PURE__ */ jsxs(
134
+ "button",
135
+ {
136
+ ref,
137
+ type: "button",
138
+ className: cn(slotVariants({ state: effectiveState }), className),
139
+ onClick: isInteractive ? () => onSelect?.(value) : void 0,
140
+ disabled: !isInteractive,
141
+ "aria-label": ariaLabel ?? value,
142
+ "aria-pressed": effectiveState === "active",
143
+ "data-slot": "time-picker-slot",
144
+ "data-state": effectiveState,
145
+ children: [
146
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1", children: [
147
+ /* @__PURE__ */ jsx("span", { children: display.main }),
148
+ display.suffix ? /* @__PURE__ */ jsx("span", { className: effectiveState === "active" ? "text-brand-violet/60" : "text-muted-foreground/60", children: display.suffix }) : null
149
+ ] }),
150
+ right ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center justify-end gap-1", children: [
151
+ /* @__PURE__ */ jsx("span", { children: right.main }),
152
+ right.suffix ? /* @__PURE__ */ jsx("span", { className: effectiveState === "active" ? "text-brand-violet/60" : "text-muted-foreground/60", children: right.suffix }) : null
153
+ ] }) : null,
154
+ effectiveState === "active" ? /* @__PURE__ */ jsx(Check, { className: "size-3.5 shrink-0 text-brand-violet", "aria-hidden": "true" }) : null
155
+ ]
156
+ }
157
+ );
158
+ }
159
+ );
160
+ TimePickerSlot.displayName = "TimePickerSlot";
161
+ const durationChipVariants = cva(
162
+ "inline-flex h-7 items-center justify-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors whitespace-nowrap",
163
+ {
164
+ variants: {
165
+ state: {
166
+ default: "bg-background border border-border shadow-xs text-muted-foreground hover:bg-muted/40 cursor-pointer",
167
+ hover: "bg-muted/40 border border-border text-muted-foreground cursor-pointer",
168
+ active: "bg-brand-violet/10 text-brand-violet cursor-pointer",
169
+ disabled: "bg-background border border-border text-muted-foreground/40 cursor-not-allowed shadow-xs"
170
+ }
171
+ },
172
+ defaultVariants: { state: "default" }
173
+ }
174
+ );
175
+ const TimePickerDurationChip = React.forwardRef(
176
+ ({ value, label, state, selected, disabled, onSelect, className }, ref) => {
177
+ const effectiveState = disabled ? "disabled" : selected ? "active" : state ?? "default";
178
+ const isInteractive = effectiveState !== "disabled";
179
+ const displayLabel = label ?? formatDuration(value);
180
+ return /* @__PURE__ */ jsxs(
181
+ "button",
182
+ {
183
+ ref,
184
+ type: "button",
185
+ className: cn(durationChipVariants({ state: effectiveState }), className),
186
+ onClick: isInteractive ? () => onSelect?.(value) : void 0,
187
+ disabled: !isInteractive,
188
+ "aria-pressed": effectiveState === "active",
189
+ "data-slot": "time-picker-duration-chip",
190
+ "data-state": effectiveState,
191
+ children: [
192
+ effectiveState === "active" ? /* @__PURE__ */ jsx(Check, { className: "size-3 shrink-0", "aria-hidden": "true" }) : null,
193
+ /* @__PURE__ */ jsx("span", { children: displayLabel })
194
+ ]
195
+ }
196
+ );
197
+ }
198
+ );
199
+ TimePickerDurationChip.displayName = "TimePickerDurationChip";
200
+ const statusChipBaseVariants = cva(
201
+ "inline-flex h-7 items-center justify-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors whitespace-nowrap border",
202
+ {
203
+ variants: {
204
+ state: {
205
+ default: "",
206
+ hover: "",
207
+ selected: "",
208
+ disabled: "cursor-not-allowed opacity-60"
209
+ }
210
+ },
211
+ defaultVariants: { state: "default" }
212
+ }
213
+ );
214
+ const TimePickerStatusChip = React.forwardRef(
215
+ ({ variant, label, state, selected, disabled, icon, onSelect, className }, ref) => {
216
+ const effectiveState = disabled ? "disabled" : selected ? "selected" : state ?? "default";
217
+ const colors = statusColorMap[variant];
218
+ const isInteractive = effectiveState !== "disabled";
219
+ const displayLabel = label ?? defaultStatusLabels[variant];
220
+ const stateClasses = effectiveState === "selected" ? cn(colors.bgSoft, colors.border, colors.text) : effectiveState === "hover" ? cn("bg-muted/30 border-border", colors.text, "cursor-pointer") : effectiveState === "disabled" ? cn("bg-background border-border", colors.text) : cn("bg-background border-border text-muted-foreground hover:bg-muted/30 cursor-pointer");
221
+ return /* @__PURE__ */ jsxs(
222
+ "button",
223
+ {
224
+ ref,
225
+ type: "button",
226
+ className: cn(statusChipBaseVariants({ state: effectiveState }), stateClasses, className),
227
+ onClick: isInteractive ? () => onSelect?.(variant) : void 0,
228
+ disabled: !isInteractive,
229
+ "aria-pressed": effectiveState === "selected",
230
+ "data-slot": "time-picker-status-chip",
231
+ "data-state": effectiveState,
232
+ "data-variant": variant,
233
+ children: [
234
+ icon ?? /* @__PURE__ */ jsx(
235
+ "span",
236
+ {
237
+ className: cn("inline-block size-1.5 shrink-0 rounded-full", colors.dot),
238
+ "aria-hidden": "true"
239
+ }
240
+ ),
241
+ /* @__PURE__ */ jsx("span", { children: displayLabel })
242
+ ]
243
+ }
244
+ );
245
+ }
246
+ );
247
+ TimePickerStatusChip.displayName = "TimePickerStatusChip";
248
+ function HorizontalScrollRow({
249
+ className,
250
+ contentClassName,
251
+ children,
252
+ ariaLabel,
253
+ arrowSize = "default",
254
+ scrollLeftAriaLabel = "Scroll left",
255
+ scrollRightAriaLabel = "Scroll right"
256
+ }) {
257
+ const scrollRef = React.useRef(null);
258
+ const [canScrollLeft, setCanScrollLeft] = React.useState(false);
259
+ const [canScrollRight, setCanScrollRight] = React.useState(false);
260
+ const refreshScrollState = React.useCallback(() => {
261
+ const el = scrollRef.current;
262
+ if (!el) return;
263
+ const { scrollLeft, scrollWidth, clientWidth } = el;
264
+ setCanScrollLeft(scrollLeft > 1);
265
+ setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
266
+ }, []);
267
+ React.useEffect(() => {
268
+ refreshScrollState();
269
+ const el = scrollRef.current;
270
+ if (!el) return;
271
+ el.addEventListener("scroll", refreshScrollState, { passive: true });
272
+ const ro = new ResizeObserver(refreshScrollState);
273
+ ro.observe(el);
274
+ return () => {
275
+ el.removeEventListener("scroll", refreshScrollState);
276
+ ro.disconnect();
277
+ };
278
+ }, [refreshScrollState]);
279
+ const scrollBy = React.useCallback((direction) => {
280
+ const el = scrollRef.current;
281
+ if (!el) return;
282
+ const amount = Math.max(80, Math.floor(el.clientWidth * 0.6));
283
+ el.scrollBy({ left: direction === "left" ? -amount : amount, behavior: "smooth" });
284
+ }, []);
285
+ const arrowButtonClass = arrowSize === "sm" ? "size-6 [&_svg]:size-3.5" : "size-7 [&_svg]:size-4";
286
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), "data-slot": "time-picker-scroll-row", children: [
287
+ /* @__PURE__ */ jsx(
288
+ "div",
289
+ {
290
+ className: cn(
291
+ "transition-[padding]",
292
+ canScrollLeft ? "pl-9" : "pl-4",
293
+ canScrollRight ? "pr-9" : "pr-4"
294
+ ),
295
+ children: /* @__PURE__ */ jsx(
296
+ "div",
297
+ {
298
+ ref: scrollRef,
299
+ className: cn(
300
+ "flex items-center gap-2 overflow-x-auto py-3.5",
301
+ "[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden",
302
+ contentClassName
303
+ ),
304
+ role: "group",
305
+ "aria-label": ariaLabel,
306
+ children
307
+ }
308
+ )
309
+ }
310
+ ),
311
+ canScrollLeft ? /* @__PURE__ */ jsx(
312
+ "div",
313
+ {
314
+ "aria-hidden": "true",
315
+ className: "pointer-events-none absolute inset-y-0 left-9 w-6 bg-gradient-to-r from-background to-transparent"
316
+ }
317
+ ) : null,
318
+ canScrollRight ? /* @__PURE__ */ jsx(
319
+ "div",
320
+ {
321
+ "aria-hidden": "true",
322
+ className: "pointer-events-none absolute inset-y-0 right-9 w-6 bg-gradient-to-l from-background to-transparent"
323
+ }
324
+ ) : null,
325
+ canScrollLeft ? /* @__PURE__ */ jsx(
326
+ "button",
327
+ {
328
+ type: "button",
329
+ onClick: () => scrollBy("left"),
330
+ "aria-label": scrollLeftAriaLabel,
331
+ className: cn(
332
+ "absolute left-1 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
333
+ arrowButtonClass
334
+ ),
335
+ "data-slot": "time-picker-scroll-left",
336
+ children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
337
+ }
338
+ ) : null,
339
+ canScrollRight ? /* @__PURE__ */ jsx(
340
+ "button",
341
+ {
342
+ type: "button",
343
+ onClick: () => scrollBy("right"),
344
+ "aria-label": scrollRightAriaLabel,
345
+ className: cn(
346
+ "absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
347
+ arrowButtonClass
348
+ ),
349
+ "data-slot": "time-picker-scroll-right",
350
+ children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
351
+ }
352
+ ) : null
353
+ ] });
354
+ }
355
+ function useControllable(controlled, defaultValue, onChange) {
356
+ const [internal, setInternal] = React.useState(defaultValue);
357
+ const isControlled = controlled !== void 0;
358
+ const value = isControlled ? controlled : internal;
359
+ const setValue = React.useCallback(
360
+ (next) => {
361
+ if (!isControlled) setInternal(next);
362
+ onChange?.(next);
363
+ },
364
+ [isControlled, onChange]
365
+ );
366
+ return [value, setValue];
367
+ }
368
+ function TimePickerCard({
369
+ value,
370
+ setValue,
371
+ showHeader,
372
+ headerIcon,
373
+ headerTitle,
374
+ headerPlaceholder,
375
+ onClose,
376
+ closeAriaLabel,
377
+ format,
378
+ durationOptions,
379
+ activeDuration,
380
+ setActiveDuration,
381
+ onDurationChange,
382
+ durationsAriaLabel,
383
+ scrollLeftAriaLabel,
384
+ scrollRightAriaLabel,
385
+ statusOptions,
386
+ activeStatus,
387
+ setActiveStatus,
388
+ onStatusChange,
389
+ statusLabel,
390
+ slotList,
391
+ slotLabel,
392
+ slotRightText,
393
+ maxHeight,
394
+ showFooter,
395
+ cancelLabel,
396
+ applyLabel,
397
+ handleCancel,
398
+ handleApply,
399
+ legacyFooterActions,
400
+ pinnedTopActions,
401
+ disabled,
402
+ className,
403
+ ariaLabel,
404
+ slotListRef
405
+ }) {
406
+ const headerDisplay = React.useMemo(() => {
407
+ if (headerTitle != null) return { main: headerTitle, suffix: null };
408
+ if (value) return formatSlotDisplay(value, format);
409
+ return { main: headerPlaceholder, suffix: null };
410
+ }, [headerTitle, value, format, headerPlaceholder]);
411
+ const handleSlotKeyDown = React.useCallback(
412
+ (event) => {
413
+ if (disabled) return;
414
+ const target = event.target;
415
+ const slotButton = target.closest('[data-slot="time-picker-slot"]');
416
+ if (!slotButton) return;
417
+ const container = slotListRef.current;
418
+ if (!container) return;
419
+ const all = Array.from(
420
+ container.querySelectorAll('[data-slot="time-picker-slot"]:not(:disabled)')
421
+ );
422
+ const currentIndex = all.indexOf(slotButton);
423
+ if (currentIndex === -1) return;
424
+ if (event.key === "ArrowDown") {
425
+ event.preventDefault();
426
+ const next = all[Math.min(all.length - 1, currentIndex + 1)];
427
+ next?.focus();
428
+ } else if (event.key === "ArrowUp") {
429
+ event.preventDefault();
430
+ const prev = all[Math.max(0, currentIndex - 1)];
431
+ prev?.focus();
432
+ } else if (event.key === "Home") {
433
+ event.preventDefault();
434
+ all[0]?.focus();
435
+ } else if (event.key === "End") {
436
+ event.preventDefault();
437
+ all[all.length - 1]?.focus();
438
+ }
439
+ },
440
+ [disabled, slotListRef]
441
+ );
442
+ return /* @__PURE__ */ jsxs(
443
+ "div",
444
+ {
445
+ className: cn(
446
+ "flex w-[348px] flex-col overflow-hidden rounded-[20px] border border-border bg-background",
447
+ "shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03),0px_24px_24px_-12px_rgba(51,51,51,0.04),0px_12px_12px_-6px_rgba(51,51,51,0.04)]",
448
+ className
449
+ ),
450
+ role: "dialog",
451
+ "aria-label": ariaLabel,
452
+ "aria-disabled": disabled || void 0,
453
+ "data-slot": "time-picker",
454
+ children: [
455
+ showHeader ? /* @__PURE__ */ jsxs(
456
+ "div",
457
+ {
458
+ className: "flex items-center gap-2 border-b border-border px-4 py-3.5",
459
+ "data-slot": "time-picker-header",
460
+ children: [
461
+ /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center text-muted-foreground", "aria-hidden": "true", children: headerIcon ?? /* @__PURE__ */ jsx(Clock, { className: "size-5", "aria-hidden": "true" }) }),
462
+ /* @__PURE__ */ jsxs("p", { className: "flex-1 min-w-0 truncate text-base font-medium text-muted-foreground", children: [
463
+ /* @__PURE__ */ jsx("span", { children: headerDisplay.main }),
464
+ headerDisplay.suffix ? /* @__PURE__ */ jsx("span", { className: "ml-1 text-muted-foreground/60", children: headerDisplay.suffix }) : null
465
+ ] }),
466
+ onClose ? /* @__PURE__ */ jsx(
467
+ "button",
468
+ {
469
+ type: "button",
470
+ onClick: onClose,
471
+ disabled,
472
+ className: "inline-flex size-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40 disabled:pointer-events-none disabled:opacity-60",
473
+ "aria-label": closeAriaLabel,
474
+ "data-slot": "time-picker-close",
475
+ children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
476
+ }
477
+ ) : null
478
+ ]
479
+ }
480
+ ) : null,
481
+ durationOptions && durationOptions.length > 0 ? /* @__PURE__ */ jsx(
482
+ HorizontalScrollRow,
483
+ {
484
+ className: "border-b border-border",
485
+ ariaLabel: durationsAriaLabel,
486
+ scrollLeftAriaLabel,
487
+ scrollRightAriaLabel,
488
+ children: durationOptions.map((option) => /* @__PURE__ */ jsx(
489
+ TimePickerDurationChip,
490
+ {
491
+ value: option.value,
492
+ label: option.label,
493
+ disabled: disabled || option.disabled,
494
+ selected: activeDuration === option.value,
495
+ onSelect: (next) => {
496
+ setActiveDuration(next);
497
+ onDurationChange?.(next);
498
+ }
499
+ },
500
+ option.value
501
+ ))
502
+ }
503
+ ) : null,
504
+ statusOptions && statusOptions.length > 0 ? /* @__PURE__ */ jsxs(
505
+ "div",
506
+ {
507
+ className: "flex flex-col gap-2 border-b border-border px-4 py-3.5",
508
+ "data-slot": "time-picker-status-row",
509
+ children: [
510
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-muted-foreground", children: statusLabel }),
511
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: statusOptions.map((option) => /* @__PURE__ */ jsx(
512
+ TimePickerStatusChip,
513
+ {
514
+ variant: option.variant,
515
+ label: option.label,
516
+ disabled: disabled || option.disabled,
517
+ selected: activeStatus === option.variant,
518
+ onSelect: (next) => {
519
+ setActiveStatus(next);
520
+ onStatusChange?.(next);
521
+ }
522
+ },
523
+ option.variant
524
+ )) })
525
+ ]
526
+ }
527
+ ) : null,
528
+ pinnedTopActions && pinnedTopActions.length > 0 ? /* @__PURE__ */ jsx(
529
+ "div",
530
+ {
531
+ className: "flex flex-col gap-0.5 border-b border-border p-2",
532
+ "data-slot": "time-picker-pinned-top",
533
+ children: pinnedTopActions.map((action) => /* @__PURE__ */ jsxs(
534
+ "button",
535
+ {
536
+ type: "button",
537
+ onClick: action.onClick,
538
+ disabled,
539
+ className: "flex h-9 w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-brand-violet transition-colors hover:bg-brand-violet/10 cursor-pointer disabled:pointer-events-none disabled:opacity-60",
540
+ "data-slot": "time-picker-pinned-action",
541
+ children: [
542
+ action.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&_svg]:size-4", "aria-hidden": "true", children: action.icon }) : null,
543
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-left", children: action.label }),
544
+ action.rightText ? /* @__PURE__ */ jsx("span", { className: "text-brand-violet/60", children: action.rightText }) : null
545
+ ]
546
+ },
547
+ action.label
548
+ ))
549
+ }
550
+ ) : null,
551
+ /* @__PURE__ */ jsxs(
552
+ "div",
553
+ {
554
+ className: "flex flex-col gap-0.5 overflow-y-auto p-2",
555
+ style: { maxHeight },
556
+ ref: slotListRef,
557
+ onKeyDown: handleSlotKeyDown,
558
+ "data-slot": "time-picker-slots",
559
+ children: [
560
+ slotLabel ? /* @__PURE__ */ jsx("p", { className: "px-3 pt-1 pb-2 text-sm font-medium text-muted-foreground", children: slotLabel }) : null,
561
+ slotList.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: "No time slots available." }) : slotList.map((slot) => /* @__PURE__ */ jsx(
562
+ TimePickerSlot,
563
+ {
564
+ value: slot,
565
+ format,
566
+ rightText: slotRightText?.(slot),
567
+ selected: value === slot,
568
+ disabled,
569
+ onSelect: (next) => setValue(next)
570
+ },
571
+ slot
572
+ ))
573
+ ]
574
+ }
575
+ ),
576
+ showFooter ? /* @__PURE__ */ jsxs(
577
+ "div",
578
+ {
579
+ className: "flex flex-wrap items-center justify-end gap-2 border-t border-border px-4 py-3",
580
+ "data-slot": "time-picker-footer",
581
+ children: [
582
+ legacyFooterActions && legacyFooterActions.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mr-auto flex items-center gap-1", children: [
583
+ legacyFooterActions.map((action) => /* @__PURE__ */ jsx(
584
+ Button,
585
+ {
586
+ type: "button",
587
+ variant: action.variant === "muted" ? "muted" : "ghost",
588
+ size: "sm",
589
+ onClick: action.onClick,
590
+ disabled,
591
+ "data-slot": "time-picker-legacy-action",
592
+ children: action.label
593
+ },
594
+ action.label
595
+ )),
596
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ml-2 mr-1 h-5 w-px bg-border" })
597
+ ] }) : null,
598
+ /* @__PURE__ */ jsx(
599
+ Button,
600
+ {
601
+ type: "button",
602
+ variant: "outline",
603
+ size: "sm",
604
+ onClick: handleCancel,
605
+ disabled,
606
+ "data-slot": "time-picker-cancel",
607
+ children: cancelLabel
608
+ }
609
+ ),
610
+ /* @__PURE__ */ jsx(
611
+ Button,
612
+ {
613
+ type: "button",
614
+ variant: "default",
615
+ size: "sm",
616
+ onClick: handleApply,
617
+ disabled,
618
+ "data-slot": "time-picker-apply",
619
+ children: applyLabel
620
+ }
621
+ )
622
+ ]
623
+ }
624
+ ) : null
625
+ ]
626
+ }
627
+ );
628
+ }
629
+ function TimePicker({
630
+ value: valueProp,
631
+ defaultValue = null,
632
+ onChange,
633
+ showHeader = true,
634
+ headerIcon,
635
+ headerTitle,
636
+ headerPlaceholder,
637
+ onClose,
638
+ durations,
639
+ activeDuration: activeDurationProp,
640
+ defaultActiveDuration,
641
+ onDurationChange,
642
+ statuses,
643
+ activeStatus: activeStatusProp,
644
+ defaultActiveStatus,
645
+ onStatusChange,
646
+ statusLabel,
647
+ slots: slotsProp,
648
+ startTime = "00:00",
649
+ endTime = "23:30",
650
+ intervalMinutes = 30,
651
+ format = "12h",
652
+ slotRightText,
653
+ slotLabel,
654
+ maxHeight = 280,
655
+ showFooter = true,
656
+ cancelLabel,
657
+ applyLabel,
658
+ onCancel,
659
+ onApply,
660
+ legacyFooterActions,
661
+ pinnedTopActions,
662
+ trigger,
663
+ open: openProp,
664
+ defaultOpen = false,
665
+ onOpenChange,
666
+ popoverAlign = "start",
667
+ popoverSide = "bottom",
668
+ className,
669
+ disabled = false,
670
+ "aria-label": ariaLabel
671
+ }) {
672
+ const t = useT();
673
+ const resolvedHeaderPlaceholder = headerPlaceholder ?? t("ui.timePicker.placeholder", "Pick a time");
674
+ const resolvedStatusLabel = statusLabel ?? t("ui.timePicker.statusLabel", "Select status");
675
+ const resolvedCancelLabel = cancelLabel ?? t("ui.timePicker.cancelButton", "Cancel");
676
+ const resolvedApplyLabel = applyLabel ?? t("ui.timePicker.applyButton", "Apply");
677
+ const resolvedAriaLabel = ariaLabel ?? t("ui.timePicker.label", "Time picker");
678
+ const resolvedCloseAriaLabel = t("ui.timePicker.closeButton", "Close");
679
+ const resolvedDurationsAriaLabel = t("ui.timePicker.durationsRowLabel", "Quick duration");
680
+ const resolvedScrollLeftAriaLabel = t("ui.timePicker.scrollLeft", "Scroll left");
681
+ const resolvedScrollRightAriaLabel = t("ui.timePicker.scrollRight", "Scroll right");
682
+ const [value, setValue] = useControllable(valueProp, defaultValue, onChange);
683
+ const [activeDuration, setActiveDuration] = useControllable(
684
+ activeDurationProp,
685
+ defaultActiveDuration
686
+ );
687
+ const [activeStatus, setActiveStatus] = useControllable(
688
+ activeStatusProp,
689
+ defaultActiveStatus
690
+ );
691
+ const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
692
+ const openSnapshotRef = React.useRef(value);
693
+ const slotListRef = React.useRef(null);
694
+ const slotList = React.useMemo(() => {
695
+ if (slotsProp && slotsProp.length > 0) return slotsProp;
696
+ return generateSlots(startTime, endTime, intervalMinutes);
697
+ }, [slotsProp, startTime, endTime, intervalMinutes]);
698
+ const setOpenWithSnapshot = React.useCallback(
699
+ (next) => {
700
+ if (next && !open) {
701
+ openSnapshotRef.current = value;
702
+ }
703
+ setOpen(next);
704
+ },
705
+ [open, setOpen, value]
706
+ );
707
+ const handleApply = React.useCallback(() => {
708
+ onApply?.(value);
709
+ if (trigger) setOpenWithSnapshot(false);
710
+ }, [onApply, setOpenWithSnapshot, trigger, value]);
711
+ const handleCancel = React.useCallback(() => {
712
+ onCancel?.();
713
+ if (trigger) {
714
+ setValue(openSnapshotRef.current);
715
+ setOpenWithSnapshot(false);
716
+ }
717
+ }, [onCancel, setOpenWithSnapshot, setValue, trigger]);
718
+ const handleKeyDown = React.useCallback(
719
+ (event) => {
720
+ if (event.key === "Escape") {
721
+ handleCancel();
722
+ } else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
723
+ event.preventDefault();
724
+ handleApply();
725
+ }
726
+ },
727
+ [handleApply, handleCancel]
728
+ );
729
+ const card = /* @__PURE__ */ jsx("div", { onKeyDown: handleKeyDown, children: /* @__PURE__ */ jsx(
730
+ TimePickerCard,
731
+ {
732
+ value,
733
+ setValue,
734
+ showHeader,
735
+ headerIcon,
736
+ headerTitle,
737
+ headerPlaceholder: resolvedHeaderPlaceholder,
738
+ onClose,
739
+ closeAriaLabel: resolvedCloseAriaLabel,
740
+ format,
741
+ durationOptions: durations,
742
+ activeDuration,
743
+ setActiveDuration,
744
+ onDurationChange,
745
+ durationsAriaLabel: resolvedDurationsAriaLabel,
746
+ scrollLeftAriaLabel: resolvedScrollLeftAriaLabel,
747
+ scrollRightAriaLabel: resolvedScrollRightAriaLabel,
748
+ statusOptions: statuses,
749
+ activeStatus,
750
+ setActiveStatus,
751
+ onStatusChange,
752
+ statusLabel: resolvedStatusLabel,
753
+ slotList,
754
+ slotLabel,
755
+ slotRightText,
756
+ maxHeight,
757
+ showFooter,
758
+ cancelLabel: resolvedCancelLabel,
759
+ applyLabel: resolvedApplyLabel,
760
+ handleCancel,
761
+ handleApply,
762
+ legacyFooterActions,
763
+ pinnedTopActions,
764
+ disabled,
765
+ className,
766
+ ariaLabel: resolvedAriaLabel,
767
+ slotListRef
768
+ }
769
+ ) });
770
+ if (!trigger) return card;
771
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpenWithSnapshot, children: [
772
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: trigger }),
773
+ /* @__PURE__ */ jsx(
774
+ PopoverContent,
775
+ {
776
+ align: popoverAlign,
777
+ side: popoverSide,
778
+ className: "w-auto border-none bg-transparent p-0 shadow-none",
779
+ children: card
780
+ }
781
+ )
782
+ ] });
783
+ }
784
+ export {
785
+ HorizontalScrollRow,
786
+ TimePicker,
787
+ TimePickerDurationChip,
788
+ TimePickerSlot,
789
+ TimePickerStatusChip,
790
+ formatDuration,
791
+ formatTimePickerDisplay
792
+ };
793
+ //# sourceMappingURL=time-picker.js.map