gridsmith-ui 0.17.0 → 0.17.2

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.js CHANGED
@@ -1,9 +1,9 @@
1
- import { Button, useAppShell, ControlSizeContext, useDirtyGuard, OverlayCloseCtx, DiscardConfirm, Spinner, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, defaultMaxWidth } from './chunk-J43HCK7Q.js';
2
- export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell, useUnsavedChanges } from './chunk-J43HCK7Q.js';
1
+ import { Button, useAppShell, REACT_VERSION_PROBLEM, ControlSizeContext, useDirtyGuard, OverlayCloseCtx, DiscardConfirm, Spinner, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, defaultMaxWidth } from './chunk-CIVSNXFG.js';
2
+ export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell, useUnsavedChanges } from './chunk-CIVSNXFG.js';
3
3
  import { Skeleton } from './chunk-Z63OSN64.js';
4
4
  export { Skeleton } from './chunk-Z63OSN64.js';
5
- import { FOCUSABLE, usePopupFocus, useOverlayLayer, useFocusTrap, useFocusBeforeOpen, rememberFocus, focusInitial, OVERLAY_CLOSE_ATTR, focusAfterRemoval, rovingTabStop, useGridKeyboard, useNonModalDialog, usePressMarks, useReturnFocus, useCloseOnFocusOut } from './chunk-YQABRUWD.js';
6
- export { useGridKeyboard } from './chunk-YQABRUWD.js';
5
+ import { FOCUSABLE, usePopupFocus, FIELD, ZOOM_KEYS, useOverlayLayer, useFocusTrap, useFocusBeforeOpen, rememberFocus, focusInitial, OVERLAY_CLOSE_ATTR, focusAfterRemoval, rovingTabStop, useGridKeyboard, useNonModalDialog, usePressMarks, useReturnFocus, useCloseOnFocusOut } from './chunk-JXANCXK6.js';
6
+ export { useGridKeyboard } from './chunk-JXANCXK6.js';
7
7
  import { Link } from './chunk-XGSQOMEY.js';
8
8
  export { CodeBlock, Divider, Link } from './chunk-XGSQOMEY.js';
9
9
  import { Badge } from './chunk-PVTGMCVR.js';
@@ -2012,8 +2012,8 @@ function DateRangePicker({
2012
2012
  };
2013
2013
  const previewEnd = value.start && !value.end && hover && hover >= value.start ? hover : null;
2014
2014
  const rangeStart = value.start;
2015
- const rangeEnd = value.end ?? previewEnd;
2016
- const inRange = (d) => !!(rangeStart && rangeEnd && d > rangeStart && d < rangeEnd);
2015
+ const rangeEnd2 = value.end ?? previewEnd;
2016
+ const inRange = (d) => !!(rangeStart && rangeEnd2 && d > rangeStart && d < rangeEnd2);
2017
2017
  const months = [viewDate, addMonths(viewDate, 1)];
2018
2018
  const summary = value.start && value.end ? `${fmt(value.start)} \u2013 ${fmt(value.end)}` : value.start ? `${fmt(value.start)} \u2013 \u2026` : null;
2019
2019
  return /* @__PURE__ */ jsxs("div", { ref, className: cn(className), children: [
@@ -2094,7 +2094,7 @@ function DateRangePicker({
2094
2094
  getMonthGrid(m.getFullYear(), m.getMonth(), weekStartsOn).map((week, w) => /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5", children: week.map((d) => {
2095
2095
  if (!isSameMonth(d, m)) return /* @__PURE__ */ jsx("div", { role: "gridcell" }, d.toISOString());
2096
2096
  const isStart = !!rangeStart && isSameDay2(d, rangeStart);
2097
- const isEnd = !!rangeEnd && isSameDay2(d, rangeEnd);
2097
+ const isEnd = !!rangeEnd2 && isSameDay2(d, rangeEnd2);
2098
2098
  const between = inRange(d);
2099
2099
  const dis = isDisabledDay(d);
2100
2100
  return /* @__PURE__ */ jsx(
@@ -9329,12 +9329,19 @@ var DAY_MS = 864e5;
9329
9329
  var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
9330
9330
  var utcDay = (y, m, d) => Math.round(Date.UTC(y, m, d) / DAY_MS);
9331
9331
  function dayOfDate(d) {
9332
- if (Number.isNaN(d.getTime())) return NaN;
9332
+ const t = d.getTime();
9333
+ if (Number.isNaN(t)) return NaN;
9334
+ const day = utcDay(d.getFullYear(), d.getMonth(), d.getDate());
9335
+ const first = new Date(t);
9336
+ first.setHours(0, 0, 0, 0);
9337
+ if (first.getTime() === t) return day;
9333
9338
  const ms = ((d.getHours() * 60 + d.getMinutes()) * 60 + d.getSeconds()) * 1e3 + d.getMilliseconds();
9334
- return utcDay(d.getFullYear(), d.getMonth(), d.getDate()) + ms / DAY_MS;
9339
+ return day + ms / DAY_MS;
9335
9340
  }
9336
9341
  function dayOf(value) {
9342
+ if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
9337
9343
  if (value instanceof Date) return dayOfDate(value);
9344
+ if (typeof value !== "string") return NaN;
9338
9345
  const m = DATE_ONLY.exec(value.trim());
9339
9346
  if (!m) return dayOfDate(new Date(value));
9340
9347
  const [y, mo, d] = [Number(m[1]), Number(m[2]) - 1, Number(m[3])];
@@ -9347,10 +9354,15 @@ function dateOfDay(day) {
9347
9354
  const u = new Date(whole * DAY_MS);
9348
9355
  return new Date(u.getUTCFullYear(), u.getUTCMonth(), u.getUTCDate(), 0, 0, 0, Math.round((day - whole) * DAY_MS));
9349
9356
  }
9357
+ function isWholeDay(value) {
9358
+ return typeof value === "string" ? DATE_ONLY.test(value.trim()) : Number.isInteger(dayOf(value));
9359
+ }
9350
9360
  function endDayOf(value) {
9351
9361
  const day = dayOf(value);
9352
- const wholeDay2 = typeof value === "string" ? DATE_ONLY.test(value.trim()) : Number.isInteger(day);
9353
- return wholeDay2 ? day + 1 : day;
9362
+ return typeof value !== "number" && isWholeDay(value) ? day + 1 : day;
9363
+ }
9364
+ function rangeEnd(start, end) {
9365
+ return end instanceof Date && !isWholeDay(start) ? dayOf(end) : endDayOf(end);
9354
9366
  }
9355
9367
  var MINOR = { day: "day", week: "week", month: "month", quarter: "quarter" };
9356
9368
  var MAJOR = { day: "month", week: "month", month: "year", quarter: "year" };
@@ -9408,26 +9420,33 @@ function labelsOf(unit, role, day, locale) {
9408
9420
  forms = role === "major" ? [f({ month: "long", year: "numeric" }), f({ month: "short", year: "numeric" }), f({ month: "short" }), f({ month: "narrow" })] : [f({ month: "long" }), f({ month: "short" }), f({ month: "narrow" })];
9409
9421
  } else if (unit === "quarter") forms = [`Q${Math.floor(new Date(day * DAY_MS).getUTCMonth() / 3) + 1}`];
9410
9422
  else forms = [f({ year: "numeric" }), f({ year: "2-digit" })];
9411
- return [...new Set(forms)];
9423
+ return forms;
9412
9424
  }
9413
9425
  var DENSITY = { day: 56, week: 16, month: 4, quarter: 1.5 };
9414
9426
  var todays = /* @__PURE__ */ new WeakMap();
9427
+ var awaiting = /* @__PURE__ */ new WeakSet();
9428
+ function awaitsToday(scale) {
9429
+ return awaiting.has(scale);
9430
+ }
9415
9431
  function calendarDay(day) {
9416
9432
  const d = Math.floor(day);
9417
9433
  return { day: d, weekday: weekday(d), key: keyOf("day", d) };
9418
9434
  }
9419
9435
  function ticks(unit, role, s) {
9420
9436
  const out = [];
9421
- for (let p = periodStart(unit, s.startDay, s.weekStartsOn); p < s.endDay; p = periodAfter(unit, p)) {
9437
+ for (let p = periodStart(unit, s.startDay, s.weekStartsOn); p < s.endDay; ) {
9438
+ const next = periodAfter(unit, p);
9422
9439
  const from = Math.max(p, s.startDay);
9423
- const to = Math.min(periodAfter(unit, p), s.endDay);
9440
+ const to = Math.min(next, s.endDay);
9424
9441
  out.push({ key: keyOf(unit, p), day: from, x: (from - s.startDay) * s.pxPerDay, width: (to - from) * s.pxPerDay, weekday: weekday(from), labels: labelsOf(unit, role, p, s.locale) });
9442
+ if (!(next > p)) break;
9443
+ p = next;
9425
9444
  }
9426
9445
  return out;
9427
9446
  }
9428
9447
  function build(s) {
9429
9448
  const { startDay, pxPerDay } = s;
9430
- const x = (value) => (startIndex(value) - startDay) * pxPerDay;
9449
+ const x = (value) => (dayOf(value) - startDay) * pxPerDay;
9431
9450
  const inRange = (d) => d >= startDay && d < s.endDay;
9432
9451
  const todayDay = Math.floor(s.todayValue);
9433
9452
  const scale = {
@@ -9437,9 +9456,9 @@ function build(s) {
9437
9456
  pxPerDay,
9438
9457
  width: (s.endDay - startDay) * pxPerDay,
9439
9458
  x,
9440
- span: (start, end = start) => {
9459
+ span: (start, end) => {
9441
9460
  const x0 = x(start);
9442
- return { x: x0, width: Math.max(0, (endIndex(end) - startDay) * pxPerDay - x0) };
9461
+ return { x: x0, width: Math.max(0, (rangeEnd(start, end ?? start) - startDay) * pxPerDay - x0) };
9443
9462
  },
9444
9463
  dayAt: (px) => startDay + px / pxPerDay,
9445
9464
  major: ticks(MAJOR[s.zoom], "major", s),
@@ -9452,9 +9471,7 @@ function build(s) {
9452
9471
  todays.set(scale, s.todayValue);
9453
9472
  return scale;
9454
9473
  }
9455
- var toMinute = (day) => Math.round(day * 1440) / 1440;
9456
- var startIndex = (value) => typeof value === "number" ? value : dayOf(value);
9457
- var endIndex = (value) => typeof value === "number" ? value : endDayOf(value);
9474
+ var toMinute = (day) => Math.floor(day * 1440 + 1e-6) / 1440;
9458
9475
  function scaleOver(from, to, zoom, todayValue, weekStartsOn, locale, pxPerDay, pad3) {
9459
9476
  const fallback = Number.isFinite(todayValue) ? Math.floor(todayValue) : 0;
9460
9477
  const a = Number.isFinite(from) ? from : fallback;
@@ -9470,28 +9487,44 @@ function scaleOver(from, to, zoom, todayValue, weekStartsOn, locale, pxPerDay, p
9470
9487
  }
9471
9488
  return build({ zoom, startDay, endDay, pxPerDay: pxPerDay > 0 ? pxPerDay : DENSITY[zoom], todayValue, weekStartsOn, locale });
9472
9489
  }
9473
- function extendTimeScale(scale, width) {
9474
- if (!(width > scale.width) || !(scale.pxPerDay > 0)) return scale;
9490
+ function extendedEnd(scale, width) {
9491
+ if (!(width > scale.width) || !(scale.pxPerDay > 0)) return scale.endDay;
9475
9492
  const unit = MINOR[scale.zoom];
9476
9493
  const last = scale.startDay + Math.ceil(width / scale.pxPerDay) - 1;
9477
- const endDay = periodAfter(unit, periodStart(unit, last, scale.weekStartsOn));
9494
+ return periodAfter(unit, periodStart(unit, last, scale.weekStartsOn));
9495
+ }
9496
+ function extendTimeScaleTo(scale, endDay) {
9497
+ if (!(endDay > scale.endDay)) return scale;
9478
9498
  const todayValue = todays.get(scale) ?? scale.now ?? scale.today ?? NaN;
9479
- return build({ zoom: scale.zoom, startDay: scale.startDay, endDay, pxPerDay: scale.pxPerDay, todayValue, weekStartsOn: scale.weekStartsOn, locale: scale.locale });
9499
+ const extended = build({ zoom: scale.zoom, startDay: scale.startDay, endDay, pxPerDay: scale.pxPerDay, todayValue, weekStartsOn: scale.weekStartsOn, locale: scale.locale });
9500
+ if (awaiting.has(scale)) awaiting.add(extended);
9501
+ return extended;
9480
9502
  }
9503
+ var noSubscription = () => () => {
9504
+ };
9505
+ var inBrowser = () => true;
9506
+ var onServer = () => false;
9481
9507
  function useTimeScale({ start, end, zoom, today, weekStartsOn = 1, locale, pxPerDay, pad: pad3 = 0 }) {
9508
+ const readable = useSyncExternalStore(noSubscription, inBrowser, onServer);
9482
9509
  const [mounted] = useState(() => toMinute(dayOf(/* @__PURE__ */ new Date())));
9483
- const from = startIndex(start);
9484
- const to = endIndex(end);
9485
- const todayValue = today === void 0 ? mounted : toMinute(dayOf(today));
9510
+ const pending = today === void 0 && !readable;
9511
+ const from = dayOf(start);
9512
+ const to = rangeEnd(start, end);
9513
+ const todayValue = today !== void 0 ? toMinute(dayOf(today)) : pending ? NaN : mounted;
9486
9514
  const density = pxPerDay?.[zoom] ?? DENSITY[zoom];
9487
- return useMemo(() => scaleOver(from, to, zoom, todayValue, weekStartsOn, locale, density, pad3), [from, to, zoom, todayValue, weekStartsOn, locale, density, pad3]);
9515
+ return useMemo(() => {
9516
+ const scale = scaleOver(from, to, zoom, todayValue, weekStartsOn, locale, density, pad3);
9517
+ if (pending) awaiting.add(scale);
9518
+ return scale;
9519
+ }, [from, to, zoom, todayValue, weekStartsOn, locale, density, pad3, pending]);
9488
9520
  }
9489
9521
  function timeExtent(ranges) {
9490
9522
  let start = Infinity;
9491
9523
  let end = -Infinity;
9492
9524
  for (const r of ranges) {
9525
+ if (r == null) continue;
9493
9526
  const s = dayOf(r.start);
9494
- const e = endDayOf(r.end ?? r.start);
9527
+ const e = r.end == null ? endDayOf(r.start) : rangeEnd(r.start, r.end);
9495
9528
  if (Number.isFinite(s)) start = Math.min(start, s);
9496
9529
  if (Number.isFinite(e)) end = Math.max(end, e);
9497
9530
  }
@@ -9499,7 +9532,6 @@ function timeExtent(ranges) {
9499
9532
  }
9500
9533
  var DAY_WORDS = { day: "numeric", month: "long" };
9501
9534
  var TIME_WORDS = { hour: "2-digit", minute: "2-digit" };
9502
- var wholeDay = (value) => typeof value === "string" ? DATE_ONLY.test(value.trim()) : Number.isInteger(value instanceof Date ? dayOf(value) : value);
9503
9535
  var SEPARATOR = /\s*[\u2010-\u2015\u2212~\uff5e\u301c-]\s*/;
9504
9536
  var english = (f) => new Intl.Locale(f.resolvedOptions().locale).language === "en";
9505
9537
  function range(f, a, b, to) {
@@ -9518,11 +9550,13 @@ function range(f, a, b, to) {
9518
9550
  if (!joined) out.splice(lastOfStart + 1, 0, to);
9519
9551
  return out.join("");
9520
9552
  }
9521
- function formatDateRange(start, end = start, { locale, to, day = DAY_WORDS, time = TIME_WORDS } = {}) {
9522
- const from = startIndex(start);
9523
- const stop = endIndex(end);
9553
+ function formatDateRange(start, end, words) {
9554
+ const { locale, to, day = DAY_WORDS, time = TIME_WORDS } = words ?? {};
9555
+ const until = end ?? start;
9556
+ const from = dayOf(start);
9557
+ const stop = rangeEnd(start, until);
9524
9558
  if (!Number.isFinite(from) || !Number.isFinite(stop)) return "";
9525
- const timed = !wholeDay(start) || !wholeDay(end);
9559
+ const timed = !isWholeDay(start) || !isWholeDay(until);
9526
9560
  const first = timed ? from : Math.floor(from);
9527
9561
  const last = timed ? Math.max(from, stop) : Math.max(first, Math.ceil(stop) - 1);
9528
9562
  const a = new Date(first * DAY_MS);
@@ -9552,7 +9586,7 @@ function createScrollStore() {
9552
9586
  var TimeViewportCtx = createContext(null);
9553
9587
  var TimeViewportScrollCtx = createContext(null);
9554
9588
  var UNMEASURED = { scrollLeft: 0, visibleWidth: 0 };
9555
- var noSubscription = () => () => {
9589
+ var noSubscription2 = () => () => {
9556
9590
  };
9557
9591
  var unmeasured = () => UNMEASURED;
9558
9592
  function useTimeViewportLayout() {
@@ -9562,7 +9596,7 @@ function useTimeViewportLayout() {
9562
9596
  }
9563
9597
  function useTimeViewport() {
9564
9598
  const store = useContext(TimeViewportScrollCtx);
9565
- const scroll = useSyncExternalStore(store?.subscribe ?? noSubscription, store?.get ?? unmeasured, unmeasured);
9599
+ const scroll = useSyncExternalStore(store?.subscribe ?? noSubscription2, store?.get ?? unmeasured, unmeasured);
9566
9600
  return { ...useTimeViewportLayout(), ...scroll };
9567
9601
  }
9568
9602
  function fitLabels(boxes, measure, { gap = 8, align = "start", visible } = {}) {
@@ -9812,17 +9846,16 @@ var ROW_HEIGHT = 40;
9812
9846
  var MARGIN = 16;
9813
9847
  var WHEEL_STEP = 50;
9814
9848
  var WHEEL_PAUSE = 250;
9815
- var FIELD = "input, select, textarea, [contenteditable='true']";
9816
9849
  var border = "border-[var(--ds-border-subtle)]";
9817
- function writeScroll(el, visibleWidth) {
9818
- el.style.setProperty("--time-scroll", String(el.scrollLeft));
9819
- el.style.setProperty("--time-view-end", String(el.scrollLeft + visibleWidth));
9850
+ function writeScroll(rows, left, visibleWidth) {
9851
+ rows?.style.setProperty("--time-scroll", String(left));
9852
+ rows?.style.setProperty("--time-view-end", String(left + visibleWidth));
9820
9853
  }
9821
9854
  var TimeViewport = forwardRef(function TimeViewport2({
9822
9855
  scale,
9823
9856
  onZoomChange,
9824
9857
  zooms = TIME_ZOOMS,
9825
- labelWidth: labelWidthProp = 240,
9858
+ labelWidth = 240,
9826
9859
  labelHeader,
9827
9860
  header,
9828
9861
  toolbar,
@@ -9837,6 +9870,7 @@ var TimeViewport = forwardRef(function TimeViewport2({
9837
9870
  }, ref) {
9838
9871
  const scrollerRef = useRef(null);
9839
9872
  const headerRef = useRef(null);
9873
+ const rowsRef = useRef(null);
9840
9874
  const [frame, setFrame] = useState({ width: 0, header: 0 });
9841
9875
  useLayoutEffect(() => {
9842
9876
  const el = scrollerRef.current;
@@ -9854,19 +9888,22 @@ var TimeViewport = forwardRef(function TimeViewport2({
9854
9888
  return () => ro.disconnect();
9855
9889
  }, []);
9856
9890
  const phone = frame.width > 0 && frame.width < PHONE_BELOW;
9857
- const labelWidth = phone ? Math.min(labelWidthProp, Math.max(MIN_LABEL, Math.round(frame.width * 0.36))) : labelWidthProp;
9858
- const visibleWidth = Math.max(0, frame.width - labelWidth);
9859
- const drawn = useMemo(() => extendTimeScale(scale, visibleWidth), [scale, visibleWidth]);
9891
+ const pinnedWidth = phone ? Math.min(labelWidth, Math.max(MIN_LABEL, Math.round(frame.width * 0.36))) : labelWidth;
9892
+ const visibleWidth = Math.max(0, frame.width - pinnedWidth);
9893
+ const drawnEnd = extendedEnd(scale, visibleWidth);
9894
+ const drawn = useMemo(() => extendTimeScaleTo(scale, drawnEnd), [scale, drawnEnd]);
9860
9895
  const [store] = useState(createScrollStore);
9861
- const lastLeft = useRef(0);
9862
- const latest = useLatest({ drawn, visibleWidth, labelWidth, headerHeight: frame.header });
9896
+ const wantLeft = useRef(0);
9897
+ const latest = useLatest({ drawn, visibleWidth, labelWidth: pinnedWidth, headerHeight: frame.header });
9863
9898
  const setLeft = useCallback((left) => {
9864
9899
  const el = scrollerRef.current;
9865
9900
  if (!el) return;
9866
- el.scrollLeft = Math.max(0, left);
9867
- lastLeft.current = el.scrollLeft;
9868
- writeScroll(el, latest.current.visibleWidth);
9869
- store.set({ scrollLeft: el.scrollLeft, visibleWidth: latest.current.visibleWidth });
9901
+ const target = Math.max(0, left);
9902
+ el.scrollLeft = target;
9903
+ const now = el.scrollLeft;
9904
+ wantLeft.current = Math.abs(now - target) < 1 ? target : now;
9905
+ writeScroll(rowsRef.current, now, latest.current.visibleWidth);
9906
+ store.set({ scrollLeft: now, visibleWidth: latest.current.visibleWidth });
9870
9907
  }, [store, latest]);
9871
9908
  const api = useMemo(() => {
9872
9909
  const ensureVisible = (x0, x1) => {
@@ -9889,9 +9926,18 @@ var TimeViewport = forwardRef(function TimeViewport2({
9889
9926
  },
9890
9927
  page(direction) {
9891
9928
  const { drawn: s, visibleWidth: vw } = latest.current;
9892
- const target = (scrollerRef.current?.scrollLeft ?? 0) + direction * vw;
9893
- const tick = s.minor.reduce((best, t) => Math.abs(t.x - target) < Math.abs(best - target) ? t.x : best, Infinity);
9894
- setLeft(Number.isFinite(tick) ? tick : target);
9929
+ const left = scrollerRef.current?.scrollLeft ?? 0;
9930
+ const target = left + direction * vw;
9931
+ let to = target;
9932
+ let gap = vw / 2;
9933
+ for (const t of s.minor) {
9934
+ const d = Math.abs(t.x - target);
9935
+ if (d <= gap && (t.x - left) * direction >= 1) {
9936
+ to = t.x;
9937
+ gap = d;
9938
+ }
9939
+ }
9940
+ setLeft(to);
9895
9941
  },
9896
9942
  ensureVisible,
9897
9943
  reveal(target) {
@@ -9912,29 +9958,34 @@ var TimeViewport = forwardRef(function TimeViewport2({
9912
9958
  useImperativeHandle(ref, () => api, [api]);
9913
9959
  const todayAtX = todayAt === "now" ? drawn.now === null ? null : drawn.x(drawn.now) : drawn.today === null ? null : drawn.x(drawn.today) + drawn.pxPerDay / 2;
9914
9960
  const todayX = showToday ? todayAtX : null;
9915
- const layout = useMemo(() => ({ scale: drawn, labelWidth, showToday, todayX, compact: phone, ...api }), [drawn, labelWidth, showToday, todayX, phone, api]);
9961
+ const layout = useMemo(() => ({ scale: drawn, labelWidth: pinnedWidth, showToday, todayX, compact: phone, ...api }), [drawn, pinnedWidth, showToday, todayX, phone, api]);
9916
9962
  const pending = useRef(null);
9917
9963
  const applied = useRef(null);
9918
9964
  useLayoutEffect(() => {
9919
9965
  const el = scrollerRef.current;
9920
9966
  if (!el || visibleWidth <= 0) return;
9921
- writeScroll(el, visibleWidth);
9922
- store.set({ scrollLeft: el.scrollLeft, visibleWidth });
9967
+ const left = el.scrollLeft;
9968
+ const publish = () => {
9969
+ writeScroll(rowsRef.current, left, visibleWidth);
9970
+ store.set({ scrollLeft: left, visibleWidth });
9971
+ };
9923
9972
  const prev = applied.current;
9924
- applied.current = { startDay: drawn.startDay, pxPerDay: drawn.pxPerDay, visibleWidth };
9973
+ let to = null;
9925
9974
  if (!prev) {
9975
+ if (initialDate === void 0 && awaitsToday(drawn)) return publish();
9926
9976
  const at = initialDate !== void 0 ? drawn.x(initialDate) : todayAtX;
9927
- if (at !== null && Number.isFinite(at)) setLeft(at - visibleWidth / 2);
9928
- return;
9977
+ if (at !== null && Number.isFinite(at)) to = at - visibleWidth / 2;
9978
+ } else if (prev.startDay !== drawn.startDay || prev.pxPerDay !== drawn.pxPerDay || prev.visibleWidth !== visibleWidth) {
9979
+ const want = pending.current;
9980
+ pending.current = null;
9981
+ const anchor = want && want.zoom === drawn.zoom ? want : { day: prev.startDay + (wantLeft.current + prev.visibleWidth / 2) / prev.pxPerDay, at: visibleWidth / 2 };
9982
+ to = drawn.x(anchor.day) - anchor.at;
9929
9983
  }
9930
- const rescaled = prev.startDay !== drawn.startDay || prev.pxPerDay !== drawn.pxPerDay;
9931
- if (!rescaled && prev.visibleWidth === visibleWidth) return;
9932
- const want = pending.current;
9933
- pending.current = null;
9934
- const anchor = want && want.zoom === drawn.zoom ? want : { day: prev.startDay + (lastLeft.current + prev.visibleWidth / 2) / prev.pxPerDay, at: visibleWidth / 2 };
9935
- setLeft(drawn.x(anchor.day) - anchor.at);
9984
+ applied.current = { startDay: drawn.startDay, pxPerDay: drawn.pxPerDay, visibleWidth };
9985
+ if (to === null) return publish();
9986
+ setLeft(to);
9936
9987
  const focused = document.activeElement;
9937
- if (focused instanceof HTMLElement && focused !== el && el.contains(focused)) api.reveal(focused);
9988
+ if (prev && focused instanceof HTMLElement && focused !== el && el.contains(focused)) api.reveal(focused);
9938
9989
  }, [drawn, visibleWidth, initialDate, todayAtX, setLeft, store, api]);
9939
9990
  const shaded = [];
9940
9991
  if (shadeDays && drawn.pxPerDay >= SHADE_MIN) {
@@ -9956,7 +10007,7 @@ var TimeViewport = forwardRef(function TimeViewport2({
9956
10007
  const f = document.activeElement;
9957
10008
  if (!(f instanceof HTMLElement) || f === el || !el.contains(f)) return void 0;
9958
10009
  const r = f.getBoundingClientRect();
9959
- const x = (r.left + r.right) / 2 - el.getBoundingClientRect().left - labelWidth;
10010
+ const x = (r.left + r.right) / 2 - el.getBoundingClientRect().left - pinnedWidth;
9960
10011
  return x >= 0 && x <= visibleWidth ? x : void 0;
9961
10012
  };
9962
10013
  const zoom = (direction, at) => {
@@ -9980,10 +10031,11 @@ var TimeViewport = forwardRef(function TimeViewport2({
9980
10031
  };
9981
10032
  const onKeyDown = (e) => {
9982
10033
  if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
10034
+ if (!e.currentTarget.contains(e.target)) return;
9983
10035
  if (e.target instanceof HTMLElement && e.target.closest(FIELD)) return;
9984
- if (e.key === "+" || e.key === "=") {
10036
+ if (ZOOM_KEYS.in.includes(e.key)) {
9985
10037
  if (zoom(-1)) e.preventDefault();
9986
- } else if (e.key === "-" || e.key === "\u2212") {
10038
+ } else if (ZOOM_KEYS.out.includes(e.key)) {
9987
10039
  if (zoom(1)) e.preventDefault();
9988
10040
  } else if (e.shiftKey && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
9989
10041
  e.preventDefault();
@@ -9993,13 +10045,15 @@ var TimeViewport = forwardRef(function TimeViewport2({
9993
10045
  const wheel = useRef({ sum: 0, at: -Infinity });
9994
10046
  const onWheel = useLatest((e) => {
9995
10047
  if (!(e.ctrlKey || e.metaKey) || !onZoomChange) return;
9996
- e.preventDefault();
9997
10048
  const w = wheel.current;
9998
- if (e.timeStamp - w.at < WHEEL_PAUSE) return;
10049
+ if (e.timeStamp - w.at < WHEEL_PAUSE) return e.preventDefault();
10050
+ const direction = e.deltaY < 0 ? -1 : e.deltaY > 0 ? 1 : 0;
10051
+ if (direction === 0 || zoomTo(direction) === null) return;
10052
+ e.preventDefault();
9999
10053
  w.sum += e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1);
10000
10054
  if (Math.abs(w.sum) < WHEEL_STEP) return;
10001
10055
  const el = scrollerRef.current;
10002
- const x = el ? e.clientX - el.getBoundingClientRect().left - labelWidth : NaN;
10056
+ const x = el ? e.clientX - el.getBoundingClientRect().left - pinnedWidth : NaN;
10003
10057
  zoom(w.sum < 0 ? -1 : 1, x >= 0 && x <= visibleWidth ? x : void 0);
10004
10058
  wheel.current = { sum: 0, at: e.timeStamp };
10005
10059
  });
@@ -10030,9 +10084,10 @@ var TimeViewport = forwardRef(function TimeViewport2({
10030
10084
  const onScroll = () => {
10031
10085
  const el = scrollerRef.current;
10032
10086
  if (!el) return;
10033
- lastLeft.current = el.scrollLeft;
10034
- writeScroll(el, visibleWidth);
10035
- store.set({ scrollLeft: el.scrollLeft, visibleWidth });
10087
+ const left = el.scrollLeft;
10088
+ if (Math.abs(left - wantLeft.current) >= 1) wantLeft.current = left;
10089
+ writeScroll(rowsRef.current, left, visibleWidth);
10090
+ store.set({ scrollLeft: left, visibleWidth });
10036
10091
  };
10037
10092
  const headerNode = header === void 0 ? /* @__PURE__ */ jsx(TimeScaleHeader, {}) : header;
10038
10093
  const hasHeader = headerNode != null || labelHeader != null;
@@ -10064,14 +10119,14 @@ var TimeViewport = forwardRef(function TimeViewport2({
10064
10119
  tabIndex: tabStop ? 0 : void 0,
10065
10120
  onScroll,
10066
10121
  className: "peer h-full overflow-auto isolate outline-none",
10067
- style: { scrollPaddingInlineStart: labelWidth, scrollPaddingTop: frame.header },
10068
- children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col", style: { width: labelWidth + drawn.width }, children: [
10122
+ style: { scrollPaddingInlineStart: pinnedWidth, scrollPaddingTop: frame.header },
10123
+ children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col", style: { width: pinnedWidth + drawn.width }, children: [
10069
10124
  hasHeader && /* @__PURE__ */ jsxs("div", { ref: headerRef, className: cn("sticky top-0 z-[3] flex shrink-0 bg-[var(--ds-bg-surface)] border-b-[length:var(--ds-border-width)]", border), children: [
10070
10125
  /* @__PURE__ */ jsx(
10071
10126
  "div",
10072
10127
  {
10073
10128
  className: cn("sticky start-0 z-[1] flex shrink-0 items-end px-ds-3 py-ds-1.5 bg-[var(--ds-bg-surface)] border-e-[length:var(--ds-border-width)] text-xs font-medium text-[var(--ds-text-secondary)]", border),
10074
- style: { width: labelWidth },
10129
+ style: { width: pinnedWidth },
10075
10130
  children: labelHeader
10076
10131
  }
10077
10132
  ),
@@ -10090,18 +10145,18 @@ var TimeViewport = forwardRef(function TimeViewport2({
10090
10145
  )
10091
10146
  ] }),
10092
10147
  /* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 flex-col", children: [
10093
- /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0", style: { insetInlineStart: labelWidth, width: drawn.width }, children: [
10148
+ /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0", style: { insetInlineStart: pinnedWidth, width: drawn.width }, children: [
10094
10149
  shaded.map((b) => /* @__PURE__ */ jsx("span", { "data-shaded-days": "", className: "absolute inset-y-0 bg-[var(--ds-bg-muted)] opacity-70", style: { insetInlineStart: b.x, width: b.width } }, b.key)),
10095
10150
  drawn.minor.slice(1).map((t) => /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 border-s-[length:1px] border-[var(--ds-border-subtle)]", style: { insetInlineStart: t.x } }, t.key)),
10096
10151
  todayX !== null && /* @__PURE__ */ jsx("span", { "data-today-line": "", className: "absolute inset-y-0 -translate-x-1/2 border-s-2 border-[var(--ds-accent-primary)]", style: { insetInlineStart: todayX } })
10097
10152
  ] }),
10098
- children,
10099
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: cn("sticky start-0 z-[2] min-h-0 flex-1 bg-[var(--ds-bg-surface)] border-e-[length:var(--ds-border-width)]", border), style: { width: labelWidth } })
10153
+ /* @__PURE__ */ jsx("div", { ref: rowsRef, "data-time-rows": "", className: "contents", children }),
10154
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: cn("sticky start-0 z-[2] min-h-0 flex-1 bg-[var(--ds-bg-surface)] border-e-[length:var(--ds-border-width)]", border), style: { width: pinnedWidth } })
10100
10155
  ] })
10101
10156
  ] })
10102
10157
  }
10103
10158
  ),
10104
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 peer-focus-visible:shadow-[inset_0_0_0_2px_var(--ds-border-focus)]" })
10159
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 peer-focus-visible:shadow-[inset_0_0_0_2px_var(--ds-border-focus)] peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-transparent" })
10105
10160
  ] })
10106
10161
  ] }) }) })
10107
10162
  );
@@ -10157,7 +10212,7 @@ function TimeBar({ start, end, minWidth = 0, labels, children, className, style,
10157
10212
  {
10158
10213
  ...props,
10159
10214
  ref: barRef,
10160
- className: cn("absolute top-ds-2 bottom-ds-2 flex items-center overflow-clip rounded-[var(--ds-radius-sm)] outline-none focus-visible:shadow-[var(--ds-shadow-focus)]", className),
10215
+ className: cn("absolute top-ds-2 bottom-ds-2 flex items-center overflow-clip rounded-[var(--ds-radius-sm)] outline-none focus-visible:outline-hidden focus-visible:shadow-[var(--ds-shadow-focus)]", className),
10161
10216
  style: { insetInlineStart: x, width: drawnWidth, ...style },
10162
10217
  children: label
10163
10218
  }
@@ -10175,13 +10230,39 @@ var HEAD = 6;
10175
10230
  var BESIDE = STUB + 6;
10176
10231
  var groupKey = (id) => `group:${id}`;
10177
10232
  var itemKey = (id) => `item:${id}`;
10233
+ var asDate = (value) => value instanceof Date || typeof value === "string" ? value : void 0;
10234
+ function datesOf(item) {
10235
+ const start = asDate(item.start);
10236
+ if (start === void 0 || !Number.isFinite(dayOf(start))) return null;
10237
+ if (item.milestone || item.end === void 0 || item.end === null) return { start, end: start };
10238
+ const end = asDate(item.end);
10239
+ return end !== void 0 && Number.isFinite(endDayOf(end)) ? { start, end } : null;
10240
+ }
10241
+ function uniqueById(list) {
10242
+ const seen = /* @__PURE__ */ new Set();
10243
+ const repeated = /* @__PURE__ */ new Set();
10244
+ const unique = [];
10245
+ for (const entry of list) {
10246
+ if (seen.has(entry.id)) repeated.add(entry.id);
10247
+ else {
10248
+ seen.add(entry.id);
10249
+ unique.push(entry);
10250
+ }
10251
+ }
10252
+ return repeated.size ? { unique, repeated: [...repeated] } : { unique: list, repeated: [] };
10253
+ }
10178
10254
  function ganttRows(items, groups, collapsed) {
10179
10255
  const known = new Set(groups.map((g) => g.id));
10180
10256
  const byGroup = /* @__PURE__ */ new Map();
10181
10257
  const loose = [];
10182
10258
  for (const item of items) {
10183
- if (item.group === void 0 || !known.has(item.group)) loose.push(item);
10184
- else byGroup.set(item.group, [...byGroup.get(item.group) ?? [], item]);
10259
+ if (item.group === void 0 || !known.has(item.group)) {
10260
+ loose.push(item);
10261
+ continue;
10262
+ }
10263
+ const list = byGroup.get(item.group);
10264
+ if (list) list.push(item);
10265
+ else byGroup.set(item.group, [item]);
10185
10266
  }
10186
10267
  const shown = groups.filter((g) => byGroup.has(g.id));
10187
10268
  const topLevel = shown.length + loose.length;
@@ -10199,16 +10280,23 @@ function ganttRows(items, groups, collapsed) {
10199
10280
  return rows;
10200
10281
  }
10201
10282
  function itemShape(item, scale) {
10283
+ const dates = datesOf(item);
10284
+ if (!dates) return { kind: item.milestone ? "milestone" : "bar", x0: NaN, x1: NaN, in: NaN, out: NaN };
10202
10285
  if (item.milestone) {
10203
- const centre = scale.x((dayOf(item.start) + endDayOf(item.start)) / 2);
10286
+ const centre = scale.x((dayOf(dates.start) + endDayOf(dates.start)) / 2);
10204
10287
  return { kind: "milestone", x0: centre - TIP, x1: centre + TIP, in: centre - TIP, out: centre + TIP };
10205
10288
  }
10206
- const { x, width } = scale.span(item.start, item.end ?? item.start);
10289
+ const { x, width } = scale.span(dates.start, dates.end);
10207
10290
  const x1 = x + Math.max(width, MIN_BAR);
10208
10291
  return { kind: "bar", x0: x, x1, in: x, out: x1 };
10209
10292
  }
10210
- function groupExtent(items) {
10211
- return timeExtent(items.map((i) => ({ start: i.start, end: i.milestone ? void 0 : i.end })));
10293
+ function itemsExtent(items) {
10294
+ const ranges = [];
10295
+ for (const item of items) {
10296
+ const dates = datesOf(item);
10297
+ if (dates) ranges.push(dates);
10298
+ }
10299
+ return timeExtent(ranges);
10212
10300
  }
10213
10301
  var drawable = (shape) => !!shape && Number.isFinite(shape.x0) && Number.isFinite(shape.x1);
10214
10302
  function ganttArrows(items, rows, shapeOf, collapsed) {
@@ -10249,19 +10337,37 @@ function labelPlace(shape, textWidth, scaleWidth, { pad: pad3 = 8, gap = BESIDE
10249
10337
  return shape.x0 - gap - textWidth >= 0 ? "before" : "after";
10250
10338
  }
10251
10339
  var TONES = {
10252
- neutral: { fill: "bg-[var(--ds-status-neutral)]", on: "text-[var(--ds-text-on-neutral)]" },
10253
- info: { fill: "bg-[var(--ds-status-info)]", on: "text-[var(--ds-text-on-info)]" },
10340
+ neutral: { fill: "bg-[var(--ds-status-neutral)]", on: "text-[var(--ds-text-on-neutral)]", icon: "minus" },
10341
+ info: { fill: "bg-[var(--ds-status-info)]", on: "text-[var(--ds-text-on-info)]", icon: "info" },
10254
10342
  success: { fill: "bg-[var(--ds-status-success)]", on: "text-[var(--ds-text-on-success)]", icon: "check" },
10255
10343
  warning: { fill: "bg-[var(--ds-status-warning)]", on: "text-[var(--ds-text-on-warning)]", icon: "warning" },
10256
10344
  error: { fill: "bg-[var(--ds-status-error)]", on: "text-[var(--ds-text-on-error)]", icon: "error" },
10257
- accent: { fill: "bg-[var(--ds-accent-primary)]", on: "text-[var(--ds-text-on-primary)]" }
10345
+ accent: { fill: "bg-[var(--ds-accent-primary)]", on: "text-[var(--ds-text-on-primary)]", icon: "star" }
10258
10346
  };
10259
10347
  var ICON = 12;
10260
10348
  var ICON_GAP = 4;
10261
10349
  var LEVEL_INDENT = 16;
10262
10350
  var TARGET = 24;
10351
+ var NO_DATES = "no dates";
10263
10352
  var KNOCKOUT = 2;
10353
+ var EDGE = "border-[length:1px] border-transparent";
10354
+ var EDGE_WIDTH = 1;
10264
10355
  var SELECTED = "outline-solid outline-2 outline-offset-2 outline-[var(--ds-text-primary)]";
10356
+ var UNDATED_INSET = 16;
10357
+ var NO_GROUPS = [];
10358
+ var NO_STATUSES = {};
10359
+ var warned = /* @__PURE__ */ new Set();
10360
+ function warnOnce(message) {
10361
+ let dev = false;
10362
+ try {
10363
+ dev = process.env.NODE_ENV !== "production";
10364
+ } catch {
10365
+ dev = false;
10366
+ }
10367
+ if (!dev || warned.has(message)) return;
10368
+ warned.add(message);
10369
+ console.warn(`Gantt: ${message}`);
10370
+ }
10265
10371
  function GanttArrows({ arrows, width, height }) {
10266
10372
  const { labelWidth } = useTimeViewportLayout();
10267
10373
  return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", "data-gantt-arrows": "", className: "pointer-events-none absolute top-0 overflow-visible", style: { insetInlineStart: labelWidth, width, height }, children: arrows.map((a) => /* @__PURE__ */ jsxs("g", { "data-from": a.from, "data-to": a.to, "data-routed": a.routed || void 0, className: "text-[var(--ds-text-muted)]", children: [
@@ -10269,23 +10375,47 @@ function GanttArrows({ arrows, width, height }) {
10269
10375
  /* @__PURE__ */ jsx("path", { d: a.head, fill: "currentColor" })
10270
10376
  ] }, a.key)) });
10271
10377
  }
10378
+ function Cue({ name, size }) {
10379
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-gantt-cue": name, className: "flex shrink-0", children: /* @__PURE__ */ jsx(Icon, { name, size }) });
10380
+ }
10272
10381
  function Legend({ statuses }) {
10273
10382
  const entries = Object.entries(statuses);
10274
10383
  if (!entries.length) return null;
10275
10384
  return /* @__PURE__ */ jsx("ul", { "aria-label": "Status key", className: "ms-auto flex flex-wrap items-center gap-x-ds-3 gap-y-ds-1 ps-ds-2 text-xs text-[var(--ds-text-secondary)]", children: entries.map(([key, s]) => {
10276
10385
  const tone = TONES[s.tone] ?? TONES.neutral;
10277
10386
  return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-ds-1.5", children: [
10278
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("grid size-4 shrink-0 place-items-center rounded-[var(--ds-radius-sm)]", tone.fill, tone.on), children: tone.icon && /* @__PURE__ */ jsx(Icon, { name: tone.icon, size: 10 }) }),
10387
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("grid size-4 shrink-0 place-items-center rounded-[var(--ds-radius-sm)]", EDGE, tone.fill, tone.on), children: /* @__PURE__ */ jsx(Cue, { name: s.icon ?? tone.icon, size: 10 }) }),
10279
10388
  s.label
10280
10389
  ] }, key);
10281
10390
  }) });
10282
10391
  }
10392
+ function NoDates({ cue, selected, className, ...props }) {
10393
+ const { labelWidth } = useTimeViewportLayout();
10394
+ return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center", children: /* @__PURE__ */ jsxs(
10395
+ "span",
10396
+ {
10397
+ ...props,
10398
+ "data-gantt-shape": "undated",
10399
+ className: cn(
10400
+ "sticky flex cursor-pointer items-center gap-ds-1 whitespace-nowrap rounded-[var(--ds-radius-sm)] border-[length:1px] border-dashed border-[var(--ds-border-default)] px-ds-1.5 py-ds-0.5 text-xs text-[var(--ds-text-secondary)]",
10401
+ "outline-none focus-visible:outline-hidden focus-visible:shadow-[var(--ds-shadow-focus)]",
10402
+ selected && SELECTED,
10403
+ className
10404
+ ),
10405
+ style: { insetInlineStart: labelWidth + UNDATED_INSET },
10406
+ children: [
10407
+ cue,
10408
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "No dates" })
10409
+ ]
10410
+ }
10411
+ ) });
10412
+ }
10283
10413
  function Gantt({
10284
10414
  items,
10285
- groups = [],
10415
+ groups = NO_GROUPS,
10286
10416
  selectedId,
10287
10417
  onSelect,
10288
- statuses = {},
10418
+ statuses = NO_STATUSES,
10289
10419
  zoom: zoomProp,
10290
10420
  defaultZoom = "week",
10291
10421
  onZoomChange,
@@ -10293,6 +10423,7 @@ function Gantt({
10293
10423
  weekStartsOn,
10294
10424
  locale,
10295
10425
  renderLabel,
10426
+ describe,
10296
10427
  labelHeader = "Task",
10297
10428
  toolbar,
10298
10429
  "aria-label": ariaLabel,
@@ -10304,31 +10435,53 @@ function Gantt({
10304
10435
  if (zoomProp === void 0) setZoomState(next);
10305
10436
  onZoomChange?.(next);
10306
10437
  };
10438
+ const zoomable = zoomProp === void 0 || onZoomChange !== void 0;
10307
10439
  const [selectedState, setSelectedState] = useState(null);
10308
- const selected = selectedId !== void 0 ? selectedId : selectedState;
10440
+ const [controlled, setControlled] = useState(selectedId !== void 0);
10441
+ const [overridden, setOverridden] = useState(null);
10442
+ if (!controlled && selectedId !== void 0) {
10443
+ setControlled(true);
10444
+ if (selectedState !== null && selectedState !== selectedId) setOverridden(selectedState);
10445
+ }
10446
+ const isControlled = controlled || selectedId !== void 0;
10447
+ const selected = isControlled ? selectedId ?? null : selectedState;
10448
+ useEffect(() => {
10449
+ if (overridden !== null) {
10450
+ warnOnce("selectedId was given a value after the Gantt had kept a selection of its own. To control the selection, pass selectedId from the first render (null for none); to let the Gantt keep it, leave selectedId out.");
10451
+ }
10452
+ }, [overridden]);
10309
10453
  const select = (item) => {
10310
- if (selectedId === void 0) setSelectedState(item?.id ?? null);
10454
+ if (!isControlled) setSelectedState(item?.id ?? null);
10311
10455
  onSelect?.(item);
10312
10456
  };
10313
10457
  const [collapsed, setCollapsed] = useState(() => new Set(groups.filter((g) => g.defaultCollapsed).map((g) => g.id)));
10314
- const extent = useMemo(() => timeExtent(items.map((i) => ({ start: i.start, end: i.milestone ? void 0 : i.end }))), [items]);
10458
+ const plan = useMemo(() => uniqueById(items), [items]);
10459
+ const phases = useMemo(() => uniqueById(groups), [groups]);
10460
+ useEffect(() => {
10461
+ const quoted = (ids) => ids.map((id) => `"${id}"`).join(", ");
10462
+ if (plan.repeated.length) warnOnce(`more than one item has the id ${quoted(plan.repeated)}; only the first of each is shown.`);
10463
+ if (phases.repeated.length) warnOnce(`more than one group has the id ${quoted(phases.repeated)}; only the first of each is shown.`);
10464
+ }, [plan, phases]);
10465
+ const tasks = plan.unique;
10466
+ const extent = useMemo(() => itemsExtent(tasks), [tasks]);
10315
10467
  const scale = useTimeScale({ start: extent.start, end: extent.end, zoom, today, weekStartsOn, locale, pad: 1 });
10316
- const rows = useMemo(() => ganttRows(items, groups, collapsed), [items, groups, collapsed]);
10317
- const shapes = useMemo(() => new Map(items.map((i) => [i.id, itemShape(i, scale)])), [items, scale]);
10318
- const extents = useMemo(() => new Map(rows.filter((r) => r.children).map((r) => [r.group.id, groupExtent(r.children)])), [rows]);
10319
- const arrows = useMemo(() => ganttArrows(items, rows, (i) => shapes.get(i.id), collapsed), [items, rows, shapes, collapsed]);
10468
+ const rows = useMemo(() => ganttRows(tasks, phases.unique, collapsed), [tasks, phases, collapsed]);
10469
+ const shapes = useMemo(() => new Map(tasks.map((i) => [i.id, itemShape(i, scale)])), [tasks, scale]);
10470
+ const extents = useMemo(() => new Map(rows.filter((r) => r.children).map((r) => [r.group.id, itemsExtent(r.children)])), [rows]);
10471
+ const arrows = useMemo(() => ganttArrows(tasks, rows, (i) => shapes.get(i.id), collapsed), [tasks, rows, shapes, collapsed]);
10320
10472
  const byKey = useMemo(() => new Map(rows.map((r) => [r.key, r])), [rows]);
10321
10473
  const names = useMemo(() => {
10322
- const byId = new Map(items.map((i) => [i.id, i]));
10474
+ const byId = new Map(tasks.map((i) => [i.id, i]));
10323
10475
  const list = new Intl.ListFormat(locale, { type: "conjunction" });
10324
10476
  const out = /* @__PURE__ */ new Map();
10325
- for (const item of items) {
10477
+ for (const item of tasks) {
10478
+ const dates = datesOf(item);
10326
10479
  const after = (item.dependsOn ?? []).flatMap((id) => byId.has(id) && id !== item.id ? [byId.get(id).title] : []);
10327
10480
  const status = item.status === void 0 ? void 0 : statuses[item.status];
10328
10481
  out.set(itemKey(item.id), [
10329
10482
  item.title,
10330
10483
  item.milestone ? "milestone" : null,
10331
- formatDateRange(item.start, item.milestone ? item.start : item.end ?? item.start, { locale }),
10484
+ dates ? formatDateRange(dates.start, dates.end, { locale }) : NO_DATES,
10332
10485
  status?.label ?? null,
10333
10486
  after.length ? `after ${list.format(after)}` : null
10334
10487
  ].filter((part) => part).join(", "));
@@ -10337,31 +10490,66 @@ function Gantt({
10337
10490
  if (!row.group || !row.children) continue;
10338
10491
  const e = extents.get(row.group.id);
10339
10492
  const count = row.children.length === 1 ? "1 item" : `${row.children.length} items`;
10340
- out.set(row.key, [row.group.title, formatDateRange(e.start, e.end, { locale }), count].filter((part) => part).join(", "));
10493
+ const span = Number.isFinite(e.start) ? formatDateRange(e.start, e.end, { locale }) : NO_DATES;
10494
+ out.set(row.key, [row.group.title, span, count].filter((part) => part).join(", "));
10341
10495
  }
10342
10496
  return out;
10343
- }, [items, rows, extents, statuses, locale]);
10497
+ }, [tasks, rows, extents, statuses, locale]);
10498
+ const nameOf = (row) => {
10499
+ const name = names.get(row.key) ?? "";
10500
+ const words = row.item && describe ? describe(row.item) : "";
10501
+ return words ? `${name}, ${words}` : name;
10502
+ };
10503
+ const regionLabel = useMemo(() => {
10504
+ const span = Number.isFinite(extent.start) ? formatDateRange(extent.start, extent.end, { locale }) : "";
10505
+ return `${ariaLabel}, ${span || NO_DATES}`;
10506
+ }, [ariaLabel, extent, locale]);
10507
+ const hasStatuses = useMemo(() => Object.keys(statuses).length > 0, [statuses]);
10508
+ const toneOf = (item) => {
10509
+ const status = item.status === void 0 ? void 0 : statuses[item.status];
10510
+ const tone = TONES[status?.tone ?? "neutral"] ?? TONES.neutral;
10511
+ return { tone, cue: hasStatuses ? status?.icon ?? tone.icon : void 0 };
10512
+ };
10344
10513
  const probeRef = useRef(null);
10345
10514
  const measure = useTextMeasure(probeRef);
10346
10515
  const treeRef = useRef(null);
10347
10516
  const viewportRef = useRef(null);
10348
10517
  const [activeState, setActive] = useState(null);
10518
+ const [focusWithin, setFocusWithin] = useState(false);
10349
10519
  const selectedKey = selected === null ? null : itemKey(selected);
10350
- const active = activeState !== null && byKey.has(activeState) ? activeState : selectedKey !== null && byKey.has(selectedKey) ? selectedKey : rows[0]?.key ?? "";
10351
- const toggle = (id) => {
10352
- const key = groupKey(id);
10353
- const closing = !collapsed.has(id);
10354
- setCollapsed((cur) => {
10355
- const next = new Set(cur);
10356
- if (next.has(id)) next.delete(id);
10357
- else next.add(id);
10358
- return next;
10359
- });
10360
- if (closing && byKey.get(active)?.parentKey === key) {
10361
- setActive(key);
10362
- if (treeRef.current?.contains(document.activeElement)) grid.focus(key);
10520
+ const selectedRow = selectedKey !== null && byKey.has(selectedKey) ? selectedKey : null;
10521
+ const lastFocused = activeState !== null && byKey.has(activeState) ? activeState : null;
10522
+ const active = (focusWithin ? lastFocused : null) ?? selectedRow ?? lastFocused ?? rows[0]?.key ?? "";
10523
+ const focusedRow = useRef(null);
10524
+ useLayoutEffect(() => {
10525
+ const was = focusedRow.current;
10526
+ if (!was) return;
10527
+ const row = byKey.get(was.key);
10528
+ if (row) {
10529
+ if (row.index !== was.index) focusedRow.current = { key: was.key, index: row.index };
10530
+ return;
10363
10531
  }
10364
- };
10532
+ focusedRow.current = null;
10533
+ const now = document.activeElement;
10534
+ if (now && now !== document.body && now.isConnected) return;
10535
+ const tree = treeRef.current;
10536
+ if (tree) focusAfterRemoval(tree.querySelectorAll("[data-grid-key]"), was.index);
10537
+ }, [byKey]);
10538
+ const lastSelected = useRef(selected);
10539
+ useEffect(() => {
10540
+ if (lastSelected.current === selected) return;
10541
+ lastSelected.current = selected;
10542
+ const tree = treeRef.current;
10543
+ if (selected === null || !tree || tree.contains(document.activeElement)) return;
10544
+ const el = tree.querySelector(`[data-grid-key="${CSS.escape(itemKey(selected))}"]`);
10545
+ if (el) viewportRef.current?.reveal(el);
10546
+ }, [selected]);
10547
+ const toggle = (id) => setCollapsed((cur) => {
10548
+ const next = new Set(cur);
10549
+ if (next.has(id)) next.delete(id);
10550
+ else next.add(id);
10551
+ return next;
10552
+ });
10365
10553
  const activate = (row) => {
10366
10554
  if (row.group) toggle(row.group.id);
10367
10555
  else if (row.item) select(row.item);
@@ -10417,6 +10605,11 @@ function Gantt({
10417
10605
  }
10418
10606
  grid.onKeyDown(e);
10419
10607
  };
10608
+ const onTreeBlur = (e) => {
10609
+ if (e.currentTarget.contains(e.relatedTarget)) return;
10610
+ setFocusWithin(false);
10611
+ if (e.relatedTarget) focusedRow.current = null;
10612
+ };
10420
10613
  const onRowClick = (row) => {
10421
10614
  if (row.item) select(row.item.id === selected ? null : row.item);
10422
10615
  else if (row.group) toggle(row.group.id);
@@ -10428,25 +10621,33 @@ function Gantt({
10428
10621
  "aria-level": row.level,
10429
10622
  "aria-setsize": row.setSize,
10430
10623
  "aria-posinset": row.posInSet,
10431
- "aria-label": names.get(row.key),
10624
+ "aria-label": nameOf(row),
10432
10625
  "data-grid-key": row.key,
10433
10626
  tabIndex: grid.tabIndexOf(row.key),
10434
- onFocus: () => setActive(row.key)
10627
+ onFocus: () => {
10628
+ setActive(row.key);
10629
+ focusedRow.current = { key: row.key, index: row.index };
10630
+ }
10435
10631
  });
10436
10632
  const renderItem = (row, item) => {
10437
10633
  const shape = shapes.get(item.id);
10438
- if (!drawable(shape)) return null;
10439
- const tone = TONES[item.status !== void 0 && statuses[item.status]?.tone || "neutral"] ?? TONES.neutral;
10634
+ const dates = datesOf(item);
10440
10635
  const isSelected = item.id === selected;
10441
- const textWidth = measure(item.title) + (tone.icon ? ICON + ICON_GAP : 0) + 2 * KNOCKOUT;
10442
- const place = labelPlace(shape, textWidth, scale.width);
10636
+ const { tone, cue } = toneOf(item);
10637
+ if (!dates || !drawable(shape)) {
10638
+ return /* @__PURE__ */ jsx(NoDates, { ...treeitem(row), "aria-selected": isSelected, selected: isSelected, cue: cue && /* @__PURE__ */ jsx(Cue, { name: cue, size: ICON }) });
10639
+ }
10640
+ const textWidth = measure(item.title) + (cue ? ICON + ICON_GAP : 0) + 2 * KNOCKOUT;
10641
+ const place = labelPlace(shape, textWidth + 2 * EDGE_WIDTH, scale.width);
10443
10642
  const beside = place === "after" ? shape.x1 + BESIDE : shape.x0 - BESIDE - textWidth;
10444
- const icon = tone.icon && /* @__PURE__ */ jsx(Icon, { name: tone.icon, size: ICON, className: "shrink-0" });
10445
10643
  const title = /* @__PURE__ */ jsxs(Fragment, { children: [
10446
- icon,
10644
+ cue && /* @__PURE__ */ jsx(Cue, { name: cue, size: ICON }),
10447
10645
  /* @__PURE__ */ jsx("span", { className: "truncate", children: item.title })
10448
10646
  ] });
10449
- const shapeBox = shape.kind === "milestone" ? /* @__PURE__ */ jsx("div", { ...treeitem(row), "aria-selected": isSelected, className: cn("group absolute grid cursor-pointer place-items-center", "outline-none"), style: { insetInlineStart: (shape.x0 + shape.x1) / 2 - TARGET / 2, top: (ROW_HEIGHT2 - TARGET) / 2, width: TARGET, height: TARGET }, children: /* @__PURE__ */ jsx("span", { "data-gantt-shape": "milestone", "data-item": item.id, className: cn("block rotate-45 rounded-[2px] group-focus-visible:shadow-[var(--ds-shadow-focus)]", tone.fill, isSelected && SELECTED), style: { width: DIAMOND, height: DIAMOND } }) }) : (
10647
+ const shapeBox = shape.kind === "milestone" ? (
10648
+ // the target's own outline is its focus indicator in forced colours, which drop the diamond's shadow ring
10649
+ /* @__PURE__ */ jsx("div", { ...treeitem(row), "aria-selected": isSelected, className: "group absolute grid cursor-pointer place-items-center outline-none focus-visible:outline-hidden", style: { insetInlineStart: (shape.x0 + shape.x1) / 2 - TARGET / 2, top: (ROW_HEIGHT2 - TARGET) / 2, width: TARGET, height: TARGET }, children: /* @__PURE__ */ jsx("span", { "data-gantt-shape": "milestone", "data-item": item.id, className: cn("block rotate-45 rounded-[2px] group-focus-visible:shadow-[var(--ds-shadow-focus)]", EDGE, tone.fill, isSelected && SELECTED), style: { width: DIAMOND, height: DIAMOND } }) })
10650
+ ) : (
10450
10651
  // TimeBar keeps the title in view beside the pinned column while the bar's start is scrolled away
10451
10652
  /* @__PURE__ */ jsx(
10452
10653
  TimeBar,
@@ -10455,10 +10656,10 @@ function Gantt({
10455
10656
  "aria-selected": isSelected,
10456
10657
  "data-gantt-shape": "bar",
10457
10658
  "data-item": item.id,
10458
- start: item.start,
10459
- end: item.end ?? item.start,
10659
+ start: dates.start,
10660
+ end: dates.end,
10460
10661
  minWidth: MIN_BAR,
10461
- className: cn("cursor-pointer text-xs font-medium", tone.fill, tone.on, isSelected && SELECTED),
10662
+ className: cn("cursor-pointer text-xs font-medium", EDGE, tone.fill, tone.on, isSelected && SELECTED),
10462
10663
  style: { top: (ROW_HEIGHT2 - BAR_HEIGHT) / 2, bottom: "auto", height: BAR_HEIGHT },
10463
10664
  children: place === "inside" && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex min-w-0 items-center gap-ds-1 overflow-hidden", children: title })
10464
10665
  }
@@ -10479,23 +10680,29 @@ function Gantt({
10479
10680
  )
10480
10681
  ] });
10481
10682
  };
10482
- const renderGroup = (row, extent2) => /* @__PURE__ */ jsxs(Fragment, { children: [
10483
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "absolute inset-0 bg-[var(--ds-bg-muted)] opacity-60" }),
10484
- extent2 && Number.isFinite(extent2.start) && // a 24 px target around the thin summary bar it draws
10485
- /* @__PURE__ */ jsx(
10486
- TimeBar,
10487
- {
10488
- ...treeitem(row),
10489
- "aria-expanded": !collapsed.has(row.group.id),
10490
- "data-gantt-shape": "summary",
10491
- start: extent2.start,
10492
- end: extent2.end,
10493
- minWidth: MIN_BAR,
10494
- className: "cursor-pointer before:absolute before:inset-x-0 before:top-1/2 before:h-ds-2 before:-translate-y-1/2 before:rounded-[var(--ds-radius-full)] before:bg-[var(--ds-text-secondary)]",
10495
- style: { top: (ROW_HEIGHT2 - TARGET) / 2, bottom: "auto", height: TARGET }
10496
- }
10497
- )
10498
- ] });
10683
+ const renderGroup = (row, group) => {
10684
+ const extent2 = extents.get(group.id);
10685
+ const expanded = !collapsed.has(group.id);
10686
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
10687
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "absolute inset-0 bg-[var(--ds-bg-muted)] opacity-60" }),
10688
+ extent2 && Number.isFinite(extent2.start) ? (
10689
+ // a 24 px target around the thin summary bar it draws
10690
+ /* @__PURE__ */ jsx(
10691
+ TimeBar,
10692
+ {
10693
+ ...treeitem(row),
10694
+ "aria-expanded": expanded,
10695
+ "data-gantt-shape": "summary",
10696
+ start: extent2.start,
10697
+ end: extent2.end,
10698
+ minWidth: MIN_BAR,
10699
+ className: "cursor-pointer before:absolute before:inset-x-0 before:top-1/2 before:h-ds-2 before:-translate-y-1/2 before:rounded-[var(--ds-radius-full)] before:border-[length:1px] before:border-transparent before:bg-[var(--ds-text-secondary)]",
10700
+ style: { top: (ROW_HEIGHT2 - TARGET) / 2, bottom: "auto", height: TARGET }
10701
+ }
10702
+ )
10703
+ ) : /* @__PURE__ */ jsx(NoDates, { ...treeitem(row), "aria-expanded": expanded })
10704
+ ] });
10705
+ };
10499
10706
  const label = (row) => {
10500
10707
  if (row.group) {
10501
10708
  return /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "flex min-w-0 items-center gap-ds-1.5 font-medium", children: [
@@ -10513,27 +10720,30 @@ function Gantt({
10513
10720
  ] })
10514
10721
  );
10515
10722
  };
10516
- return /* @__PURE__ */ jsx(
10723
+ const legend = /* @__PURE__ */ jsx(Legend, { statuses });
10724
+ const legendApart = toolbar === false && hasStatuses;
10725
+ const chart = /* @__PURE__ */ jsx(
10517
10726
  TimeViewport,
10518
10727
  {
10519
10728
  ref: viewportRef,
10520
10729
  scale,
10521
- onZoomChange: changeZoom,
10730
+ onZoomChange: zoomable ? changeZoom : void 0,
10522
10731
  labelHeader,
10523
10732
  toolbar: toolbar === false ? false : /* @__PURE__ */ jsxs(Fragment, { children: [
10524
10733
  toolbar,
10525
- /* @__PURE__ */ jsx(Legend, { statuses })
10734
+ legend
10526
10735
  ] }),
10527
10736
  tabStop: false,
10528
- "aria-label": ariaLabel,
10529
- className,
10530
- children: /* @__PURE__ */ jsxs("div", { ref: treeRef, role: "tree", "aria-label": ariaLabel, className: "relative", onKeyDown, children: [
10737
+ "aria-label": regionLabel,
10738
+ className: legendApart ? "h-auto flex-1" : className,
10739
+ children: /* @__PURE__ */ jsxs("div", { ref: treeRef, role: "tree", "aria-label": ariaLabel, className: "relative", onKeyDown, onFocus: () => setFocusWithin(true), onBlur: onTreeBlur, children: [
10531
10740
  /* @__PURE__ */ jsx("span", { ref: probeRef, "aria-hidden": "true", className: "pointer-events-none invisible absolute whitespace-nowrap text-xs font-medium", children: "0" }),
10532
10741
  /* @__PURE__ */ jsx(GanttArrows, { arrows, width: scale.width, height: rows.length * ROW_HEIGHT2 }),
10533
10742
  rows.map((row) => /* @__PURE__ */ jsx(
10534
10743
  TimeRow,
10535
10744
  {
10536
10745
  "data-row-key": row.key,
10746
+ height: ROW_HEIGHT2,
10537
10747
  label: label(row),
10538
10748
  className: "cursor-pointer",
10539
10749
  labelClassName: cn(
@@ -10541,13 +10751,18 @@ function Gantt({
10541
10751
  row.item && row.item.id === selected && "bg-[image:linear-gradient(var(--ds-accent-subtle),var(--ds-accent-subtle))] text-[var(--ds-accent-subtle-text)]"
10542
10752
  ),
10543
10753
  onClick: () => onRowClick(row),
10544
- children: row.group ? renderGroup(row, extents.get(row.group.id)) : renderItem(row, row.item)
10754
+ children: row.group ? renderGroup(row, row.group) : renderItem(row, row.item)
10545
10755
  },
10546
10756
  row.key
10547
10757
  ))
10548
10758
  ] })
10549
10759
  }
10550
10760
  );
10761
+ if (!legendApart) return chart;
10762
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 w-full min-w-0 flex-col", className), children: [
10763
+ /* @__PURE__ */ jsx("div", { className: "flex shrink-0 px-ds-2 py-ds-1.5", children: legend }),
10764
+ chart
10765
+ ] });
10551
10766
  }
10552
10767
  var DEFAULT_OFFSET2 = "var(--ds-space-4)";
10553
10768
  var LAUNCHER_SIZES = {
@@ -13053,6 +13268,7 @@ var PageContainer = forwardRef(function PageContainer2({
13053
13268
  className,
13054
13269
  renderHeader
13055
13270
  }, ref) {
13271
+ if (REACT_VERSION_PROBLEM) throw new Error(REACT_VERSION_PROBLEM);
13056
13272
  const shell = useAppShell();
13057
13273
  const rootRef = useRef(null);
13058
13274
  const setRoot = useCallback((node) => {
@@ -16028,7 +16244,9 @@ function Toast({ variant = "default", message, visible, onDismiss, duration = 5e
16028
16244
  var ToastContext = createContext(null);
16029
16245
  function useToast() {
16030
16246
  const ctx = useContext(ToastContext);
16031
- if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
16247
+ if (!ctx) {
16248
+ throw new Error("useToast must be used within <ToastProvider>: the app shell's (get_app_shell writes it around AppShell). A page outside the shell (sign-in, a public page) shows its messages inline instead: an Alert above the form, and each field's error below the field.");
16249
+ }
16032
16250
  return ctx;
16033
16251
  }
16034
16252
  function useToastOptional() {