@allhailai/tempusmachina-react 1.0.0 → 1.1.1

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
@@ -194,6 +194,7 @@ function useCalendar(options) {
194
194
  slotMinTime,
195
195
  slotMaxTime,
196
196
  resources = [],
197
+ resourceOverlays = [],
197
198
  businessHours,
198
199
  eventMaxStack = 3,
199
200
  eventDefaults,
@@ -350,6 +351,7 @@ function useCalendar(options) {
350
351
  viewLayout,
351
352
  visibleRange,
352
353
  events: eventStore.getAll(),
354
+ resourceOverlays,
353
355
  next,
354
356
  prev,
355
357
  today: todayAction,
@@ -1576,8 +1578,57 @@ function TimeGrid({
1576
1578
  stickyFooterScrollbar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky bottom-0 h-3 bg-cal-bg border-t border-cal-border overflow-x-auto shrink-0" })
1577
1579
  ] });
1578
1580
  }
1579
- function ResourceLane({ lane, slotCount }) {
1580
- const { slots } = useCalendarContext();
1581
+ var PATTERN_STYLES = {
1582
+ striped: {
1583
+ backgroundImage: "repeating-linear-gradient(135deg, transparent, transparent 4px, rgba(0,0,0,0.06) 4px, rgba(0,0,0,0.06) 8px)"
1584
+ },
1585
+ hatched: {
1586
+ backgroundImage: "repeating-linear-gradient(45deg, transparent, transparent 3px, rgba(0,0,0,0.08) 3px, rgba(0,0,0,0.08) 6px), repeating-linear-gradient(-45deg, transparent, transparent 3px, rgba(0,0,0,0.08) 3px, rgba(0,0,0,0.08) 6px)"
1587
+ }
1588
+ };
1589
+ function sanitizeCategory(category) {
1590
+ return category.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
1591
+ }
1592
+ function computeBandPosition(band, rangeStartMs, rangeTotalMs) {
1593
+ if (rangeTotalMs <= 0) return null;
1594
+ const bandStartMs = band.start.epochMilliseconds;
1595
+ const bandEndMs = band.end.epochMilliseconds;
1596
+ const clampedStart = Math.max(bandStartMs, rangeStartMs);
1597
+ const clampedEnd = Math.min(bandEndMs, rangeStartMs + rangeTotalMs);
1598
+ if (clampedEnd <= clampedStart) return null;
1599
+ const left = (clampedStart - rangeStartMs) / rangeTotalMs * 100;
1600
+ const width = (clampedEnd - clampedStart) / rangeTotalMs * 100;
1601
+ return { left, width };
1602
+ }
1603
+ function ResourceLane({ lane, slotCount, overlays }) {
1604
+ const { slots, viewLayout, timezone } = useCalendarContext();
1605
+ const { rangeStartMs, rangeTotalMs } = React16.useMemo(() => {
1606
+ if (!viewLayout?.dateRange) return { rangeStartMs: 0, rangeTotalMs: 0 };
1607
+ const timeSlots = viewLayout.timeSlots;
1608
+ if (timeSlots && timeSlots.length > 0) {
1609
+ const firstSlot = timeSlots[0];
1610
+ const lastSlot = timeSlots[timeSlots.length - 1];
1611
+ const startMs2 = firstSlot.start.toZonedDateTime(timezone).epochMilliseconds;
1612
+ const endMs2 = lastSlot.end.toZonedDateTime(timezone).epochMilliseconds;
1613
+ return {
1614
+ rangeStartMs: startMs2,
1615
+ rangeTotalMs: Math.max(0, endMs2 - startMs2)
1616
+ };
1617
+ }
1618
+ const { start: rangeStart, end: rangeEnd } = viewLayout.dateRange;
1619
+ const startMs = rangeStart.toZonedDateTime({
1620
+ timeZone: timezone,
1621
+ plainTime: temporalPolyfill.Temporal.PlainTime.from("00:00")
1622
+ }).epochMilliseconds;
1623
+ const endMs = rangeEnd.toZonedDateTime({
1624
+ timeZone: timezone,
1625
+ plainTime: temporalPolyfill.Temporal.PlainTime.from("00:00")
1626
+ }).epochMilliseconds;
1627
+ return {
1628
+ rangeStartMs: startMs,
1629
+ rangeTotalMs: Math.max(0, endMs - startMs)
1630
+ };
1631
+ }, [viewLayout?.dateRange, viewLayout?.timeSlots, timezone]);
1581
1632
  return /* @__PURE__ */ jsxRuntime.jsxs(
1582
1633
  "div",
1583
1634
  {
@@ -1594,36 +1645,114 @@ function ResourceLane({ lane, slotCount }) {
1594
1645
  ),
1595
1646
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium truncate", children: lane.resource.title })
1596
1647
  ] }) }),
1597
- /* @__PURE__ */ jsxRuntime.jsx(
1648
+ /* @__PURE__ */ jsxRuntime.jsxs(
1598
1649
  "div",
1599
1650
  {
1600
1651
  className: cn("relative col-span-full", `col-start-2`),
1601
1652
  style: { gridColumn: `2 / -1`, minHeight: "var(--cal-slot-height)" },
1602
- children: lane.events.map((posEvent) => /* @__PURE__ */ jsxRuntime.jsx(
1603
- "div",
1604
- {
1605
- className: "tm-event",
1606
- style: {
1607
- left: `${posEvent.left}%`,
1608
- width: `${posEvent.width}%`,
1609
- top: "2px",
1610
- height: "calc(100% - 4px)"
1653
+ children: [
1654
+ overlays && rangeTotalMs > 0 && overlays.map((band) => {
1655
+ const pos = computeBandPosition(band, rangeStartMs, rangeTotalMs);
1656
+ if (!pos) return null;
1657
+ return /* @__PURE__ */ jsxRuntime.jsx(
1658
+ OverlayBandElement,
1659
+ {
1660
+ band,
1661
+ resource: lane.resource,
1662
+ left: pos.left,
1663
+ width: pos.width
1664
+ },
1665
+ band.id
1666
+ );
1667
+ }),
1668
+ lane.events.map((posEvent) => /* @__PURE__ */ jsxRuntime.jsx(
1669
+ "div",
1670
+ {
1671
+ className: "tm-event",
1672
+ style: {
1673
+ left: `${posEvent.left}%`,
1674
+ width: `${posEvent.width}%`,
1675
+ top: "2px",
1676
+ height: "calc(100% - 4px)"
1677
+ },
1678
+ children: /* @__PURE__ */ jsxRuntime.jsx(EventCard, { positionedEvent: posEvent, compact: true })
1611
1679
  },
1612
- children: /* @__PURE__ */ jsxRuntime.jsx(EventCard, { positionedEvent: posEvent, compact: true })
1613
- },
1614
- posEvent.event.id
1615
- ))
1680
+ posEvent.event.id
1681
+ ))
1682
+ ]
1616
1683
  }
1617
1684
  )
1618
1685
  ]
1619
1686
  }
1620
1687
  );
1621
1688
  }
1622
- function Timeline({ className }) {
1623
- const { viewLayout, locale } = useCalendarContext();
1689
+ function OverlayBandElement({ band, resource, left, width }) {
1690
+ const { slots } = useCalendarContext();
1691
+ const safeCategory = sanitizeCategory(band.category);
1692
+ const bgColor = band.color ?? `var(--cal-overlay-${safeCategory}, var(--cal-overlay-default))`;
1693
+ const bandStyle = {
1694
+ position: "absolute",
1695
+ top: 0,
1696
+ bottom: 0,
1697
+ left: `${left}%`,
1698
+ width: `${width}%`,
1699
+ backgroundColor: bgColor,
1700
+ opacity: 0.3,
1701
+ pointerEvents: "none",
1702
+ ...band.pattern && band.pattern !== "solid" ? PATTERN_STYLES[band.pattern] : {}
1703
+ };
1704
+ if (slots.overlayBandContent) {
1705
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: slots.overlayBandContent({ band, resource, style: bandStyle }) });
1706
+ }
1707
+ return /* @__PURE__ */ jsxRuntime.jsx(
1708
+ "div",
1709
+ {
1710
+ className: cn("tm-overlay-band", `tm-overlay-band--${safeCategory}`),
1711
+ style: bandStyle,
1712
+ "aria-hidden": "true",
1713
+ title: band.label ?? band.category
1714
+ }
1715
+ );
1716
+ }
1717
+ function Timeline({
1718
+ className,
1719
+ collapsibleGroups = true,
1720
+ expandedGroupIds: controlledExpandedIds,
1721
+ onGroupToggle
1722
+ }) {
1723
+ const { viewLayout, locale, slots, resourceOverlays } = useCalendarContext();
1724
+ const [internalExpanded, setInternalExpanded] = React16.useState({});
1725
+ const isGroupExpanded = React16.useCallback(
1726
+ (groupId) => {
1727
+ if (controlledExpandedIds) {
1728
+ return controlledExpandedIds.includes(groupId);
1729
+ }
1730
+ return internalExpanded[groupId] !== false;
1731
+ },
1732
+ [controlledExpandedIds, internalExpanded]
1733
+ );
1734
+ const handleGroupToggle = React16.useCallback(
1735
+ (groupId) => {
1736
+ const newState = !isGroupExpanded(groupId);
1737
+ if (!controlledExpandedIds) {
1738
+ setInternalExpanded((prev) => ({ ...prev, [groupId]: newState }));
1739
+ }
1740
+ onGroupToggle?.(groupId, newState);
1741
+ },
1742
+ [controlledExpandedIds, isGroupExpanded, onGroupToggle]
1743
+ );
1624
1744
  if (!viewLayout?.lanes) return null;
1625
1745
  const lanes = viewLayout.lanes;
1626
1746
  const timeSlots = viewLayout.timeSlots ?? [];
1747
+ const resourceGroups = viewLayout.resourceGroups;
1748
+ const laneByResourceId = new Map(lanes.map((lane) => [lane.resource.id, lane]));
1749
+ const overlaysByResourceId = /* @__PURE__ */ new Map();
1750
+ if (resourceOverlays) {
1751
+ for (const overlay of resourceOverlays) {
1752
+ overlaysByResourceId.set(overlay.resourceId, overlay.bands);
1753
+ }
1754
+ }
1755
+ const hasGroups = resourceGroups && resourceGroups.length > 0;
1627
1756
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex-1 overflow-hidden tm-timeline", className), children: [
1628
1757
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tm-timeline__header border-b border-cal-border bg-cal-header-bg", children: /* @__PURE__ */ jsxRuntime.jsxs(
1629
1758
  "div",
@@ -1645,7 +1774,135 @@ function Timeline({ className }) {
1645
1774
  ]
1646
1775
  }
1647
1776
  ) }),
1648
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-y-auto tm-scrollable", children: lanes.map((lane) => /* @__PURE__ */ jsxRuntime.jsx(ResourceLane, { lane, slotCount: timeSlots.length }, lane.resource.id)) })
1777
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-y-auto tm-scrollable", children: [
1778
+ hasGroups ? resourceGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsx(
1779
+ TimelineGroup,
1780
+ {
1781
+ group,
1782
+ laneByResourceId,
1783
+ overlaysByResourceId,
1784
+ slotCount: timeSlots.length,
1785
+ collapsible: collapsibleGroups,
1786
+ isExpanded: isGroupExpanded(group.id),
1787
+ onToggle: () => handleGroupToggle(group.id),
1788
+ slots
1789
+ },
1790
+ group.id
1791
+ )) : lanes.map((lane) => /* @__PURE__ */ jsxRuntime.jsx(
1792
+ ResourceLane,
1793
+ {
1794
+ lane,
1795
+ slotCount: timeSlots.length,
1796
+ overlays: overlaysByResourceId.get(lane.resource.id)
1797
+ },
1798
+ lane.resource.id
1799
+ )),
1800
+ hasGroups && lanes.filter(
1801
+ (lane) => !resourceGroups.some(
1802
+ (g) => g.resources.some((r) => r.id === lane.resource.id)
1803
+ ) && lane.resource.id !== "__unassigned__"
1804
+ ).map((lane) => /* @__PURE__ */ jsxRuntime.jsx(
1805
+ ResourceLane,
1806
+ {
1807
+ lane,
1808
+ slotCount: timeSlots.length,
1809
+ overlays: overlaysByResourceId.get(lane.resource.id)
1810
+ },
1811
+ lane.resource.id
1812
+ )),
1813
+ lanes.filter((lane) => lane.resource.id === "__unassigned__").map((lane) => /* @__PURE__ */ jsxRuntime.jsx(
1814
+ ResourceLane,
1815
+ {
1816
+ lane,
1817
+ slotCount: timeSlots.length,
1818
+ overlays: overlaysByResourceId.get(lane.resource.id)
1819
+ },
1820
+ lane.resource.id
1821
+ ))
1822
+ ] })
1823
+ ] });
1824
+ }
1825
+ function TimelineGroup({
1826
+ group,
1827
+ laneByResourceId,
1828
+ overlaysByResourceId,
1829
+ slotCount,
1830
+ collapsible,
1831
+ isExpanded,
1832
+ onToggle,
1833
+ slots
1834
+ }) {
1835
+ const memberCount = group.resources.length;
1836
+ const groupSubtitle = group.subtitle;
1837
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tm-timeline-group", children: [
1838
+ slots.resourceGroupContent ? slots.resourceGroupContent({
1839
+ group,
1840
+ isExpanded,
1841
+ memberCount,
1842
+ onToggle
1843
+ }) : /* @__PURE__ */ jsxRuntime.jsxs(
1844
+ "div",
1845
+ {
1846
+ className: cn(
1847
+ "tm-timeline-group__header",
1848
+ "flex items-center gap-2 px-3 py-2",
1849
+ "bg-cal-header-bg border-b border-cal-border",
1850
+ collapsible && "cursor-pointer hover:bg-cal-slot-bg"
1851
+ ),
1852
+ onClick: collapsible ? onToggle : void 0,
1853
+ onKeyDown: collapsible ? (e) => {
1854
+ if (e.key === "Enter" || e.key === " ") {
1855
+ e.preventDefault();
1856
+ onToggle();
1857
+ }
1858
+ } : void 0,
1859
+ role: collapsible ? "button" : void 0,
1860
+ tabIndex: collapsible ? 0 : void 0,
1861
+ "aria-expanded": collapsible ? isExpanded : void 0,
1862
+ "aria-label": `${isExpanded ? "Collapse" : "Expand"} ${group.title}`,
1863
+ children: [
1864
+ collapsible && /* @__PURE__ */ jsxRuntime.jsx(
1865
+ "svg",
1866
+ {
1867
+ width: "14",
1868
+ height: "14",
1869
+ viewBox: "0 0 14 14",
1870
+ fill: "none",
1871
+ className: cn(
1872
+ "transition-transform duration-200 text-cal-fg opacity-50",
1873
+ isExpanded ? "rotate-90" : ""
1874
+ ),
1875
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1876
+ "path",
1877
+ {
1878
+ d: "M5 3L9 7L5 11",
1879
+ stroke: "currentColor",
1880
+ strokeWidth: "1.5",
1881
+ strokeLinecap: "round",
1882
+ strokeLinejoin: "round"
1883
+ }
1884
+ )
1885
+ }
1886
+ ),
1887
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-cal-fg", children: group.title }),
1888
+ groupSubtitle && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-cal-fg opacity-50", children: groupSubtitle }),
1889
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto text-[10px] font-medium text-cal-fg opacity-40 bg-cal-slot-bg rounded-full px-1.5 py-0.5", children: memberCount })
1890
+ ]
1891
+ }
1892
+ ),
1893
+ isExpanded && group.resources.map((resource) => {
1894
+ const lane = laneByResourceId.get(resource.id);
1895
+ if (!lane) return null;
1896
+ return /* @__PURE__ */ jsxRuntime.jsx(
1897
+ ResourceLane,
1898
+ {
1899
+ lane,
1900
+ slotCount,
1901
+ overlays: overlaysByResourceId.get(resource.id)
1902
+ },
1903
+ resource.id
1904
+ );
1905
+ })
1649
1906
  ] });
1650
1907
  }
1651
1908
  function Agenda({ className }) {
@@ -1838,6 +2095,7 @@ function Calendar({
1838
2095
  const contextValue = {
1839
2096
  ...calendarState,
1840
2097
  resources: options.resources ?? [],
2098
+ resourceOverlays: calendarState.resourceOverlays,
1841
2099
  timezone: options.timezone ?? "UTC",
1842
2100
  locale: options.locale ?? "en-US",
1843
2101
  firstDayOfWeek: options.firstDayOfWeek ?? 0,