@underverse-ui/underverse 0.2.98 → 0.2.99

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
@@ -8121,6 +8121,22 @@ function startOfZonedDay(date, timeZone) {
8121
8121
  const p = getZonedParts(date, timeZone);
8122
8122
  return new Date(zonedTimeToUtcMs({ ...p, hour: 0, minute: 0, second: 0 }, timeZone));
8123
8123
  }
8124
+ function zonedDateAtTime(date, timeZone, time) {
8125
+ const p = getZonedParts(date, timeZone);
8126
+ return new Date(
8127
+ zonedTimeToUtcMs(
8128
+ {
8129
+ year: p.year,
8130
+ month: p.month,
8131
+ day: p.day,
8132
+ hour: time.hour,
8133
+ minute: time.minute ?? 0,
8134
+ second: time.second ?? 0
8135
+ },
8136
+ timeZone
8137
+ )
8138
+ );
8139
+ }
8124
8140
  function startOfZonedMonth(date, timeZone) {
8125
8141
  const p = getZonedParts(date, timeZone);
8126
8142
  return new Date(zonedTimeToUtcMs({ year: p.year, month: p.month, day: 1, hour: 0, minute: 0, second: 0 }, timeZone));
@@ -8429,17 +8445,24 @@ function getGroupResourceCounts(resources) {
8429
8445
  return counts;
8430
8446
  }
8431
8447
  function computeSlotStarts(args) {
8432
- const { view, date, timeZone, weekStartsOn, dayTimeStepMinutes } = args;
8433
- const start = view === "month" ? startOfZonedMonth(date, timeZone) : view === "week" ? startOfZonedWeek(date, weekStartsOn, timeZone) : startOfZonedDay(date, timeZone);
8448
+ const { view, date, timeZone, weekStartsOn, dayTimeStepMinutes, dayRangeMode, workHours } = args;
8449
+ const baseDayStart = startOfZonedDay(date, timeZone);
8450
+ const start = view === "month" ? startOfZonedMonth(date, timeZone) : view === "week" ? startOfZonedWeek(date, weekStartsOn, timeZone) : baseDayStart;
8434
8451
  if (view === "day") {
8435
8452
  const step = Math.max(5, Math.min(240, Math.trunc(dayTimeStepMinutes)));
8436
8453
  const stepMs = step * 6e4;
8437
- const end2 = addZonedDays(start, 1, timeZone);
8454
+ const hours = workHours ?? { startHour: 8, endHour: 17 };
8455
+ const boundedStartHour = clamp3(Math.trunc(hours.startHour), 0, 23);
8456
+ const boundedEndHour = clamp3(Math.trunc(hours.endHour), 1, 24);
8457
+ const isWork = dayRangeMode === "work";
8458
+ const start2 = isWork ? zonedDateAtTime(baseDayStart, timeZone, { hour: boundedStartHour }) : start;
8459
+ const end2 = isWork ? boundedEndHour === 24 ? addZonedDays(baseDayStart, 1, timeZone) : zonedDateAtTime(baseDayStart, timeZone, { hour: clamp3(boundedEndHour, 0, 23) }) : addZonedDays(start, 1, timeZone);
8460
+ const end3 = end2.getTime() > start2.getTime() ? end2 : addZonedDays(start2, 1, timeZone);
8438
8461
  const slotStarts2 = [];
8439
- for (let cur2 = start.getTime(), guard2 = 0; cur2 < end2.getTime() && guard2++ < 2e3; cur2 += stepMs) {
8462
+ for (let cur2 = start2.getTime(), guard2 = 0; cur2 < end3.getTime() && guard2++ < 2e3; cur2 += stepMs) {
8440
8463
  slotStarts2.push(new Date(cur2));
8441
8464
  }
8442
- return { start, end: end2, slotStarts: slotStarts2 };
8465
+ return { start: start2, end: end3, slotStarts: slotStarts2 };
8443
8466
  }
8444
8467
  const end = view === "month" ? startOfZonedMonth(addZonedMonths(start, 1, timeZone), timeZone) : addZonedDays(start, 7, timeZone);
8445
8468
  const slotStarts = [];
@@ -8739,6 +8762,8 @@ function CalendarTimeline({
8739
8762
  enableLayoutResize,
8740
8763
  slotMinWidth,
8741
8764
  dayTimeStepMinutes = 60,
8765
+ dayRangeMode,
8766
+ workHours,
8742
8767
  maxLanesPerRow = 3,
8743
8768
  now,
8744
8769
  renderResource,
@@ -8900,7 +8925,9 @@ function CalendarTimeline({
8900
8925
  date: activeDate,
8901
8926
  timeZone: resolvedTimeZone,
8902
8927
  weekStartsOn,
8903
- dayTimeStepMinutes
8928
+ dayTimeStepMinutes,
8929
+ dayRangeMode,
8930
+ workHours
8904
8931
  });
8905
8932
  const todayStart = startOfZonedDay(resolvedNow, resolvedTimeZone).getTime();
8906
8933
  const slotItems = slotStarts2.map((s) => ({
@@ -8909,7 +8936,7 @@ function CalendarTimeline({
8909
8936
  isToday: startOfZonedDay(s, resolvedTimeZone).getTime() === todayStart
8910
8937
  }));
8911
8938
  return { slots: slotItems, range: { start, end } };
8912
- }, [activeView, activeDate, resolvedTimeZone, resolvedLocale, weekStartsOn, dayTimeStepMinutes, resolvedNow, formatters]);
8939
+ }, [activeView, activeDate, resolvedTimeZone, resolvedLocale, weekStartsOn, dayTimeStepMinutes, dayRangeMode, workHours, resolvedNow, formatters]);
8913
8940
  React28.useEffect(() => {
8914
8941
  onRangeChange?.(range);
8915
8942
  }, [range.start, range.end, onRangeChange]);
@@ -8919,7 +8946,7 @@ function CalendarTimeline({
8919
8946
  const bodyClientWidth = useClientWidth(bodyRef);
8920
8947
  const slotStarts = React28.useMemo(() => slots.map((s) => s.start), [slots]);
8921
8948
  const slotWidth = React28.useMemo(() => {
8922
- const baseSlotWidth = activeView === "month" ? effectiveSlotMinWidth * 3 : effectiveSlotMinWidth;
8949
+ const baseSlotWidth = activeView === "month" ? effectiveSlotMinWidth * 3 : activeView === "day" ? effectiveSlotMinWidth * 3 : effectiveSlotMinWidth;
8923
8950
  if (activeView !== "week") return baseSlotWidth;
8924
8951
  if (bodyClientWidth <= 0) return baseSlotWidth;
8925
8952
  if (slots.length <= 0) return baseSlotWidth;
@@ -9180,18 +9207,33 @@ function CalendarTimeline({
9180
9207
  const dragRef = React28.useRef(null);
9181
9208
  const [preview, setPreview] = React28.useState(null);
9182
9209
  const suppressNextEventClickRef = React28.useRef(false);
9210
+ const autoScrollStateRef = React28.useRef({
9211
+ dir: 0,
9212
+ speed: 0,
9213
+ lastClientX: 0,
9214
+ lastClientY: 0
9215
+ });
9216
+ const autoScrollRafRef = React28.useRef(null);
9217
+ const stopAutoScroll = React28.useCallback(() => {
9218
+ if (autoScrollRafRef.current != null) cancelAnimationFrame(autoScrollRafRef.current);
9219
+ autoScrollRafRef.current = null;
9220
+ autoScrollStateRef.current.dir = 0;
9221
+ autoScrollStateRef.current.speed = 0;
9222
+ }, []);
9183
9223
  const getPointerContext = React28.useCallback(
9184
9224
  (clientX, clientY, opts) => {
9185
9225
  const body = bodyRef.current;
9186
9226
  if (!body) return null;
9187
- const el = document.elementFromPoint(clientX, clientY);
9188
- if (!el || !body.contains(el)) return null;
9189
9227
  const bodyRect = body.getBoundingClientRect();
9190
- const x = clientX - bodyRect.left + body.scrollLeft;
9228
+ const probeX = clamp3(clientX, bodyRect.left + 1, bodyRect.right - 1);
9229
+ const probeY = clamp3(clientY, bodyRect.top + 1, bodyRect.bottom - 1);
9230
+ const el = document.elementFromPoint(probeX, probeY);
9231
+ const x = probeX - bodyRect.left + body.scrollLeft;
9191
9232
  const epsilon = opts?.biasLeft ? 0.01 : 0;
9192
9233
  const slotIdx = clamp3(Math.floor((x - epsilon) / slotWidth), 0, Math.max(0, slots.length - 1));
9193
- const rowEl = el?.closest?.("[data-uv-ct-row]");
9194
- const rid = rowEl?.dataset?.uvCtRow ?? null;
9234
+ const rowEl = el && body.contains(el) ? el.closest?.("[data-uv-ct-row]") ?? null : null;
9235
+ const rid = rowEl?.dataset?.uvCtRow ?? opts?.fallbackResourceId ?? null;
9236
+ if (!rid) return null;
9195
9237
  return { slotIdx, resourceId: rid, x };
9196
9238
  },
9197
9239
  [slotWidth, slots.length]
@@ -9207,6 +9249,97 @@ function CalendarTimeline({
9207
9249
  },
9208
9250
  [activeView, dayTimeStepMinutes, resolvedTimeZone, slotStarts]
9209
9251
  );
9252
+ const updateDragPreview = React28.useCallback(
9253
+ (clientX, clientY) => {
9254
+ const drag = dragRef.current;
9255
+ if (!drag) return;
9256
+ const ctx = getPointerContext(clientX, clientY, drag.mode === "create" ? { biasLeft: true, fallbackResourceId: drag.resourceId } : { fallbackResourceId: drag.resourceId });
9257
+ if (!ctx) return;
9258
+ const { slotIdx } = ctx;
9259
+ const movedEnough = Math.abs(clientX - drag.startClientX) > 3 || Math.abs(clientY - drag.startClientY) > 3 || slotIdx !== drag.startSlotIdx || ctx.resourceId !== drag.startRowResourceId;
9260
+ if (movedEnough) suppressNextEventClickRef.current = true;
9261
+ if (drag.mode === "create") {
9262
+ const a = Math.min(drag.startSlotIdx, slotIdx);
9263
+ const b = Math.max(drag.startSlotIdx, slotIdx) + 1;
9264
+ const s = slotToDate(a).start;
9265
+ const e2 = b >= slots.length ? range.end : slotToDate(b).start;
9266
+ setPreview({ resourceId: drag.resourceId, start: s, end: e2 });
9267
+ return;
9268
+ }
9269
+ const targetSlotStart = slotToDate(slotIdx).start;
9270
+ const originSlotStart = slotToDate(drag.startSlotIdx).start;
9271
+ const deltaMs = targetSlotStart.getTime() - originSlotStart.getTime();
9272
+ if (drag.mode === "move") {
9273
+ const nextStart = new Date(drag.originStart.getTime() + deltaMs);
9274
+ const nextEnd = new Date(drag.originEnd.getTime() + deltaMs);
9275
+ setPreview({ eventId: drag.eventId, resourceId: ctx.resourceId, start: nextStart, end: nextEnd });
9276
+ drag.resourceId = ctx.resourceId;
9277
+ return;
9278
+ }
9279
+ if (drag.mode === "resize-start") {
9280
+ const nextStart = new Date(clamp3(targetSlotStart.getTime(), range.start.getTime(), drag.originEnd.getTime() - 6e4));
9281
+ setPreview({ eventId: drag.eventId, resourceId: drag.resourceId, start: nextStart, end: drag.originEnd });
9282
+ return;
9283
+ }
9284
+ if (drag.mode === "resize-end") {
9285
+ const nextEnd = new Date(clamp3(targetSlotStart.getTime(), drag.originStart.getTime() + 6e4, range.end.getTime()));
9286
+ setPreview({ eventId: drag.eventId, resourceId: drag.resourceId, start: drag.originStart, end: nextEnd });
9287
+ return;
9288
+ }
9289
+ },
9290
+ [getPointerContext, range.end, range.start, slotToDate, slots.length]
9291
+ );
9292
+ const autoScrollTick = React28.useCallback(() => {
9293
+ const drag = dragRef.current;
9294
+ const body = bodyRef.current;
9295
+ const st = autoScrollStateRef.current;
9296
+ if (!drag || !body || st.dir === 0) {
9297
+ stopAutoScroll();
9298
+ return;
9299
+ }
9300
+ const maxScrollLeft = Math.max(0, body.scrollWidth - body.clientWidth);
9301
+ const prevLeft = body.scrollLeft;
9302
+ const nextLeft = clamp3(prevLeft + st.dir * st.speed, 0, maxScrollLeft);
9303
+ if (nextLeft === prevLeft) {
9304
+ stopAutoScroll();
9305
+ return;
9306
+ }
9307
+ body.scrollLeft = nextLeft;
9308
+ updateDragPreview(st.lastClientX, st.lastClientY);
9309
+ autoScrollRafRef.current = requestAnimationFrame(autoScrollTick);
9310
+ }, [stopAutoScroll, updateDragPreview]);
9311
+ const updateAutoScrollFromPointer = React28.useCallback(
9312
+ (clientX, clientY) => {
9313
+ const body = bodyRef.current;
9314
+ if (!body) return;
9315
+ const rect = body.getBoundingClientRect();
9316
+ const edge = 56;
9317
+ let dir = 0;
9318
+ let speed = 0;
9319
+ if (clientX < rect.left + edge) {
9320
+ dir = -1;
9321
+ const dist = clientX - rect.left;
9322
+ const t2 = clamp3(1 - dist / edge, 0, 1);
9323
+ speed = 8 + t2 * 28;
9324
+ } else if (clientX > rect.right - edge) {
9325
+ dir = 1;
9326
+ const dist = rect.right - clientX;
9327
+ const t2 = clamp3(1 - dist / edge, 0, 1);
9328
+ speed = 8 + t2 * 28;
9329
+ }
9330
+ autoScrollStateRef.current.lastClientX = clientX;
9331
+ autoScrollStateRef.current.lastClientY = clientY;
9332
+ autoScrollStateRef.current.dir = dir;
9333
+ autoScrollStateRef.current.speed = speed;
9334
+ if (dir === 0) {
9335
+ stopAutoScroll();
9336
+ return;
9337
+ }
9338
+ if (autoScrollRafRef.current == null) autoScrollRafRef.current = requestAnimationFrame(autoScrollTick);
9339
+ },
9340
+ [autoScrollTick, stopAutoScroll]
9341
+ );
9342
+ React28.useEffect(() => stopAutoScroll, [stopAutoScroll]);
9210
9343
  const onPointerDownEvent = (e, ev, mode) => {
9211
9344
  if (e.button !== 0 || e.ctrlKey) return;
9212
9345
  if (isViewOnly) return;
@@ -9218,6 +9351,8 @@ function CalendarTimeline({
9218
9351
  suppressNextEventClickRef.current = false;
9219
9352
  const startIdx = binarySearchLastLE(slotStarts, ev._start);
9220
9353
  const endIdx = binarySearchFirstGE(slotStarts, ev._end);
9354
+ const pointerCtx = getPointerContext(e.clientX, e.clientY, { fallbackResourceId: ev.resourceId });
9355
+ const grabSlotIdx = pointerCtx?.slotIdx ?? startIdx;
9221
9356
  dragRef.current = {
9222
9357
  mode,
9223
9358
  eventId: ev.id,
@@ -9226,7 +9361,7 @@ function CalendarTimeline({
9226
9361
  originEnd: ev._end,
9227
9362
  durationMs: ev._end.getTime() - ev._start.getTime(),
9228
9363
  pointerId: e.pointerId,
9229
- startSlotIdx: startIdx,
9364
+ startSlotIdx: grabSlotIdx,
9230
9365
  startRowResourceId: ev.resourceId,
9231
9366
  startClientX: e.clientX,
9232
9367
  startClientY: e.clientY
@@ -9282,44 +9417,14 @@ function CalendarTimeline({
9282
9417
  const onPointerMove = (e) => {
9283
9418
  const drag = dragRef.current;
9284
9419
  if (!drag || drag.pointerId !== e.pointerId) return;
9285
- const ctx = getPointerContext(e.clientX, e.clientY, drag.mode === "create" ? { biasLeft: true } : void 0);
9286
- if (!ctx || !ctx.resourceId) return;
9287
- const { slotIdx } = ctx;
9288
- const movedEnough = Math.abs(e.clientX - drag.startClientX) > 3 || Math.abs(e.clientY - drag.startClientY) > 3 || slotIdx !== drag.startSlotIdx || ctx.resourceId !== drag.startRowResourceId;
9289
- if (movedEnough) suppressNextEventClickRef.current = true;
9290
- if (drag.mode === "create") {
9291
- const a = Math.min(drag.startSlotIdx, slotIdx);
9292
- const b = Math.max(drag.startSlotIdx, slotIdx) + 1;
9293
- const s = slotToDate(a).start;
9294
- const e2 = b >= slots.length ? range.end : slotToDate(b).start;
9295
- setPreview({ resourceId: drag.resourceId, start: s, end: e2 });
9296
- return;
9297
- }
9298
- const targetSlotStart = slotToDate(slotIdx).start;
9299
- const originSlotStart = slotToDate(drag.startSlotIdx).start;
9300
- const deltaMs = targetSlotStart.getTime() - originSlotStart.getTime();
9301
- if (drag.mode === "move") {
9302
- const nextStart = new Date(drag.originStart.getTime() + deltaMs);
9303
- const nextEnd = new Date(drag.originEnd.getTime() + deltaMs);
9304
- setPreview({ eventId: drag.eventId, resourceId: ctx.resourceId, start: nextStart, end: nextEnd });
9305
- drag.resourceId = ctx.resourceId;
9306
- return;
9307
- }
9308
- if (drag.mode === "resize-start") {
9309
- const nextStart = new Date(clamp3(targetSlotStart.getTime(), range.start.getTime(), drag.originEnd.getTime() - 6e4));
9310
- setPreview({ eventId: drag.eventId, resourceId: drag.resourceId, start: nextStart, end: drag.originEnd });
9311
- return;
9312
- }
9313
- if (drag.mode === "resize-end") {
9314
- const nextEnd = new Date(clamp3(targetSlotStart.getTime(), drag.originStart.getTime() + 6e4, range.end.getTime()));
9315
- setPreview({ eventId: drag.eventId, resourceId: drag.resourceId, start: drag.originStart, end: nextEnd });
9316
- return;
9317
- }
9420
+ updateAutoScrollFromPointer(e.clientX, e.clientY);
9421
+ updateDragPreview(e.clientX, e.clientY);
9318
9422
  };
9319
9423
  const onPointerUp = (e) => {
9320
9424
  const drag = dragRef.current;
9321
9425
  if (!drag || drag.pointerId !== e.pointerId) return;
9322
9426
  dragRef.current = null;
9427
+ stopAutoScroll();
9323
9428
  if (!preview) {
9324
9429
  setPreview(null);
9325
9430
  return;