@arbidocs/blocks 0.3.177 → 0.3.179

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