@arbidocs/blocks 0.3.177 → 0.3.180

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.
@@ -6,6 +6,7 @@ var react = require('react');
6
6
  var lucideReact = require('lucide-react');
7
7
  var ui = require('@arbidocs/react/ui');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
+ var reactDom = require('react-dom');
9
10
 
10
11
  // src/theme/tokens.ts
11
12
  var THEME_STYLE_ID = "arbi-theme-vars";
@@ -678,20 +679,62 @@ function formatSpan(startIso, endIso, timezone) {
678
679
  return `${day(start)} \u2013 ${day(end)}`;
679
680
  }
680
681
  var DEFAULT_MAX_LANES = 3;
681
- var MAX_WEEKS = 520;
682
- function isPrecise(item) {
683
- if (!item.start || !item.end) return false;
684
- return item.start.slice(0, 10) === item.end.slice(0, 10);
685
- }
682
+ var MAX_WEEKS = 2600;
686
683
  function TimelineCalendar({
687
684
  items,
688
685
  onSelect,
686
+ selectedIds,
689
687
  maxLanes = DEFAULT_MAX_LANES,
688
+ navigatorContainer,
689
+ navigatorTotal,
690
690
  emptyState,
691
691
  className,
692
692
  testId = "timeline-calendar"
693
693
  }) {
694
694
  const dated = react.useMemo(() => items.filter((i) => i.start), [items]);
695
+ const sortedItems = react.useMemo(
696
+ () => [...dated].sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()),
697
+ [dated]
698
+ );
699
+ const indexById = react.useMemo(() => new Map(sortedItems.map((it, i) => [it.id, i])), [sortedItems]);
700
+ const [activeIndex, setActiveIndex] = react.useState(null);
701
+ const scrollRef = react.useRef(null);
702
+ const select = react.useCallback(
703
+ (index) => {
704
+ const clamped = Math.max(0, Math.min(sortedItems.length - 1, index));
705
+ setActiveIndex(clamped);
706
+ onSelect?.(sortedItems[clamped]);
707
+ return clamped;
708
+ },
709
+ [onSelect, sortedItems]
710
+ );
711
+ const scrollToItem = react.useCallback(
712
+ (id, behavior) => {
713
+ scrollRef.current?.querySelector(`[data-testid="${testId}-entry-${id}"]`)?.scrollIntoView({ block: "center", behavior });
714
+ },
715
+ [testId]
716
+ );
717
+ const lastNavScrollRef = react.useRef(null);
718
+ const goTo = react.useCallback(
719
+ (index) => {
720
+ const target = sortedItems[select(index)];
721
+ if (target) {
722
+ lastNavScrollRef.current = target.id;
723
+ scrollToItem(target.id, "smooth");
724
+ }
725
+ },
726
+ [select, sortedItems, scrollToItem]
727
+ );
728
+ const selectedKey = (selectedIds ?? []).join("\0");
729
+ react.useEffect(() => {
730
+ const id = selectedKey ? selectedKey.split("\0")[0] : null;
731
+ if (!id) return;
732
+ if (lastNavScrollRef.current === id) {
733
+ lastNavScrollRef.current = null;
734
+ return;
735
+ }
736
+ scrollToItem(id, "auto");
737
+ }, [selectedKey, scrollToItem]);
695
738
  const weeks = react.useMemo(() => {
696
739
  if (dated.length === 0) return [];
697
740
  const times = dated.flatMap((i) => [
@@ -717,22 +760,32 @@ function TimelineCalendar({
717
760
  return out;
718
761
  }, [dated]);
719
762
  const segmentsByWeek = react.useMemo(() => {
720
- return weeks.map((week) => {
763
+ if (weeks.length === 0) return [];
764
+ const gridStartMs = weeks[0][0].getTime();
765
+ const WEEK_MS2 = 7 * DAY_MS;
766
+ const ranged = dated.map((item) => ({
767
+ item,
768
+ start: utcDay(new Date(item.start)),
769
+ end: utcDay(new Date(item.end ?? item.start))
770
+ }));
771
+ ranged.sort((a, b) => {
772
+ const lenA = b.end.getTime() - b.start.getTime();
773
+ const lenB = a.end.getTime() - a.start.getTime();
774
+ return lenA - lenB || a.start.getTime() - b.start.getTime();
775
+ });
776
+ const buckets = weeks.map(() => []);
777
+ for (const r of ranged) {
778
+ const from = Math.max(0, Math.floor((r.start.getTime() - gridStartMs) / WEEK_MS2));
779
+ const to = Math.min(weeks.length - 1, Math.floor((r.end.getTime() - gridStartMs) / WEEK_MS2));
780
+ for (let w = from; w <= to; w++) buckets[w].push(r);
781
+ }
782
+ return weeks.map((week, wi) => {
721
783
  const weekStart = week[0];
722
784
  const weekEnd = week[6];
723
- const overlapping = dated.map((item) => ({
724
- item,
725
- start: utcDay(new Date(item.start)),
726
- end: utcDay(new Date(item.end ?? item.start))
727
- })).filter(({ start, end }) => start <= weekEnd && end >= weekStart).sort((a, b) => {
728
- const lenA = b.end.getTime() - b.start.getTime();
729
- const lenB = a.end.getTime() - a.start.getTime();
730
- return lenA - lenB || a.start.getTime() - b.start.getTime();
731
- });
732
785
  const laneEnds = [];
733
786
  const segments = [];
734
787
  let overflow = 0;
735
- for (const { item, start, end } of overlapping) {
788
+ for (const { item, start, end } of buckets[wi]) {
736
789
  const from = start < weekStart ? weekStart : start;
737
790
  const to = end > weekEnd ? weekEnd : end;
738
791
  const startCol = Math.round((from.getTime() - weekStart.getTime()) / DAY_MS);
@@ -759,9 +812,52 @@ function TimelineCalendar({
759
812
  if (weeks.length === 0 && emptyState) {
760
813
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: emptyState });
761
814
  }
815
+ const selectedSet = new Set(selectedIds ?? []);
816
+ const selectedIndex = sortedItems.findIndex((i) => selectedSet.has(i.id));
817
+ const currentIndex = selectedIndex >= 0 ? selectedIndex : activeIndex;
818
+ const navigator = /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-0.5", "data-testid": `${testId}-nav`, children: [
819
+ /* @__PURE__ */ jsxRuntime.jsx(
820
+ "button",
821
+ {
822
+ type: "button",
823
+ "aria-label": "Previous event",
824
+ "data-testid": `${testId}-nav-prev`,
825
+ disabled: sortedItems.length === 0 || currentIndex === null || currentIndex <= 0,
826
+ onClick: () => goTo((currentIndex ?? 0) - 1),
827
+ className: "rounded p-0.5 text-muted-foreground enabled:hover:bg-accent enabled:hover:text-foreground disabled:opacity-30",
828
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4" })
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsxRuntime.jsxs(
832
+ "span",
833
+ {
834
+ className: "tabular-nums text-sm font-light text-muted-foreground",
835
+ "data-testid": `${testId}-nav-position`,
836
+ children: [
837
+ currentIndex !== null ? currentIndex + 1 : "\u2013",
838
+ " /",
839
+ " ",
840
+ navigatorTotal ?? (sortedItems.length > 0 ? sortedItems.length : "\u2013")
841
+ ]
842
+ }
843
+ ),
844
+ /* @__PURE__ */ jsxRuntime.jsx(
845
+ "button",
846
+ {
847
+ type: "button",
848
+ "aria-label": "Next event",
849
+ "data-testid": `${testId}-nav-next`,
850
+ disabled: sortedItems.length === 0 || currentIndex !== null && currentIndex >= sortedItems.length - 1,
851
+ onClick: () => goTo(currentIndex === null ? 0 : currentIndex + 1),
852
+ className: "rounded p-0.5 text-muted-foreground enabled:hover:bg-accent enabled:hover:text-foreground disabled:opacity-30",
853
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" })
854
+ }
855
+ )
856
+ ] });
762
857
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ui.cn("flex flex-col h-full", className), "data-testid": testId, children: [
858
+ navigatorContainer === void 0 ? navigator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center justify-end border-b border-border px-2 py-1", children: navigator }) : navigatorContainer && navigator && reactDom.createPortal(navigator, navigatorContainer),
763
859
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 border-b border-border bg-background", children: WEEKDAY_NAMES.map((d) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 py-1 text-[11px] text-muted-foreground text-center", children: d }, d)) }),
764
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", "data-testid": `${testId}-scroll`, children: weeks.map((week, wi) => {
860
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto", "data-testid": `${testId}-scroll`, children: weeks.map((week, wi) => {
765
861
  const { segments, overflow } = segmentsByWeek[wi];
766
862
  const monthStartDay = week.find((d) => d.getUTCDate() === 1);
767
863
  const marker = monthStartDay ?? (wi === 0 ? week[0] : void 0);
@@ -796,13 +892,13 @@ function TimelineCalendar({
796
892
  "div",
797
893
  {
798
894
  className: "grid grid-cols-7 gap-y-0.5 px-0.5 pb-1 bg-muted/40",
799
- style: { gridAutoRows: "minmax(18px, auto)", minHeight: 62 },
895
+ style: { gridAutoRows: "18px", minHeight: 62 },
800
896
  children: [
801
897
  segments.map((seg) => /* @__PURE__ */ jsxRuntime.jsx(
802
898
  "button",
803
899
  {
804
900
  type: "button",
805
- onClick: () => onSelect?.(seg.item),
901
+ onClick: () => select(indexById.get(seg.item.id) ?? 0),
806
902
  title: `${formatSpan(seg.item.start, seg.item.end)} \u2014 ${seg.item.label}`,
807
903
  "data-testid": `${testId}-entry-${seg.item.id}`,
808
904
  style: {
@@ -810,8 +906,13 @@ function TimelineCalendar({
810
906
  gridRow: seg.lane + 1
811
907
  },
812
908
  className: ui.cn(
813
- "text-left text-[10px] leading-[13px] py-[2px] px-1 min-h-[18px] whitespace-normal break-words line-clamp-3",
814
- isPrecise(seg.item) ? "bg-primary/20 text-foreground" : "bg-muted text-muted-foreground",
909
+ // One line per event (truncated), so several events stack
910
+ // compactly on the same day instead of a few tall bars
911
+ // pushing the rest into a "+n more". Every event is the
912
+ // SAME neutral colour; the one open in the composer is
913
+ // marked by the caller (a ring), not by a different fill.
914
+ "block h-full w-full truncate text-left text-[10px] leading-[14px] py-[1px] px-1",
915
+ "bg-muted text-muted-foreground",
815
916
  seg.isStart && "rounded-l",
816
917
  seg.isEnd && "rounded-r"
817
918
  ),
@@ -839,38 +940,152 @@ function TimelineCalendar({
839
940
  }) })
840
941
  ] });
841
942
  }
842
- var BAR_PALETTE = [
843
- {
844
- gradient: "linear-gradient(90deg, #6366f1, #818cf8)",
845
- shadow: "0 2px 6px rgba(99,102,241,.35)"
846
- },
847
- {
848
- gradient: "linear-gradient(90deg, #8b5cf6, #a78bfa)",
849
- shadow: "0 2px 6px rgba(139,92,246,.35)"
850
- },
851
- { gradient: "linear-gradient(90deg, #ef4444, #f87171)", shadow: "0 2px 6px rgba(239,68,68,.35)" },
852
- {
853
- gradient: "linear-gradient(90deg, #f59e0b, #fbbf24)",
854
- shadow: "0 2px 6px rgba(245,158,11,.35)"
855
- },
856
- { gradient: "linear-gradient(90deg, #22c55e, #4ade80)", shadow: "0 2px 6px rgba(34,197,94,.35)" },
857
- { gradient: "linear-gradient(90deg, #06b6d4, #22d3ee)", shadow: "0 2px 6px rgba(6,182,212,.35)" }
858
- ];
943
+ var TIMELINE_ZOOM = { MIN: 1, MAX: 10, STEP: 0.25 };
944
+ function TimelineZoomControl({
945
+ zoom,
946
+ onZoomChange,
947
+ className,
948
+ testId = "timeline-gantt"
949
+ }) {
950
+ return /* @__PURE__ */ jsxRuntime.jsxs(
951
+ "div",
952
+ {
953
+ className: ui.cn(
954
+ "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1",
955
+ className
956
+ ),
957
+ "data-testid": `${testId}-zoom`,
958
+ children: [
959
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZoomOut, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
960
+ /* @__PURE__ */ jsxRuntime.jsx(
961
+ "input",
962
+ {
963
+ type: "range",
964
+ "aria-label": "Timeline zoom",
965
+ "data-testid": `${testId}-zoom-slider`,
966
+ min: TIMELINE_ZOOM.MIN,
967
+ max: TIMELINE_ZOOM.MAX,
968
+ step: TIMELINE_ZOOM.STEP,
969
+ value: zoom,
970
+ onChange: (e) => onZoomChange(Number(e.target.value)),
971
+ className: "h-1 w-24 cursor-pointer accent-primary"
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZoomIn, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
975
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "w-11 shrink-0 text-right text-[11px] tabular-nums text-muted-foreground", children: [
976
+ Math.round(zoom * 100),
977
+ "%"
978
+ ] })
979
+ ]
980
+ }
981
+ );
982
+ }
983
+ var NEUTRAL_BAR = "#94a3b8";
984
+ function barBackground(colors) {
985
+ const c = (colors ?? []).filter(Boolean).slice(0, 3);
986
+ if (c.length === 0) return NEUTRAL_BAR;
987
+ if (c.length === 1) return c[0];
988
+ const step = 100 / c.length;
989
+ const stops = c.map((col, i) => `${col} ${i * step}% ${(i + 1) * step}%`).join(", ");
990
+ return `linear-gradient(180deg, ${stops})`;
991
+ }
859
992
  var WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
860
- function formatAxisDate(d) {
861
- return d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
993
+ function formatAxisDate(d, intervalDays) {
994
+ if (intervalDays < 45) {
995
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
996
+ }
997
+ if (intervalDays < 365 * 2) {
998
+ return d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
999
+ }
1000
+ return d.toLocaleDateString("en-US", { year: "numeric" });
862
1001
  }
863
1002
  function TimelineGantt({
864
1003
  items,
865
1004
  onSelect,
1005
+ selectedIds,
866
1006
  renderDetail,
867
1007
  labelWidth = 240,
1008
+ onLabelWidthChange,
1009
+ minLabelWidth = 120,
1010
+ maxLabelWidth = 600,
1011
+ zoom: zoomProp,
1012
+ onZoomChange,
1013
+ navigatorContainer,
1014
+ navigatorTotal,
868
1015
  emptyMessage = "Nothing dated yet.",
869
1016
  className,
870
1017
  testId = "timeline-gantt"
871
1018
  }) {
872
1019
  const [activeIndex, setActiveIndex] = react.useState(null);
1020
+ const [internalZoom, setInternalZoom] = react.useState(1);
1021
+ const zoomControlled = zoomProp != null && onZoomChange != null;
1022
+ const zoomValue = zoomControlled ? zoomProp : internalZoom;
1023
+ const setZoomValue = zoomControlled ? onZoomChange : setInternalZoom;
1024
+ const dragRef = react.useRef(null);
1025
+ const chartRef = react.useRef(null);
1026
+ const gutterBodyRef = react.useRef(null);
1027
+ const startResize = (e) => {
1028
+ if (!onLabelWidthChange) return;
1029
+ e.preventDefault();
1030
+ dragRef.current = { startX: e.clientX, startWidth: labelWidth };
1031
+ const onMove = (ev) => {
1032
+ const drag = dragRef.current;
1033
+ if (!drag) return;
1034
+ const next = Math.min(
1035
+ maxLabelWidth,
1036
+ Math.max(minLabelWidth, drag.startWidth + (ev.clientX - drag.startX))
1037
+ );
1038
+ onLabelWidthChange(next);
1039
+ };
1040
+ const onUp = () => {
1041
+ dragRef.current = null;
1042
+ window.removeEventListener("mousemove", onMove);
1043
+ window.removeEventListener("mouseup", onUp);
1044
+ };
1045
+ window.addEventListener("mousemove", onMove);
1046
+ window.addEventListener("mouseup", onUp);
1047
+ };
873
1048
  const rows = items.filter((i) => parseIso2(i.start) !== null).slice().sort((a, b) => parseIso2(a.start).getTime() - parseIso2(b.start).getTime());
1049
+ const select = (index) => {
1050
+ setActiveIndex(index);
1051
+ onSelect?.(index === null ? null : rows[index]);
1052
+ };
1053
+ const selectedSet = new Set(selectedIds ?? []);
1054
+ const selectedRowIndex = rows.findIndex((r) => selectedSet.has(r.id));
1055
+ const currentIndex = selectedRowIndex >= 0 ? selectedRowIndex : activeIndex;
1056
+ const goTo = (index) => select(Math.max(0, Math.min(rows.length - 1, index)));
1057
+ const navigator = /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-0.5 text-sm font-light", "data-testid": `${testId}-nav`, children: [
1058
+ /* @__PURE__ */ jsxRuntime.jsx(
1059
+ "button",
1060
+ {
1061
+ type: "button",
1062
+ "aria-label": "Previous event",
1063
+ "data-testid": `${testId}-nav-prev`,
1064
+ disabled: rows.length === 0 || currentIndex === null || currentIndex <= 0,
1065
+ onClick: () => goTo((currentIndex ?? 0) - 1),
1066
+ className: "rounded p-0.5 text-muted-foreground enabled:hover:bg-accent enabled:hover:text-foreground disabled:opacity-30",
1067
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4" })
1068
+ }
1069
+ ),
1070
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "tabular-nums text-muted-foreground", "data-testid": `${testId}-nav-position`, children: [
1071
+ currentIndex !== null ? currentIndex + 1 : "\u2013",
1072
+ " /",
1073
+ " ",
1074
+ navigatorTotal ?? (rows.length > 0 ? rows.length : "\u2013")
1075
+ ] }),
1076
+ /* @__PURE__ */ jsxRuntime.jsx(
1077
+ "button",
1078
+ {
1079
+ type: "button",
1080
+ "aria-label": "Next event",
1081
+ "data-testid": `${testId}-nav-next`,
1082
+ disabled: rows.length === 0 || currentIndex !== null && currentIndex >= rows.length - 1,
1083
+ onClick: () => goTo(currentIndex === null ? 0 : currentIndex + 1),
1084
+ className: "rounded p-0.5 text-muted-foreground enabled:hover:bg-accent enabled:hover:text-foreground disabled:opacity-30",
1085
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" })
1086
+ }
1087
+ )
1088
+ ] });
874
1089
  if (rows.length === 0) {
875
1090
  return /* @__PURE__ */ jsxRuntime.jsxs(
876
1091
  "div",
@@ -882,7 +1097,8 @@ function TimelineGantt({
882
1097
  ),
883
1098
  children: [
884
1099
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Calendar, { className: "w-12 h-12 mb-3 opacity-30" }),
885
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: emptyMessage })
1100
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: emptyMessage }),
1101
+ navigatorContainer && reactDom.createPortal(navigator, navigatorContainer)
886
1102
  ]
887
1103
  }
888
1104
  );
@@ -898,89 +1114,200 @@ function TimelineGantt({
898
1114
  const axisMax = new Date(maxTime + pad);
899
1115
  const axisRange = axisMax.getTime() - axisMin.getTime();
900
1116
  const toPercent = (d) => Math.min(100, Math.max(0, (d.getTime() - axisMin.getTime()) / axisRange * 100));
1117
+ const tickCount = Math.max(2, Math.min(50, Math.round(8 * zoomValue) + 1));
1118
+ const tickStep = axisRange / (tickCount - 1);
1119
+ const tickIntervalDays = tickStep / DAY_MS;
901
1120
  const ticks = Array.from(
902
- { length: 5 },
903
- (_, i) => new Date(axisMin.getTime() + axisRange * i / 4)
1121
+ { length: tickCount },
1122
+ (_, i) => new Date(axisMin.getTime() + tickStep * i)
904
1123
  );
905
1124
  const today = /* @__PURE__ */ new Date();
906
1125
  const todayPct = toPercent(today);
907
1126
  const showToday = today > axisMin && today < axisMax;
908
1127
  const activeItem = activeIndex !== null ? rows[activeIndex] : null;
909
1128
  const detail = activeItem ? renderDetail?.(activeItem) : null;
910
- const select = (index) => {
911
- setActiveIndex(index);
912
- onSelect?.(index === null ? null : rows[index]);
913
- };
914
1129
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ui.cn("flex h-full min-h-0", className), children: [
915
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 p-5 overflow-x-auto", children: [
916
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex mb-1", children: [
1130
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col p-5", children: [
1131
+ /* @__PURE__ */ jsxRuntime.jsx(
1132
+ "div",
1133
+ {
1134
+ "aria-hidden": true,
1135
+ "data-testid": `${testId}-gutter-divider`,
1136
+ style: { left: `calc(20px + ${labelWidth}px)` },
1137
+ className: "pointer-events-none absolute top-0 bottom-0 z-30 w-[3px] -translate-x-1/2 bg-border"
1138
+ }
1139
+ ),
1140
+ onLabelWidthChange && /* @__PURE__ */ jsxRuntime.jsx(
1141
+ "div",
1142
+ {
1143
+ role: "separator",
1144
+ "aria-orientation": "vertical",
1145
+ "aria-label": "Resize event column",
1146
+ "data-testid": `${testId}-label-resizer`,
1147
+ onMouseDown: startResize,
1148
+ style: { left: `calc(20px + ${labelWidth}px)` },
1149
+ className: "absolute top-0 bottom-0 z-30 w-2 -translate-x-1/2 cursor-col-resize group",
1150
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto h-full w-[3px] transition-colors group-hover:bg-primary" })
1151
+ }
1152
+ ),
1153
+ !zoomControlled && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-2 top-2 z-30 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx(TimelineZoomControl, { zoom: zoomValue, onZoomChange: setZoomValue, testId }) }),
1154
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex min-h-0 flex-1", children: [
1155
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { width: labelWidth, flexShrink: 0 }, className: "flex min-h-0 flex-col", children: [
1156
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex h-6 shrink-0 items-center gap-1 border-b border-border pr-3 text-sm font-medium text-muted-foreground", children: [
1157
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Event" }),
1158
+ navigatorContainer === void 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto", children: navigator }) : navigatorContainer && reactDom.createPortal(navigator, navigatorContainer)
1159
+ ] }),
1160
+ /* @__PURE__ */ jsxRuntime.jsx(
1161
+ "div",
1162
+ {
1163
+ ref: gutterBodyRef,
1164
+ "data-testid": `${testId}-gutter`,
1165
+ className: "min-h-0 flex-1 overflow-hidden",
1166
+ onWheel: (e) => {
1167
+ if (chartRef.current) chartRef.current.scrollTop += e.deltaY;
1168
+ },
1169
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-4 pb-6", children: rows.map((item, i) => {
1170
+ const isActive = activeIndex === i;
1171
+ const isSelected = selectedIds?.includes(item.id) ?? false;
1172
+ const highlighted = isActive || isSelected;
1173
+ const zebra = i % 2 === 1;
1174
+ return (
1175
+ // Fixed h-9 = the track row's h-7 bar + py-1, so the two
1176
+ // columns stay row-for-row aligned. Clicking the label
1177
+ // selects the row exactly like its bar does — the label IS
1178
+ // the event's title, so it should be a target too. Keep the
1179
+ // `break-words leading-snug` tail on THIS element — a scoped
1180
+ // CSS rule (.facts-gantt .break-words.leading-snug) clamps it
1181
+ // to one line. A selected row gets the tint plus a left
1182
+ // accent bar, mirroring the facts table's edited-row mark.
1183
+ /* @__PURE__ */ jsxRuntime.jsx(
1184
+ "div",
1185
+ {
1186
+ onClick: () => select(isActive ? null : i),
1187
+ className: ui.cn(
1188
+ "flex h-9 cursor-pointer items-center rounded border-l-[3px] pr-3 text-xs font-medium",
1189
+ isSelected ? "border-l-primary bg-primary/10" : zebra ? "border-l-transparent bg-muted" : "border-l-transparent bg-background",
1190
+ highlighted ? "text-foreground" : "text-muted-foreground",
1191
+ "break-words leading-snug"
1192
+ ),
1193
+ title: item.label,
1194
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1 truncate", children: [
1195
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-1 tabular-nums text-muted-foreground/60", children: [
1196
+ i + 1,
1197
+ "."
1198
+ ] }),
1199
+ item.label
1200
+ ] })
1201
+ },
1202
+ item.id
1203
+ )
1204
+ );
1205
+ }) })
1206
+ }
1207
+ )
1208
+ ] }),
917
1209
  /* @__PURE__ */ jsxRuntime.jsx(
918
1210
  "div",
919
1211
  {
920
- style: { width: labelWidth, flexShrink: 0 },
921
- className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground pb-1",
922
- children: "Event"
923
- }
924
- ),
925
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 relative h-6 border-b border-border", children: ticks.map((tick, i) => /* @__PURE__ */ jsxRuntime.jsx(
926
- "span",
927
- {
928
- style: { left: `${toPercent(tick)}%` },
929
- className: "absolute bottom-1 text-[10px] text-muted-foreground -translate-x-1/2 whitespace-nowrap select-none",
930
- children: formatAxisDate(tick)
931
- },
932
- i
933
- )) })
934
- ] }),
935
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative mt-2", children: [
936
- showToday && /* @__PURE__ */ jsxRuntime.jsxs(
937
- "div",
938
- {
939
- className: "absolute top-0 bottom-0 pointer-events-none z-10",
940
- style: {
941
- left: `calc(${labelWidth}px + (100% - ${labelWidth}px) * ${todayPct / 100})`
1212
+ ref: chartRef,
1213
+ "data-testid": `${testId}-scroller`,
1214
+ onScroll: (e) => {
1215
+ if (gutterBodyRef.current) gutterBodyRef.current.scrollTop = e.currentTarget.scrollTop;
942
1216
  },
943
- children: [
944
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full border-l-2 border-dashed border-muted-foreground/50" }),
945
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-5 -translate-x-1/2 text-[9px] text-muted-foreground bg-background border rounded px-1", children: "today" })
946
- ]
947
- }
948
- ),
949
- rows.map((item, i) => {
950
- const startDate = parseIso2(item.start);
951
- const endDate = parseIso2(item.end ?? null) ?? startDate;
952
- const leftPct = toPercent(startDate);
953
- const widthPct = Math.max(1, toPercent(endDate) - toPercent(startDate));
954
- const color = BAR_PALETTE[i % BAR_PALETTE.length];
955
- const isActive = activeIndex === i;
956
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center mb-2", children: [
957
- /* @__PURE__ */ jsxRuntime.jsx(
958
- "div",
959
- {
960
- style: { width: labelWidth, flexShrink: 0 },
961
- className: "text-xs font-medium text-muted-foreground pr-3 break-words leading-snug",
962
- children: item.label
963
- }
1217
+ className: ui.cn(
1218
+ "scrollbar-subtle relative min-h-0 min-w-0 flex-1 overflow-y-auto",
1219
+ zoomValue > 1 ? "overflow-x-auto" : "overflow-x-hidden"
964
1220
  ),
965
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 h-7 bg-muted/30 border border-border rounded-md relative overflow-visible", children: /* @__PURE__ */ jsxRuntime.jsx(
966
- "button",
1221
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1222
+ "div",
967
1223
  {
968
- "data-testid": `${testId}-bar-${i}`,
969
- "aria-label": item.label,
970
- "aria-pressed": isActive,
971
- style: {
972
- left: `${leftPct}%`,
973
- width: `${widthPct}%`,
974
- background: color.gradient,
975
- boxShadow: isActive ? "0 0 0 2px white, 0 0 0 4px #6366f1" : color.shadow
976
- },
977
- title: formatSpan(item.start, item.end) || void 0,
978
- className: "absolute top-0.5 h-6 min-w-[6px] rounded cursor-pointer transition-[filter] hover:brightness-110 focus:outline-none",
979
- onClick: () => select(isActive ? null : i)
1224
+ className: "relative min-h-full",
1225
+ style: { width: `${zoomValue * 100}%`, minWidth: "100%" },
1226
+ children: [
1227
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-20 mb-1 h-6 border-b border-border bg-background", children: ticks.map((tick, i) => {
1228
+ const isFirst = i === 0;
1229
+ const isLast = i === ticks.length - 1;
1230
+ const edgeShift = isFirst ? "translate-x-1" : isLast ? "-translate-x-full" : "-translate-x-1/2";
1231
+ return /* @__PURE__ */ jsxRuntime.jsx(
1232
+ "span",
1233
+ {
1234
+ style: { left: `${toPercent(tick)}%` },
1235
+ className: `absolute bottom-1 text-[10px] text-muted-foreground whitespace-nowrap select-none ${edgeShift}`,
1236
+ children: formatAxisDate(tick, tickIntervalDays)
1237
+ },
1238
+ i
1239
+ );
1240
+ }) }),
1241
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative pt-4", children: [
1242
+ ticks.map((tick, i) => /* @__PURE__ */ jsxRuntime.jsx(
1243
+ "div",
1244
+ {
1245
+ "aria-hidden": true,
1246
+ className: "pointer-events-none absolute top-0 bottom-0 z-10 border-l-2 border-border/30",
1247
+ style: { left: `${toPercent(tick)}%` }
1248
+ },
1249
+ `grid-${i}`
1250
+ )),
1251
+ showToday && /* @__PURE__ */ jsxRuntime.jsxs(
1252
+ "div",
1253
+ {
1254
+ className: "pointer-events-none absolute top-0 bottom-0 z-10",
1255
+ style: { left: `${todayPct}%` },
1256
+ children: [
1257
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full border-l-2 border-dashed border-muted-foreground/50" }),
1258
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute top-0 -translate-x-1/2 rounded border bg-background px-1 text-[9px] text-muted-foreground", children: "today" })
1259
+ ]
1260
+ }
1261
+ ),
1262
+ rows.map((item, i) => {
1263
+ const startDate = parseIso2(item.start);
1264
+ const endDate = parseIso2(item.end ?? null) ?? startDate;
1265
+ const leftPct = toPercent(startDate);
1266
+ const dayPct = DAY_MS / axisRange * 100;
1267
+ const widthPct = Math.max(dayPct, toPercent(endDate) - toPercent(startDate));
1268
+ const isActive = activeIndex === i;
1269
+ const isSelected = selectedIds?.includes(item.id) ?? false;
1270
+ const highlighted = isActive || isSelected;
1271
+ const zebra = i % 2 === 1;
1272
+ return (
1273
+ // Fixed h-9 (h-7 bar + py-1) — the gutter's label rows use
1274
+ // the same height, which is what keeps the columns aligned.
1275
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-9 py-1", children: /* @__PURE__ */ jsxRuntime.jsx(
1276
+ "div",
1277
+ {
1278
+ className: ui.cn(
1279
+ "relative h-7 overflow-visible rounded-md border border-border",
1280
+ isSelected ? "bg-primary/10" : zebra ? "bg-muted" : "bg-background"
1281
+ ),
1282
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1283
+ "button",
1284
+ {
1285
+ "data-testid": `${testId}-bar-${i}`,
1286
+ "aria-label": item.label,
1287
+ "aria-pressed": highlighted,
1288
+ style: {
1289
+ left: `${leftPct}%`,
1290
+ width: `${widthPct}%`,
1291
+ // Colour-coded by the item's labels (up to three), not by
1292
+ // row order — see barBackground / TimelineItem.colors.
1293
+ background: barBackground(item.colors),
1294
+ boxShadow: highlighted ? "0 0 0 2px white, 0 0 0 4px #6366f1" : "0 1px 3px rgba(0,0,0,.2)"
1295
+ },
1296
+ title: formatSpan(item.start, item.end) || void 0,
1297
+ className: "absolute top-0.5 h-6 min-w-[6px] rounded cursor-pointer transition-[filter] hover:brightness-110 focus:outline-none",
1298
+ onClick: () => select(isActive ? null : i)
1299
+ }
1300
+ )
1301
+ }
1302
+ ) }, item.id)
1303
+ );
1304
+ })
1305
+ ] })
1306
+ ]
980
1307
  }
981
- ) })
982
- ] }, item.id);
983
- })
1308
+ )
1309
+ }
1310
+ )
984
1311
  ] })
985
1312
  ] }),
986
1313
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1010,12 +1337,18 @@ function TimelineGantt({
1010
1337
  }
1011
1338
  ) }),
1012
1339
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1013
- /* @__PURE__ */ jsxRuntime.jsx(
1340
+ /* @__PURE__ */ jsxRuntime.jsxs(
1014
1341
  "div",
1015
1342
  {
1016
1343
  "data-testid": `${testId}-panel-title`,
1017
1344
  className: "text-sm font-semibold text-foreground leading-snug",
1018
- children: activeItem.label
1345
+ children: [
1346
+ activeIndex !== null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-1 tabular-nums text-muted-foreground", children: [
1347
+ activeIndex + 1,
1348
+ "."
1349
+ ] }),
1350
+ activeItem.label
1351
+ ]
1019
1352
  }
1020
1353
  ),
1021
1354
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 mt-1 text-xs text-muted-foreground", children: [
@@ -2038,6 +2371,7 @@ exports.SectionHeading = SectionHeading;
2038
2371
  exports.Spacer = Spacer;
2039
2372
  exports.Steps = Steps;
2040
2373
  exports.THEME_STYLE_ID = THEME_STYLE_ID;
2374
+ exports.TIMELINE_ZOOM = TIMELINE_ZOOM;
2041
2375
  exports.Tabs = Tabs;
2042
2376
  exports.TeamGrid = TeamGrid;
2043
2377
  exports.Testimonial = Testimonial;
@@ -2046,6 +2380,7 @@ exports.ThemeEditor = ThemeEditor;
2046
2380
  exports.Timeline = Timeline;
2047
2381
  exports.TimelineCalendar = TimelineCalendar;
2048
2382
  exports.TimelineGantt = TimelineGantt;
2383
+ exports.TimelineZoomControl = TimelineZoomControl;
2049
2384
  exports.VideoEmbed = VideoEmbed;
2050
2385
  exports.applyFontVars = applyFontVars;
2051
2386
  exports.applyStoredTheme = applyStoredTheme;
@@ -2078,5 +2413,5 @@ exports.toHslTriplet = toHslTriplet;
2078
2413
  exports.useAssetRenderer = useAssetRenderer;
2079
2414
  exports.useAssetResolverActive = useAssetResolverActive;
2080
2415
  exports.utcDay = utcDay;
2081
- //# sourceMappingURL=chunk-6HGMAQLB.cjs.map
2082
- //# sourceMappingURL=chunk-6HGMAQLB.cjs.map
2416
+ //# sourceMappingURL=chunk-DBAUZD7A.cjs.map
2417
+ //# sourceMappingURL=chunk-DBAUZD7A.cjs.map