@ashley-shrok/viewmodel-shell 0.4.6 → 0.4.8

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.
Files changed (2) hide show
  1. package/dist/tui.js +42 -14
  2. package/package.json +1 -1
package/dist/tui.js CHANGED
@@ -989,6 +989,7 @@ function App(props) {
989
989
  focusKey: (o) => map.get(o),
990
990
  interactive,
991
991
  fill: fillViewport,
992
+ fillCols: fillViewport ? vp.cols : undefined,
992
993
  draft: (k) => draftFor(k),
993
994
  onFieldChange: (k, v) => setDraft((s) => ({ ...s, [k]: v })),
994
995
  onFieldSubmit,
@@ -1244,29 +1245,52 @@ export class TuiAdapter {
1244
1245
  }
1245
1246
  }
1246
1247
  /** page & section share the 4 layout presets. Information-honest, not pixel. */
1247
- layoutContainer(layout, children, density, rctx) {
1248
+ layoutContainer(layout, children, density, rctx, topLevel = false) {
1248
1249
  const sp = this.spacing(density);
1249
1250
  const kids = (children ?? []).map((c, i) => this.renderNode(c, this.keyOf(c, i), density, undefined, rctx));
1250
- // 0.4.6when filling a real terminal, the layout spine must carry the
1251
- // terminal width into the panes. Probed fact: Yoga align-stretch ALONE
1252
- // does NOT fill here; an explicit width:"100%" on the content wrapper
1253
- // does. `cards` is intentionally excluded (a uniform small-tile grid by
1254
- // design force-filling defeats the preset). fill=false on the static /
1255
- // non-TTY path {} byte-identical to pre-0.4.6.
1256
- const fillW = rctx.fill ? { width: "100%" } : {};
1251
+ // 0.4.7numeric width ONLY at the page's TOP layout container (it IS
1252
+ // the full terminal width). Everything below fills via Yoga align-stretch
1253
+ // from this numeric anchor (proven on the real-adapter oracle). NESTED
1254
+ // layoutContainer calls (a section's own children) pass topLevel=false
1255
+ // no width they get the correct PANE-relative width via stretch (a
1256
+ // global numeric cols here would overflow a narrower pane). `cards` never
1257
+ // gets it (uniform small-tile grid by design). Not top-level / not
1258
+ // filling ⇒ {} ⇒ byte-identical.
1259
+ const fillW = topLevel && rctx.fillCols ? { width: rctx.fillCols } : {};
1257
1260
  switch (layout) {
1258
1261
  case "split":
1259
- return (_jsx(Box, { flexDirection: "row", gap: sp.gap || 1, ...fillW, children: kids.map((el, i) => (_jsx(Box, { flexGrow: 1, flexShrink: 1, flexBasis: 0, children: rctx.fill ? (_jsx(Box, { width: "100%", flexDirection: "column", children: el })) : (el) }, i))) }));
1262
+ return (_jsx(Box, { flexDirection: "row", gap: sp.gap || 1, ...fillW, children: kids.map((el, i) => (_jsx(Box, { flexGrow: 1, flexShrink: 1, flexBasis: 0, children: el }, i))) }));
1260
1263
  case "cards":
1261
1264
  return (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", gap: sp.gap || 1, children: kids.map((el, i) => (_jsx(Box, { flexGrow: 0, flexShrink: 1, flexBasis: 28, minWidth: 20, children: el }, i))) }));
1262
1265
  case "sidebar": {
1263
1266
  if (kids.length === 0)
1264
1267
  return _jsx(Box, {});
1265
1268
  const [rail, ...rest] = kids;
1269
+ // Deterministic NUMERIC split when filling. `flexGrow` does NOT
1270
+ // distribute reliably past a flexShrink:0 fixed-basis rail (real-
1271
+ // adapter oracle: sidebar stayed 34 at cols 80 AND 160, while `split`
1272
+ // — symmetric flexGrow — scaled). So when filling: fixed numeric rail
1273
+ // + the exact remaining width on the main pane (it then carries the
1274
+ // terminal width to its section via align-stretch). Not filling ⇒ the
1275
+ // ORIGINAL flex props, byte-identical.
1276
+ const cols = topLevel && rctx.fillCols ? rctx.fillCols : 0;
1277
+ const filling = cols > 0;
1278
+ const RAIL = 24;
1279
+ const g = sp.gap || 1;
1280
+ const mw = Math.max(1, cols - RAIL - g);
1266
1281
  if (rest.length === 0) {
1267
- return (_jsx(Box, { flexShrink: 0, flexBasis: 24, minWidth: 18, children: rail }));
1282
+ return filling ? (
1283
+ // flexDirection:column ⇒ the lone section align-stretches to the
1284
+ // numeric width (a default-row box would not width-stretch it).
1285
+ _jsx(Box, { flexDirection: "column", flexShrink: 0, ...fillW, children: rail })) : (_jsx(Box, { flexShrink: 0, flexBasis: 24, minWidth: 18, children: rail }));
1268
1286
  }
1269
- return (_jsxs(Box, { flexDirection: "row", gap: sp.gap || 1, ...fillW, children: [_jsx(Box, { flexShrink: 0, flexBasis: 24, minWidth: 18, children: rail }), _jsx(Box, { flexGrow: 1, flexShrink: 1, flexBasis: 0, children: _jsx(Box, { flexDirection: "column", gap: sp.gap, ...fillW, children: rest }) })] }));
1287
+ return (_jsxs(Box, { flexDirection: "row", gap: g, ...fillW, children: [filling ? (_jsx(Box, { flexShrink: 0, width: RAIL, children: rail })) : (_jsx(Box, { flexShrink: 0, flexBasis: 24, minWidth: 18, children: rail })), filling ? (
1288
+ // Single numeric-width column directly holding the detail
1289
+ // sections — exactly the `stack` shape the oracle proves scales
1290
+ // (a card section align-stretches to a numeric-width parent).
1291
+ // The extra auto-width wrapper that used to sit here broke that
1292
+ // align-stretch chain (oracle: card detail stuck at 38).
1293
+ _jsx(Box, { flexDirection: "column", gap: sp.gap, flexShrink: 1, width: mw, children: rest })) : (_jsx(Box, { flexGrow: 1, flexShrink: 1, flexBasis: 0, children: _jsx(Box, { flexDirection: "column", gap: sp.gap, children: rest }) }))] }));
1270
1294
  }
1271
1295
  default: // "stack" / undefined
1272
1296
  return (_jsx(Box, { flexDirection: "column", gap: sp.gap, ...fillW, children: kids }));
@@ -1278,7 +1302,7 @@ export class TuiAdapter {
1278
1302
  case "page": {
1279
1303
  const d = node.density === "compact" ? "compact" : "comfortable";
1280
1304
  const sp = this.spacing(d);
1281
- return (_jsxs(Box, { flexDirection: "column", gap: sp.gap, ...(rctx.fill ? { width: "100%", flexGrow: 1 } : {}), children: [node.title ? (_jsx(Box, { children: _jsx(Text, { bold: true, underline: true, children: node.title }) })) : null, this.layoutContainer(node.layout, node.children ?? [], d, rctx)] }, key));
1305
+ return (_jsxs(Box, { flexDirection: "column", gap: sp.gap, ...(rctx.fillCols ? { width: rctx.fillCols, flexGrow: 1 } : {}), children: [node.title ? (_jsx(Box, { children: _jsx(Text, { bold: true, underline: true, children: node.title }) })) : null, this.layoutContainer(node.layout, node.children ?? [], d, rctx, true)] }, key));
1282
1306
  }
1283
1307
  case "section": {
1284
1308
  const sp = this.spacing(density);
@@ -1304,10 +1328,14 @@ export class TuiAdapter {
1304
1328
  if (!href) {
1305
1329
  return (_jsx(Box, { children: _jsx(Text, { underline: true, color: inherited?.color, dimColor: inherited?.dim, children: node.label }) }, key));
1306
1330
  }
1307
- // OSC 8 hyperlink. As the SOLE child of its own Box with no wrap, the
1331
+ // OSC 8 hyperlink: ESC ]8;; <uri> BEL <label> ESC ]8;; BEL. The
1332
+ // ESC introducer + BEL terminator are LOAD-BEARING — without them
1333
+ // (a long-latent bug fixed in 0.4.8) this is plain `]8;;…` garbage
1334
+ // text in every terminal. `\x1b`/`\x07` escapes match osc52()'s
1335
+ // proven style. As the SOLE child of its own Box with no wrap, the
1308
1336
  // string-width over-count (string-width does not strip OSC 8) stays
1309
1337
  // contained to this line and cannot corrupt sibling layout.
1310
- const osc = `]8;;${node.href}${node.label}]8;;${copied ? " ✓ copied" : ""}`;
1338
+ const osc = `\x1b]8;;${node.href}\x07${node.label}\x1b]8;;\x07${copied ? " ✓ copied" : ""}`;
1311
1339
  return this.focusWrap(_jsx(Box, { children: _jsx(Text, { wrap: "truncate-end", underline: true, color: inherited?.color, dimColor: inherited?.dim, children: osc }) }, key), focused, key);
1312
1340
  }
1313
1341
  case "stat-bar":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "A server-driven UI framework where the wire format is structured enough that agents can build full-stack apps without ever opening a browser and all UI tests are pure unit tests with no browser runtime. Server returns a JSON tree of typed nodes; a thin TypeScript adapter renders it to DOM. Backend-agnostic — a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
5
5
  "type": "module",
6
6
  "license": "MIT",