@underverse-ui/underverse 0.2.95 → 0.2.97

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.
package/dist/index.cjs CHANGED
@@ -8308,9 +8308,9 @@ function useClientWidth(ref) {
8308
8308
  var SIZE_CONFIG_BY_SIZE = {
8309
8309
  sm: {
8310
8310
  resourceColumnWidth: 200,
8311
- rowHeight: 44,
8311
+ rowHeight: 66,
8312
8312
  slotMinWidth: 52,
8313
- eventHeight: 20,
8313
+ eventHeight: 40,
8314
8314
  laneGap: 3,
8315
8315
  lanePaddingY: 5,
8316
8316
  densityClass: "text-xs",
@@ -8324,9 +8324,9 @@ var SIZE_CONFIG_BY_SIZE = {
8324
8324
  },
8325
8325
  md: {
8326
8326
  resourceColumnWidth: 240,
8327
- rowHeight: 52,
8327
+ rowHeight: 78,
8328
8328
  slotMinWidth: 64,
8329
- eventHeight: 24,
8329
+ eventHeight: 48,
8330
8330
  laneGap: 4,
8331
8331
  lanePaddingY: 6,
8332
8332
  densityClass: "text-sm",
@@ -8340,9 +8340,9 @@ var SIZE_CONFIG_BY_SIZE = {
8340
8340
  },
8341
8341
  xl: {
8342
8342
  resourceColumnWidth: 280,
8343
- rowHeight: 60,
8343
+ rowHeight: 90,
8344
8344
  slotMinWidth: 76,
8345
- eventHeight: 28,
8345
+ eventHeight: 56,
8346
8346
  laneGap: 5,
8347
8347
  lanePaddingY: 8,
8348
8348
  densityClass: "text-base",
@@ -8492,6 +8492,9 @@ function CalendarTimelineHeader(props) {
8492
8492
  title,
8493
8493
  resourcesHeaderLabel,
8494
8494
  labels,
8495
+ newEventLabel,
8496
+ newEventDisabled,
8497
+ onNewEventClick,
8495
8498
  activeView,
8496
8499
  sizeConfig,
8497
8500
  navigate,
@@ -8542,24 +8545,38 @@ function CalendarTimelineHeader(props) {
8542
8545
  ] }),
8543
8546
  /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("h2", { className: cn("ml-3 font-semibold tracking-tight truncate text-foreground", sizeConfig.titleClass), children: title })
8544
8547
  ] }),
8545
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex items-center bg-muted/40 rounded-xl p-1 gap-0.5", children: ["month", "week", "day"].map((v) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
8546
- Button_default,
8547
- {
8548
- variant: activeView === v ? "default" : "ghost",
8549
- size: "sm",
8550
- onClick: () => setView(v),
8551
- className: cn(
8552
- sizeConfig.controlButtonTextClass,
8553
- "rounded-lg font-medium transition-all duration-200 gap-1.5",
8554
- activeView === v ? "bg-primary text-primary-foreground shadow-sm shadow-primary/25" : "hover:bg-background/80 text-muted-foreground hover:text-foreground"
8555
- ),
8556
- children: [
8557
- VIEW_ICONS[v],
8558
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "hidden sm:inline", children: labels[v] })
8559
- ]
8560
- },
8561
- v
8562
- )) })
8548
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-center gap-2", children: [
8549
+ onNewEventClick ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
8550
+ Button_default,
8551
+ {
8552
+ variant: "default",
8553
+ size: "sm",
8554
+ icon: import_lucide_react18.Plus,
8555
+ disabled: newEventDisabled,
8556
+ onClick: onNewEventClick,
8557
+ className: cn(sizeConfig.controlButtonTextClass, "rounded-lg font-medium transition-all duration-200 gap-1.5"),
8558
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "hidden sm:inline", children: newEventLabel })
8559
+ }
8560
+ ) : null,
8561
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "flex items-center bg-muted/40 rounded-xl p-1 gap-0.5", children: ["month", "week", "day"].map((v) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
8562
+ Button_default,
8563
+ {
8564
+ variant: activeView === v ? "default" : "ghost",
8565
+ size: "sm",
8566
+ onClick: () => setView(v),
8567
+ className: cn(
8568
+ sizeConfig.controlButtonTextClass,
8569
+ "rounded-lg font-medium transition-all duration-200 gap-1.5",
8570
+ activeView === v ? "bg-primary text-primary-foreground shadow-sm shadow-primary/25" : "hover:bg-background/80 text-muted-foreground hover:text-foreground"
8571
+ ),
8572
+ children: [
8573
+ VIEW_ICONS[v],
8574
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "hidden sm:inline", children: labels[v] })
8575
+ ]
8576
+ },
8577
+ v
8578
+ )) })
8579
+ ] })
8563
8580
  ] }),
8564
8581
  /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex border-t border-border/20", children: [
8565
8582
  /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
@@ -8728,6 +8745,7 @@ function CalendarTimeline({
8728
8745
  onRangeChange,
8729
8746
  onEventClick,
8730
8747
  onEventDoubleClick,
8748
+ onCreateEventClick,
8731
8749
  onCreateEvent,
8732
8750
  onEventMove,
8733
8751
  onEventResize,
@@ -8742,6 +8760,7 @@ function CalendarTimeline({
8742
8760
  const resolvedLocale = React28.useMemo(() => localeToBCP47(locale ?? detectedLocale), [locale, detectedLocale]);
8743
8761
  const resolvedTimeZone = React28.useMemo(() => timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC", [timeZone]);
8744
8762
  const effectiveEnableEventSheet = enableEventSheet ?? Boolean(renderEventSheet);
8763
+ const isViewOnly = interactions?.mode === "view";
8745
8764
  const isControlledSelectedEventId = selectedEventId !== void 0;
8746
8765
  const [internalSelectedEventId, setInternalSelectedEventId] = React28.useState(defaultSelectedEventId ?? null);
8747
8766
  const activeSelectedEventId = isControlledSelectedEventId ? selectedEventId : internalSelectedEventId;
@@ -8767,15 +8786,17 @@ function CalendarTimeline({
8767
8786
  const canResizeColumn = React28.useMemo(() => {
8768
8787
  const cfg = enableLayoutResize;
8769
8788
  if (!cfg) return false;
8789
+ if (isViewOnly) return false;
8770
8790
  if (cfg === true) return true;
8771
8791
  return cfg.column !== false;
8772
- }, [enableLayoutResize]);
8792
+ }, [enableLayoutResize, isViewOnly]);
8773
8793
  const canResizeRow = React28.useMemo(() => {
8774
8794
  const cfg = enableLayoutResize;
8775
8795
  if (!cfg) return false;
8796
+ if (isViewOnly) return false;
8776
8797
  if (cfg === true) return true;
8777
8798
  return cfg.row !== false;
8778
- }, [enableLayoutResize]);
8799
+ }, [enableLayoutResize, isViewOnly]);
8779
8800
  const isControlledResourceColumnWidth = resourceColumnWidth !== void 0;
8780
8801
  const [internalResourceColumnWidth, setInternalResourceColumnWidth] = React28.useState(() => {
8781
8802
  const init = defaultResourceColumnWidth ?? sizeConfig.resourceColumnWidth;
@@ -8815,6 +8836,13 @@ function CalendarTimeline({
8815
8836
  month: labels?.month ?? t("month"),
8816
8837
  week: labels?.week ?? t("week"),
8817
8838
  day: labels?.day ?? t("day"),
8839
+ newEvent: labels?.newEvent ?? t("newEvent"),
8840
+ createEventTitle: labels?.createEventTitle ?? t("createEventTitle"),
8841
+ create: labels?.create ?? t("create"),
8842
+ cancel: labels?.cancel ?? t("cancel"),
8843
+ resource: labels?.resource ?? t("resource"),
8844
+ start: labels?.start ?? t("start"),
8845
+ end: labels?.end ?? t("end"),
8818
8846
  expandGroup: labels?.expandGroup ?? t("expandGroup"),
8819
8847
  collapseGroup: labels?.collapseGroup ?? t("collapseGroup"),
8820
8848
  more: labels?.more ?? ((n) => t("more", { n })),
@@ -9082,6 +9110,69 @@ function CalendarTimeline({
9082
9110
  const eventHeight = sizeConfig.eventHeight;
9083
9111
  const laneGap = sizeConfig.laneGap;
9084
9112
  const lanePaddingY = sizeConfig.lanePaddingY;
9113
+ const createMode = interactions?.createMode ?? "drag";
9114
+ const canCreate = !isViewOnly && (interactions?.creatable ?? false) && !!onCreateEvent;
9115
+ const [createOpen, setCreateOpen] = React28.useState(false);
9116
+ const [createResourceId, setCreateResourceId] = React28.useState(null);
9117
+ const [createStartIdx, setCreateStartIdx] = React28.useState(0);
9118
+ const [createEndIdx, setCreateEndIdx] = React28.useState(1);
9119
+ const resourceOptions = React28.useMemo(() => {
9120
+ return resources.map((r) => ({
9121
+ label: typeof r.label === "string" ? r.label : r.id,
9122
+ value: r.id,
9123
+ description: r.groupId ? String(r.groupId) : void 0,
9124
+ disabled: r.disabled ?? false
9125
+ }));
9126
+ }, [resources]);
9127
+ const slotPickerLabel = React28.useMemo(() => {
9128
+ const timeFmt = getDtf(resolvedLocale, resolvedTimeZone, { hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
9129
+ const dayFmt = getDtf(resolvedLocale, resolvedTimeZone, { weekday: "short", month: "short", day: "numeric" });
9130
+ return (d) => activeView === "day" ? timeFmt.format(d) : dayFmt.format(d);
9131
+ }, [activeView, resolvedLocale, resolvedTimeZone]);
9132
+ const openCreate = React28.useCallback(() => {
9133
+ if (!canCreate) return;
9134
+ if (activeEventSheetOpen) setEventSheetOpen(false);
9135
+ const firstResource = resources.find((r) => !r.disabled)?.id ?? resources[0]?.id ?? null;
9136
+ setCreateResourceId(firstResource ?? null);
9137
+ let startIdx = 0;
9138
+ if (slots.length > 0) {
9139
+ if (activeView === "day") {
9140
+ const inRange = resolvedNow.getTime() >= range.start.getTime() && resolvedNow.getTime() < range.end.getTime();
9141
+ startIdx = clamp3(inRange ? binarySearchFirstGE(slotStarts, resolvedNow) : 0, 0, slots.length - 1);
9142
+ } else {
9143
+ const dayStart = startOfZonedDay(activeDate, resolvedTimeZone);
9144
+ startIdx = clamp3(binarySearchLastLE(slotStarts, dayStart), 0, slots.length - 1);
9145
+ }
9146
+ }
9147
+ setCreateStartIdx(startIdx);
9148
+ setCreateEndIdx(Math.min(slots.length, startIdx + 1));
9149
+ setCreateOpen(true);
9150
+ }, [activeDate, activeEventSheetOpen, activeView, canCreate, range.end, range.start, resolvedNow, resolvedTimeZone, resources, setEventSheetOpen, slotStarts, slots.length]);
9151
+ React28.useEffect(() => {
9152
+ setCreateEndIdx((prev) => Math.min(slots.length, Math.max(prev, createStartIdx + 1)));
9153
+ }, [createStartIdx, slots.length]);
9154
+ const createStartOptions = React28.useMemo(() => {
9155
+ return slotStarts.map((d, idx) => ({ label: slotPickerLabel(d), value: idx }));
9156
+ }, [slotStarts, slotPickerLabel]);
9157
+ const createEndOptions = React28.useMemo(() => {
9158
+ const out = [];
9159
+ for (let idx = createStartIdx + 1; idx <= slotStarts.length; idx++) {
9160
+ const boundary = idx >= slotStarts.length ? range.end : slotStarts[idx];
9161
+ out.push({ label: slotPickerLabel(boundary), value: idx });
9162
+ }
9163
+ return out;
9164
+ }, [createStartIdx, range.end, slotPickerLabel, slotStarts]);
9165
+ const commitCreate = React28.useCallback(() => {
9166
+ if (!onCreateEvent) return;
9167
+ if (!createResourceId) return;
9168
+ const start = slotStarts[clamp3(createStartIdx, 0, Math.max(0, slotStarts.length - 1))];
9169
+ if (!start) return;
9170
+ const endBoundary = createEndIdx >= slotStarts.length ? range.end : slotStarts[createEndIdx];
9171
+ if (!endBoundary) return;
9172
+ if (endBoundary.getTime() <= start.getTime()) return;
9173
+ onCreateEvent({ resourceId: createResourceId, start, end: endBoundary });
9174
+ setCreateOpen(false);
9175
+ }, [createEndIdx, createResourceId, createStartIdx, onCreateEvent, range.end, slotStarts]);
9085
9176
  const dragRef = React28.useRef(null);
9086
9177
  const [preview, setPreview] = React28.useState(null);
9087
9178
  const suppressNextEventClickRef = React28.useRef(false);
@@ -9114,6 +9205,7 @@ function CalendarTimeline({
9114
9205
  );
9115
9206
  const onPointerDownEvent = (e, ev, mode) => {
9116
9207
  if (e.button !== 0 || e.ctrlKey) return;
9208
+ if (isViewOnly) return;
9117
9209
  if (ev.resourceId == null) return;
9118
9210
  const allowDrag = interactions?.draggableEvents ?? true;
9119
9211
  const allowResize = interactions?.resizableEvents ?? true;
@@ -9141,7 +9233,9 @@ function CalendarTimeline({
9141
9233
  };
9142
9234
  const onPointerDownCell = (e) => {
9143
9235
  if (e.button !== 0 || e.ctrlKey) return;
9236
+ if (isViewOnly) return;
9144
9237
  if (!(interactions?.creatable ?? false) || !onCreateEvent) return;
9238
+ if (createMode === "click") return;
9145
9239
  const ctx = getPointerContext(e.clientX, e.clientY, { biasLeft: true });
9146
9240
  if (!ctx?.resourceId) return;
9147
9241
  const { start } = slotToDate(ctx.slotIdx);
@@ -9162,6 +9256,25 @@ function CalendarTimeline({
9162
9256
  e.currentTarget.setPointerCapture(e.pointerId);
9163
9257
  e.preventDefault();
9164
9258
  };
9259
+ const onClickCell = (e) => {
9260
+ if (e.button !== 0 || e.ctrlKey) return;
9261
+ if (isViewOnly) return;
9262
+ if (!(interactions?.creatable ?? false)) return;
9263
+ if (createMode !== "click") return;
9264
+ if (!onCreateEventClick) return;
9265
+ const ctx = getPointerContext(e.clientX, e.clientY, { biasLeft: true });
9266
+ if (!ctx?.resourceId) return;
9267
+ const { start, end } = slotToDate(ctx.slotIdx);
9268
+ onCreateEventClick({
9269
+ resourceId: ctx.resourceId,
9270
+ start,
9271
+ end: ctx.slotIdx + 1 >= slots.length ? range.end : end,
9272
+ slotIdx: ctx.slotIdx,
9273
+ view: activeView,
9274
+ locale: resolvedLocale,
9275
+ timeZone: resolvedTimeZone
9276
+ });
9277
+ };
9165
9278
  const onPointerMove = (e) => {
9166
9279
  const drag = dragRef.current;
9167
9280
  if (!drag || drag.pointerId !== e.pointerId) return;
@@ -9276,6 +9389,9 @@ function CalendarTimeline({
9276
9389
  title,
9277
9390
  resourcesHeaderLabel: t("resourcesHeader"),
9278
9391
  labels: { today: l.today, prev: l.prev, next: l.next, month: l.month, week: l.week, day: l.day },
9392
+ newEventLabel: l.newEvent,
9393
+ newEventDisabled: isViewOnly || !canCreate || resources.length === 0,
9394
+ onNewEventClick: isViewOnly ? void 0 : openCreate,
9279
9395
  activeView,
9280
9396
  sizeConfig,
9281
9397
  navigate,
@@ -9391,7 +9507,7 @@ function CalendarTimeline({
9391
9507
  "data-uv-ct-row": r.id,
9392
9508
  children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "relative shrink-0", style: { width: gridWidth, minWidth: gridWidth, height: "100%" }, children: [
9393
9509
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 h-px bg-border/25" }),
9394
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "absolute inset-0", onPointerDown: onPointerDownCell, "data-uv-ct-timeline": true, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "absolute inset-0 flex", children: slots.map((s, i2) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9510
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "absolute inset-0", onPointerDown: onPointerDownCell, onClick: onClickCell, "data-uv-ct-timeline": true, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "absolute inset-0 flex", children: slots.map((s, i2) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9395
9511
  "div",
9396
9512
  {
9397
9513
  className: cn(
@@ -9451,6 +9567,7 @@ function CalendarTimeline({
9451
9567
  tabIndex: 0,
9452
9568
  "aria-label": aria,
9453
9569
  onContextMenu: (e) => {
9570
+ if (isViewOnly) return;
9454
9571
  if (!onEventDelete) return;
9455
9572
  if (interactions?.deletableEvents === false) return;
9456
9573
  e.preventDefault();
@@ -9473,7 +9590,7 @@ function CalendarTimeline({
9473
9590
  onDoubleClick: () => onEventDoubleClick?.(ev),
9474
9591
  onPointerDown: (e) => onPointerDownEvent(e, ev, "move"),
9475
9592
  children: [
9476
- (interactions?.resizableEvents ?? true) && ev.resizable !== false ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
9593
+ !isViewOnly && (interactions?.resizableEvents ?? true) && ev.resizable !== false ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
9477
9594
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9478
9595
  "div",
9479
9596
  {
@@ -9563,6 +9680,70 @@ function CalendarTimeline({
9563
9680
  ] })
9564
9681
  ] })
9565
9682
  }
9683
+ ) : null,
9684
+ canCreate ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9685
+ Sheet,
9686
+ {
9687
+ open: createOpen,
9688
+ onOpenChange: setCreateOpen,
9689
+ side: "right",
9690
+ size: "md",
9691
+ title: l.createEventTitle,
9692
+ description: activeView === "day" ? l.day : activeView === "week" ? l.week : l.month,
9693
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "space-y-4", children: [
9694
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "space-y-2", children: [
9695
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "text-xs text-muted-foreground", children: l.resource }),
9696
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9697
+ Combobox,
9698
+ {
9699
+ options: resourceOptions,
9700
+ value: createResourceId,
9701
+ onChange: (v) => setCreateResourceId(v),
9702
+ placeholder: l.resource
9703
+ }
9704
+ )
9705
+ ] }),
9706
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "grid grid-cols-2 gap-3", children: [
9707
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "space-y-2", children: [
9708
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "text-xs text-muted-foreground", children: l.start }),
9709
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9710
+ Combobox,
9711
+ {
9712
+ options: createStartOptions,
9713
+ value: createStartIdx,
9714
+ onChange: (v) => setCreateStartIdx(Number(v)),
9715
+ placeholder: l.start
9716
+ }
9717
+ )
9718
+ ] }),
9719
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "space-y-2", children: [
9720
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "text-xs text-muted-foreground", children: l.end }),
9721
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9722
+ Combobox,
9723
+ {
9724
+ options: createEndOptions,
9725
+ value: createEndIdx,
9726
+ onChange: (v) => setCreateEndIdx(Number(v)),
9727
+ placeholder: l.end
9728
+ }
9729
+ )
9730
+ ] })
9731
+ ] }),
9732
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex items-center justify-end gap-2 pt-2", children: [
9733
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Button_default, { variant: "ghost", size: "sm", onClick: () => setCreateOpen(false), children: l.cancel }),
9734
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9735
+ Button_default,
9736
+ {
9737
+ variant: "default",
9738
+ size: "sm",
9739
+ onClick: commitCreate,
9740
+ disabled: !createResourceId || createEndIdx <= createStartIdx || createStartOptions.length === 0,
9741
+ children: l.create
9742
+ }
9743
+ )
9744
+ ] })
9745
+ ] })
9746
+ }
9566
9747
  ) : null
9567
9748
  ]
9568
9749
  }