@camstack/ui-library 1.2.1 → 1.2.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.
@@ -70,6 +70,12 @@ export type CellState = {
70
70
  readonly kind: 'enabled';
71
71
  readonly modelId: string;
72
72
  readonly overridden?: boolean;
73
+ /**
74
+ * Step-tree device jump (phase 1): a short label of the OTHER same-node
75
+ * device this step runs on (operator manual override). Renders a subtle
76
+ * "→ label" badge on the cell.
77
+ */
78
+ readonly jumpTo?: string;
73
79
  } | {
74
80
  readonly kind: 'disabled';
75
81
  readonly overridden?: boolean;
@@ -77,6 +83,12 @@ export type CellState = {
77
83
  readonly kind: 'na';
78
84
  } | {
79
85
  readonly kind: 'skip';
86
+ /**
87
+ * Step-tree device jump (phase 1): this device's format can't run the
88
+ * step, but it AUTO-jumps to this labelled same-node device — renders a
89
+ * subtle "→ label" badge in place of the "⚠ skip" degrade marker.
90
+ */
91
+ readonly jumpTo?: string;
80
92
  };
81
93
  /**
82
94
  * Fixed column geometry (rem). Both matrices use CSS-grid STATIC track widths
@@ -94,14 +106,25 @@ export interface FlatRow {
94
106
  export declare function flattenTree(nodes: readonly StepTreeNode[]): readonly FlatRow[];
95
107
  /**
96
108
  * Layout decision for the responsive A↔B switch. Pure so it can be unit-tested
97
- * in isolation from the DOM.
109
+ * in isolation from the DOM. An unmeasured container (`width <= 0`) always
110
+ * defaults to the wide matrix so there is no first-paint flash into the
111
+ * fallback.
98
112
  *
99
- * Returns `true` (single-node fallback) when there is MORE than one node but
100
- * the container is too narrow to show the step column plus at least TWO node
101
- * columns side by side. An unmeasured container (`width <= 0`) defaults to the
102
- * wide matrix so there is no first-paint flash into the fallback.
113
+ * Two regimes, because the sticky step column occludes the scroll area on a
114
+ * narrow viewport:
115
+ * - MULTI-node (`nodeCount >= 2`): fall back when the container can't show the
116
+ * step column plus at least TWO node columns side by side — the fallback
117
+ * picks ONE node.
118
+ * - SINGLE node expanded into `columnCount` device sub-columns (the per-node
119
+ * BASE matrix): the wide grid pins a `sticky left-0` step column (`stepColPx`
120
+ * wide); on a phone that column covers nearly the whole viewport, so the
121
+ * horizontal-scroll area behind it shows only slivers of each device column
122
+ * and no column can be brought fully into view. Fall back to the FLUID
123
+ * single-node layout whenever all `columnCount` device columns can't sit
124
+ * beside the step column without scrolling. `columnCount` defaults to
125
+ * `nodeCount` (one column per node) for the multi-node consumers.
103
126
  */
104
- export declare function shouldUseSingleNode(width: number, nodeCount: number, stepColPx: number, agentColPx?: number): boolean;
127
+ export declare function shouldUseSingleNode(width: number, nodeCount: number, stepColPx: number, agentColPx?: number, columnCount?: number): boolean;
105
128
  export declare function SlotHeading({ slot }: {
106
129
  readonly slot: PipelineSlot;
107
130
  }): import("react").JSX.Element;
package/dist/index.cjs CHANGED
@@ -14669,16 +14669,27 @@ function flattenTree(nodes) {
14669
14669
  }
14670
14670
  /**
14671
14671
  * Layout decision for the responsive A↔B switch. Pure so it can be unit-tested
14672
- * in isolation from the DOM.
14673
- *
14674
- * Returns `true` (single-node fallback) when there is MORE than one node but
14675
- * the container is too narrow to show the step column plus at least TWO node
14676
- * columns side by side. An unmeasured container (`width <= 0`) defaults to the
14677
- * wide matrix so there is no first-paint flash into the fallback.
14678
- */
14679
- function shouldUseSingleNode(width, nodeCount, stepColPx, agentColPx = 208) {
14680
- if (nodeCount <= 1) return false;
14672
+ * in isolation from the DOM. An unmeasured container (`width <= 0`) always
14673
+ * defaults to the wide matrix so there is no first-paint flash into the
14674
+ * fallback.
14675
+ *
14676
+ * Two regimes, because the sticky step column occludes the scroll area on a
14677
+ * narrow viewport:
14678
+ * - MULTI-node (`nodeCount >= 2`): fall back when the container can't show the
14679
+ * step column plus at least TWO node columns side by side — the fallback
14680
+ * picks ONE node.
14681
+ * - SINGLE node expanded into `columnCount` device sub-columns (the per-node
14682
+ * BASE matrix): the wide grid pins a `sticky left-0` step column (`stepColPx`
14683
+ * wide); on a phone that column covers nearly the whole viewport, so the
14684
+ * horizontal-scroll area behind it shows only slivers of each device column
14685
+ * and no column can be brought fully into view. Fall back to the FLUID
14686
+ * single-node layout whenever all `columnCount` device columns can't sit
14687
+ * beside the step column without scrolling. `columnCount` defaults to
14688
+ * `nodeCount` (one column per node) for the multi-node consumers.
14689
+ */
14690
+ function shouldUseSingleNode(width, nodeCount, stepColPx, agentColPx = 208, columnCount = nodeCount) {
14681
14691
  if (width <= 0) return false;
14692
+ if (nodeCount <= 1) return width < stepColPx + agentColPx * columnCount;
14682
14693
  return width < stepColPx + agentColPx * 2;
14683
14694
  }
14684
14695
  var SLOT_LABEL = {
@@ -14797,7 +14808,12 @@ function renderCellContent(state) {
14797
14808
  className: "font-mono text-foreground truncate",
14798
14809
  children: state.modelId
14799
14810
  }),
14800
- state.overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14811
+ state.jumpTo !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14812
+ className: "ml-auto shrink-0 text-[8px] font-semibold text-sky-500",
14813
+ title: `runs on ${state.jumpTo}`,
14814
+ children: ["→ ", state.jumpTo]
14815
+ }),
14816
+ state.overridden && state.jumpTo === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14801
14817
  className: "ml-auto shrink-0 text-[8px] font-semibold uppercase text-amber-500",
14802
14818
  children: "ovr"
14803
14819
  })
@@ -14818,14 +14834,24 @@ function renderCellContent(state) {
14818
14834
  })
14819
14835
  ]
14820
14836
  });
14821
- if (state.kind === "skip") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14822
- className: "flex min-w-0 items-center gap-1 text-amber-500",
14823
- title: "Gated on for this camera, but no compatible model on this node — click to provision",
14824
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14825
- className: "truncate font-medium",
14826
- children: " skip"
14827
- })]
14828
- });
14837
+ if (state.kind === "skip") {
14838
+ if (state.jumpTo !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14839
+ className: "flex min-w-0 items-center gap-1 text-sky-500",
14840
+ title: `no build for this device auto-jumps to ${state.jumpTo}`,
14841
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-sky-500 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14842
+ className: "truncate font-medium",
14843
+ children: ["→ ", state.jumpTo]
14844
+ })]
14845
+ });
14846
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14847
+ className: "flex min-w-0 items-center gap-1 text-amber-500",
14848
+ title: "Gated on for this camera, but no compatible model on this node — click to provision",
14849
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14850
+ className: "truncate font-medium",
14851
+ children: "⚠ skip"
14852
+ })]
14853
+ });
14854
+ }
14829
14855
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14830
14856
  className: "flex min-w-0 items-center gap-1 text-foreground-subtle/70",
14831
14857
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-muted shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -15209,7 +15235,7 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15209
15235
  return () => RO?.disconnect();
15210
15236
  }, []);
15211
15237
  const nodes = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
15212
- const single = shouldUseSingleNode(width, nodes.length, STEP_COL_PX);
15238
+ const single = shouldUseSingleNode(width, nodes.length, STEP_COL_PX, 208, agents.length);
15213
15239
  const selectedNode = (0, react$1.useMemo)(() => {
15214
15240
  if (nodes.length === 0) return null;
15215
15241
  return nodes.find((n) => n.agentNodeId === fallbackNodeId) ?? nodes[0] ?? null;
@@ -15218,7 +15244,7 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15218
15244
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15219
15245
  ref: containerRef,
15220
15246
  className: "w-full space-y-2",
15221
- children: [single && selectedNode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectorBar, {
15247
+ children: [single && selectedNode && nodes.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectorBar, {
15222
15248
  nodes,
15223
15249
  selected: selectedNode.agentNodeId,
15224
15250
  onSelect: onFallbackNodeChange
package/dist/index.js CHANGED
@@ -14645,16 +14645,27 @@ function flattenTree(nodes) {
14645
14645
  }
14646
14646
  /**
14647
14647
  * Layout decision for the responsive A↔B switch. Pure so it can be unit-tested
14648
- * in isolation from the DOM.
14649
- *
14650
- * Returns `true` (single-node fallback) when there is MORE than one node but
14651
- * the container is too narrow to show the step column plus at least TWO node
14652
- * columns side by side. An unmeasured container (`width <= 0`) defaults to the
14653
- * wide matrix so there is no first-paint flash into the fallback.
14654
- */
14655
- function shouldUseSingleNode(width, nodeCount, stepColPx, agentColPx = 208) {
14656
- if (nodeCount <= 1) return false;
14648
+ * in isolation from the DOM. An unmeasured container (`width <= 0`) always
14649
+ * defaults to the wide matrix so there is no first-paint flash into the
14650
+ * fallback.
14651
+ *
14652
+ * Two regimes, because the sticky step column occludes the scroll area on a
14653
+ * narrow viewport:
14654
+ * - MULTI-node (`nodeCount >= 2`): fall back when the container can't show the
14655
+ * step column plus at least TWO node columns side by side — the fallback
14656
+ * picks ONE node.
14657
+ * - SINGLE node expanded into `columnCount` device sub-columns (the per-node
14658
+ * BASE matrix): the wide grid pins a `sticky left-0` step column (`stepColPx`
14659
+ * wide); on a phone that column covers nearly the whole viewport, so the
14660
+ * horizontal-scroll area behind it shows only slivers of each device column
14661
+ * and no column can be brought fully into view. Fall back to the FLUID
14662
+ * single-node layout whenever all `columnCount` device columns can't sit
14663
+ * beside the step column without scrolling. `columnCount` defaults to
14664
+ * `nodeCount` (one column per node) for the multi-node consumers.
14665
+ */
14666
+ function shouldUseSingleNode(width, nodeCount, stepColPx, agentColPx = 208, columnCount = nodeCount) {
14657
14667
  if (width <= 0) return false;
14668
+ if (nodeCount <= 1) return width < stepColPx + agentColPx * columnCount;
14658
14669
  return width < stepColPx + agentColPx * 2;
14659
14670
  }
14660
14671
  var SLOT_LABEL = {
@@ -14773,7 +14784,12 @@ function renderCellContent(state) {
14773
14784
  className: "font-mono text-foreground truncate",
14774
14785
  children: state.modelId
14775
14786
  }),
14776
- state.overridden && /* @__PURE__ */ jsx("span", {
14787
+ state.jumpTo !== void 0 && /* @__PURE__ */ jsxs("span", {
14788
+ className: "ml-auto shrink-0 text-[8px] font-semibold text-sky-500",
14789
+ title: `runs on ${state.jumpTo}`,
14790
+ children: ["→ ", state.jumpTo]
14791
+ }),
14792
+ state.overridden && state.jumpTo === void 0 && /* @__PURE__ */ jsx("span", {
14777
14793
  className: "ml-auto shrink-0 text-[8px] font-semibold uppercase text-amber-500",
14778
14794
  children: "ovr"
14779
14795
  })
@@ -14794,14 +14810,24 @@ function renderCellContent(state) {
14794
14810
  })
14795
14811
  ]
14796
14812
  });
14797
- if (state.kind === "skip") return /* @__PURE__ */ jsxs("span", {
14798
- className: "flex min-w-0 items-center gap-1 text-amber-500",
14799
- title: "Gated on for this camera, but no compatible model on this node — click to provision",
14800
- children: [/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }), /* @__PURE__ */ jsx("span", {
14801
- className: "truncate font-medium",
14802
- children: " skip"
14803
- })]
14804
- });
14813
+ if (state.kind === "skip") {
14814
+ if (state.jumpTo !== void 0) return /* @__PURE__ */ jsxs("span", {
14815
+ className: "flex min-w-0 items-center gap-1 text-sky-500",
14816
+ title: `no build for this device auto-jumps to ${state.jumpTo}`,
14817
+ children: [/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-sky-500 shrink-0" }), /* @__PURE__ */ jsxs("span", {
14818
+ className: "truncate font-medium",
14819
+ children: ["→ ", state.jumpTo]
14820
+ })]
14821
+ });
14822
+ return /* @__PURE__ */ jsxs("span", {
14823
+ className: "flex min-w-0 items-center gap-1 text-amber-500",
14824
+ title: "Gated on for this camera, but no compatible model on this node — click to provision",
14825
+ children: [/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }), /* @__PURE__ */ jsx("span", {
14826
+ className: "truncate font-medium",
14827
+ children: "⚠ skip"
14828
+ })]
14829
+ });
14830
+ }
14805
14831
  return /* @__PURE__ */ jsxs("span", {
14806
14832
  className: "flex min-w-0 items-center gap-1 text-foreground-subtle/70",
14807
14833
  children: [/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted shrink-0" }), /* @__PURE__ */ jsx("span", {
@@ -15185,7 +15211,7 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15185
15211
  return () => RO?.disconnect();
15186
15212
  }, []);
15187
15213
  const nodes = useMemo(() => groupAgentColumns(agents), [agents]);
15188
- const single = shouldUseSingleNode(width, nodes.length, STEP_COL_PX);
15214
+ const single = shouldUseSingleNode(width, nodes.length, STEP_COL_PX, 208, agents.length);
15189
15215
  const selectedNode = useMemo(() => {
15190
15216
  if (nodes.length === 0) return null;
15191
15217
  return nodes.find((n) => n.agentNodeId === fallbackNodeId) ?? nodes[0] ?? null;
@@ -15194,7 +15220,7 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15194
15220
  return /* @__PURE__ */ jsxs("div", {
15195
15221
  ref: containerRef,
15196
15222
  className: "w-full space-y-2",
15197
- children: [single && selectedNode && /* @__PURE__ */ jsx(NodeSelectorBar, {
15223
+ children: [single && selectedNode && nodes.length > 1 && /* @__PURE__ */ jsx(NodeSelectorBar, {
15198
15224
  nodes,
15199
15225
  selected: selectedNode.agentNodeId,
15200
15226
  onSelect: onFallbackNodeChange
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",