@super-calendar/dom 2.4.0 → 2.5.0

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.d.mts CHANGED
@@ -627,7 +627,7 @@ declare function DateRangePicker({ value, onChange, formatLabel, weekStartsOn, h
627
627
  * Styleable parts of {@link ResourceTimeline}. Pass a class or inline style per
628
628
  * slot via the `classNames` / `styles` props.
629
629
  */
630
- type ResourceTimelineSlot = "header" | "corner" | "timeAxis" | "hourTick" | "resourceLabel" | "row" | "track" | "gridLines" | "event" | "eventBox";
630
+ type ResourceTimelineSlot = "header" | "corner" | "timeAxis" | "hourTick" | "resourceLabel" | "row" | "track" | "gridLines" | "businessHours" | "event" | "eventBox" | "createGhost";
631
631
  /** A schedulable lane (room, person, machine) that events are grouped under. */
632
632
  interface Resource {
633
633
  /** Stable id events reference via their `resourceId`. */
@@ -689,6 +689,19 @@ interface ResourceTimelineProps<T = unknown> extends SlotStyleProps<ResourceTime
689
689
  * proposed new start/end. Return `false` to reject the drop (it snaps back).
690
690
  */
691
691
  onDragEvent?: (event: CalendarEvent$1<T>, start: Date, end: Date) => void | boolean;
692
+ /** Tap empty lane space; called with the snapped time and the lane's resource. */
693
+ onPressCell?: (at: Date, resource: Resource) => void;
694
+ /** Drag empty lane space to create; called with the swept start/end and the lane's resource. */
695
+ onCreateEvent?: (start: Date, end: Date, resource: Resource) => void;
696
+ /**
697
+ * Shade the hours outside this window, per lane. Same shape as the Calendar's
698
+ * `businessHours` plus the lane's resource, so per-resource opening hours work
699
+ * (return `null` for a fully closed lane). A date-only function is accepted.
700
+ */
701
+ businessHours?: (date: Date, resource: Resource) => {
702
+ start: number;
703
+ end: number;
704
+ } | null;
692
705
  /** Snap dragged events to this many minutes (default 15). */
693
706
  dragStepMinutes?: number;
694
707
  /** Class applied to the root element. */
@@ -712,6 +725,6 @@ interface ResourceTimelineProps<T = unknown> extends SlotStyleProps<ResourceTime
712
725
  * />
713
726
  * ```
714
727
  */
715
- declare function ResourceTimeline<T = unknown>({ date, orientation, resources, events, resourceId, startHour, endHour, hourWidth, hourHeight, rowHeight, labelWidth, ampm, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, dragStepMinutes, className, style, classNames, styles }: ResourceTimelineProps<T>): ReactElement;
728
+ declare function ResourceTimeline<T = unknown>({ date, orientation, resources, events, resourceId, startHour, endHour, hourWidth, hourHeight, rowHeight, labelWidth, ampm, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, onPressCell, onCreateEvent, businessHours, dragStepMinutes, className, style, classNames, styles }: ResourceTimelineProps<T>): ReactElement;
716
729
  //#endregion
717
730
  export { Agenda, type AgendaProps, type AgendaSlot, type BusinessHours, Calendar, type CalendarEvent, type CalendarMode, type CalendarProps, type CalendarSlot, DatePicker, type DatePickerProps, type DatePickerSlot, type DateRange, DateRangePicker, type DateRangePickerProps, type DateSelectionConstraints, type DaySelectionState, type DomAgendaEvent, type DomAgendaEventArgs, type DomCalendarTheme, type DomMonthEvent, type DomMonthEventArgs, type DomRenderEvent, type DomRenderEventArgs, type EventAccessibilityLabelContext, type EventAccessibilityLabeler, type ICalEvent, type ICalendarEvent, type MonthGrid, type MonthGridDay, type MonthGridWeek, type MonthGridWeekday, MonthList, type MonthListProps, type MonthListSlot, MonthView, type MonthViewProps, type MonthViewSlot, type PositionedEvent, type RecurrenceFrequency, type RecurrenceRule, type ResolvedSlot, type Resource, type ResourceEventArgs, ResourceTimeline, type ResourceTimelineProps, type ResourceTimelineSlot, type SlotDefault, type SlotStyleProps, TimeGrid, type TimeGridMode, type TimeGridProps, type TimeGridSlot, type ToICalendarOptions, type UseDateRangeOptions, type UseMonthGridOptions, type WeekStartsOn, type WeekdayFormat, buildMonthGrid, darkDomTheme, daySelectionState, defaultDomTheme, expandRecurringEvents, getViewDays, isAllDayEvent, isDateSelectable, isRangeEndpoint, isWithinDateRange, layoutDayEvents, mergeDomTheme, nextDateRange, parseICalendar, toICalendar, useDateRange, useMonthGrid, weekdayFormatToken };
package/dist/index.d.ts CHANGED
@@ -627,7 +627,7 @@ declare function DateRangePicker({ value, onChange, formatLabel, weekStartsOn, h
627
627
  * Styleable parts of {@link ResourceTimeline}. Pass a class or inline style per
628
628
  * slot via the `classNames` / `styles` props.
629
629
  */
630
- type ResourceTimelineSlot = "header" | "corner" | "timeAxis" | "hourTick" | "resourceLabel" | "row" | "track" | "gridLines" | "event" | "eventBox";
630
+ type ResourceTimelineSlot = "header" | "corner" | "timeAxis" | "hourTick" | "resourceLabel" | "row" | "track" | "gridLines" | "businessHours" | "event" | "eventBox" | "createGhost";
631
631
  /** A schedulable lane (room, person, machine) that events are grouped under. */
632
632
  interface Resource {
633
633
  /** Stable id events reference via their `resourceId`. */
@@ -689,6 +689,19 @@ interface ResourceTimelineProps<T = unknown> extends SlotStyleProps<ResourceTime
689
689
  * proposed new start/end. Return `false` to reject the drop (it snaps back).
690
690
  */
691
691
  onDragEvent?: (event: CalendarEvent$1<T>, start: Date, end: Date) => void | boolean;
692
+ /** Tap empty lane space; called with the snapped time and the lane's resource. */
693
+ onPressCell?: (at: Date, resource: Resource) => void;
694
+ /** Drag empty lane space to create; called with the swept start/end and the lane's resource. */
695
+ onCreateEvent?: (start: Date, end: Date, resource: Resource) => void;
696
+ /**
697
+ * Shade the hours outside this window, per lane. Same shape as the Calendar's
698
+ * `businessHours` plus the lane's resource, so per-resource opening hours work
699
+ * (return `null` for a fully closed lane). A date-only function is accepted.
700
+ */
701
+ businessHours?: (date: Date, resource: Resource) => {
702
+ start: number;
703
+ end: number;
704
+ } | null;
692
705
  /** Snap dragged events to this many minutes (default 15). */
693
706
  dragStepMinutes?: number;
694
707
  /** Class applied to the root element. */
@@ -712,6 +725,6 @@ interface ResourceTimelineProps<T = unknown> extends SlotStyleProps<ResourceTime
712
725
  * />
713
726
  * ```
714
727
  */
715
- declare function ResourceTimeline<T = unknown>({ date, orientation, resources, events, resourceId, startHour, endHour, hourWidth, hourHeight, rowHeight, labelWidth, ampm, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, dragStepMinutes, className, style, classNames, styles }: ResourceTimelineProps<T>): ReactElement;
728
+ declare function ResourceTimeline<T = unknown>({ date, orientation, resources, events, resourceId, startHour, endHour, hourWidth, hourHeight, rowHeight, labelWidth, ampm, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, onPressCell, onCreateEvent, businessHours, dragStepMinutes, className, style, classNames, styles }: ResourceTimelineProps<T>): ReactElement;
716
729
  //#endregion
717
730
  export { Agenda, type AgendaProps, type AgendaSlot, type BusinessHours, Calendar, type CalendarEvent, type CalendarMode, type CalendarProps, type CalendarSlot, DatePicker, type DatePickerProps, type DatePickerSlot, type DateRange, DateRangePicker, type DateRangePickerProps, type DateSelectionConstraints, type DaySelectionState, type DomAgendaEvent, type DomAgendaEventArgs, type DomCalendarTheme, type DomMonthEvent, type DomMonthEventArgs, type DomRenderEvent, type DomRenderEventArgs, type EventAccessibilityLabelContext, type EventAccessibilityLabeler, type ICalEvent, type ICalendarEvent, type MonthGrid, type MonthGridDay, type MonthGridWeek, type MonthGridWeekday, MonthList, type MonthListProps, type MonthListSlot, MonthView, type MonthViewProps, type MonthViewSlot, type PositionedEvent, type RecurrenceFrequency, type RecurrenceRule, type ResolvedSlot, type Resource, type ResourceEventArgs, ResourceTimeline, type ResourceTimelineProps, type ResourceTimelineSlot, type SlotDefault, type SlotStyleProps, TimeGrid, type TimeGridMode, type TimeGridProps, type TimeGridSlot, type ToICalendarOptions, type UseDateRangeOptions, type UseMonthGridOptions, type WeekStartsOn, type WeekdayFormat, buildMonthGrid, darkDomTheme, daySelectionState, defaultDomTheme, expandRecurringEvents, getViewDays, isAllDayEvent, isDateSelectable, isRangeEndpoint, isWithinDateRange, layoutDayEvents, mergeDomTheme, nextDateRange, parseICalendar, toICalendar, useDateRange, useMonthGrid, weekdayFormatToken };
package/dist/index.js CHANGED
@@ -2092,7 +2092,7 @@ function DefaultBar({ event, height, boxProps, theme }) {
2092
2092
  * />
2093
2093
  * ```
2094
2094
  */
2095
- function ResourceTimeline({ date, orientation = "horizontal", resources, events, resourceId = (event) => event.resourceId, startHour = 0, endHour = 24, hourWidth = 80, hourHeight = 48, rowHeight = 56, labelWidth = 140, ampm = false, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, dragStepMinutes = 15, className, style, classNames, styles }) {
2095
+ function ResourceTimeline({ date, orientation = "horizontal", resources, events, resourceId = (event) => event.resourceId, startHour = 0, endHour = 24, hourWidth = 80, hourHeight = 48, rowHeight = 56, labelWidth = 140, ampm = false, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, onPressCell, onCreateEvent, businessHours, dragStepMinutes = 15, className, style, classNames, styles }) {
2096
2096
  const theme = (0, react.useMemo)(() => mergeDomTheme(themeOverrides), [themeOverrides]);
2097
2097
  const slot = createSlots({
2098
2098
  classNames,
@@ -2171,6 +2171,69 @@ function ResourceTimeline({ date, orientation = "horizontal", resources, events,
2171
2171
  applyDrag(null);
2172
2172
  origin.current = null;
2173
2173
  };
2174
+ const [createBox, setCreateBox] = (0, react.useState)(null);
2175
+ const createOrigin = (0, react.useRef)(null);
2176
+ const cellEnabled = !!onPressCell || !!onCreateEvent;
2177
+ const pxAlongAxis = (el, e) => {
2178
+ const rect = el.getBoundingClientRect();
2179
+ return vertical ? e.clientY - rect.top : e.clientX - rect.left;
2180
+ };
2181
+ const beginCreate = (e, resource) => {
2182
+ if (!cellEnabled || e.pointerType === "touch") return;
2183
+ if (e.target !== e.currentTarget || e.button > 0) return;
2184
+ const el = e.currentTarget;
2185
+ const startPx = pxAlongAxis(el, e);
2186
+ try {
2187
+ el.setPointerCapture?.(e.pointerId);
2188
+ } catch {}
2189
+ createOrigin.current = {
2190
+ el,
2191
+ resource,
2192
+ startPx
2193
+ };
2194
+ if (onCreateEvent) setCreateBox({
2195
+ resourceKey: resource.id,
2196
+ startPx,
2197
+ curPx: startPx
2198
+ });
2199
+ };
2200
+ const moveCreate = (e) => {
2201
+ const o = createOrigin.current;
2202
+ if (!o || !onCreateEvent) return;
2203
+ setCreateBox({
2204
+ resourceKey: o.resource.id,
2205
+ startPx: o.startPx,
2206
+ curPx: pxAlongAxis(o.el, e)
2207
+ });
2208
+ };
2209
+ const endCreate = (e) => {
2210
+ const o = createOrigin.current;
2211
+ if (!o) return;
2212
+ try {
2213
+ o.el.releasePointerCapture?.(e.pointerId);
2214
+ } catch {}
2215
+ const endPx = pxAlongAxis(o.el, e);
2216
+ if (Math.abs(endPx - o.startPx) > 4 && onCreateEvent) {
2217
+ const range = (0, _super_calendar_core.cellRangeFromDrag)(date, o.startPx, endPx, hourSize, startHour, dragStepMinutes);
2218
+ if (range) onCreateEvent(range.start, range.end, o.resource);
2219
+ } else if (onPressCell) {
2220
+ const at = (0, _super_calendar_core.cellRangeFromDrag)(date, o.startPx, o.startPx, hourSize, startHour, dragStepMinutes);
2221
+ if (at) onPressCell(at.start, o.resource);
2222
+ }
2223
+ createOrigin.current = null;
2224
+ setCreateBox(null);
2225
+ };
2226
+ const cancelCreate = () => {
2227
+ createOrigin.current = null;
2228
+ setCreateBox(null);
2229
+ };
2230
+ const trackInteractionProps = (resource) => cellEnabled ? {
2231
+ onPointerDown: (e) => beginCreate(e, resource),
2232
+ onPointerMove: moveCreate,
2233
+ onPointerUp: endCreate,
2234
+ onPointerCancel: cancelCreate
2235
+ } : null;
2236
+ const bandsFor = (resource) => businessHours ? (0, _super_calendar_core.closedHourBands)(date, (d) => businessHours(d, resource), startHour, endHour) : [];
2174
2237
  const hours = (0, react.useMemo)(() => Array.from({ length: Math.max(0, endHour - startHour) }, (_, i) => startHour + i), [startHour, endHour]);
2175
2238
  const trackWidth = (endHour - startHour) * hourWidth;
2176
2239
  const byResource = (0, react.useMemo)(() => {
@@ -2267,85 +2330,124 @@ function ResourceTimeline({ date, orientation = "horizontal", resources, events,
2267
2330
  themed: { borderLeft: `1px solid ${theme.gridLine}` }
2268
2331
  }),
2269
2332
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2333
+ ...trackInteractionProps(resource),
2270
2334
  ...slot("track", { base: {
2271
2335
  position: "relative",
2272
2336
  height: trackHeight
2273
2337
  } }),
2274
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2275
- "aria-hidden": true,
2276
- ...slot("gridLines", {
2277
- base: {
2278
- position: "absolute",
2279
- inset: 0,
2280
- pointerEvents: "none"
2281
- },
2282
- themed: { backgroundImage: vGridLines }
2283
- })
2284
- }), positioned.map((pe, idx) => {
2285
- const key = `${resource.id}:${idx}`;
2286
- const active = drag?.key === key ? drag : null;
2287
- const startH = active ? active.startHours : pe.startHours;
2288
- const durH = active ? active.durationHours : pe.durationHours;
2289
- const top = clamp(startH - startHour, 0, endHour - startHour) * hourHeight;
2290
- const bottom = clamp(startH + durH - startHour, 0, endHour - startHour) * hourHeight;
2291
- const height = Math.max(bottom - top, 2);
2292
- const lanePct = 100 / pe.columns;
2293
- const onPress = () => onPressEvent?.(pe.event);
2294
- const args = {
2295
- event: pe.event,
2296
- width: 0,
2297
- height,
2298
- onPress
2299
- };
2300
- const draggable = !!onDragEvent;
2301
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2302
- type: "button",
2303
- onClick: draggable ? void 0 : onPress,
2304
- "aria-label": pe.event.title,
2305
- ...dataState({ "data-dragging": !!active }),
2306
- onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2307
- onPointerMove: draggable ? moveDrag : void 0,
2308
- onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2309
- onPointerCancel: draggable ? cancelDrag : void 0,
2310
- ...slot("event", { base: {
2311
- position: "absolute",
2312
- top,
2313
- height,
2314
- left: `${pe.column * lanePct}%`,
2315
- width: `${lanePct}%`,
2316
- padding: 1,
2317
- border: "none",
2318
- background: "transparent",
2319
- cursor: draggable ? "grab" : "pointer",
2320
- touchAction: draggable ? "none" : "auto",
2321
- font: "inherit",
2322
- textAlign: "left",
2323
- boxSizing: "border-box",
2324
- zIndex: active ? 3 : 1,
2325
- opacity: active ? .85 : 1
2326
- } }),
2327
- children: [Renderer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Renderer, { ...args }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultBar, {
2328
- ...args,
2329
- theme,
2330
- boxProps: slot("eventBox")
2331
- }), draggable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2332
- "aria-hidden": true,
2333
- onPointerDown: (e) => {
2334
- e.stopPropagation();
2335
- beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2336
- },
2337
- style: {
2338
+ children: [
2339
+ bandsFor(resource).map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2340
+ "aria-hidden": true,
2341
+ ...slot("businessHours", {
2342
+ base: {
2338
2343
  position: "absolute",
2339
2344
  left: 0,
2340
2345
  right: 0,
2341
- bottom: 0,
2342
- height: 8,
2343
- cursor: "ns-resize",
2344
- touchAction: "none"
2346
+ top: (b.start - startHour) * hourHeight,
2347
+ height: (b.end - b.start) * hourHeight,
2348
+ pointerEvents: "none",
2349
+ zIndex: 0
2350
+ },
2351
+ themed: { background: theme.outsideHoursBackground }
2352
+ })
2353
+ }, b.start)),
2354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2355
+ "aria-hidden": true,
2356
+ ...slot("gridLines", {
2357
+ base: {
2358
+ position: "absolute",
2359
+ inset: 0,
2360
+ pointerEvents: "none"
2361
+ },
2362
+ themed: { backgroundImage: vGridLines }
2363
+ })
2364
+ }),
2365
+ positioned.map((pe, idx) => {
2366
+ const key = `${resource.id}:${idx}`;
2367
+ const active = drag?.key === key ? drag : null;
2368
+ const startH = active ? active.startHours : pe.startHours;
2369
+ const durH = active ? active.durationHours : pe.durationHours;
2370
+ const top = clamp(startH - startHour, 0, endHour - startHour) * hourHeight;
2371
+ const bottom = clamp(startH + durH - startHour, 0, endHour - startHour) * hourHeight;
2372
+ const height = Math.max(bottom - top, 2);
2373
+ const lanePct = 100 / pe.columns;
2374
+ const onPress = () => onPressEvent?.(pe.event);
2375
+ const args = {
2376
+ event: pe.event,
2377
+ width: 0,
2378
+ height,
2379
+ onPress
2380
+ };
2381
+ const draggable = !!onDragEvent;
2382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2383
+ type: "button",
2384
+ onClick: draggable ? void 0 : onPress,
2385
+ "aria-label": pe.event.title,
2386
+ ...dataState({ "data-dragging": !!active }),
2387
+ onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2388
+ onPointerMove: draggable ? moveDrag : void 0,
2389
+ onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2390
+ onPointerCancel: draggable ? cancelDrag : void 0,
2391
+ ...slot("event", { base: {
2392
+ position: "absolute",
2393
+ top,
2394
+ height,
2395
+ left: `${pe.column * lanePct}%`,
2396
+ width: `${lanePct}%`,
2397
+ padding: 1,
2398
+ border: "none",
2399
+ background: "transparent",
2400
+ cursor: draggable ? "grab" : "pointer",
2401
+ touchAction: draggable ? "none" : "auto",
2402
+ font: "inherit",
2403
+ textAlign: "left",
2404
+ boxSizing: "border-box",
2405
+ zIndex: active ? 3 : 1,
2406
+ opacity: active ? .85 : 1
2407
+ } }),
2408
+ children: [Renderer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Renderer, { ...args }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultBar, {
2409
+ ...args,
2410
+ theme,
2411
+ boxProps: slot("eventBox")
2412
+ }), draggable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2413
+ "aria-hidden": true,
2414
+ onPointerDown: (e) => {
2415
+ e.stopPropagation();
2416
+ beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2417
+ },
2418
+ style: {
2419
+ position: "absolute",
2420
+ left: 0,
2421
+ right: 0,
2422
+ bottom: 0,
2423
+ height: 8,
2424
+ cursor: "ns-resize",
2425
+ touchAction: "none"
2426
+ }
2427
+ }) : null]
2428
+ }, idx);
2429
+ }),
2430
+ createBox?.resourceKey === resource.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2431
+ "aria-hidden": true,
2432
+ ...slot("createGhost", {
2433
+ base: {
2434
+ position: "absolute",
2435
+ left: 1,
2436
+ right: 1,
2437
+ top: Math.min(createBox.startPx, createBox.curPx),
2438
+ height: Math.max(Math.abs(createBox.curPx - createBox.startPx), 2),
2439
+ opacity: .7,
2440
+ pointerEvents: "none",
2441
+ zIndex: 2
2442
+ },
2443
+ themed: {
2444
+ background: theme.rangeBackground,
2445
+ border: `1px solid ${theme.selectedBackground}`,
2446
+ borderRadius: 6
2345
2447
  }
2346
- }) : null]
2347
- }, idx);
2348
- })]
2448
+ })
2449
+ }) : null
2450
+ ]
2349
2451
  })
2350
2452
  }, resource.id);
2351
2453
  })]
@@ -2420,84 +2522,123 @@ function ResourceTimeline({ date, orientation = "horizontal", resources, events,
2420
2522
  }),
2421
2523
  children: resource.title ?? resource.id
2422
2524
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2525
+ ...trackInteractionProps(resource),
2423
2526
  ...slot("track", { base: {
2424
2527
  position: "relative",
2425
2528
  width: trackWidth
2426
2529
  } }),
2427
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2428
- "aria-hidden": true,
2429
- ...slot("gridLines", {
2430
- base: {
2431
- position: "absolute",
2432
- inset: 0,
2433
- pointerEvents: "none"
2434
- },
2435
- themed: { backgroundImage: gridLines }
2436
- })
2437
- }), positioned.map((pe, idx) => {
2438
- const key = `${resource.id}:${idx}`;
2439
- const active = drag?.key === key ? drag : null;
2440
- const startH = active ? active.startHours : pe.startHours;
2441
- const durH = active ? active.durationHours : pe.durationHours;
2442
- const left = clamp(startH - startHour, 0, endHour - startHour) * hourWidth;
2443
- const right = clamp(startH + durH - startHour, 0, endHour - startHour) * hourWidth;
2444
- const width = Math.max(right - left, 2);
2445
- const laneHeight = rowHeight / pe.columns;
2446
- const onPress = () => onPressEvent?.(pe.event);
2447
- const args = {
2448
- event: pe.event,
2449
- width,
2450
- onPress
2451
- };
2452
- const draggable = !!onDragEvent;
2453
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2454
- type: "button",
2455
- onClick: draggable ? void 0 : onPress,
2456
- "aria-label": pe.event.title,
2457
- ...dataState({ "data-dragging": !!active }),
2458
- onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2459
- onPointerMove: draggable ? moveDrag : void 0,
2460
- onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2461
- onPointerCancel: draggable ? cancelDrag : void 0,
2462
- ...slot("event", { base: {
2463
- position: "absolute",
2464
- left,
2465
- width,
2466
- top: pe.column * laneHeight,
2467
- height: laneHeight,
2468
- padding: 1,
2469
- border: "none",
2470
- background: "transparent",
2471
- cursor: draggable ? "grab" : "pointer",
2472
- touchAction: draggable ? "none" : "auto",
2473
- font: "inherit",
2474
- textAlign: "left",
2475
- boxSizing: "border-box",
2476
- zIndex: active ? 3 : 1,
2477
- opacity: active ? .85 : 1
2478
- } }),
2479
- children: [Renderer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Renderer, { ...args }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultBar, {
2480
- ...args,
2481
- theme,
2482
- boxProps: slot("eventBox")
2483
- }), draggable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2484
- "aria-hidden": true,
2485
- onPointerDown: (e) => {
2486
- e.stopPropagation();
2487
- beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2488
- },
2489
- style: {
2530
+ children: [
2531
+ bandsFor(resource).map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2532
+ "aria-hidden": true,
2533
+ ...slot("businessHours", {
2534
+ base: {
2490
2535
  position: "absolute",
2491
2536
  top: 0,
2492
2537
  bottom: 0,
2493
- right: 0,
2494
- width: 8,
2495
- cursor: "ew-resize",
2496
- touchAction: "none"
2538
+ left: (b.start - startHour) * hourWidth,
2539
+ width: (b.end - b.start) * hourWidth,
2540
+ pointerEvents: "none",
2541
+ zIndex: 0
2542
+ },
2543
+ themed: { background: theme.outsideHoursBackground }
2544
+ })
2545
+ }, b.start)),
2546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2547
+ "aria-hidden": true,
2548
+ ...slot("gridLines", {
2549
+ base: {
2550
+ position: "absolute",
2551
+ inset: 0,
2552
+ pointerEvents: "none"
2553
+ },
2554
+ themed: { backgroundImage: gridLines }
2555
+ })
2556
+ }),
2557
+ positioned.map((pe, idx) => {
2558
+ const key = `${resource.id}:${idx}`;
2559
+ const active = drag?.key === key ? drag : null;
2560
+ const startH = active ? active.startHours : pe.startHours;
2561
+ const durH = active ? active.durationHours : pe.durationHours;
2562
+ const left = clamp(startH - startHour, 0, endHour - startHour) * hourWidth;
2563
+ const right = clamp(startH + durH - startHour, 0, endHour - startHour) * hourWidth;
2564
+ const width = Math.max(right - left, 2);
2565
+ const laneHeight = rowHeight / pe.columns;
2566
+ const onPress = () => onPressEvent?.(pe.event);
2567
+ const args = {
2568
+ event: pe.event,
2569
+ width,
2570
+ onPress
2571
+ };
2572
+ const draggable = !!onDragEvent;
2573
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2574
+ type: "button",
2575
+ onClick: draggable ? void 0 : onPress,
2576
+ "aria-label": pe.event.title,
2577
+ ...dataState({ "data-dragging": !!active }),
2578
+ onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2579
+ onPointerMove: draggable ? moveDrag : void 0,
2580
+ onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2581
+ onPointerCancel: draggable ? cancelDrag : void 0,
2582
+ ...slot("event", { base: {
2583
+ position: "absolute",
2584
+ left,
2585
+ width,
2586
+ top: pe.column * laneHeight,
2587
+ height: laneHeight,
2588
+ padding: 1,
2589
+ border: "none",
2590
+ background: "transparent",
2591
+ cursor: draggable ? "grab" : "pointer",
2592
+ touchAction: draggable ? "none" : "auto",
2593
+ font: "inherit",
2594
+ textAlign: "left",
2595
+ boxSizing: "border-box",
2596
+ zIndex: active ? 3 : 1,
2597
+ opacity: active ? .85 : 1
2598
+ } }),
2599
+ children: [Renderer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Renderer, { ...args }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultBar, {
2600
+ ...args,
2601
+ theme,
2602
+ boxProps: slot("eventBox")
2603
+ }), draggable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2604
+ "aria-hidden": true,
2605
+ onPointerDown: (e) => {
2606
+ e.stopPropagation();
2607
+ beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2608
+ },
2609
+ style: {
2610
+ position: "absolute",
2611
+ top: 0,
2612
+ bottom: 0,
2613
+ right: 0,
2614
+ width: 8,
2615
+ cursor: "ew-resize",
2616
+ touchAction: "none"
2617
+ }
2618
+ }) : null]
2619
+ }, idx);
2620
+ }),
2621
+ createBox?.resourceKey === resource.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2622
+ "aria-hidden": true,
2623
+ ...slot("createGhost", {
2624
+ base: {
2625
+ position: "absolute",
2626
+ top: 1,
2627
+ bottom: 1,
2628
+ left: Math.min(createBox.startPx, createBox.curPx),
2629
+ width: Math.max(Math.abs(createBox.curPx - createBox.startPx), 2),
2630
+ opacity: .7,
2631
+ pointerEvents: "none",
2632
+ zIndex: 2
2633
+ },
2634
+ themed: {
2635
+ background: theme.rangeBackground,
2636
+ border: `1px solid ${theme.selectedBackground}`,
2637
+ borderRadius: 6
2497
2638
  }
2498
- }) : null]
2499
- }, idx);
2500
- })]
2639
+ })
2640
+ }) : null
2641
+ ]
2501
2642
  })]
2502
2643
  }, resource.id);
2503
2644
  })]
package/dist/index.mjs CHANGED
@@ -2091,7 +2091,7 @@ function DefaultBar({ event, height, boxProps, theme }) {
2091
2091
  * />
2092
2092
  * ```
2093
2093
  */
2094
- function ResourceTimeline({ date, orientation = "horizontal", resources, events, resourceId = (event) => event.resourceId, startHour = 0, endHour = 24, hourWidth = 80, hourHeight = 48, rowHeight = 56, labelWidth = 140, ampm = false, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, dragStepMinutes = 15, className, style, classNames, styles }) {
2094
+ function ResourceTimeline({ date, orientation = "horizontal", resources, events, resourceId = (event) => event.resourceId, startHour = 0, endHour = 24, hourWidth = 80, hourHeight = 48, rowHeight = 56, labelWidth = 140, ampm = false, theme: themeOverrides, renderEvent, onPressEvent, onDragEvent, onPressCell, onCreateEvent, businessHours, dragStepMinutes = 15, className, style, classNames, styles }) {
2095
2095
  const theme = useMemo(() => mergeDomTheme(themeOverrides), [themeOverrides]);
2096
2096
  const slot = createSlots({
2097
2097
  classNames,
@@ -2170,6 +2170,69 @@ function ResourceTimeline({ date, orientation = "horizontal", resources, events,
2170
2170
  applyDrag(null);
2171
2171
  origin.current = null;
2172
2172
  };
2173
+ const [createBox, setCreateBox] = useState(null);
2174
+ const createOrigin = useRef(null);
2175
+ const cellEnabled = !!onPressCell || !!onCreateEvent;
2176
+ const pxAlongAxis = (el, e) => {
2177
+ const rect = el.getBoundingClientRect();
2178
+ return vertical ? e.clientY - rect.top : e.clientX - rect.left;
2179
+ };
2180
+ const beginCreate = (e, resource) => {
2181
+ if (!cellEnabled || e.pointerType === "touch") return;
2182
+ if (e.target !== e.currentTarget || e.button > 0) return;
2183
+ const el = e.currentTarget;
2184
+ const startPx = pxAlongAxis(el, e);
2185
+ try {
2186
+ el.setPointerCapture?.(e.pointerId);
2187
+ } catch {}
2188
+ createOrigin.current = {
2189
+ el,
2190
+ resource,
2191
+ startPx
2192
+ };
2193
+ if (onCreateEvent) setCreateBox({
2194
+ resourceKey: resource.id,
2195
+ startPx,
2196
+ curPx: startPx
2197
+ });
2198
+ };
2199
+ const moveCreate = (e) => {
2200
+ const o = createOrigin.current;
2201
+ if (!o || !onCreateEvent) return;
2202
+ setCreateBox({
2203
+ resourceKey: o.resource.id,
2204
+ startPx: o.startPx,
2205
+ curPx: pxAlongAxis(o.el, e)
2206
+ });
2207
+ };
2208
+ const endCreate = (e) => {
2209
+ const o = createOrigin.current;
2210
+ if (!o) return;
2211
+ try {
2212
+ o.el.releasePointerCapture?.(e.pointerId);
2213
+ } catch {}
2214
+ const endPx = pxAlongAxis(o.el, e);
2215
+ if (Math.abs(endPx - o.startPx) > 4 && onCreateEvent) {
2216
+ const range = cellRangeFromDrag(date, o.startPx, endPx, hourSize, startHour, dragStepMinutes);
2217
+ if (range) onCreateEvent(range.start, range.end, o.resource);
2218
+ } else if (onPressCell) {
2219
+ const at = cellRangeFromDrag(date, o.startPx, o.startPx, hourSize, startHour, dragStepMinutes);
2220
+ if (at) onPressCell(at.start, o.resource);
2221
+ }
2222
+ createOrigin.current = null;
2223
+ setCreateBox(null);
2224
+ };
2225
+ const cancelCreate = () => {
2226
+ createOrigin.current = null;
2227
+ setCreateBox(null);
2228
+ };
2229
+ const trackInteractionProps = (resource) => cellEnabled ? {
2230
+ onPointerDown: (e) => beginCreate(e, resource),
2231
+ onPointerMove: moveCreate,
2232
+ onPointerUp: endCreate,
2233
+ onPointerCancel: cancelCreate
2234
+ } : null;
2235
+ const bandsFor = (resource) => businessHours ? closedHourBands(date, (d) => businessHours(d, resource), startHour, endHour) : [];
2173
2236
  const hours = useMemo(() => Array.from({ length: Math.max(0, endHour - startHour) }, (_, i) => startHour + i), [startHour, endHour]);
2174
2237
  const trackWidth = (endHour - startHour) * hourWidth;
2175
2238
  const byResource = useMemo(() => {
@@ -2266,85 +2329,124 @@ function ResourceTimeline({ date, orientation = "horizontal", resources, events,
2266
2329
  themed: { borderLeft: `1px solid ${theme.gridLine}` }
2267
2330
  }),
2268
2331
  children: /* @__PURE__ */ jsxs("div", {
2332
+ ...trackInteractionProps(resource),
2269
2333
  ...slot("track", { base: {
2270
2334
  position: "relative",
2271
2335
  height: trackHeight
2272
2336
  } }),
2273
- children: [/* @__PURE__ */ jsx("div", {
2274
- "aria-hidden": true,
2275
- ...slot("gridLines", {
2276
- base: {
2277
- position: "absolute",
2278
- inset: 0,
2279
- pointerEvents: "none"
2280
- },
2281
- themed: { backgroundImage: vGridLines }
2282
- })
2283
- }), positioned.map((pe, idx) => {
2284
- const key = `${resource.id}:${idx}`;
2285
- const active = drag?.key === key ? drag : null;
2286
- const startH = active ? active.startHours : pe.startHours;
2287
- const durH = active ? active.durationHours : pe.durationHours;
2288
- const top = clamp(startH - startHour, 0, endHour - startHour) * hourHeight;
2289
- const bottom = clamp(startH + durH - startHour, 0, endHour - startHour) * hourHeight;
2290
- const height = Math.max(bottom - top, 2);
2291
- const lanePct = 100 / pe.columns;
2292
- const onPress = () => onPressEvent?.(pe.event);
2293
- const args = {
2294
- event: pe.event,
2295
- width: 0,
2296
- height,
2297
- onPress
2298
- };
2299
- const draggable = !!onDragEvent;
2300
- return /* @__PURE__ */ jsxs("button", {
2301
- type: "button",
2302
- onClick: draggable ? void 0 : onPress,
2303
- "aria-label": pe.event.title,
2304
- ...dataState({ "data-dragging": !!active }),
2305
- onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2306
- onPointerMove: draggable ? moveDrag : void 0,
2307
- onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2308
- onPointerCancel: draggable ? cancelDrag : void 0,
2309
- ...slot("event", { base: {
2310
- position: "absolute",
2311
- top,
2312
- height,
2313
- left: `${pe.column * lanePct}%`,
2314
- width: `${lanePct}%`,
2315
- padding: 1,
2316
- border: "none",
2317
- background: "transparent",
2318
- cursor: draggable ? "grab" : "pointer",
2319
- touchAction: draggable ? "none" : "auto",
2320
- font: "inherit",
2321
- textAlign: "left",
2322
- boxSizing: "border-box",
2323
- zIndex: active ? 3 : 1,
2324
- opacity: active ? .85 : 1
2325
- } }),
2326
- children: [Renderer ? /* @__PURE__ */ jsx(Renderer, { ...args }) : /* @__PURE__ */ jsx(DefaultBar, {
2327
- ...args,
2328
- theme,
2329
- boxProps: slot("eventBox")
2330
- }), draggable ? /* @__PURE__ */ jsx("span", {
2331
- "aria-hidden": true,
2332
- onPointerDown: (e) => {
2333
- e.stopPropagation();
2334
- beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2335
- },
2336
- style: {
2337
+ children: [
2338
+ bandsFor(resource).map((b) => /* @__PURE__ */ jsx("div", {
2339
+ "aria-hidden": true,
2340
+ ...slot("businessHours", {
2341
+ base: {
2337
2342
  position: "absolute",
2338
2343
  left: 0,
2339
2344
  right: 0,
2340
- bottom: 0,
2341
- height: 8,
2342
- cursor: "ns-resize",
2343
- touchAction: "none"
2345
+ top: (b.start - startHour) * hourHeight,
2346
+ height: (b.end - b.start) * hourHeight,
2347
+ pointerEvents: "none",
2348
+ zIndex: 0
2349
+ },
2350
+ themed: { background: theme.outsideHoursBackground }
2351
+ })
2352
+ }, b.start)),
2353
+ /* @__PURE__ */ jsx("div", {
2354
+ "aria-hidden": true,
2355
+ ...slot("gridLines", {
2356
+ base: {
2357
+ position: "absolute",
2358
+ inset: 0,
2359
+ pointerEvents: "none"
2360
+ },
2361
+ themed: { backgroundImage: vGridLines }
2362
+ })
2363
+ }),
2364
+ positioned.map((pe, idx) => {
2365
+ const key = `${resource.id}:${idx}`;
2366
+ const active = drag?.key === key ? drag : null;
2367
+ const startH = active ? active.startHours : pe.startHours;
2368
+ const durH = active ? active.durationHours : pe.durationHours;
2369
+ const top = clamp(startH - startHour, 0, endHour - startHour) * hourHeight;
2370
+ const bottom = clamp(startH + durH - startHour, 0, endHour - startHour) * hourHeight;
2371
+ const height = Math.max(bottom - top, 2);
2372
+ const lanePct = 100 / pe.columns;
2373
+ const onPress = () => onPressEvent?.(pe.event);
2374
+ const args = {
2375
+ event: pe.event,
2376
+ width: 0,
2377
+ height,
2378
+ onPress
2379
+ };
2380
+ const draggable = !!onDragEvent;
2381
+ return /* @__PURE__ */ jsxs("button", {
2382
+ type: "button",
2383
+ onClick: draggable ? void 0 : onPress,
2384
+ "aria-label": pe.event.title,
2385
+ ...dataState({ "data-dragging": !!active }),
2386
+ onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2387
+ onPointerMove: draggable ? moveDrag : void 0,
2388
+ onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2389
+ onPointerCancel: draggable ? cancelDrag : void 0,
2390
+ ...slot("event", { base: {
2391
+ position: "absolute",
2392
+ top,
2393
+ height,
2394
+ left: `${pe.column * lanePct}%`,
2395
+ width: `${lanePct}%`,
2396
+ padding: 1,
2397
+ border: "none",
2398
+ background: "transparent",
2399
+ cursor: draggable ? "grab" : "pointer",
2400
+ touchAction: draggable ? "none" : "auto",
2401
+ font: "inherit",
2402
+ textAlign: "left",
2403
+ boxSizing: "border-box",
2404
+ zIndex: active ? 3 : 1,
2405
+ opacity: active ? .85 : 1
2406
+ } }),
2407
+ children: [Renderer ? /* @__PURE__ */ jsx(Renderer, { ...args }) : /* @__PURE__ */ jsx(DefaultBar, {
2408
+ ...args,
2409
+ theme,
2410
+ boxProps: slot("eventBox")
2411
+ }), draggable ? /* @__PURE__ */ jsx("span", {
2412
+ "aria-hidden": true,
2413
+ onPointerDown: (e) => {
2414
+ e.stopPropagation();
2415
+ beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2416
+ },
2417
+ style: {
2418
+ position: "absolute",
2419
+ left: 0,
2420
+ right: 0,
2421
+ bottom: 0,
2422
+ height: 8,
2423
+ cursor: "ns-resize",
2424
+ touchAction: "none"
2425
+ }
2426
+ }) : null]
2427
+ }, idx);
2428
+ }),
2429
+ createBox?.resourceKey === resource.id ? /* @__PURE__ */ jsx("div", {
2430
+ "aria-hidden": true,
2431
+ ...slot("createGhost", {
2432
+ base: {
2433
+ position: "absolute",
2434
+ left: 1,
2435
+ right: 1,
2436
+ top: Math.min(createBox.startPx, createBox.curPx),
2437
+ height: Math.max(Math.abs(createBox.curPx - createBox.startPx), 2),
2438
+ opacity: .7,
2439
+ pointerEvents: "none",
2440
+ zIndex: 2
2441
+ },
2442
+ themed: {
2443
+ background: theme.rangeBackground,
2444
+ border: `1px solid ${theme.selectedBackground}`,
2445
+ borderRadius: 6
2344
2446
  }
2345
- }) : null]
2346
- }, idx);
2347
- })]
2447
+ })
2448
+ }) : null
2449
+ ]
2348
2450
  })
2349
2451
  }, resource.id);
2350
2452
  })]
@@ -2419,84 +2521,123 @@ function ResourceTimeline({ date, orientation = "horizontal", resources, events,
2419
2521
  }),
2420
2522
  children: resource.title ?? resource.id
2421
2523
  }), /* @__PURE__ */ jsxs("div", {
2524
+ ...trackInteractionProps(resource),
2422
2525
  ...slot("track", { base: {
2423
2526
  position: "relative",
2424
2527
  width: trackWidth
2425
2528
  } }),
2426
- children: [/* @__PURE__ */ jsx("div", {
2427
- "aria-hidden": true,
2428
- ...slot("gridLines", {
2429
- base: {
2430
- position: "absolute",
2431
- inset: 0,
2432
- pointerEvents: "none"
2433
- },
2434
- themed: { backgroundImage: gridLines }
2435
- })
2436
- }), positioned.map((pe, idx) => {
2437
- const key = `${resource.id}:${idx}`;
2438
- const active = drag?.key === key ? drag : null;
2439
- const startH = active ? active.startHours : pe.startHours;
2440
- const durH = active ? active.durationHours : pe.durationHours;
2441
- const left = clamp(startH - startHour, 0, endHour - startHour) * hourWidth;
2442
- const right = clamp(startH + durH - startHour, 0, endHour - startHour) * hourWidth;
2443
- const width = Math.max(right - left, 2);
2444
- const laneHeight = rowHeight / pe.columns;
2445
- const onPress = () => onPressEvent?.(pe.event);
2446
- const args = {
2447
- event: pe.event,
2448
- width,
2449
- onPress
2450
- };
2451
- const draggable = !!onDragEvent;
2452
- return /* @__PURE__ */ jsxs("button", {
2453
- type: "button",
2454
- onClick: draggable ? void 0 : onPress,
2455
- "aria-label": pe.event.title,
2456
- ...dataState({ "data-dragging": !!active }),
2457
- onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2458
- onPointerMove: draggable ? moveDrag : void 0,
2459
- onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2460
- onPointerCancel: draggable ? cancelDrag : void 0,
2461
- ...slot("event", { base: {
2462
- position: "absolute",
2463
- left,
2464
- width,
2465
- top: pe.column * laneHeight,
2466
- height: laneHeight,
2467
- padding: 1,
2468
- border: "none",
2469
- background: "transparent",
2470
- cursor: draggable ? "grab" : "pointer",
2471
- touchAction: draggable ? "none" : "auto",
2472
- font: "inherit",
2473
- textAlign: "left",
2474
- boxSizing: "border-box",
2475
- zIndex: active ? 3 : 1,
2476
- opacity: active ? .85 : 1
2477
- } }),
2478
- children: [Renderer ? /* @__PURE__ */ jsx(Renderer, { ...args }) : /* @__PURE__ */ jsx(DefaultBar, {
2479
- ...args,
2480
- theme,
2481
- boxProps: slot("eventBox")
2482
- }), draggable ? /* @__PURE__ */ jsx("span", {
2483
- "aria-hidden": true,
2484
- onPointerDown: (e) => {
2485
- e.stopPropagation();
2486
- beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2487
- },
2488
- style: {
2529
+ children: [
2530
+ bandsFor(resource).map((b) => /* @__PURE__ */ jsx("div", {
2531
+ "aria-hidden": true,
2532
+ ...slot("businessHours", {
2533
+ base: {
2489
2534
  position: "absolute",
2490
2535
  top: 0,
2491
2536
  bottom: 0,
2492
- right: 0,
2493
- width: 8,
2494
- cursor: "ew-resize",
2495
- touchAction: "none"
2537
+ left: (b.start - startHour) * hourWidth,
2538
+ width: (b.end - b.start) * hourWidth,
2539
+ pointerEvents: "none",
2540
+ zIndex: 0
2541
+ },
2542
+ themed: { background: theme.outsideHoursBackground }
2543
+ })
2544
+ }, b.start)),
2545
+ /* @__PURE__ */ jsx("div", {
2546
+ "aria-hidden": true,
2547
+ ...slot("gridLines", {
2548
+ base: {
2549
+ position: "absolute",
2550
+ inset: 0,
2551
+ pointerEvents: "none"
2552
+ },
2553
+ themed: { backgroundImage: gridLines }
2554
+ })
2555
+ }),
2556
+ positioned.map((pe, idx) => {
2557
+ const key = `${resource.id}:${idx}`;
2558
+ const active = drag?.key === key ? drag : null;
2559
+ const startH = active ? active.startHours : pe.startHours;
2560
+ const durH = active ? active.durationHours : pe.durationHours;
2561
+ const left = clamp(startH - startHour, 0, endHour - startHour) * hourWidth;
2562
+ const right = clamp(startH + durH - startHour, 0, endHour - startHour) * hourWidth;
2563
+ const width = Math.max(right - left, 2);
2564
+ const laneHeight = rowHeight / pe.columns;
2565
+ const onPress = () => onPressEvent?.(pe.event);
2566
+ const args = {
2567
+ event: pe.event,
2568
+ width,
2569
+ onPress
2570
+ };
2571
+ const draggable = !!onDragEvent;
2572
+ return /* @__PURE__ */ jsxs("button", {
2573
+ type: "button",
2574
+ onClick: draggable ? void 0 : onPress,
2575
+ "aria-label": pe.event.title,
2576
+ ...dataState({ "data-dragging": !!active }),
2577
+ onPointerDown: draggable ? (e) => beginDrag(e, key, "move", pe.startHours, pe.durationHours) : void 0,
2578
+ onPointerMove: draggable ? moveDrag : void 0,
2579
+ onPointerUp: draggable ? (e) => endDrag(e, pe.event, onPress) : void 0,
2580
+ onPointerCancel: draggable ? cancelDrag : void 0,
2581
+ ...slot("event", { base: {
2582
+ position: "absolute",
2583
+ left,
2584
+ width,
2585
+ top: pe.column * laneHeight,
2586
+ height: laneHeight,
2587
+ padding: 1,
2588
+ border: "none",
2589
+ background: "transparent",
2590
+ cursor: draggable ? "grab" : "pointer",
2591
+ touchAction: draggable ? "none" : "auto",
2592
+ font: "inherit",
2593
+ textAlign: "left",
2594
+ boxSizing: "border-box",
2595
+ zIndex: active ? 3 : 1,
2596
+ opacity: active ? .85 : 1
2597
+ } }),
2598
+ children: [Renderer ? /* @__PURE__ */ jsx(Renderer, { ...args }) : /* @__PURE__ */ jsx(DefaultBar, {
2599
+ ...args,
2600
+ theme,
2601
+ boxProps: slot("eventBox")
2602
+ }), draggable ? /* @__PURE__ */ jsx("span", {
2603
+ "aria-hidden": true,
2604
+ onPointerDown: (e) => {
2605
+ e.stopPropagation();
2606
+ beginDrag(e, key, "resize", pe.startHours, pe.durationHours);
2607
+ },
2608
+ style: {
2609
+ position: "absolute",
2610
+ top: 0,
2611
+ bottom: 0,
2612
+ right: 0,
2613
+ width: 8,
2614
+ cursor: "ew-resize",
2615
+ touchAction: "none"
2616
+ }
2617
+ }) : null]
2618
+ }, idx);
2619
+ }),
2620
+ createBox?.resourceKey === resource.id ? /* @__PURE__ */ jsx("div", {
2621
+ "aria-hidden": true,
2622
+ ...slot("createGhost", {
2623
+ base: {
2624
+ position: "absolute",
2625
+ top: 1,
2626
+ bottom: 1,
2627
+ left: Math.min(createBox.startPx, createBox.curPx),
2628
+ width: Math.max(Math.abs(createBox.curPx - createBox.startPx), 2),
2629
+ opacity: .7,
2630
+ pointerEvents: "none",
2631
+ zIndex: 2
2632
+ },
2633
+ themed: {
2634
+ background: theme.rangeBackground,
2635
+ border: `1px solid ${theme.selectedBackground}`,
2636
+ borderRadius: 6
2496
2637
  }
2497
- }) : null]
2498
- }, idx);
2499
- })]
2638
+ })
2639
+ }) : null
2640
+ ]
2500
2641
  })]
2501
2642
  }, resource.id);
2502
2643
  })]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@super-calendar/dom",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Virtualized month / week / day calendar and date picker for react-dom. No React Native, no react-native-web.",
5
5
  "keywords": [
6
6
  "calendar",
@@ -48,7 +48,7 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@super-calendar/core": "2.4.0"
51
+ "@super-calendar/core": "2.5.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@legendapp/list": ">=3",
@@ -8,6 +8,8 @@ import type {
8
8
  import { useMemo, useRef, useState } from "react";
9
9
  import {
10
10
  type CalendarEvent,
11
+ cellRangeFromDrag,
12
+ closedHourBands,
11
13
  eventTimeLabel,
12
14
  formatHour,
13
15
  layoutDayEvents,
@@ -28,8 +30,10 @@ export type ResourceTimelineSlot =
28
30
  | "row"
29
31
  | "track"
30
32
  | "gridLines"
33
+ | "businessHours"
31
34
  | "event"
32
- | "eventBox";
35
+ | "eventBox"
36
+ | "createGhost";
33
37
 
34
38
  /** A schedulable lane (room, person, machine) that events are grouped under. */
35
39
  export interface Resource {
@@ -94,6 +98,16 @@ export interface ResourceTimelineProps<T = unknown> extends SlotStyleProps<Resou
94
98
  * proposed new start/end. Return `false` to reject the drop (it snaps back).
95
99
  */
96
100
  onDragEvent?: (event: CalendarEvent<T>, start: Date, end: Date) => void | boolean;
101
+ /** Tap empty lane space; called with the snapped time and the lane's resource. */
102
+ onPressCell?: (at: Date, resource: Resource) => void;
103
+ /** Drag empty lane space to create; called with the swept start/end and the lane's resource. */
104
+ onCreateEvent?: (start: Date, end: Date, resource: Resource) => void;
105
+ /**
106
+ * Shade the hours outside this window, per lane. Same shape as the Calendar's
107
+ * `businessHours` plus the lane's resource, so per-resource opening hours work
108
+ * (return `null` for a fully closed lane). A date-only function is accepted.
109
+ */
110
+ businessHours?: (date: Date, resource: Resource) => { start: number; end: number } | null;
97
111
  /** Snap dragged events to this many minutes (default 15). */
98
112
  dragStepMinutes?: number;
99
113
  /** Class applied to the root element. */
@@ -198,6 +212,9 @@ export function ResourceTimeline<T = unknown>({
198
212
  renderEvent,
199
213
  onPressEvent,
200
214
  onDragEvent,
215
+ onPressCell,
216
+ onCreateEvent,
217
+ businessHours,
201
218
  dragStepMinutes = 15,
202
219
  className,
203
220
  style,
@@ -285,6 +302,90 @@ export function ResourceTimeline<T = unknown>({
285
302
  origin.current = null;
286
303
  };
287
304
 
305
+ // Create-by-drag / tap on empty lane space, mirroring the time grid: mouse and
306
+ // pen sweep out a new event, and a tap without movement fires onPressCell. Touch
307
+ // is left free to scroll the board, so cell taps are pointer (mouse/pen) only.
308
+ const [createBox, setCreateBox] = useState<{
309
+ resourceKey: string;
310
+ startPx: number;
311
+ curPx: number;
312
+ } | null>(null);
313
+ const createOrigin = useRef<{ el: HTMLElement; resource: Resource; startPx: number } | null>(
314
+ null,
315
+ );
316
+ const cellEnabled = !!onPressCell || !!onCreateEvent;
317
+ const pxAlongAxis = (el: HTMLElement, e: ReactPointerEvent) => {
318
+ const rect = el.getBoundingClientRect();
319
+ return vertical ? e.clientY - rect.top : e.clientX - rect.left;
320
+ };
321
+ const beginCreate = (e: ReactPointerEvent, resource: Resource) => {
322
+ // Only from the lane background, primary button, never on an event bar.
323
+ if (!cellEnabled || e.pointerType === "touch") return;
324
+ if (e.target !== e.currentTarget || e.button > 0) return;
325
+ const el = e.currentTarget as HTMLElement;
326
+ const startPx = pxAlongAxis(el, e);
327
+ try {
328
+ el.setPointerCapture?.(e.pointerId);
329
+ } catch {
330
+ // Best-effort capture.
331
+ }
332
+ createOrigin.current = { el, resource, startPx };
333
+ // Only preview a ghost when a create can happen; in press-only mode
334
+ // (onPressCell without onCreateEvent) the tap still commits via endCreate.
335
+ if (onCreateEvent) setCreateBox({ resourceKey: resource.id, startPx, curPx: startPx });
336
+ };
337
+ const moveCreate = (e: ReactPointerEvent) => {
338
+ const o = createOrigin.current;
339
+ if (!o || !onCreateEvent) return;
340
+ setCreateBox({ resourceKey: o.resource.id, startPx: o.startPx, curPx: pxAlongAxis(o.el, e) });
341
+ };
342
+ const endCreate = (e: ReactPointerEvent) => {
343
+ const o = createOrigin.current;
344
+ if (!o) return;
345
+ try {
346
+ o.el.releasePointerCapture?.(e.pointerId);
347
+ } catch {
348
+ // Best-effort release.
349
+ }
350
+ const endPx = pxAlongAxis(o.el, e);
351
+ const moved = Math.abs(endPx - o.startPx) > 4;
352
+ if (moved && onCreateEvent) {
353
+ const range = cellRangeFromDrag(date, o.startPx, endPx, hourSize, startHour, dragStepMinutes);
354
+ if (range) onCreateEvent(range.start, range.end, o.resource);
355
+ } else if (onPressCell) {
356
+ const at = cellRangeFromDrag(
357
+ date,
358
+ o.startPx,
359
+ o.startPx,
360
+ hourSize,
361
+ startHour,
362
+ dragStepMinutes,
363
+ );
364
+ if (at) onPressCell(at.start, o.resource);
365
+ }
366
+ createOrigin.current = null;
367
+ setCreateBox(null);
368
+ };
369
+ // A gesture the browser cancels (scroll takeover, etc.) must not commit.
370
+ const cancelCreate = () => {
371
+ createOrigin.current = null;
372
+ setCreateBox(null);
373
+ };
374
+ const trackInteractionProps = (resource: Resource) =>
375
+ cellEnabled
376
+ ? {
377
+ onPointerDown: (e: ReactPointerEvent) => beginCreate(e, resource),
378
+ onPointerMove: moveCreate,
379
+ onPointerUp: endCreate,
380
+ onPointerCancel: cancelCreate,
381
+ }
382
+ : null;
383
+ // The closed-hours bands to shade in a lane, resolved per resource.
384
+ const bandsFor = (resource: Resource) =>
385
+ businessHours
386
+ ? closedHourBands(date, (d) => businessHours(d, resource), startHour, endHour)
387
+ : [];
388
+
288
389
  const hours = useMemo(
289
390
  () => Array.from({ length: Math.max(0, endHour - startHour) }, (_, i) => startHour + i),
290
391
  [startHour, endHour],
@@ -384,7 +485,29 @@ export function ResourceTimeline<T = unknown>({
384
485
  themed: { borderLeft: `1px solid ${theme.gridLine}` },
385
486
  })}
386
487
  >
387
- <div {...slot("track", { base: { position: "relative", height: trackHeight } })}>
488
+ <div
489
+ {...trackInteractionProps(resource)}
490
+ {...slot("track", { base: { position: "relative", height: trackHeight } })}
491
+ >
492
+ {/* Closed-hours shade, behind the grid lines and events. */}
493
+ {bandsFor(resource).map((b) => (
494
+ <div
495
+ key={b.start}
496
+ aria-hidden
497
+ {...slot("businessHours", {
498
+ base: {
499
+ position: "absolute",
500
+ left: 0,
501
+ right: 0,
502
+ top: (b.start - startHour) * hourHeight,
503
+ height: (b.end - b.start) * hourHeight,
504
+ pointerEvents: "none",
505
+ zIndex: 0,
506
+ },
507
+ themed: { background: theme.outsideHoursBackground },
508
+ })}
509
+ />
510
+ ))}
388
511
  <div
389
512
  aria-hidden
390
513
  {...slot("gridLines", {
@@ -472,6 +595,28 @@ export function ResourceTimeline<T = unknown>({
472
595
  </button>
473
596
  );
474
597
  })}
598
+ {createBox?.resourceKey === resource.id ? (
599
+ <div
600
+ aria-hidden
601
+ {...slot("createGhost", {
602
+ base: {
603
+ position: "absolute",
604
+ left: 1,
605
+ right: 1,
606
+ top: Math.min(createBox.startPx, createBox.curPx),
607
+ height: Math.max(Math.abs(createBox.curPx - createBox.startPx), 2),
608
+ opacity: 0.7,
609
+ pointerEvents: "none",
610
+ zIndex: 2,
611
+ },
612
+ themed: {
613
+ background: theme.rangeBackground,
614
+ border: `1px solid ${theme.selectedBackground}`,
615
+ borderRadius: 6,
616
+ },
617
+ })}
618
+ />
619
+ ) : null}
475
620
  </div>
476
621
  </div>
477
622
  );
@@ -544,7 +689,29 @@ export function ResourceTimeline<T = unknown>({
544
689
  >
545
690
  {resource.title ?? resource.id}
546
691
  </div>
547
- <div {...slot("track", { base: { position: "relative", width: trackWidth } })}>
692
+ <div
693
+ {...trackInteractionProps(resource)}
694
+ {...slot("track", { base: { position: "relative", width: trackWidth } })}
695
+ >
696
+ {/* Closed-hours shade, behind the grid lines and events. */}
697
+ {bandsFor(resource).map((b) => (
698
+ <div
699
+ key={b.start}
700
+ aria-hidden
701
+ {...slot("businessHours", {
702
+ base: {
703
+ position: "absolute",
704
+ top: 0,
705
+ bottom: 0,
706
+ left: (b.start - startHour) * hourWidth,
707
+ width: (b.end - b.start) * hourWidth,
708
+ pointerEvents: "none",
709
+ zIndex: 0,
710
+ },
711
+ themed: { background: theme.outsideHoursBackground },
712
+ })}
713
+ />
714
+ ))}
548
715
  <div
549
716
  aria-hidden
550
717
  {...slot("gridLines", {
@@ -627,6 +794,28 @@ export function ResourceTimeline<T = unknown>({
627
794
  </button>
628
795
  );
629
796
  })}
797
+ {createBox?.resourceKey === resource.id ? (
798
+ <div
799
+ aria-hidden
800
+ {...slot("createGhost", {
801
+ base: {
802
+ position: "absolute",
803
+ top: 1,
804
+ bottom: 1,
805
+ left: Math.min(createBox.startPx, createBox.curPx),
806
+ width: Math.max(Math.abs(createBox.curPx - createBox.startPx), 2),
807
+ opacity: 0.7,
808
+ pointerEvents: "none",
809
+ zIndex: 2,
810
+ },
811
+ themed: {
812
+ background: theme.rangeBackground,
813
+ border: `1px solid ${theme.selectedBackground}`,
814
+ borderRadius: 6,
815
+ },
816
+ })}
817
+ />
818
+ ) : null}
630
819
  </div>
631
820
  </div>
632
821
  );