@valyrianjs/terminal 0.2.1 → 0.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.
Files changed (80) hide show
  1. package/dist/ansi.d.ts.map +1 -1
  2. package/dist/ansi.js +12 -14
  3. package/dist/ansi.js.map +1 -1
  4. package/dist/events.d.ts.map +1 -1
  5. package/dist/events.js +4 -0
  6. package/dist/events.js.map +1 -1
  7. package/dist/frame-style.d.ts +7 -0
  8. package/dist/frame-style.d.ts.map +1 -0
  9. package/dist/frame-style.js +27 -0
  10. package/dist/frame-style.js.map +1 -0
  11. package/dist/layout.d.ts +5 -1
  12. package/dist/layout.d.ts.map +1 -1
  13. package/dist/layout.js +53 -23
  14. package/dist/layout.js.map +1 -1
  15. package/dist/mouse.d.ts.map +1 -1
  16. package/dist/mouse.js +8 -1
  17. package/dist/mouse.js.map +1 -1
  18. package/dist/render.d.ts.map +1 -1
  19. package/dist/render.js +87 -48
  20. package/dist/render.js.map +1 -1
  21. package/dist/session.d.ts.map +1 -1
  22. package/dist/session.js +2 -0
  23. package/dist/session.js.map +1 -1
  24. package/dist/text.d.ts +7 -0
  25. package/dist/text.d.ts.map +1 -1
  26. package/dist/text.js +114 -0
  27. package/dist/text.js.map +1 -1
  28. package/dist/types.d.ts +3 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/docs/api-reference.md +6 -3
  31. package/docs/cookbook.md +1 -1
  32. package/docs/interaction-model.md +5 -5
  33. package/docs/primitive-gallery.md +4 -4
  34. package/examples/basic.tsx +22 -0
  35. package/examples/cli.tsx +55 -0
  36. package/examples/demo.tsx +98 -0
  37. package/examples/docs/background-fill.tsx +107 -0
  38. package/examples/docs/component-composition.tsx +140 -0
  39. package/examples/docs/cursor.tsx +121 -0
  40. package/examples/docs/employees-list.tsx +138 -0
  41. package/examples/docs/hello.tsx +98 -0
  42. package/examples/docs/interactive-note.tsx +111 -0
  43. package/examples/docs/module-api-dashboard.tsx +307 -0
  44. package/examples/docs/module-flux-store.tsx +181 -0
  45. package/examples/docs/module-form-workflow.tsx +339 -0
  46. package/examples/docs/module-forms.tsx +218 -0
  47. package/examples/docs/module-money.tsx +175 -0
  48. package/examples/docs/module-native-store.tsx +188 -0
  49. package/examples/docs/module-pulses.tsx +142 -0
  50. package/examples/docs/module-query.tsx +209 -0
  51. package/examples/docs/module-request.tsx +194 -0
  52. package/examples/docs/module-state-workbench.tsx +283 -0
  53. package/examples/docs/module-tasks.tsx +223 -0
  54. package/examples/docs/module-translate.tsx +194 -0
  55. package/examples/docs/module-utils.tsx +168 -0
  56. package/examples/docs/module-valyrian-core.tsx +159 -0
  57. package/examples/docs/pizza-builder.tsx +463 -0
  58. package/examples/docs/primitive-activity-console.tsx +113 -0
  59. package/examples/docs/primitive-command-panel.tsx +186 -0
  60. package/examples/docs/primitive-data-explorer.tsx +155 -0
  61. package/examples/docs/primitive-input-workbench.tsx +128 -0
  62. package/examples/docs/primitive-layout-shell.tsx +115 -0
  63. package/examples/docs/responsive-split.tsx +186 -0
  64. package/examples/docs/style-system.tsx +209 -0
  65. package/examples/docs/theme-colors.tsx +225 -0
  66. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  67. package/examples/opencode-dogfood-app.tsx +215 -0
  68. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  69. package/examples/opencode-dogfood.tsx +11 -0
  70. package/llms-full.txt +16 -13
  71. package/package.json +3 -2
  72. package/src/ansi.ts +12 -14
  73. package/src/events.ts +2 -0
  74. package/src/frame-style.ts +36 -0
  75. package/src/layout.ts +57 -24
  76. package/src/mouse.ts +10 -1
  77. package/src/render.ts +92 -48
  78. package/src/session.ts +2 -0
  79. package/src/text.ts +148 -0
  80. package/src/types.ts +3 -0
package/dist/render.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { constrainFrame, createFrame, cropFrame, fitFrame, getFrameHeight, getFrameWidth, mergeHorizontal, mergeVertical, overlayFrame, shiftFrame } from "./layout.js";
2
2
  import { getSelectionRange, normalizeInputState } from "./events.js";
3
+ import { markFullFrameSpan, markFullRowSpan } from "./frame-style.js";
3
4
  import { createEditorState } from "./editor-state.js";
4
5
  import { isFocusable, textContent } from "./tree.js";
5
6
  import { renderValyrianTerminal } from "./runtime.js";
6
- import { plainText } from "./text.js";
7
+ import { cursorCellOffset, padEndTerminalCells, plainText, sliceTerminalCells, terminalCellToStringIndex, terminalCellWidth, terminalGraphemes } from "./text.js";
7
8
  import { resolveTerminalStyle } from "./theme.js";
8
9
  function validateRenderContextDimension(name, value) {
9
10
  if (!Number.isInteger(value) || value < 1) {
@@ -148,7 +149,7 @@ function fullFrameSpans(kinds, width, height) {
148
149
  const spans = [];
149
150
  for (const kind of kinds) {
150
151
  for (let y = 1; y <= height; y += 1) {
151
- spans.push({ kind, x1: 1, x2: width + 1, y });
152
+ spans.push(markFullFrameSpan({ kind, x1: 1, x2: width + 1, y }));
152
153
  }
153
154
  }
154
155
  return spans;
@@ -184,7 +185,7 @@ function padFrameSides(frame, padding) {
184
185
  const bottomLines = new Array(padding.bottom).fill(" ".repeat(contentWidth));
185
186
  const lines = [
186
187
  ...topLines,
187
- ...frame.lines.map((line) => `${" ".repeat(padding.left)}${line.padEnd(width, " ")}${" ".repeat(padding.right)}`),
188
+ ...frame.lines.map((line) => `${" ".repeat(padding.left)}${padEndTerminalCells(line, width)}${" ".repeat(padding.right)}`),
188
189
  ...bottomLines
189
190
  ];
190
191
  return shiftFrame(createFrame(lines, frame.hitboxes, frame.cursor, frame.spans), padding.left, padding.top);
@@ -202,7 +203,7 @@ function addBorder(frame, border) {
202
203
  lines.push(`${border.left ? chars.topLeft : chars.horizontal}${chars.horizontal.repeat(Math.max(0, width - left - right))}${border.right ? chars.topRight : chars.horizontal}`.slice(0, width));
203
204
  }
204
205
  for (const line of frame.lines) {
205
- lines.push(`${border.left ? chars.vertical : ""}${line.padEnd(innerWidth, " ")}${border.right ? chars.vertical : ""}`);
206
+ lines.push(`${border.left ? chars.vertical : ""}${padEndTerminalCells(line, innerWidth)}${border.right ? chars.vertical : ""}`);
206
207
  }
207
208
  if (border.bottom) {
208
209
  lines.push(`${border.left ? chars.bottomLeft : chars.horizontal}${chars.horizontal.repeat(Math.max(0, width - left - right))}${border.right ? chars.bottomRight : chars.horizontal}`.slice(0, width));
@@ -226,7 +227,7 @@ function addFullFrameSpans(frame, kinds) {
226
227
  const spans = frame.spans.slice();
227
228
  for (const span of kinds) {
228
229
  for (let y = 1; y <= height; y += 1) {
229
- spans.push({ ...span, x1: 1, x2: width + 1, y });
230
+ spans.push(markFullFrameSpan({ ...span, x1: 1, x2: width + 1, y }));
230
231
  }
231
232
  }
232
233
  return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
@@ -288,8 +289,14 @@ function wrapPlainText(value, width) {
288
289
  continue;
289
290
  }
290
291
  let remaining = sourceRow;
291
- while (remaining.length > width) {
292
- const slice = remaining.slice(0, width);
292
+ while (terminalCellWidth(remaining) > width) {
293
+ const slice = sliceTerminalCells(remaining, width);
294
+ if (slice.length === 0) {
295
+ const [firstGrapheme = ""] = terminalGraphemes(remaining);
296
+ rows.push(firstGrapheme);
297
+ remaining = remaining.slice(firstGrapheme.length);
298
+ continue;
299
+ }
293
300
  const breakAt = slice.lastIndexOf(" ");
294
301
  if (breakAt > 0 && breakAt >= Math.floor(width * 0.6)) {
295
302
  rows.push(remaining.slice(0, breakAt));
@@ -297,7 +304,7 @@ function wrapPlainText(value, width) {
297
304
  }
298
305
  else {
299
306
  rows.push(slice);
300
- remaining = remaining.slice(width);
307
+ remaining = remaining.slice(slice.length);
301
308
  }
302
309
  }
303
310
  rows.push(remaining);
@@ -412,7 +419,8 @@ function decorateContainerFrame(frame, node, options = {}, context) {
412
419
  if ((typeof width === "number" && width > 0) || (typeof height === "number" && height > 0)) {
413
420
  next = constrainFrame(next, {
414
421
  width: typeof width === "number" && width > 0 ? width : undefined,
415
- height: typeof height === "number" && height > 0 ? height : undefined
422
+ height: typeof height === "number" && height > 0 ? height : undefined,
423
+ expandFullFrameSpans: true
416
424
  });
417
425
  }
418
426
  }
@@ -421,7 +429,8 @@ function decorateContainerFrame(frame, node, options = {}, context) {
421
429
  if (options.constrain) {
422
430
  next = constrainFrame(next, {
423
431
  width: typeof options.width === "undefined" ? positiveDimension(node.props.width, "width") : options.width,
424
- height: typeof options.height === "undefined" ? positiveDimension(node.props.height, "height") : options.height
432
+ height: typeof options.height === "undefined" ? positiveDimension(node.props.height, "height") : options.height,
433
+ expandFullFrameSpans: true
425
434
  });
426
435
  }
427
436
  next = addContainerStyleSpans(next, node, resolved);
@@ -473,6 +482,16 @@ function resolveContainerChildContext(node, dimensions, context) {
473
482
  theme: context?.theme
474
483
  };
475
484
  }
485
+ function interactiveTextMetadata(value) {
486
+ const textLength = value.length;
487
+ const textCellToStringIndex = terminalCellToStringIndex(value);
488
+ const usesLinearIndexes = textCellToStringIndex.length === textLength + 1
489
+ && textCellToStringIndex.every((index, cellOffset) => index === cellOffset);
490
+ if (usesLinearIndexes) {
491
+ return { textLength };
492
+ }
493
+ return { textLength, textCellToStringIndex };
494
+ }
476
495
  function renderInputLine(value, inputState, padding = { top: 0, right: 0, bottom: 0, left: 0 }) {
477
496
  const state = normalizeInputState(inputState, value.length);
478
497
  const { start, end } = getSelectionRange(state);
@@ -481,11 +500,27 @@ function renderInputLine(value, inputState, padding = { top: 0, right: 0, bottom
481
500
  const textStart = padding.left + 1;
482
501
  return {
483
502
  line: paddedLine,
484
- cursor: { x: textStart + state.cursor, y: 1 },
485
- spans: start === end ? [] : [{ kind: "input.selection", x1: textStart + start, x2: textStart + end, y: 1 }]
503
+ cursor: { x: textStart + cursorCellOffset(value, state.cursor), y: 1 },
504
+ spans: start === end ? [] : [{ kind: "input.selection", x1: textStart + cursorCellOffset(value, start), x2: textStart + cursorCellOffset(value, end), y: 1 }]
505
+ };
506
+ }
507
+ function resolveEditorDimensions(node, context) {
508
+ const explicitWidth = positiveDimension(node.props.width, "width");
509
+ const explicitHeight = positiveDimension(node.props.height, "height");
510
+ return {
511
+ width: typeof explicitWidth !== "undefined"
512
+ ? explicitWidth
513
+ : node.props.fill === true
514
+ ? fillContextDimension(context, "width", "Editor")
515
+ : undefined,
516
+ height: typeof explicitHeight !== "undefined"
517
+ ? explicitHeight
518
+ : node.props.fill === true
519
+ ? fillContextDimension(context, "height", "Editor")
520
+ : undefined
486
521
  };
487
522
  }
488
- function renderEditorFrame(node) {
523
+ function renderEditorFrame(node, context) {
489
524
  const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : "";
490
525
  const placeholder = typeof node.props.placeholder !== "undefined" ? plainText(node.props.placeholder) : "";
491
526
  const displayValue = value.length === 0 && !node.props.__focused && placeholder ? placeholder : value;
@@ -499,21 +534,26 @@ function renderEditorFrame(node) {
499
534
  }
500
535
  return `> ${line.slice(0, focusedColumn)}|${line.slice(focusedColumn)}`;
501
536
  });
502
- const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
503
- const cursor = node.props.__focused ? { x: 3 + focusedColumn, y: focusedLine + 1 } : null;
504
- const spans = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, lines[focusedLine].length + 1), y: focusedLine + 1 }] : [];
505
- const height = typeof node.props.height === "undefined" ? undefined : positiveDimension(node.props.height, "height");
537
+ const width = lines.reduce((max, line) => Math.max(max, terminalCellWidth(line)), 0);
538
+ const cursor = node.props.__focused ? { x: 3 + cursorCellOffset(focusedState.lines[focusedLine], focusedColumn), y: focusedLine + 1 } : null;
539
+ const spans = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, terminalCellWidth(lines[focusedLine]) + 1), y: focusedLine + 1 }] : [];
540
+ const dimensions = resolveEditorDimensions(node, context);
506
541
  const frame = createFrame(lines, [], cursor, spans);
507
- const scrollOffset = node.props.__focused && typeof height !== "undefined"
508
- ? Math.min(Math.max(0, focusedLine - height + 1), Math.max(0, lines.length - height))
542
+ const scrollOffset = node.props.__focused && typeof dimensions.height !== "undefined"
543
+ ? Math.min(Math.max(0, focusedLine - dimensions.height + 1), Math.max(0, lines.length - dimensions.height))
509
544
  : 0;
510
- const constrainedFrame = typeof height === "undefined"
545
+ const croppedFrame = typeof dimensions.height === "undefined"
511
546
  ? frame
512
- : constrainFrame(scrollOffset > 0 ? cropFrame(frame, scrollOffset, height) : frame, { height });
547
+ : scrollOffset > 0
548
+ ? cropFrame(frame, scrollOffset, dimensions.height)
549
+ : frame;
550
+ const constrainedFrame = typeof dimensions.width === "undefined" && typeof dimensions.height === "undefined"
551
+ ? croppedFrame
552
+ : constrainFrame(croppedFrame, { ...dimensions, expandFullFrameSpans: true });
513
553
  if (!node.props.id) {
514
554
  return constrainedFrame;
515
555
  }
516
- return createFrame(constrainedFrame.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(1, getFrameWidth(constrainedFrame), width), y1: 1, y2: getFrameHeight(constrainedFrame), textStartX: 3, textLength: value.length }], constrainedFrame.cursor, constrainedFrame.spans);
556
+ return createFrame(constrainedFrame.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(1, getFrameWidth(constrainedFrame)), y1: 1, y2: getFrameHeight(constrainedFrame), textStartX: 3, ...interactiveTextMetadata(value) }], constrainedFrame.cursor, constrainedFrame.spans);
517
557
  }
518
558
  function notifyLayoutContextProbe(node, context) {
519
559
  if (context && typeof node.props.__layoutContextProbe === "function") {
@@ -543,12 +583,11 @@ function renderTableFrame(node, context) {
543
583
  const cells = new Array(columnCount).fill(createFrame([""])).map((cell, index) => row[index] || cell);
544
584
  const rowHeight = cells.reduce((max, cell) => Math.max(max, getFrameHeight(cell)), 0);
545
585
  const normalized = cells.map((cell, index) => ({
546
- frame: cell,
547
- width: columnWidths[index],
548
- lines: [...cell.lines.map((line) => line.padEnd(columnWidths[index], " ")), ...new Array(Math.max(0, rowHeight - getFrameHeight(cell))).fill(" ".repeat(columnWidths[index]))]
586
+ frame: fitFrame(cell, columnWidths[index], rowHeight, { expandFullFrameSpans: true }),
587
+ width: columnWidths[index]
549
588
  }));
550
589
  for (let rowIndex = 0; rowIndex < rowHeight; rowIndex += 1) {
551
- lines.push(normalized.map((cell) => cell.lines[rowIndex]).join(" | "));
590
+ lines.push(normalized.map((cell) => cell.frame.lines[rowIndex]).join(" | "));
552
591
  }
553
592
  let xOffset = 0;
554
593
  for (let index = 0; index < normalized.length; index += 1) {
@@ -694,7 +733,7 @@ function renderSplitFrame(node, context) {
694
733
  if (cellWidth <= 0 || cellHeight <= 0) {
695
734
  continue;
696
735
  }
697
- frames.push(constrainFrame(renderSplitChildFrame(node.children[index], cellWidth, cellHeight, context), { width: cellWidth, height: cellHeight }));
736
+ frames.push(constrainFrame(renderSplitChildFrame(node.children[index], cellWidth, cellHeight, context), { width: cellWidth, height: cellHeight, expandFullFrameSpans: true }));
698
737
  }
699
738
  const frame = frames.length
700
739
  ? direction === "row" ? mergeHorizontal(frames, { gap }) : mergeVertical(frames, { gap })
@@ -728,7 +767,7 @@ function renderBodyFrame(children, props, context) {
728
767
  return direction === "row" ? mergeHorizontal(frames, { gap }) : mergeVertical(frames, { gap });
729
768
  }
730
769
  function renderFixedChildFrame(node, width, height) {
731
- return constrainFrame(renderBodyFrame(node.children, {}, { cols: width, rows: height }), { width, height });
770
+ return constrainFrame(renderBodyFrame(node.children, {}, { cols: width, rows: height }), { width, height, expandFullFrameSpans: true });
732
771
  }
733
772
  function renderFixedCompositionFrame(node, width, height) {
734
773
  if (!Number.isFinite(width) || !Number.isInteger(width) || width <= 0 || !Number.isFinite(height) || !Number.isInteger(height) || height <= 0) {
@@ -766,12 +805,12 @@ function renderFixedCompositionFrame(node, width, height) {
766
805
  const leftFrames = fixedNodes.left.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
767
806
  const rightFrames = fixedNodes.right.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
768
807
  const bodyFrames = bodyWidth > 0
769
- ? [constrainFrame(renderBodyFrame(bodyChildren, node.props, { cols: bodyWidth, rows: middleHeight }), { width: bodyWidth, height: middleHeight })]
808
+ ? [constrainFrame(renderBodyFrame(bodyChildren, node.props, { cols: bodyWidth, rows: middleHeight }), { width: bodyWidth, height: middleHeight, expandFullRowSpans: true })]
770
809
  : [];
771
- middleFrames.push(constrainFrame(mergeHorizontal([...leftFrames, ...bodyFrames, ...rightFrames]), { width, height: middleHeight }));
810
+ middleFrames.push(constrainFrame(mergeHorizontal([...leftFrames, ...bodyFrames, ...rightFrames]), { width, height: middleHeight, expandFullRowSpans: true }));
772
811
  }
773
812
  const frame = mergeVertical([...topFrames, ...middleFrames, ...bottomFrames]);
774
- return constrainFrame(frame, { width, height });
813
+ return constrainFrame(frame, { width, height, expandFullFrameSpans: true });
775
814
  }
776
815
  function renderStandaloneFixedFrame(node, context) {
777
816
  const position = fixedPosition(node.props.position);
@@ -832,7 +871,7 @@ function overlayGeometry(node, width, height) {
832
871
  }
833
872
  function renderOverlayChildFrame(node, width, height, context) {
834
873
  const geometry = overlayGeometry(node, width, height);
835
- let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height });
874
+ let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height, expandFullRowSpans: true });
836
875
  frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
837
876
  if (node.props.id && isFocusable(node)) {
838
877
  frame = addFocusableHitbox(frame, node);
@@ -883,7 +922,7 @@ function renderScreenFrame(node, context) {
883
922
  }
884
923
  base = mergeVertical(parts);
885
924
  if (context && overlays.length) {
886
- base = constrainFrame(base, { width: context.cols, height: context.rows });
925
+ base = constrainFrame(base, { width: context.cols, height: context.rows, expandFullRowSpans: true });
887
926
  }
888
927
  }
889
928
  return overlays.length ? applyDirectOverlays(base, overlays, context) : base;
@@ -949,13 +988,13 @@ function renderLogViewFrame(node, context) {
949
988
  }
950
989
  }
951
990
  if (typeof innerWidth !== "undefined") {
952
- frame = constrainFrame(frame, { width: innerWidth, height: innerHeight });
991
+ frame = constrainFrame(frame, { width: innerWidth, height: innerHeight, expandFullFrameSpans: true });
953
992
  }
954
993
  }
955
994
  frame = padFrameSides(frame, padding);
956
995
  frame = addBorder(frame, border);
957
996
  if (typeof dimensions.width !== "undefined") {
958
- frame = constrainFrame(frame, { width: dimensions.width, height: dimensions.height });
997
+ frame = constrainFrame(frame, { width: dimensions.width, height: dimensions.height, expandFullFrameSpans: true });
959
998
  }
960
999
  else if (typeof dimensions.height !== "undefined") {
961
1000
  frame = cropFrame(frame, 0, dimensions.height);
@@ -984,18 +1023,18 @@ function renderSeparatedRowFrame(frames, separator = " | ") {
984
1023
  const frame = frames[index];
985
1024
  const width = widths[index];
986
1025
  for (let row = 0; row < height; row += 1) {
987
- lines[row] += (frame.lines[row] || "").padEnd(width, " ");
1026
+ lines[row] += padEndTerminalCells(frame.lines[row] || "", width);
988
1027
  if (index < frames.length - 1) {
989
1028
  lines[row] += separator;
990
1029
  }
991
1030
  }
992
- const shifted = shiftFrame(frame, xOffset, 0);
1031
+ const shifted = shiftFrame(fitFrame(frame, width, height, { expandFullFrameSpans: true }), xOffset, 0);
993
1032
  hitboxes.push(...shifted.hitboxes);
994
1033
  spans.push(...shifted.spans);
995
1034
  if (!cursor && frame.cursor) {
996
1035
  cursor = { x: frame.cursor.x + xOffset, y: frame.cursor.y };
997
1036
  }
998
- xOffset += width + (index < frames.length - 1 ? separator.length : 0);
1037
+ xOffset += width + (index < frames.length - 1 ? terminalCellWidth(separator) : 0);
999
1038
  }
1000
1039
  return createFrame(lines, hitboxes, cursor, spans);
1001
1040
  }
@@ -1023,7 +1062,7 @@ function renderElementFrame(node, context) {
1023
1062
  if (node.tag === "terminal-scroll") {
1024
1063
  const offset = numeric(node.props.__scrollOffset, 0);
1025
1064
  if (typeof dimensions.width !== "undefined") {
1026
- frame = constrainFrame(frame, { width: dimensions.width });
1065
+ frame = constrainFrame(frame, { width: dimensions.width, expandFullFrameSpans: true });
1027
1066
  }
1028
1067
  const height = numeric(dimensions.height ?? node.props.height, getFrameHeight(frame));
1029
1068
  frame = cropFrame(frame, offset, height || getFrameHeight(frame));
@@ -1032,7 +1071,7 @@ function renderElementFrame(node, context) {
1032
1071
  const spans = frame.spans.slice();
1033
1072
  for (let index = 0; index < frame.lines.length; index += 1) {
1034
1073
  const row = index + 1;
1035
- const width = frame.lines[index].length + 1;
1074
+ const width = terminalCellWidth(frame.lines[index]) + 1;
1036
1075
  if (highlightRows.includes(row)) {
1037
1076
  spans.push({ kind: "highlight", x1: 1, x2: width, y: row });
1038
1077
  }
@@ -1111,15 +1150,15 @@ function renderElementFrame(node, context) {
1111
1150
  for (let index = 0; index < frameLines.length; index += 1) {
1112
1151
  const sourceIndex = frameItemIndexes[index];
1113
1152
  const y = itemY + index;
1114
- spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
1153
+ spans.push(markFullRowSpan({ kind: "list.base", x1: 1, x2: width + 1, y }));
1115
1154
  if (selectedIndex !== null && selectedIndex !== activeIndex && sourceIndex === selectedIndex) {
1116
- spans.push({ kind: "list.selected", x1: 1, x2: width + 1, y });
1155
+ spans.push(markFullRowSpan({ kind: "list.selected", x1: 1, x2: width + 1, y }));
1117
1156
  }
1118
1157
  if (sourceIndex === activeIndex) {
1119
- spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
1158
+ spans.push(markFullRowSpan({ kind: "list.current", x1: 1, x2: width + 1, y }));
1120
1159
  }
1121
1160
  if (sourceIndex === hoveredIndex) {
1122
- spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
1161
+ spans.push(markFullRowSpan({ kind: "list.hover", x1: 1, x2: width + 1, y }));
1123
1162
  }
1124
1163
  }
1125
1164
  const listHitboxes = [];
@@ -1159,7 +1198,7 @@ function renderElementFrame(node, context) {
1159
1198
  }
1160
1199
  const frame = createFrame(decorated.lines, [...listHitboxes, ...itemHitboxes, ...decorated.hitboxes], decorated.cursor, spans);
1161
1200
  const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1162
- return typeof frameHeight === "number" ? constrainFrame(styled, { height: frameHeight }) : styled;
1201
+ return typeof frameHeight === "number" ? constrainFrame(styled, { height: frameHeight, expandFullFrameSpans: true }) : styled;
1163
1202
  }
1164
1203
  case "terminal-table":
1165
1204
  return renderTableFrame(node, context);
@@ -1193,7 +1232,7 @@ function renderElementFrame(node, context) {
1193
1232
  const decorated = addBorder(createFrame([line]), inputBorder);
1194
1233
  const width = Math.max(1, getFrameWidth(decorated));
1195
1234
  const height = Math.max(1, getFrameHeight(decorated));
1196
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
1235
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, ...interactiveTextMetadata(stringValue) }] : [];
1197
1236
  const spans = fullFrameSpans(["input.base"], width, height);
1198
1237
  return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1199
1238
  }
@@ -1201,12 +1240,12 @@ function renderElementFrame(node, context) {
1201
1240
  const decorated = addBorder(createFrame([rendered.line], [], rendered.cursor, rendered.spans), inputBorder);
1202
1241
  const width = Math.max(1, getFrameWidth(decorated));
1203
1242
  const height = Math.max(1, getFrameHeight(decorated));
1204
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
1243
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, ...interactiveTextMetadata(stringValue) }] : [];
1205
1244
  const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
1206
1245
  return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1207
1246
  }
1208
1247
  case "terminal-editor":
1209
- return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
1248
+ return addFullFrameSpans(renderEditorFrame(node, context), resolveNodeStyle(node, context).spanKinds);
1210
1249
  case "terminal-button": {
1211
1250
  const label = typeof node.props.label !== "undefined" ? plainText(node.props.label) : plainText(node.children.map(textContent).join(""));
1212
1251
  const layoutStyle = resolveLayoutStyle("button.base", node, context);