tickmarkr 1.83.0 → 1.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, useApp, useStdout } from "ink";
3
- import { cloneElement, useInsertionEffect, useMemo, useRef, } from "react";
3
+ import { cloneElement, createContext, useContext, useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore, } from "react";
4
4
  import { GLYPHS, PLAIN_COMPACT_LOCKUP, } from "../../brand.js";
5
5
  import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, keyRosterLines, Panel, PANEL_CHROME_ROWS, ProgressMeter, Sparkline, StatTile, StatusStrip, } from "./components.js";
6
6
  import { fieldReading } from "./derive.js";
7
7
  import { initialRunInteractionState, projectRunKeyEntries, reconcileRunInteraction, RUN_SIDE_RAIL_COLUMN_FLOOR, runPanelFocusOrder, runSideRailVisible, selectableRunViewRowIds, } from "./keys.js";
8
8
  import { DECISIONS_TAB_TITLE, RUN_VIEWS, runViewById, setupSectionAt, SETUP_SECTIONS, } from "./views.js";
9
- import { FRAME_HEADER_GAP, FULL_JOURNAL_ROWS, planFrame, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
9
+ import { FRAME_HEADER_GAP, FRAME_NESTED_ROW_BANDS, FULL_JOURNAL_ROWS, planFrame, RUN_PANEL_HORIZONTAL_CHROME, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
10
+ import { pointerHoverRow, pointerRestingCell, sessionRailOverride, watchPointerRest, watchSessionRailOverride, } from "./pointer.js";
10
11
  import { cellWidth, fitCells, wrapCells } from "./width.js";
11
12
  export { deriveRunCockpitData } from "./derive.js";
12
13
  export { PANEL_CHROME_ROWS } from "./components.js";
13
- /** The width focus and key routing resolve at. */
14
+ /**
15
+ * The width focus and key routing resolve at, from a measured width — the form
16
+ * used before a plan exists. Once one does, `plannedKeyColumns` (pointer.ts)
17
+ * reads the same answer off the bands the plan actually drew.
18
+ */
14
19
  export function runKeyColumns(columns) {
15
20
  return columns >= SIDEBAR_COLUMN_FLOOR
16
21
  ? Math.max(columns, RUN_SIDE_RAIL_COLUMN_FLOOR)
@@ -61,8 +66,6 @@ function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
61
66
  return (_jsxs(CockpitGrid, { columns: columns, columnContents: content, allocation: NARROW_HEADER_ALLOCATION, children: [_jsx(BandLines, { columns: Math.max(1, widths[0]), lines: content[0].lines, emphasis: "strong" }), _jsx(Box, { alignItems: "flex-end", flexDirection: "column", children: _jsx(BandLines, { columns: Math.max(1, widths[1]), lines: content[1].lines, emphasis: rightStrong ? "strong" : "normal" }) })] }));
62
67
  }
63
68
  const RUN_SIDE_RAIL_WIDTH = 16;
64
- /** Border plus horizontal padding charged by Panel around its body. */
65
- const RUN_PANEL_HORIZONTAL_CHROME = 4;
66
69
  /** The header caption: the run's identity line, planned as the header's refinable sub-region. */
67
70
  function frameCaption(data) {
68
71
  return `${data.runId} · ${data.branch} · ${data.status} · ${data.elapsed}`;
@@ -116,6 +119,36 @@ function setupSectionLines(section, data) {
116
119
  return ["No parked decisions in this engagement"];
117
120
  return parked.map((row) => `${row.taskId} · parked · attempt ${row.attempts ?? 0}`);
118
121
  }
122
+ /**
123
+ * The plan's item rows, and the ref the frame registers their painted node
124
+ * through. Every body panel draws its list into this one box, so the rectangle
125
+ * the conformance oracle measures off the paint is the rectangle planFrame
126
+ * planned — which is exactly the pin a field standing beside the plan could
127
+ * never carry. Absent on the unsized legacy surface, which has no plan.
128
+ */
129
+ const ItemRowsBand = createContext(undefined);
130
+ /**
131
+ * The identity of the row a live pointer is resting on, or none. It is drawn
132
+ * state and only drawn state: nothing reads it back, no interaction state
133
+ * carries it, and the one provider is fed by a `hover` prop the frame's own
134
+ * callers supply. A capture supplies none — `capture.ts` builds this frame from
135
+ * data, size and interaction alone — so every golden, anchor and colour capture
136
+ * records the pointerless appearance by construction rather than by a guard
137
+ * stripping a highlight back out afterwards.
138
+ */
139
+ const HoveredRow = createContext(null);
140
+ /**
141
+ * A body panel's list, drawn into the plan's own item rows and nowhere else.
142
+ * The band is consumed by the outermost panel that asks for it — a panel nested
143
+ * inside that one (the RUN view's own journal) is part of the list, not a second
144
+ * claim on the region, so the band is cleared for everything below.
145
+ */
146
+ function ItemRows({ children }) {
147
+ const band = useContext(ItemRowsBand);
148
+ if (band === undefined)
149
+ return _jsx(_Fragment, { children: children });
150
+ return (_jsx(Box, { ref: band.regionRef, flexDirection: "column", height: band.region.rows, width: band.region.columns, flexShrink: 0, children: _jsx(ItemRowsBand.Provider, { value: undefined, children: children }) }));
151
+ }
119
152
  /**
120
153
  * The setup tab's body, drawn inside the same row budget the watch tab's views
121
154
  * are drawn inside — the tab an operator switches to can never be taller than
@@ -207,17 +240,44 @@ function windowedRows(rows, bodyRows, selection) {
207
240
  ...(selectedIndex < 0 ? {} : { selection: selectedIndex - offset }),
208
241
  };
209
242
  }
243
+ /**
244
+ * The identities the body's rows carry in the order the body draws them — the
245
+ * same window `SizedJournalPanel` paints, so a hit on a body row resolves to
246
+ * the row an operator is looking at rather than to a position in a collection
247
+ * the frame scrolled past. A body that draws no selectable list (the overview,
248
+ * the journal tail, an open detail, the help overlay, the decisions tab) owns
249
+ * no rows here.
250
+ */
251
+ export function drawnRunViewRowIds(data, interaction, bodyRows) {
252
+ if (interaction.tab === "setup"
253
+ || interaction.activeView === "run"
254
+ || interaction.opened !== null
255
+ || interaction.help)
256
+ return [];
257
+ const rows = deriveRunViewRows(data, interaction.activeView, interaction.filterQuery);
258
+ return selectableRunViewRowIds(interaction.activeView, windowedRows(rows, bodyRows, interaction.selection).rows.map((row) => row.id));
259
+ }
210
260
  function SizedJournalPanel({ rows, bodyRows, title, selection, focused, }) {
211
261
  const window = windowedRows(rows, bodyRows, selection);
262
+ // The hovered row is carried as its identity, never as a screen row: it was
263
+ // resolved through the plan once, and this panel finds it in the very window
264
+ // it is about to paint or does not draw it at all. A list this pointer cell
265
+ // acts on nothing in — the passive overview tail, the journal's unselectable
266
+ // rows — finds nothing here, so no row is decorated for being passed over.
267
+ const hovered = useContext(HoveredRow);
268
+ const hover = hovered === null
269
+ ? -1
270
+ : window.rows.findIndex((row) => row.id === hovered);
212
271
  const emptyRows = Math.max(0, bodyRows - window.rows.length);
213
272
  const journal = JournalRowPanel({
214
273
  rows: window.rows,
215
274
  title,
216
275
  selection: window.selection,
217
276
  focused,
277
+ ...(hover < 0 ? {} : { hover }),
218
278
  });
219
279
  const journalProps = journal.props;
220
- return cloneElement(journal, {}, journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null);
280
+ return cloneElement(journal, {}, _jsxs(ItemRows, { children: [journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null] }, "items"));
221
281
  }
222
282
  /** A body panel (help overlay, opened event) padded to the journal's rows. */
223
283
  function SizedLinesPanel({ title, lines, bodyRows, wrapColumns, focused, flexGrow, }) {
@@ -226,7 +286,7 @@ function SizedLinesPanel({ title, lines, bodyRows, wrapColumns, focused, flexGro
226
286
  : lines.flatMap((line) => composeBandLine(line, wrapColumns));
227
287
  const visible = drawnLines.slice(0, bodyRows);
228
288
  const emptyRows = Math.max(0, bodyRows - visible.length);
229
- return (_jsxs(Panel, { title: title, focused: focused, flexGrow: flexGrow, children: [visible.map((line, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: line }) }, `${index}:${line}`))), emptyRows > 0 && _jsx(Box, { height: emptyRows })] }));
289
+ return (_jsx(Panel, { title: title, focused: focused, flexGrow: flexGrow, children: _jsxs(ItemRows, { children: [visible.map((line, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: line }) }, `${index}:${line}`))), emptyRows > 0 && _jsx(Box, { height: emptyRows })] }) }));
230
290
  }
231
291
  /** The lines the help overlay draws: every advertised key and its promise. */
232
292
  function helpLines(entries) {
@@ -246,7 +306,7 @@ function RunViewPanel({ data, viewId, interaction, focused, keyEntries, bodyRows
246
306
  const view = runViewById(viewId);
247
307
  const rows = deriveRunViewRows(data, viewId, interaction.filterQuery);
248
308
  if (interaction.help) {
249
- return (_jsx(Panel, { title: "HELP", focused: focused, flexGrow: 1, children: helpLines(keyEntries).map((line) => _jsx(BodyText, { children: line }, line)) }));
309
+ return (_jsx(Panel, { title: "HELP", focused: focused, flexGrow: 1, children: _jsx(ItemRows, { children: helpLines(keyEntries).map((line) => _jsx(BodyText, { children: line }, line)) }) }));
250
310
  }
251
311
  const opened = interaction.opened === null
252
312
  ? undefined
@@ -255,7 +315,7 @@ function RunViewPanel({ data, viewId, interaction, focused, keyEntries, bodyRows
255
315
  return (_jsx(SizedLinesPanel, { title: view.detailTitle, lines: eventLines(opened), bodyRows: bodyRows, wrapColumns: bodyColumns, focused: focused, flexGrow: 1 }));
256
316
  }
257
317
  if (rows.length === 0) {
258
- return (_jsx(Panel, { title: view.title, focused: focused, flexGrow: 1, children: _jsxs(BodyText, { emphasis: "dim", children: ["No ", view.id, " in this engagement"] }) }));
318
+ return (_jsx(Panel, { title: view.title, focused: focused, flexGrow: 1, children: _jsx(ItemRows, { children: _jsxs(BodyText, { emphasis: "dim", children: ["No ", view.id, " in this engagement"] }) }) }));
259
319
  }
260
320
  return (_jsx(SizedJournalPanel, { rows: rows, bodyRows: bodyRows, title: interaction.filterPrompt
261
321
  ? `${view.title} /${interaction.filterQuery}`
@@ -414,12 +474,13 @@ function resolveRunViewContent(bodySpan, bodyColumns, data) {
414
474
  }
415
475
  /** What fills the regions this paint draws, resolved from the plan's own spans. */
416
476
  function resolveRegionContent(plan, data, interaction) {
417
- const bodyRegion = requiredRegion(plan, "body");
418
- const bodySpan = bodyRegion.rows;
477
+ const bodySpan = requiredRegion(plan, "body").rows;
419
478
  const sideRails = plan.band === "sidebar";
420
- const body = Math.max(1, bodySpan - PANEL_CHROME_ROWS);
421
- const bodyColumns = Math.max(1, bodyRegion.columns - RUN_PANEL_HORIZONTAL_CHROME);
422
- const common = { body, bodyColumns, sideRails };
479
+ // The plan's own item rows, consumed unmodified: the paint draws its list at
480
+ // this region and the pointer resolves a hit through it — one geometry.
481
+ const items = requiredRegion(plan, "items");
482
+ const bodyColumns = items.columns;
483
+ const common = { items, sideRails };
423
484
  if (interaction.tab !== "setup" && interaction.activeView === "run") {
424
485
  return {
425
486
  ...common,
@@ -447,11 +508,11 @@ function resolveRegionContent(plan, data, interaction) {
447
508
  /** The RUN view drawn at its planned spans: every row budget arrives resolved. */
448
509
  function LadderRunPanel({ data, content, compactMeterWidth, interaction, keyEntries, focused = true, }) {
449
510
  if (interaction?.help === true) {
450
- return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: content.body, wrapColumns: content.bodyColumns, focused: focused, flexGrow: 1 }));
511
+ return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: content.items.rows, wrapColumns: content.items.columns, focused: focused, flexGrow: 1 }));
451
512
  }
452
- return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: content.stats }), content.progressBar && (content.approvedProgress
453
- ? _jsx(ApprovedProgress, { data: data, caption: content.progressCaption })
454
- : (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }));
513
+ return (_jsx(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: _jsxs(ItemRows, { children: [_jsx(RunStats, { data: data, mode: content.stats }), content.progressBar && (content.approvedProgress
514
+ ? _jsx(ApprovedProgress, { data: data, caption: content.progressCaption })
515
+ : (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }) }));
455
516
  }
456
517
  function LegacyRunPanel({ data, interaction, keyEntries, focused = true, }) {
457
518
  const rows = deriveRunViewRows(data, "journal");
@@ -524,14 +585,14 @@ function RunCockpitPaint({ data, columns, plan, content, interaction, keyProject
524
585
  const setup = currentInteraction.tab === "setup";
525
586
  return (_jsxs(Box, { flexDirection: "column", width: plan.size.columns, height: plan.size.rows, overflow: "hidden", children: [_jsxs(FrameBand, { rows: header.rows, row: true, regionRef: regionRef("header"), children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, height: header.rows, overflow: "hidden", children: _jsx(PlannedHeader, { data: data, plan: plan, columns: plan.size.columns - (caption?.columns ?? 0) }) }), caption !== undefined && (_jsx(Box, { ref: regionRef("caption"), width: caption.columns, height: caption.rows, flexShrink: 0, overflow: "hidden", children: _jsx(PlannedCaption, { data: data, columns: caption.columns }) }))] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule").rows, regionRef: regionRef("rule"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), strip !== undefined && (_jsx(FrameBand, { rows: strip.rows, regionRef: regionRef("strip"), children: _jsx(ViewStrip, { selection: railSelection, columns: strip.columns }) })), _jsxs(FrameBand, { rows: body.rows, row: true, children: [rail !== undefined && (_jsx(Box, { ref: regionRef("rail"), width: rail.columns, height: rail.rows, flexShrink: 0, overflow: "hidden", children: setup
526
587
  ? (_jsx(SectionsBand, { focused: railFocused, selection: railSelection, columns: rail.columns }))
527
- : (_jsx(RailBand, { data: data, focused: railFocused, selection: railSelection, columns: rail.columns, rows: rail.rows, sidebar: plan.sidebar })) })), _jsx(Box, { ref: regionRef("body"), width: body.columns, height: body.rows, flexShrink: 0, flexDirection: "column", overflow: "hidden", children: setup
528
- ? (_jsx(SetupTabPanel, { data: data, selection: railSelection, focused: contentFocused, keyEntries: keyEntries, help: currentInteraction.help, bodyRows: content.body, bodyColumns: content.bodyColumns }))
529
- : activeView === "run"
530
- ? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: content.sideRails ? undefined : 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
531
- : (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: content.body, bodyColumns: content.bodyColumns })) })] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule2").rows, regionRef: regionRef("rule2"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), tail !== undefined && (_jsx(FrameBand, { rows: tail.rows, regionRef: regionRef("tail"), children: _jsx(TailBand, { data: data, rows: tail.rows, columns: tail.columns, pendingWrites: plan.content.tail === "pending-writes" }) })), _jsx(FrameBand, { rows: requiredRegion(plan, "status").rows, regionRef: regionRef("status"), children: _jsx(StatusStrip, { items: reportedStatusItems, width: plan.size.columns }) }), _jsx(FrameBand, { rows: requiredRegion(plan, "keybar").rows, regionRef: regionRef("keybar"), children: _jsx(RunKeybar, { entries: keyEntries, width: plan.size.columns, help: currentInteraction.help }) })] }));
588
+ : (_jsx(RailBand, { data: data, focused: railFocused, selection: railSelection, columns: rail.columns, rows: rail.rows, sidebar: plan.sidebar })) })), _jsx(Box, { ref: regionRef("body"), width: body.columns, height: body.rows, flexShrink: 0, flexDirection: "column", overflow: "hidden", children: _jsx(ItemRowsBand.Provider, { value: { region: content.items, regionRef: regionRef("items") }, children: setup
589
+ ? (_jsx(SetupTabPanel, { data: data, selection: railSelection, focused: contentFocused, keyEntries: keyEntries, help: currentInteraction.help, bodyRows: content.items.rows, bodyColumns: content.items.columns }))
590
+ : activeView === "run"
591
+ ? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: content.sideRails ? undefined : 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
592
+ : (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: content.items.rows, bodyColumns: content.items.columns })) }) })] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule2").rows, regionRef: regionRef("rule2"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), tail !== undefined && (_jsx(FrameBand, { rows: tail.rows, regionRef: regionRef("tail"), children: _jsx(TailBand, { data: data, rows: tail.rows, columns: tail.columns, pendingWrites: plan.content.tail === "pending-writes" }) })), _jsx(FrameBand, { rows: requiredRegion(plan, "status").rows, regionRef: regionRef("status"), children: _jsx(StatusStrip, { items: reportedStatusItems, width: plan.size.columns }) }), _jsx(FrameBand, { rows: requiredRegion(plan, "keybar").rows, regionRef: regionRef("keybar"), children: _jsx(RunKeybar, { entries: keyEntries, width: plan.size.columns, help: currentInteraction.help }) })] }));
532
593
  }
533
594
  /** Resolve every draw-time input; the plan is planFrame's own return, consumed unmodified. */
534
- export function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, }) {
595
+ export function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, railColumns, }) {
535
596
  const suppliedInteraction = interaction ?? initialRunInteractionState();
536
597
  const suppliedRows = deriveRunViewRows(data, suppliedInteraction.activeView, suppliedInteraction.filterQuery);
537
598
  const rowIds = suppliedRows.map((row) => row.id);
@@ -547,6 +608,7 @@ export function planRunCockpitFrame({ data, columns, rows, interaction, keyProje
547
608
  tab: repaired.tab,
548
609
  detail: repaired.opened !== null,
549
610
  captionCells: cellWidth(frameCaption(data)),
611
+ ...(railColumns === undefined ? {} : { railColumns }),
550
612
  };
551
613
  const plan = planFrame(size, view, state);
552
614
  const content = rows === undefined || plan.kind !== "frame"
@@ -565,15 +627,56 @@ export function planRunCockpitFrame({ data, columns, rows, interaction, keyProje
565
627
  };
566
628
  }
567
629
  /** The public surface always enters paint through one measured plan. */
568
- export function RunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, }) {
630
+ export function RunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, onCommittedFrame, }) {
631
+ // The session's panel resize override, read from the pointer layer the live
632
+ // drag feeds and redrawn when the boundary moves. It enters the frame only
633
+ // as an input to this planning call — the drawn frame is the recomputed
634
+ // plan, so a resize can never paint beside the plan. Nothing persists it: a
635
+ // relaunch plans the default layout.
636
+ const railOverride = useSyncExternalStore(watchSessionRailOverride, sessionRailOverride, sessionRailOverride);
569
637
  const plannedFrame = useMemo(() => planRunCockpitFrame({
570
638
  data,
571
639
  columns,
572
640
  rows,
573
641
  interaction,
574
642
  keyProjection,
575
- }), [data, columns, rows, interaction, keyProjection]);
576
- return (_jsx(RunCockpitFrameFromPlan, { data: data, plannedFrame: plannedFrame }));
643
+ railColumns: railOverride ?? undefined,
644
+ }), [data, columns, rows, interaction, keyProjection, railOverride]);
645
+ useEffect(() => {
646
+ onCommittedFrame?.(plannedFrame, data);
647
+ }, [plannedFrame, data, onCommittedFrame]);
648
+ // Where the pointer is resting, read from the pointer layer that the live
649
+ // input path feeds and redrawn when it moves. There is no prop beside it: a
650
+ // hover cannot be handed to this frame at all, only reported to the terminal.
651
+ const resting = useSyncExternalStore(watchPointerRest, pointerRestingCell, pointerRestingCell);
652
+ // And only a frame that publishes its geometry for hit testing can be hovered
653
+ // — publishing it is what makes a pointer report resolve against this frame
654
+ // at all. A capture publishes none and resolves none, so no golden, anchor or
655
+ // colour capture can carry a highlight however the pointer came to rest: the
656
+ // pointerless appearance is what that path draws by construction, not what a
657
+ // guard strips back out of it.
658
+ const hover = onCommittedFrame === undefined ? null : resting;
659
+ const hoveredRow = useMemo(() => {
660
+ const plan = plannedFrame.plan;
661
+ if (hover === null || plan.kind !== "frame"
662
+ || plannedFrame.content === undefined)
663
+ return null;
664
+ const interaction = plannedFrame.interaction;
665
+ // The whole surface a click resolves through, not only the geometry: what a
666
+ // click at this cell ACTS ON is the highlight's answer, and that route
667
+ // passes through the focus the frame is drawn with and the rows a key may
668
+ // stand on. Deriving the highlight from the row lookup alone would advertise
669
+ // a row of the drawn view while the rail marker stands on another — where
670
+ // the click opens the marked view and selects nothing.
671
+ return pointerHoverRow({
672
+ plan,
673
+ interaction,
674
+ drawnRowIds: drawnRunViewRowIds(data, interaction, plannedFrame.content.items.rows),
675
+ rowIds: selectableRunViewRowIds(interaction.activeView, deriveRunViewRows(data, interaction.activeView, interaction.filterQuery)
676
+ .map((row) => row.id)),
677
+ }, hover) ?? null;
678
+ }, [plannedFrame, data, hover]);
679
+ return (_jsx(HoveredRow.Provider, { value: hoveredRow, children: _jsx(RunCockpitFrameFromPlan, { data: data, plannedFrame: plannedFrame }) }));
577
680
  }
578
681
  function requiredRegion(plan, id) {
579
682
  const region = plan.regions.find((candidate) => candidate.id === id);
@@ -588,6 +691,21 @@ function assertPlanSpans(plan) {
588
691
  let nextRow = 0;
589
692
  for (const [id, span] of Object.entries(plan.rowSpans)) {
590
693
  const region = requiredRegion(plan, id);
694
+ const host = FRAME_NESTED_ROW_BANDS[id];
695
+ if (host !== undefined) {
696
+ // A nested band refines its host's rows rather than tiling beside them,
697
+ // so it advances no row cursor — and is held to its span and to staying
698
+ // inside the band that hosts it.
699
+ const within = requiredRegion(plan, host);
700
+ if (region.rows !== span ||
701
+ region.row < within.row ||
702
+ region.row + region.rows > within.row + within.rows ||
703
+ region.column < within.column ||
704
+ region.column + region.columns > within.column + within.columns) {
705
+ throw new Error(`planned row region ${id} has offset/span ${region.row}/${region.rows} column ${region.column}/${region.columns}, expected span ${span} inside ${host} at ${within.row}/${within.rows} column ${within.column}/${within.columns}`);
706
+ }
707
+ continue;
708
+ }
591
709
  const fullWidth = id !== "body";
592
710
  if (region.row !== nextRow ||
593
711
  region.rows !== span ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.83.0",
3
+ "version": "1.84.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",