@valyrianjs/terminal 0.1.2 → 0.2.1

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 (62) hide show
  1. package/dist/ansi.d.ts +2 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +12 -0
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/events.js +6 -2
  7. package/dist/events.js.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/keymap.d.ts.map +1 -1
  12. package/dist/keymap.js +4 -2
  13. package/dist/keymap.js.map +1 -1
  14. package/dist/layout.d.ts.map +1 -1
  15. package/dist/layout.js +2 -1
  16. package/dist/layout.js.map +1 -1
  17. package/dist/mouse.d.ts +6 -0
  18. package/dist/mouse.d.ts.map +1 -1
  19. package/dist/mouse.js +30 -16
  20. package/dist/mouse.js.map +1 -1
  21. package/dist/primitives.d.ts +8 -3
  22. package/dist/primitives.d.ts.map +1 -1
  23. package/dist/primitives.js +8 -1
  24. package/dist/primitives.js.map +1 -1
  25. package/dist/render.d.ts +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +290 -51
  28. package/dist/render.js.map +1 -1
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +13 -11
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/session.js +434 -65
  34. package/dist/session.js.map +1 -1
  35. package/dist/theme.d.ts.map +1 -1
  36. package/dist/theme.js +3 -0
  37. package/dist/theme.js.map +1 -1
  38. package/dist/tree.d.ts.map +1 -1
  39. package/dist/tree.js +18 -4
  40. package/dist/tree.js.map +1 -1
  41. package/dist/types.d.ts +61 -13
  42. package/dist/types.d.ts.map +1 -1
  43. package/docs/api-reference.md +40 -28
  44. package/docs/cookbook.md +2 -2
  45. package/docs/core-concepts.md +1 -1
  46. package/docs/interaction-model.md +18 -6
  47. package/docs/primitive-gallery.md +19 -10
  48. package/llms-full.txt +80 -47
  49. package/package.json +1 -1
  50. package/src/ansi.ts +12 -0
  51. package/src/events.ts +4 -2
  52. package/src/index.ts +3 -0
  53. package/src/keymap.ts +4 -2
  54. package/src/layout.ts +2 -1
  55. package/src/mouse.ts +31 -15
  56. package/src/primitives.ts +15 -5
  57. package/src/render.ts +320 -52
  58. package/src/runtime.ts +13 -11
  59. package/src/session.ts +469 -59
  60. package/src/theme.ts +3 -0
  61. package/src/tree.ts +19 -4
  62. package/src/types.ts +72 -12
package/dist/render.js CHANGED
@@ -5,6 +5,22 @@ import { isFocusable, textContent } from "./tree.js";
5
5
  import { renderValyrianTerminal } from "./runtime.js";
6
6
  import { plainText } from "./text.js";
7
7
  import { resolveTerminalStyle } from "./theme.js";
8
+ function validateRenderContextDimension(name, value) {
9
+ if (!Number.isInteger(value) || value < 1) {
10
+ throw new RangeError(`Invalid render context ${name}: expected an integer >= 1`);
11
+ }
12
+ return value;
13
+ }
14
+ function validateRenderContext(context) {
15
+ if (typeof context === "undefined") {
16
+ return undefined;
17
+ }
18
+ return {
19
+ cols: validateRenderContextDimension("cols", context.cols),
20
+ rows: validateRenderContextDimension("rows", context.rows),
21
+ theme: context.theme
22
+ };
23
+ }
8
24
  const VISUAL_STATE_ORDER = [
9
25
  "disabled", "readonly", "loading", "empty", "muted", "error", "warning", "success", "invalid", "valid", "placeholder", "selection", "selected", "current", "expanded", "collapsed", "checked", "unchecked", "indeterminate", "editing", "submitted", "dragging", "dropTarget", "capturing", "focus", "hover", "pressed"
10
26
  ];
@@ -86,6 +102,18 @@ function mergeStyleDefinitions(base, next) {
86
102
  function styleSpan(kind, style) {
87
103
  return typeof style === "undefined" ? { kind } : { kind, style };
88
104
  }
105
+ const BASE_STYLE_KIND_BY_TAG = {
106
+ "terminal-button": "button.base",
107
+ "terminal-input": "input.base",
108
+ "terminal-editor": "editor.base",
109
+ "terminal-list": "list.base",
110
+ "terminal-scroll": "scroll.base",
111
+ "terminal-log-view": "log.base",
112
+ "terminal-overlay": "overlay.base"
113
+ };
114
+ function baseStyleKindForNode(node) {
115
+ return BASE_STYLE_KIND_BY_TAG[node.tag];
116
+ }
89
117
  function nodeStates(node) {
90
118
  const declared = Array.isArray(node.props.state) ? node.props.state : typeof node.props.state === "string" ? [node.props.state] : [];
91
119
  const states = new Set();
@@ -109,7 +137,7 @@ function resolveLayoutStyle(baseKind, node, context) {
109
137
  return mergeStyleDefinitions(resolveTerminalStyle(baseKind, context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
110
138
  }
111
139
  function resolveNodeLayoutStyle(node, context) {
112
- return resolveTerminalStyle(node.props.style, context?.theme);
140
+ return mergeStyleDefinitions(resolveTerminalStyle(baseStyleKindForNode(node), context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
113
141
  }
114
142
  function decoratedControlFrame(content, style) {
115
143
  const padding = normalizeSpacing(style?.padding, "Control padding");
@@ -125,11 +153,18 @@ function fullFrameSpans(kinds, width, height) {
125
153
  }
126
154
  return spans;
127
155
  }
128
- function resolveNodeStyle(node, context) {
129
- let style = resolveTerminalStyle(node.props.style, context?.theme);
130
- const spanKinds = typeof node.props.style === "string"
131
- ? [styleSpan(node.props.style)]
132
- : style ? [styleSpan("#style", style)] : [];
156
+ function resolveNodeStyle(node, context, options = {}) {
157
+ const baseKind = options.includeBase === false ? undefined : baseStyleKindForNode(node);
158
+ let style = resolveTerminalStyle(baseKind, context?.theme);
159
+ const spanKinds = typeof baseKind === "undefined" ? [] : [styleSpan(baseKind)];
160
+ const explicitStyle = resolveTerminalStyle(node.props.style, context?.theme);
161
+ if (typeof node.props.style === "string") {
162
+ spanKinds.push(styleSpan(node.props.style));
163
+ }
164
+ else if (explicitStyle) {
165
+ spanKinds.push(styleSpan("#style", explicitStyle));
166
+ }
167
+ style = mergeStyleDefinitions(style, explicitStyle);
133
168
  for (const state of nodeStates(node)) {
134
169
  const stateStyle = node.props.styles?.[state];
135
170
  if (stateStyle) {
@@ -196,21 +231,104 @@ function addFullFrameSpans(frame, kinds) {
196
231
  }
197
232
  return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
198
233
  }
199
- function listVirtualRange(node, itemCount, selectedIndex, context) {
234
+ function listViewportRows(node, itemCount, context) {
235
+ const explicitHeight = positiveDimension(node.props.height, "height");
236
+ const viewportSourceRows = explicitHeight ?? context?.rows ?? (itemCount || 1);
237
+ return Math.max(1, Math.min(itemCount || 1, positiveInteger(viewportSourceRows, "List viewport height")));
238
+ }
239
+ function clampListIndex(index, itemCount) {
240
+ if (itemCount <= 0) {
241
+ return 0;
242
+ }
243
+ return Math.max(0, Math.min(itemCount - 1, index));
244
+ }
245
+ function listVirtualRange(node, itemCount, context) {
200
246
  if (!node.props.virtualized) {
201
- return { start: 0, end: itemCount };
247
+ return { start: 0, end: itemCount, visibleStart: 0, viewportRows: itemCount || 1 };
202
248
  }
203
249
  if (typeof node.props.itemHeight !== "undefined" && node.props.itemHeight !== 1) {
204
250
  throw new RangeError("List itemHeight must be 1");
205
251
  }
206
252
  const overscan = nonNegativeInteger(node.props.overscan, "List overscan");
207
- const viewportSourceRows = context?.rows ?? (itemCount || 1);
208
- const viewportRows = Math.max(1, Math.min(itemCount || 1, positiveInteger(viewportSourceRows, "List viewport height")));
209
- const selected = Math.max(0, Math.min(itemCount - 1, selectedIndex));
210
- const visibleStart = Math.max(0, Math.min(selected, selected - viewportRows + 1));
253
+ const viewportRows = listViewportRows(node, itemCount, context);
254
+ const maxOffset = Math.max(0, itemCount - viewportRows);
255
+ let visibleStart = Math.max(0, Math.min(maxOffset, nonNegativeInteger(node.props.__scrollOffset, "List viewport offset")));
211
256
  const start = Math.max(0, visibleStart - overscan);
212
257
  const end = Math.min(itemCount, visibleStart + viewportRows + overscan);
213
- return { start, end };
258
+ return { start, end, visibleStart, viewportRows };
259
+ }
260
+ function listItemKey(node, item, index) {
261
+ if (typeof node.props.itemKey === "function") {
262
+ const key = node.props.itemKey(item, index);
263
+ if (typeof key !== "string" && typeof key !== "number") {
264
+ throw new RangeError("List itemKey must return a string or number");
265
+ }
266
+ return String(key);
267
+ }
268
+ return String(index);
269
+ }
270
+ function listItemRenderer(node) {
271
+ if (typeof node.props.__childrenRenderer === "function") {
272
+ return { type: "children", render: node.props.__childrenRenderer };
273
+ }
274
+ if (typeof node.props.renderItem === "function") {
275
+ return { type: "renderItem", render: node.props.renderItem };
276
+ }
277
+ return undefined;
278
+ }
279
+ function wrapPlainText(value, width) {
280
+ if (!Number.isFinite(width) || !Number.isInteger(width) || width <= 0) {
281
+ return [""];
282
+ }
283
+ const rows = [];
284
+ const sourceRows = value.split("\n");
285
+ for (const sourceRow of sourceRows) {
286
+ if (sourceRow.length === 0) {
287
+ rows.push("");
288
+ continue;
289
+ }
290
+ let remaining = sourceRow;
291
+ while (remaining.length > width) {
292
+ const slice = remaining.slice(0, width);
293
+ const breakAt = slice.lastIndexOf(" ");
294
+ if (breakAt > 0 && breakAt >= Math.floor(width * 0.6)) {
295
+ rows.push(remaining.slice(0, breakAt));
296
+ remaining = remaining.slice(breakAt + 1);
297
+ }
298
+ else {
299
+ rows.push(slice);
300
+ remaining = remaining.slice(width);
301
+ }
302
+ }
303
+ rows.push(remaining);
304
+ }
305
+ return rows.length ? rows : [""];
306
+ }
307
+ function renderListItemFrame(node, item, index, viewportIndex, activeIndex, selectedIndex, wrapWidth, context) {
308
+ const key = listItemKey(node, item, index);
309
+ const renderer = listItemRenderer(node);
310
+ if (!renderer) {
311
+ const label = plainText(item);
312
+ return createFrame(node.props.wrap === true ? wrapPlainText(label, wrapWidth) : label.split("\n"));
313
+ }
314
+ const ctx = {
315
+ index,
316
+ key,
317
+ active: index === activeIndex,
318
+ selected: selectedIndex !== null && index === selectedIndex,
319
+ viewportIndex,
320
+ item
321
+ };
322
+ const rendered = renderer.type === "children" ? renderer.render(item, ctx) : renderer.render(item, index);
323
+ if (typeof rendered === "string" || typeof rendered === "number") {
324
+ const label = plainText(rendered);
325
+ return createFrame(node.props.wrap === true ? wrapPlainText(label, wrapWidth) : label.split("\n"));
326
+ }
327
+ const frame = mergeVertical(renderValyrianTerminal(rendered).map((child) => renderTerminalFrame(child, context)));
328
+ if (node.props.wrap === true && frame.hitboxes.length === 0) {
329
+ return createFrame(frame.lines.flatMap((line) => wrapPlainText(line, wrapWidth)));
330
+ }
331
+ return frame;
214
332
  }
215
333
  function fixedPosition(value) {
216
334
  if (value === "top" || value === "bottom" || value === "left" || value === "right") {
@@ -663,27 +781,73 @@ function renderStandaloneFixedFrame(node, context) {
663
781
  ? constrainFrame(frame, { height: size })
664
782
  : constrainFrame(frame, { width: size });
665
783
  }
666
- function overlayGeometry(node) {
784
+ function overlayMarginValue(value, axisSize, label) {
785
+ if (typeof value === "number") {
786
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
787
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
788
+ }
789
+ return value;
790
+ }
791
+ if (typeof value === "string") {
792
+ const match = value.match(/^(\d+(?:\.\d+)?)%$/);
793
+ if (!match) {
794
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
795
+ }
796
+ const percent = Number(match[1]);
797
+ if (!Number.isFinite(percent) || percent < 0) {
798
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
799
+ }
800
+ return Math.round(axisSize * percent / 100);
801
+ }
802
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
803
+ }
804
+ function overlayMargins(margin, width, height) {
805
+ if (typeof margin === "number" || typeof margin === "string") {
806
+ const x = overlayMarginValue(margin, width, "Overlay margin");
807
+ const y = overlayMarginValue(margin, height, "Overlay margin");
808
+ return { x, y };
809
+ }
810
+ if (margin && typeof margin === "object" && !Array.isArray(margin)) {
811
+ const axes = margin;
812
+ return {
813
+ x: overlayMarginValue(axes.x, width, "Overlay margin x"),
814
+ y: overlayMarginValue(axes.y, height, "Overlay margin y")
815
+ };
816
+ }
817
+ throw new RangeError("Overlay margin is required");
818
+ }
819
+ function overlayGeometry(node, width, height) {
820
+ const margin = overlayMargins(node.props.margin, width, height);
821
+ const overlayWidth = width - margin.x * 2;
822
+ const overlayHeight = height - margin.y * 2;
823
+ if (overlayWidth < 1 || overlayHeight < 1) {
824
+ throw new RangeError("Overlay margin leaves no renderable area");
825
+ }
667
826
  return {
668
- x: positiveInteger(node.props.x, "Overlay x"),
669
- y: positiveInteger(node.props.y, "Overlay y"),
670
- width: positiveInteger(node.props.width, "Overlay width"),
671
- height: positiveInteger(node.props.height, "Overlay height")
827
+ x: margin.x + 1,
828
+ y: margin.y + 1,
829
+ width: overlayWidth,
830
+ height: overlayHeight
672
831
  };
673
832
  }
674
- function renderOverlayChildFrame(node, context) {
675
- const geometry = overlayGeometry(node);
676
- let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height }), { width: geometry.width, height: geometry.height });
833
+ function renderOverlayChildFrame(node, width, height, context) {
834
+ 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 });
677
836
  frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
678
837
  if (node.props.id && isFocusable(node)) {
679
838
  frame = addFocusableHitbox(frame, node);
680
839
  }
681
840
  return { frame, geometry };
682
841
  }
842
+ function orderedDirectOverlays(overlays) {
843
+ return overlays.map((overlay, sourceOrder) => ({ overlay, sourceOrder })).sort((a, b) => a.sourceOrder - b.sourceOrder).map(({ overlay }) => overlay);
844
+ }
683
845
  function applyDirectOverlays(base, overlays, context) {
684
846
  let frame = base;
685
- for (const overlay of overlays) {
686
- const rendered = renderOverlayChildFrame(overlay, context);
847
+ const width = Math.max(1, getFrameWidth(base));
848
+ const height = Math.max(1, getFrameHeight(base));
849
+ for (const overlay of orderedDirectOverlays(overlays)) {
850
+ const rendered = renderOverlayChildFrame(overlay, width, height, context);
687
851
  frame = overlayFrame(frame, rendered.frame, rendered.geometry);
688
852
  }
689
853
  return frame;
@@ -741,8 +905,11 @@ function renderPaneFrame(node, context) {
741
905
  }
742
906
  return overlays.length ? applyDirectOverlays(frame, overlays, context) : frame;
743
907
  }
744
- function renderStandaloneOverlayFrame(node) {
745
- const rendered = renderOverlayChildFrame(node);
908
+ function renderStandaloneOverlayFrame(node, context) {
909
+ if (!context) {
910
+ throw new RangeError("Standalone Overlay requires exact render context dimensions");
911
+ }
912
+ const rendered = renderOverlayChildFrame(node, context.cols, context.rows, context);
746
913
  return rendered.frame;
747
914
  }
748
915
  function renderLogViewFrame(node, context) {
@@ -882,46 +1049,117 @@ function renderElementFrame(node, context) {
882
1049
  case "terminal-fixed":
883
1050
  return renderStandaloneFixedFrame(node, context);
884
1051
  case "terminal-overlay":
885
- return renderStandaloneOverlayFrame(node);
1052
+ return renderStandaloneOverlayFrame(node, context);
886
1053
  case "terminal-log-view":
887
1054
  return renderLogViewFrame(node, context);
888
1055
  case "terminal-list": {
889
1056
  const items = Array.isArray(node.props.items) ? node.props.items : [];
890
- const selectedIndex = numeric(node.props.__selectedIndex, 0);
1057
+ const activeIndex = clampListIndex(numeric(node.props.__activeIndex ?? node.props.__selectedIndex, 0), items.length);
1058
+ const selectedIndex = typeof node.props.__selectedIndex === "number" ? clampListIndex(Number(node.props.__selectedIndex), items.length) : null;
891
1059
  const hoveredIndex = typeof node.props.__hoveredIndex === "number" ? Number(node.props.__hoveredIndex) : -1;
892
- const range = listVirtualRange(node, items.length, selectedIndex, context);
893
- const lines = [];
894
- for (let index = range.start; index < range.end; index += 1) {
895
- const item = items[index];
896
- const label = typeof node.props.renderItem === "function" ? plainText(node.props.renderItem(item, index)) : plainText(item);
897
- lines.push(label);
898
- }
899
- const visibleLines = lines.length ? lines : [""];
1060
+ const range = listVirtualRange(node, items.length, context);
900
1061
  const layoutStyle = resolveLayoutStyle("list.base", node, context);
901
1062
  const padding = normalizeSpacing(layoutStyle.padding, "List padding");
902
1063
  const border = normalizeBorder(layoutStyle.border);
903
- const decorated = addBorder(padFrameSides(createFrame(visibleLines), padding), border);
1064
+ const horizontalDecoration = padding.left + padding.right + (border.left ? 1 : 0) + (border.right ? 1 : 0);
1065
+ const wrapWidth = typeof context?.cols === "number" ? Math.max(1, context.cols - horizontalDecoration) : 1;
1066
+ const visibleLines = [];
1067
+ const itemIndexes = [];
1068
+ const childHitboxes = [];
1069
+ for (let index = range.start; index < range.end; index += 1) {
1070
+ const item = items[index];
1071
+ const itemFrame = renderListItemFrame(node, item, index, index - range.visibleStart, activeIndex, selectedIndex, wrapWidth, context);
1072
+ const rowOffset = visibleLines.length;
1073
+ visibleLines.push(...itemFrame.lines);
1074
+ for (let row = 0; row < itemFrame.lines.length; row += 1) {
1075
+ itemIndexes.push(index);
1076
+ }
1077
+ childHitboxes.push(...shiftFrame(itemFrame, 0, rowOffset).hitboxes);
1078
+ }
1079
+ if (!visibleLines.length) {
1080
+ visibleLines.push("");
1081
+ itemIndexes.push(0);
1082
+ }
1083
+ const frameHeight = node.props.virtualized && node.props.wrap === true
1084
+ ? positiveDimension(node.props.height, "height") ?? (typeof context?.rows === "number" ? Math.max(1, context.rows) : undefined)
1085
+ : undefined;
1086
+ const contentHeight = typeof frameHeight === "number"
1087
+ ? Math.max(1, frameHeight - padding.top - padding.bottom - (border.top ? 1 : 0) - (border.bottom ? 1 : 0))
1088
+ : visibleLines.length;
1089
+ let visibleLineStart = 0;
1090
+ if (node.props.virtualized && node.props.wrap === true && visibleLines.length > contentHeight) {
1091
+ const activeLineIndex = itemIndexes.findIndex((sourceIndex) => sourceIndex === activeIndex);
1092
+ if (activeLineIndex >= contentHeight) {
1093
+ visibleLineStart = activeLineIndex - contentHeight + 1;
1094
+ }
1095
+ }
1096
+ const frameLines = visibleLines.slice(visibleLineStart, visibleLineStart + contentHeight);
1097
+ const frameItemIndexes = itemIndexes.slice(visibleLineStart, visibleLineStart + contentHeight);
1098
+ const frameChildHitboxes = childHitboxes
1099
+ .filter((box) => box.y2 > visibleLineStart && box.y1 <= visibleLineStart + contentHeight)
1100
+ .map((box) => ({
1101
+ ...box,
1102
+ y1: Math.max(1, box.y1 - visibleLineStart),
1103
+ y2: Math.min(contentHeight, box.y2 - visibleLineStart),
1104
+ contentY: typeof box.contentY === "number" ? Math.max(1, box.contentY - visibleLineStart) : undefined
1105
+ }));
1106
+ const decorated = addBorder(padFrameSides(createFrame(frameLines, frameChildHitboxes), padding), border);
904
1107
  const width = Math.max(1, getFrameWidth(decorated));
905
1108
  const height = Math.max(1, getFrameHeight(decorated));
906
1109
  const itemY = 1 + (border.top ? 1 : 0) + padding.top;
907
1110
  const spans = [];
908
- for (let index = 0; index < visibleLines.length; index += 1) {
909
- const sourceIndex = range.start + index;
1111
+ for (let index = 0; index < frameLines.length; index += 1) {
1112
+ const sourceIndex = frameItemIndexes[index];
910
1113
  const y = itemY + index;
911
1114
  spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
912
- if (sourceIndex === selectedIndex) {
1115
+ if (selectedIndex !== null && selectedIndex !== activeIndex && sourceIndex === selectedIndex) {
1116
+ spans.push({ kind: "list.selected", x1: 1, x2: width + 1, y });
1117
+ }
1118
+ if (sourceIndex === activeIndex) {
913
1119
  spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
914
1120
  }
915
1121
  if (sourceIndex === hoveredIndex) {
916
1122
  spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
917
1123
  }
918
1124
  }
919
- const frame = createFrame(decorated.lines, [], decorated.cursor, spans);
920
- const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context).spanKinds);
921
- if (!node.props.id) {
922
- return styled;
1125
+ const listHitboxes = [];
1126
+ const itemHitboxes = [];
1127
+ if (node.props.id) {
1128
+ listHitboxes.push({
1129
+ id: node.props.id,
1130
+ tag: node.tag,
1131
+ x1: 1,
1132
+ x2: width,
1133
+ y1: 1,
1134
+ y2: Math.min(height, typeof frameHeight === "number" ? frameHeight : height),
1135
+ itemOffset: range.start,
1136
+ itemIndexes: frameItemIndexes,
1137
+ contentY: itemY
1138
+ });
1139
+ let itemStart = 0;
1140
+ while (itemStart < frameItemIndexes.length) {
1141
+ const sourceIndex = frameItemIndexes[itemStart];
1142
+ let itemEnd = itemStart;
1143
+ while (itemEnd + 1 < frameItemIndexes.length && frameItemIndexes[itemEnd + 1] === sourceIndex) {
1144
+ itemEnd += 1;
1145
+ }
1146
+ itemHitboxes.push({
1147
+ id: node.props.id,
1148
+ tag: node.tag,
1149
+ x1: 1,
1150
+ x2: width,
1151
+ y1: itemY + itemStart,
1152
+ y2: itemY + itemEnd,
1153
+ itemOffset: range.start,
1154
+ __listItemIndex: sourceIndex,
1155
+ itemIndexes: new Array(itemEnd - itemStart + 1).fill(sourceIndex)
1156
+ });
1157
+ itemStart = itemEnd + 1;
1158
+ }
923
1159
  }
924
- return createFrame(styled.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, itemOffset: range.start }], styled.cursor, styled.spans);
1160
+ const frame = createFrame(decorated.lines, [...listHitboxes, ...itemHitboxes, ...decorated.hitboxes], decorated.cursor, spans);
1161
+ const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1162
+ return typeof frameHeight === "number" ? constrainFrame(styled, { height: frameHeight }) : styled;
925
1163
  }
926
1164
  case "terminal-table":
927
1165
  return renderTableFrame(node, context);
@@ -938,7 +1176,7 @@ function renderElementFrame(node, context) {
938
1176
  case "terminal-text": {
939
1177
  const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : plainText(node.children.map(textContent).join(""));
940
1178
  const frame = createFrame(value.split("\n"));
941
- return addFullFrameSpans(frame, resolveNodeStyle(node, context).spanKinds);
1179
+ return addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
942
1180
  }
943
1181
  case "terminal-input": {
944
1182
  const value = typeof node.props.value !== "undefined" ? node.props.value : node.props.placeholder || "";
@@ -957,7 +1195,7 @@ function renderElementFrame(node, context) {
957
1195
  const height = Math.max(1, getFrameHeight(decorated));
958
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 }] : [];
959
1197
  const spans = fullFrameSpans(["input.base"], width, height);
960
- return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
1198
+ return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
961
1199
  }
962
1200
  const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length }, inputPadding);
963
1201
  const decorated = addBorder(createFrame([rendered.line], [], rendered.cursor, rendered.spans), inputBorder);
@@ -965,7 +1203,7 @@ function renderElementFrame(node, context) {
965
1203
  const height = Math.max(1, getFrameHeight(decorated));
966
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 }] : [];
967
1205
  const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
968
- return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
1206
+ return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
969
1207
  }
970
1208
  case "terminal-editor":
971
1209
  return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
@@ -975,10 +1213,10 @@ function renderElementFrame(node, context) {
975
1213
  const decorated = decoratedControlFrame([String(label)], layoutStyle);
976
1214
  const width = Math.max(1, getFrameWidth(decorated));
977
1215
  const height = Math.max(1, getFrameHeight(decorated));
978
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height }] : [];
1216
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, __pressHandler: typeof node.props.onpress === "function" ? node.props.onpress : undefined }] : [];
979
1217
  const kinds = ["button.base", ...nodeStates(node).map((state) => `button.${state}`)];
980
1218
  const spans = fullFrameSpans(kinds, width, height);
981
- return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
1219
+ return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
982
1220
  }
983
1221
  default:
984
1222
  return mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
@@ -993,9 +1231,10 @@ export function renderTerminalFrame(node, context) {
993
1231
  export function renderTerminalNode(node, context) {
994
1232
  return renderTerminalFrame(node, context).lines.join("\n");
995
1233
  }
996
- export function renderTerminal(input) {
1234
+ export function renderTerminal(input, context) {
1235
+ const renderContext = validateRenderContext(context);
997
1236
  return renderValyrianTerminal(input)
998
- .map((node) => renderTerminalNode(node))
1237
+ .map((node) => renderTerminalNode(node, renderContext))
999
1238
  .filter(Boolean)
1000
1239
  .join("\n")
1001
1240
  .trimEnd();