@valyrianjs/terminal 0.1.1 → 0.2.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.
Files changed (107) hide show
  1. package/README.md +105 -55
  2. package/dist/ansi.d.ts +20 -4
  3. package/dist/ansi.d.ts.map +1 -1
  4. package/dist/ansi.js +171 -47
  5. package/dist/ansi.js.map +1 -1
  6. package/dist/editor-state.d.ts +22 -0
  7. package/dist/editor-state.d.ts.map +1 -0
  8. package/dist/editor-state.js +110 -0
  9. package/dist/editor-state.js.map +1 -0
  10. package/dist/events.d.ts +1 -4
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/events.js +15 -38
  13. package/dist/events.js.map +1 -1
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/keymap.d.ts +7 -0
  19. package/dist/keymap.d.ts.map +1 -0
  20. package/dist/keymap.js +133 -0
  21. package/dist/keymap.js.map +1 -0
  22. package/dist/layout.d.ts +10 -1
  23. package/dist/layout.d.ts.map +1 -1
  24. package/dist/layout.js +97 -7
  25. package/dist/layout.js.map +1 -1
  26. package/dist/mouse.d.ts +1 -0
  27. package/dist/mouse.d.ts.map +1 -1
  28. package/dist/mouse.js +24 -1
  29. package/dist/mouse.js.map +1 -1
  30. package/dist/output-writer.d.ts +9 -0
  31. package/dist/output-writer.d.ts.map +1 -0
  32. package/dist/output-writer.js +79 -0
  33. package/dist/output-writer.js.map +1 -0
  34. package/dist/paste.d.ts +7 -0
  35. package/dist/paste.d.ts.map +1 -0
  36. package/dist/paste.js +18 -0
  37. package/dist/paste.js.map +1 -0
  38. package/dist/primitives.d.ts +15 -3
  39. package/dist/primitives.d.ts.map +1 -1
  40. package/dist/primitives.js +9 -1
  41. package/dist/primitives.js.map +1 -1
  42. package/dist/render.d.ts +9 -4
  43. package/dist/render.d.ts.map +1 -1
  44. package/dist/render.js +923 -68
  45. package/dist/render.js.map +1 -1
  46. package/dist/runtime.d.ts +29 -0
  47. package/dist/runtime.d.ts.map +1 -0
  48. package/dist/runtime.js +209 -0
  49. package/dist/runtime.js.map +1 -0
  50. package/dist/scheduler.d.ts +8 -0
  51. package/dist/scheduler.d.ts.map +1 -0
  52. package/dist/scheduler.js +24 -0
  53. package/dist/scheduler.js.map +1 -0
  54. package/dist/session.d.ts.map +1 -1
  55. package/dist/session.js +858 -199
  56. package/dist/session.js.map +1 -1
  57. package/dist/stream-log.d.ts +40 -0
  58. package/dist/stream-log.d.ts.map +1 -0
  59. package/dist/stream-log.js +73 -0
  60. package/dist/stream-log.js.map +1 -0
  61. package/dist/text.d.ts +3 -0
  62. package/dist/text.d.ts.map +1 -0
  63. package/dist/text.js +19 -0
  64. package/dist/text.js.map +1 -0
  65. package/dist/theme.d.ts +7 -0
  66. package/dist/theme.d.ts.map +1 -0
  67. package/dist/theme.js +254 -0
  68. package/dist/theme.js.map +1 -0
  69. package/dist/tree.d.ts +2 -0
  70. package/dist/tree.d.ts.map +1 -1
  71. package/dist/tree.js +42 -1
  72. package/dist/tree.js.map +1 -1
  73. package/dist/types.d.ts +203 -24
  74. package/dist/types.d.ts.map +1 -1
  75. package/docs/api-reference.md +313 -142
  76. package/docs/assets/quick-note.svg +13 -0
  77. package/docs/cookbook.md +296 -201
  78. package/docs/core-concepts.md +143 -55
  79. package/docs/getting-started.md +209 -90
  80. package/docs/interaction-model.md +98 -54
  81. package/docs/primitive-gallery.md +370 -0
  82. package/docs/session-runtime.md +131 -362
  83. package/docs/valyrian-modules.md +3196 -0
  84. package/llms-full.txt +5377 -0
  85. package/package.json +21 -8
  86. package/src/ansi.ts +269 -0
  87. package/src/clipboard.ts +76 -0
  88. package/src/editor-state.ts +162 -0
  89. package/src/events.ts +163 -0
  90. package/src/index.ts +95 -0
  91. package/src/keymap.ts +151 -0
  92. package/src/layout.ts +282 -0
  93. package/src/mouse.ts +68 -0
  94. package/src/output-writer.ts +93 -0
  95. package/src/paste.ts +23 -0
  96. package/src/primitives.ts +55 -0
  97. package/src/render.ts +1204 -0
  98. package/src/runtime.ts +267 -0
  99. package/src/scheduler.ts +33 -0
  100. package/src/session.ts +1408 -0
  101. package/src/stream-log.ts +96 -0
  102. package/src/text.ts +20 -0
  103. package/src/theme.ts +263 -0
  104. package/src/tree.ts +169 -0
  105. package/src/types.ts +541 -0
  106. package/tsconfig.json +4 -7
  107. package/docs/local-demo.md +0 -28
package/dist/render.js CHANGED
@@ -1,6 +1,29 @@
1
- import { borderFrame, createFrame, cropFrame, fitFrame, getFrameHeight, getFrameWidth, mergeHorizontal, mergeVertical, padFrame, shiftFrame } from "./layout.js";
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 { isFocusable, resolveRoot, textContent } from "./tree.js";
3
+ import { createEditorState } from "./editor-state.js";
4
+ import { isFocusable, textContent } from "./tree.js";
5
+ import { renderValyrianTerminal } from "./runtime.js";
6
+ import { plainText } from "./text.js";
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
+ }
24
+ const VISUAL_STATE_ORDER = [
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"
26
+ ];
4
27
  function numeric(value, fallback = 0) {
5
28
  const result = Number(value);
6
29
  return Number.isFinite(result) ? result : fallback;
@@ -14,35 +37,412 @@ function addFocusableHitbox(frame, node) {
14
37
  frame.hitboxes.unshift({ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height });
15
38
  return frame;
16
39
  }
17
- function decorateContainerFrame(frame, node) {
40
+ function positiveDimension(value, prop) {
41
+ if (typeof value === "undefined") {
42
+ return undefined;
43
+ }
44
+ const size = Number(value);
45
+ if (!Number.isFinite(size) || size <= 0) {
46
+ throw new RangeError(`${prop} must be greater than zero`);
47
+ }
48
+ return size;
49
+ }
50
+ function positiveInteger(value, prop) {
51
+ const size = Number(value);
52
+ if (!Number.isFinite(size) || !Number.isInteger(size) || size <= 0) {
53
+ throw new RangeError(`${prop} must be a positive finite integer`);
54
+ }
55
+ return size;
56
+ }
57
+ function nonNegativeInteger(value, prop) {
58
+ const size = typeof value === "undefined" ? 0 : Number(value);
59
+ if (!Number.isFinite(size) || !Number.isInteger(size) || size < 0) {
60
+ throw new RangeError(`${prop} must be a non-negative finite integer`);
61
+ }
62
+ return size;
63
+ }
64
+ function normalizeSpacing(value, prop) {
65
+ if (typeof value === "undefined")
66
+ return { top: 0, right: 0, bottom: 0, left: 0 };
67
+ if (typeof value === "number") {
68
+ const size = nonNegativeInteger(value, prop);
69
+ return { top: size, right: size, bottom: size, left: size };
70
+ }
71
+ const x = nonNegativeInteger(value.x, `${prop}.x`);
72
+ const y = nonNegativeInteger(value.y, `${prop}.y`);
73
+ return {
74
+ top: typeof value.top === "undefined" ? y : nonNegativeInteger(value.top, `${prop}.top`),
75
+ right: typeof value.right === "undefined" ? x : nonNegativeInteger(value.right, `${prop}.right`),
76
+ bottom: typeof value.bottom === "undefined" ? y : nonNegativeInteger(value.bottom, `${prop}.bottom`),
77
+ left: typeof value.left === "undefined" ? x : nonNegativeInteger(value.left, `${prop}.left`)
78
+ };
79
+ }
80
+ function normalizeBorder(value) {
81
+ if (value === true)
82
+ return { top: true, right: true, bottom: true, left: true, style: "solid" };
83
+ if (typeof value === "string")
84
+ return { top: true, right: true, bottom: true, left: true, style: value };
85
+ if (value && typeof value === "object") {
86
+ return { top: value.top === true, right: value.right === true, bottom: value.bottom === true, left: value.left === true, style: value.style || "solid", color: value.color };
87
+ }
88
+ return { top: false, right: false, bottom: false, left: false, style: "solid" };
89
+ }
90
+ function borderChars(style) {
91
+ if (style === "solid")
92
+ return { topLeft: "┌", topRight: "┐", bottomLeft: "└", bottomRight: "┘", horizontal: "─", vertical: "│" };
93
+ if (style === "dotted")
94
+ return { topLeft: "+", topRight: "+", bottomLeft: "+", bottomRight: "+", horizontal: "·", vertical: "⋮" };
95
+ if (style === "double")
96
+ return { topLeft: "╔", topRight: "╗", bottomLeft: "╚", bottomRight: "╝", horizontal: "═", vertical: "║" };
97
+ throw new RangeError(`Unknown terminal border style: ${style}`);
98
+ }
99
+ function mergeStyleDefinitions(base, next) {
100
+ return { ...base, ...next };
101
+ }
102
+ function styleSpan(kind, style) {
103
+ return typeof style === "undefined" ? { kind } : { kind, style };
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
+ }
117
+ function nodeStates(node) {
118
+ const declared = Array.isArray(node.props.state) ? node.props.state : typeof node.props.state === "string" ? [node.props.state] : [];
119
+ const states = new Set();
120
+ for (const state of declared)
121
+ states.add(state);
122
+ if (node.props.disabled === true)
123
+ states.add("disabled");
124
+ if (node.props.pressed === true)
125
+ states.add("pressed");
126
+ if (node.props.__focused === true)
127
+ states.add("focus");
128
+ if (typeof node.props.__hoveredIndex === "number" || typeof node.props.__hoveredRow === "number")
129
+ states.add("hover");
130
+ if (Array.isArray(node.props.items) && node.props.items.length === 0)
131
+ states.add("empty");
132
+ if (Array.isArray(node.props.entries) && node.props.entries.length === 0)
133
+ states.add("empty");
134
+ return VISUAL_STATE_ORDER.filter((state) => states.has(state));
135
+ }
136
+ function resolveLayoutStyle(baseKind, node, context) {
137
+ return mergeStyleDefinitions(resolveTerminalStyle(baseKind, context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
138
+ }
139
+ function resolveNodeLayoutStyle(node, context) {
140
+ return mergeStyleDefinitions(resolveTerminalStyle(baseStyleKindForNode(node), context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
141
+ }
142
+ function decoratedControlFrame(content, style) {
143
+ const padding = normalizeSpacing(style?.padding, "Control padding");
144
+ const border = normalizeBorder(style?.border);
145
+ return addBorder(padFrameSides(createFrame(content), padding), border);
146
+ }
147
+ function fullFrameSpans(kinds, width, height) {
148
+ const spans = [];
149
+ for (const kind of kinds) {
150
+ for (let y = 1; y <= height; y += 1) {
151
+ spans.push({ kind, x1: 1, x2: width + 1, y });
152
+ }
153
+ }
154
+ return spans;
155
+ }
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);
168
+ for (const state of nodeStates(node)) {
169
+ const stateStyle = node.props.styles?.[state];
170
+ if (stateStyle) {
171
+ const resolvedStateStyle = resolveTerminalStyle(stateStyle, context?.theme);
172
+ style = mergeStyleDefinitions(style, resolvedStateStyle);
173
+ spanKinds.push(typeof stateStyle === "string" ? styleSpan(stateStyle) : styleSpan("#style", resolvedStateStyle));
174
+ }
175
+ }
176
+ return { style, spanKinds };
177
+ }
178
+ function padFrameSides(frame, padding) {
179
+ if (!padding.top && !padding.right && !padding.bottom && !padding.left)
180
+ return frame;
181
+ const width = Math.max(1, getFrameWidth(frame));
182
+ const contentWidth = width + padding.left + padding.right;
183
+ const topLines = new Array(padding.top).fill(" ".repeat(contentWidth));
184
+ const bottomLines = new Array(padding.bottom).fill(" ".repeat(contentWidth));
185
+ const lines = [
186
+ ...topLines,
187
+ ...frame.lines.map((line) => `${" ".repeat(padding.left)}${line.padEnd(width, " ")}${" ".repeat(padding.right)}`),
188
+ ...bottomLines
189
+ ];
190
+ return shiftFrame(createFrame(lines, frame.hitboxes, frame.cursor, frame.spans), padding.left, padding.top);
191
+ }
192
+ function addBorder(frame, border) {
193
+ if (!border.top && !border.right && !border.bottom && !border.left)
194
+ return frame;
195
+ const chars = borderChars(border.style);
196
+ const innerWidth = getFrameWidth(frame);
197
+ const left = border.left ? 1 : 0;
198
+ const right = border.right ? 1 : 0;
199
+ const width = innerWidth + left + right;
200
+ const lines = [];
201
+ if (border.top) {
202
+ 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
+ for (const line of frame.lines) {
205
+ lines.push(`${border.left ? chars.vertical : ""}${line.padEnd(innerWidth, " ")}${border.right ? chars.vertical : ""}`);
206
+ }
207
+ if (border.bottom) {
208
+ 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));
209
+ }
210
+ return shiftFrame(createFrame(lines, frame.hitboxes, frame.cursor, frame.spans), left, border.top ? 1 : 0);
211
+ }
212
+ function containerDecorationSize(node, context) {
213
+ const layoutStyle = resolveNodeLayoutStyle(node, context);
214
+ const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, `${node.tag} padding`);
215
+ const border = normalizeBorder(layoutStyle?.border);
216
+ return {
217
+ horizontal: padding.left + padding.right + (border.left ? 1 : 0) + (border.right ? 1 : 0),
218
+ vertical: padding.top + padding.bottom + (border.top ? 1 : 0) + (border.bottom ? 1 : 0)
219
+ };
220
+ }
221
+ function addFullFrameSpans(frame, kinds) {
222
+ const width = Math.max(1, getFrameWidth(frame));
223
+ const height = getFrameHeight(frame);
224
+ if (width <= 0 || height <= 0 || kinds.length === 0)
225
+ return frame;
226
+ const spans = frame.spans.slice();
227
+ for (const span of kinds) {
228
+ for (let y = 1; y <= height; y += 1) {
229
+ spans.push({ ...span, x1: 1, x2: width + 1, y });
230
+ }
231
+ }
232
+ return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
233
+ }
234
+ function listVirtualRange(node, itemCount, selectedIndex, context) {
235
+ if (!node.props.virtualized) {
236
+ return { start: 0, end: itemCount };
237
+ }
238
+ if (typeof node.props.itemHeight !== "undefined" && node.props.itemHeight !== 1) {
239
+ throw new RangeError("List itemHeight must be 1");
240
+ }
241
+ const overscan = nonNegativeInteger(node.props.overscan, "List overscan");
242
+ const viewportSourceRows = context?.rows ?? (itemCount || 1);
243
+ const viewportRows = Math.max(1, Math.min(itemCount || 1, positiveInteger(viewportSourceRows, "List viewport height")));
244
+ const selected = Math.max(0, Math.min(itemCount - 1, selectedIndex));
245
+ const visibleStart = Math.max(0, Math.min(selected, selected - viewportRows + 1));
246
+ const start = Math.max(0, visibleStart - overscan);
247
+ const end = Math.min(itemCount, visibleStart + viewportRows + overscan);
248
+ return { start, end };
249
+ }
250
+ function fixedPosition(value) {
251
+ if (value === "top" || value === "bottom" || value === "left" || value === "right") {
252
+ return value;
253
+ }
254
+ throw new RangeError("Fixed position must be top, bottom, left, or right");
255
+ }
256
+ function fillContextDimension(context, prop, label) {
257
+ if (!context) {
258
+ throw new RangeError(`${label} fill requires render context to resolve ${prop}`);
259
+ }
260
+ return positiveInteger(prop === "width" ? context.cols : context.rows, `${label} ${prop}`);
261
+ }
262
+ function resolveLayoutDimension(node, prop, label, context) {
263
+ const explicit = positiveDimension(node.props[prop], prop);
264
+ if (typeof explicit !== "undefined") {
265
+ return explicit;
266
+ }
267
+ if (node.props.fill === true) {
268
+ return fillContextDimension(context, prop, label);
269
+ }
270
+ return undefined;
271
+ }
272
+ function resolveLayoutDimensions(node, label, context) {
273
+ return {
274
+ width: resolveLayoutDimension(node, "width", label, context),
275
+ height: resolveLayoutDimension(node, "height", label, context)
276
+ };
277
+ }
278
+ function contextBackedDimension(context, prop, label) {
279
+ if (!context) {
280
+ return undefined;
281
+ }
282
+ return positiveInteger(prop === "width" ? context.cols : context.rows, `${label} ${prop}`);
283
+ }
284
+ function resolveBlockLayoutDimensions(node, label, context, options = {}) {
285
+ const explicitWidth = positiveDimension(node.props.width, "width");
286
+ const explicitHeight = positiveDimension(node.props.height, "height");
287
+ return {
288
+ width: typeof explicitWidth !== "undefined"
289
+ ? explicitWidth
290
+ : node.props.fill === true
291
+ ? fillContextDimension(context, "width", label)
292
+ : contextBackedDimension(context, "width", label),
293
+ height: typeof explicitHeight !== "undefined"
294
+ ? explicitHeight
295
+ : node.props.fill === true
296
+ ? fillContextDimension(context, "height", label)
297
+ : options.exactHeightFromContext === true
298
+ ? contextBackedDimension(context, "height", label)
299
+ : undefined
300
+ };
301
+ }
302
+ function resolveSplitDimension(node, prop, context) {
303
+ const explicit = positiveDimension(node.props[prop], prop);
304
+ if (typeof explicit !== "undefined") {
305
+ return explicit;
306
+ }
307
+ if (node.props.fill === true) {
308
+ return fillContextDimension(context, prop, "Split");
309
+ }
310
+ const fromContext = contextBackedDimension(context, prop, "Split");
311
+ if (typeof fromContext !== "undefined") {
312
+ return fromContext;
313
+ }
314
+ throw new RangeError("Split requires width/height or render context");
315
+ }
316
+ function decorateContainerFrame(frame, node, options = {}, context) {
18
317
  let next = frame;
19
- next = fitFrame(next, numeric(node.props.width, 0), numeric(node.props.height, 0));
20
- if (node.props.padding) {
21
- next = padFrame(next, numeric(node.props.padding, 0));
318
+ const resolved = resolveNodeStyle(node, context);
319
+ const layoutStyle = resolveNodeLayoutStyle(node, context);
320
+ const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, `${node.tag} padding`);
321
+ const border = normalizeBorder(layoutStyle?.border);
322
+ const decoration = containerDecorationSize(node, context);
323
+ if (!options.constrain) {
324
+ next = fitFrame(next, numeric(options.width ?? node.props.width, 0), numeric(options.height ?? node.props.height, 0));
325
+ }
326
+ else if (node.tag === "terminal-pane" && (typeof node.props.width !== "undefined" || typeof node.props.height !== "undefined")) {
327
+ const width = typeof node.props.width === "undefined" || typeof options.width === "undefined" ? undefined : options.width - decoration.horizontal;
328
+ const height = typeof node.props.height === "undefined" || typeof options.height === "undefined" ? undefined : options.height - decoration.vertical;
329
+ if ((typeof width === "number" && width > 0) || (typeof height === "number" && height > 0)) {
330
+ next = constrainFrame(next, {
331
+ width: typeof width === "number" && width > 0 ? width : undefined,
332
+ height: typeof height === "number" && height > 0 ? height : undefined
333
+ });
334
+ }
22
335
  }
23
- if (node.props.border) {
24
- next = borderFrame(next);
336
+ next = padFrameSides(next, padding);
337
+ next = addBorder(next, border);
338
+ if (options.constrain) {
339
+ next = constrainFrame(next, {
340
+ width: typeof options.width === "undefined" ? positiveDimension(node.props.width, "width") : options.width,
341
+ height: typeof options.height === "undefined" ? positiveDimension(node.props.height, "height") : options.height
342
+ });
25
343
  }
344
+ next = addContainerStyleSpans(next, node, resolved);
26
345
  if (node.props.id && isFocusable(node)) {
27
346
  next = addFocusableHitbox(next, node);
28
347
  }
29
348
  return next;
30
349
  }
31
- function renderInputLine(value, inputState) {
350
+ function addContainerStyleSpans(frame, node, resolved = resolveNodeStyle(node)) {
351
+ const width = getFrameWidth(frame);
352
+ const height = getFrameHeight(frame);
353
+ if (width <= 0 || height <= 0) {
354
+ return frame;
355
+ }
356
+ const spans = frame.spans.slice();
357
+ const containerSpans = resolved.spanKinds.slice();
358
+ const border = normalizeBorder(resolved.style?.border);
359
+ if (!resolved.style?.color && border.color) {
360
+ containerSpans.push(styleSpan("#style", { color: border.color }));
361
+ }
362
+ for (const span of containerSpans) {
363
+ for (let y = 1; y <= height; y += 1) {
364
+ spans.push({ ...span, x1: 1, x2: width + 1, y });
365
+ }
366
+ }
367
+ if (node.props.__focused && isFocusable(node)) {
368
+ for (let y = 1; y <= height; y += 1) {
369
+ spans.push({ kind: "focus", x1: 1, x2: width + 1, y });
370
+ }
371
+ }
372
+ return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
373
+ }
374
+ function decoratedContentDimension(node, prop, outer, context) {
375
+ positiveInteger(outer, `Pane ${prop}`);
376
+ const decoration = containerDecorationSize(node, context);
377
+ const inner = outer - (prop === "width" ? decoration.horizontal : decoration.vertical);
378
+ if (!Number.isFinite(inner) || !Number.isInteger(inner) || inner <= 0) {
379
+ throw new RangeError(`Pane ${prop} leaves no room for Fixed content`);
380
+ }
381
+ return inner;
382
+ }
383
+ function resolveContainerChildContext(node, dimensions, context) {
384
+ if (typeof dimensions.width !== "number" && typeof dimensions.height !== "number") {
385
+ return context;
386
+ }
387
+ return {
388
+ cols: typeof dimensions.width === "number" ? decoratedContentDimension(node, "width", dimensions.width, context) : context?.cols ?? 1,
389
+ rows: typeof dimensions.height === "number" ? decoratedContentDimension(node, "height", dimensions.height, context) : context?.rows ?? 1,
390
+ theme: context?.theme
391
+ };
392
+ }
393
+ function renderInputLine(value, inputState, padding = { top: 0, right: 0, bottom: 0, left: 0 }) {
32
394
  const state = normalizeInputState(inputState, value.length);
33
395
  const { start, end } = getSelectionRange(state);
396
+ const line = `${value.slice(0, state.cursor)}|${value.slice(state.cursor)}`;
397
+ const paddedLine = `${" ".repeat(padding.left)}${line}${" ".repeat(padding.right)}`;
398
+ const textStart = padding.left + 1;
34
399
  return {
35
- line: `> ${value.slice(0, state.cursor)}|${value.slice(state.cursor)}`,
36
- cursor: { x: 3 + state.cursor, y: 1 },
37
- spans: start === end ? [] : [{ kind: "selection", x1: 3 + start, x2: 3 + end, y: 1 }]
400
+ line: paddedLine,
401
+ cursor: { x: textStart + state.cursor, y: 1 },
402
+ spans: start === end ? [] : [{ kind: "input.selection", x1: textStart + start, x2: textStart + end, y: 1 }]
38
403
  };
39
404
  }
40
- function renderTableFrame(node) {
405
+ function renderEditorFrame(node) {
406
+ const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : "";
407
+ const placeholder = typeof node.props.placeholder !== "undefined" ? plainText(node.props.placeholder) : "";
408
+ const displayValue = value.length === 0 && !node.props.__focused && placeholder ? placeholder : value;
409
+ const state = createEditorState(displayValue, node.props.__editorState?.cursor);
410
+ const focusedState = createEditorState(value, node.props.__editorState?.cursor);
411
+ const focusedLine = focusedState.cursor.line;
412
+ const focusedColumn = focusedState.cursor.column;
413
+ const lines = state.lines.map((line, index) => {
414
+ if (!node.props.__focused || index !== focusedLine) {
415
+ return ` ${line}`;
416
+ }
417
+ return `> ${line.slice(0, focusedColumn)}|${line.slice(focusedColumn)}`;
418
+ });
419
+ const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
420
+ const cursor = node.props.__focused ? { x: 3 + focusedColumn, y: focusedLine + 1 } : null;
421
+ const spans = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, lines[focusedLine].length + 1), y: focusedLine + 1 }] : [];
422
+ const height = typeof node.props.height === "undefined" ? undefined : positiveDimension(node.props.height, "height");
423
+ const frame = createFrame(lines, [], cursor, spans);
424
+ const scrollOffset = node.props.__focused && typeof height !== "undefined"
425
+ ? Math.min(Math.max(0, focusedLine - height + 1), Math.max(0, lines.length - height))
426
+ : 0;
427
+ const constrainedFrame = typeof height === "undefined"
428
+ ? frame
429
+ : constrainFrame(scrollOffset > 0 ? cropFrame(frame, scrollOffset, height) : frame, { height });
430
+ if (!node.props.id) {
431
+ return constrainedFrame;
432
+ }
433
+ 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);
434
+ }
435
+ function notifyLayoutContextProbe(node, context) {
436
+ if (context && typeof node.props.__layoutContextProbe === "function") {
437
+ node.props.__layoutContextProbe(context);
438
+ }
439
+ }
440
+ function renderTableFrame(node, context) {
41
441
  const rowNodes = node.children.filter((child) => child.type === "element");
42
442
  if (!rowNodes.length) {
43
443
  return createFrame([""]);
44
444
  }
45
- const rowFrames = rowNodes.map((row) => row.children.map(renderTerminalFrame));
445
+ const rowFrames = rowNodes.map((row) => row.children.map((child) => renderTerminalFrame(child, context)));
46
446
  const columnCount = rowFrames.reduce((max, row) => Math.max(max, row.length), 0);
47
447
  const columnWidths = new Array(columnCount).fill(0);
48
448
  for (const row of rowFrames) {
@@ -82,6 +482,407 @@ function renderTableFrame(node) {
82
482
  }
83
483
  return createFrame(lines, hitboxes, cursor, spans);
84
484
  }
485
+ function resolveSplitBreakpoint(node, width, height) {
486
+ const breakpoints = Array.isArray(node.props.breakpoints) ? node.props.breakpoints : [];
487
+ for (const breakpoint of breakpoints) {
488
+ if (typeof breakpoint.maxCols === "number" && width > breakpoint.maxCols)
489
+ continue;
490
+ if (typeof breakpoint.maxRows === "number" && height > breakpoint.maxRows)
491
+ continue;
492
+ return breakpoint;
493
+ }
494
+ return undefined;
495
+ }
496
+ function parseSplitSize(value, index) {
497
+ if (typeof value === "number") {
498
+ return { type: "absolute", value: positiveInteger(value, `Split sizes[${index}]`) };
499
+ }
500
+ if (typeof value !== "string") {
501
+ throw new RangeError(`Split sizes[${index}] must be a number, percent, or fr value`);
502
+ }
503
+ const percent = value.match(/^([+-]?\d+(?:\.\d+)?)%$/);
504
+ if (percent) {
505
+ const amount = Number(percent[1]);
506
+ if (!Number.isFinite(amount) || amount <= 0)
507
+ throw new RangeError(`Split sizes[${index}] percent must be greater than zero`);
508
+ return { type: "percent", value: amount };
509
+ }
510
+ const fraction = value.match(/^([+-]?\d+(?:\.\d+)?)fr$/);
511
+ if (fraction) {
512
+ const amount = Number(fraction[1]);
513
+ if (!Number.isFinite(amount) || amount <= 0)
514
+ throw new RangeError(`Split sizes[${index}] fr must be greater than zero`);
515
+ return { type: "fr", value: amount };
516
+ }
517
+ throw new RangeError(`Invalid Split sizes[${index}]: ${value}`);
518
+ }
519
+ function allocateDecimals(ideals, total) {
520
+ const sizes = ideals.map((value) => Math.floor(value));
521
+ let remainder = total - sizes.reduce((sum, value) => sum + value, 0);
522
+ const order = ideals.map((value, index) => ({ index, fraction: value - Math.floor(value) }))
523
+ .sort((a, b) => b.fraction - a.fraction || a.index - b.index);
524
+ for (let index = 0; index < order.length && remainder > 0; index += 1) {
525
+ sizes[order[index].index] += 1;
526
+ remainder -= 1;
527
+ }
528
+ return sizes;
529
+ }
530
+ function resolveSplitSizes(sizesInput, childCount, available) {
531
+ if (!Array.isArray(sizesInput)) {
532
+ const base = Math.floor(available / childCount);
533
+ const remainder = available % childCount;
534
+ const sizes = new Array(childCount);
535
+ for (let index = 0; index < childCount; index += 1)
536
+ sizes[index] = base + (index < remainder ? 1 : 0);
537
+ return sizes;
538
+ }
539
+ if (sizesInput.length !== childCount)
540
+ throw new RangeError("Split sizes length must match child count");
541
+ const parsed = sizesInput.map(parseSplitSize);
542
+ const percentTotal = parsed.reduce((sum, size) => sum + (size.type === "percent" ? size.value : 0), 0);
543
+ if (percentTotal > 100)
544
+ throw new RangeError("Split percentage sizes must not exceed 100%");
545
+ const ideals = new Array(childCount).fill(0);
546
+ let absoluteUsed = 0;
547
+ for (let index = 0; index < parsed.length; index += 1) {
548
+ const size = parsed[index];
549
+ if (size.type === "absolute") {
550
+ ideals[index] = size.value;
551
+ absoluteUsed += size.value;
552
+ }
553
+ else if (size.type === "percent") {
554
+ ideals[index] = available * (size.value / 100);
555
+ }
556
+ }
557
+ const percentUsed = ideals.reduce((sum, value, index) => sum + (parsed[index].type === "percent" ? value : 0), 0);
558
+ const remaining = available - absoluteUsed - percentUsed;
559
+ if (remaining < -0.000001)
560
+ throw new RangeError("Split sizes plus gaps must fit within the major axis");
561
+ const frTotal = parsed.reduce((sum, size) => sum + (size.type === "fr" ? size.value : 0), 0);
562
+ for (let index = 0; index < parsed.length; index += 1) {
563
+ const size = parsed[index];
564
+ if (size.type === "fr")
565
+ ideals[index] = frTotal > 0 ? remaining * (size.value / frTotal) : 0;
566
+ }
567
+ const allocationTotal = frTotal > 0 ? available : Math.round(ideals.reduce((sum, value) => sum + value, 0));
568
+ const allocated = allocateDecimals(ideals, allocationTotal);
569
+ if (allocated.reduce((sum, size) => sum + size, 0) > available)
570
+ throw new RangeError("Split sizes plus gaps must fit within the major axis");
571
+ return allocated;
572
+ }
573
+ function isDirectSplitLayoutContainer(node) {
574
+ return node.type === "element" && (node.tag === "terminal-pane" || node.tag === "terminal-box" || node.tag === "terminal-view" || node.tag === "terminal-scroll");
575
+ }
576
+ function renderSplitChildFrame(child, cellWidth, cellHeight, context) {
577
+ const childContext = { cols: cellWidth, rows: cellHeight, theme: context?.theme };
578
+ if (!isDirectSplitLayoutContainer(child)) {
579
+ return renderTerminalFrame(child, childContext);
580
+ }
581
+ const props = { ...child.props };
582
+ if (typeof props.width === "undefined" && props.fill !== true) {
583
+ props.width = cellWidth;
584
+ }
585
+ if (typeof props.height === "undefined" && props.fill !== true) {
586
+ props.height = cellHeight;
587
+ }
588
+ return renderTerminalFrame({ ...child, props }, childContext);
589
+ }
590
+ function renderSplitFrame(node, context) {
591
+ const width = resolveSplitDimension(node, "width", context);
592
+ const height = resolveSplitDimension(node, "height", context);
593
+ const breakpoint = resolveSplitBreakpoint(node, width, height);
594
+ const gap = nonNegativeInteger(breakpoint?.gap ?? node.props.gap, "Split gap");
595
+ const direction = (breakpoint?.direction ?? node.props.direction) === "column" ? "column" : "row";
596
+ const childCount = node.children.length;
597
+ if (!childCount) {
598
+ const frame = createFrame(new Array(height).fill(" ".repeat(width)));
599
+ return node.props.id && isFocusable(node) ? addFocusableHitbox(frame, node) : frame;
600
+ }
601
+ const majorSize = direction === "row" ? width : height;
602
+ const available = majorSize - gap * (childCount - 1);
603
+ if (available < 0) {
604
+ throw new RangeError("Split gap leaves insufficient space for children");
605
+ }
606
+ const sizes = resolveSplitSizes(breakpoint?.sizes ?? node.props.sizes, childCount, available);
607
+ const frames = [];
608
+ for (let index = 0; index < childCount; index += 1) {
609
+ const cellWidth = direction === "row" ? sizes[index] : width;
610
+ const cellHeight = direction === "row" ? height : sizes[index];
611
+ if (cellWidth <= 0 || cellHeight <= 0) {
612
+ continue;
613
+ }
614
+ frames.push(constrainFrame(renderSplitChildFrame(node.children[index], cellWidth, cellHeight, context), { width: cellWidth, height: cellHeight }));
615
+ }
616
+ const frame = frames.length
617
+ ? direction === "row" ? mergeHorizontal(frames, { gap }) : mergeVertical(frames, { gap })
618
+ : createFrame(new Array(height).fill(" ".repeat(width)));
619
+ const constrained = constrainFrame(frame, { width, height });
620
+ return node.props.id && isFocusable(node) ? addFocusableHitbox(constrained, node) : constrained;
621
+ }
622
+ function hasDirectFixedChildren(node) {
623
+ return node.children.some((child) => child.type === "element" && child.tag === "terminal-fixed");
624
+ }
625
+ function hasDirectOverlayChildren(node) {
626
+ return node.children.some((child) => child.type === "element" && child.tag === "terminal-overlay");
627
+ }
628
+ function splitOverlayChildren(children) {
629
+ const baseChildren = [];
630
+ const overlays = [];
631
+ for (const child of children) {
632
+ if (child.type === "element" && child.tag === "terminal-overlay") {
633
+ overlays.push(child);
634
+ }
635
+ else {
636
+ baseChildren.push(child);
637
+ }
638
+ }
639
+ return { baseChildren, overlays };
640
+ }
641
+ function renderBodyFrame(children, props, context) {
642
+ const direction = props.direction === "row" ? "row" : "column";
643
+ const gap = numeric(props.gap, 0);
644
+ const frames = children.map((child) => renderTerminalFrame(child, context));
645
+ return direction === "row" ? mergeHorizontal(frames, { gap }) : mergeVertical(frames, { gap });
646
+ }
647
+ function renderFixedChildFrame(node, width, height) {
648
+ return constrainFrame(renderBodyFrame(node.children, {}, { cols: width, rows: height }), { width, height });
649
+ }
650
+ function renderFixedCompositionFrame(node, width, height) {
651
+ if (!Number.isFinite(width) || !Number.isInteger(width) || width <= 0 || !Number.isFinite(height) || !Number.isInteger(height) || height <= 0) {
652
+ throw new RangeError("Fixed composition requires exact positive parent dimensions");
653
+ }
654
+ const fixedNodes = { top: [], bottom: [], left: [], right: [] };
655
+ const bodyChildren = [];
656
+ for (const child of node.children) {
657
+ if (child.type === "element" && child.tag === "terminal-fixed") {
658
+ fixedNodes[fixedPosition(child.props.position)].push(child);
659
+ }
660
+ else if (child.type === "element" && child.tag === "terminal-overlay") {
661
+ continue;
662
+ }
663
+ else {
664
+ bodyChildren.push(child);
665
+ }
666
+ }
667
+ const topSize = fixedNodes.top.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
668
+ const bottomSize = fixedNodes.bottom.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
669
+ const middleHeight = height - topSize - bottomSize;
670
+ if (middleHeight < 0) {
671
+ throw new RangeError("Fixed top and bottom regions exceed parent height");
672
+ }
673
+ const leftSize = fixedNodes.left.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
674
+ const rightSize = fixedNodes.right.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
675
+ const bodyWidth = width - leftSize - rightSize;
676
+ if (bodyWidth < 0) {
677
+ throw new RangeError("Fixed left and right regions exceed parent width");
678
+ }
679
+ const topFrames = fixedNodes.top.map((fixed) => renderFixedChildFrame(fixed, width, positiveInteger(fixed.props.size, "Fixed size")));
680
+ const bottomFrames = fixedNodes.bottom.map((fixed) => renderFixedChildFrame(fixed, width, positiveInteger(fixed.props.size, "Fixed size")));
681
+ const middleFrames = [];
682
+ if (middleHeight > 0) {
683
+ const leftFrames = fixedNodes.left.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
684
+ const rightFrames = fixedNodes.right.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
685
+ const bodyFrames = bodyWidth > 0
686
+ ? [constrainFrame(renderBodyFrame(bodyChildren, node.props, { cols: bodyWidth, rows: middleHeight }), { width: bodyWidth, height: middleHeight })]
687
+ : [];
688
+ middleFrames.push(constrainFrame(mergeHorizontal([...leftFrames, ...bodyFrames, ...rightFrames]), { width, height: middleHeight }));
689
+ }
690
+ const frame = mergeVertical([...topFrames, ...middleFrames, ...bottomFrames]);
691
+ return constrainFrame(frame, { width, height });
692
+ }
693
+ function renderStandaloneFixedFrame(node, context) {
694
+ const position = fixedPosition(node.props.position);
695
+ const size = positiveInteger(node.props.size, "Fixed size");
696
+ const frame = renderBodyFrame(node.children, {}, context);
697
+ return position === "top" || position === "bottom"
698
+ ? constrainFrame(frame, { height: size })
699
+ : constrainFrame(frame, { width: size });
700
+ }
701
+ function overlayMarginValue(value, axisSize, label) {
702
+ if (typeof value === "number") {
703
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
704
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
705
+ }
706
+ return value;
707
+ }
708
+ if (typeof value === "string") {
709
+ const match = value.match(/^(\d+(?:\.\d+)?)%$/);
710
+ if (!match) {
711
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
712
+ }
713
+ const percent = Number(match[1]);
714
+ if (!Number.isFinite(percent) || percent < 0) {
715
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
716
+ }
717
+ return Math.round(axisSize * percent / 100);
718
+ }
719
+ throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
720
+ }
721
+ function overlayMargins(margin, width, height) {
722
+ if (typeof margin === "number" || typeof margin === "string") {
723
+ const x = overlayMarginValue(margin, width, "Overlay margin");
724
+ const y = overlayMarginValue(margin, height, "Overlay margin");
725
+ return { x, y };
726
+ }
727
+ if (margin && typeof margin === "object" && !Array.isArray(margin)) {
728
+ const axes = margin;
729
+ return {
730
+ x: overlayMarginValue(axes.x, width, "Overlay margin x"),
731
+ y: overlayMarginValue(axes.y, height, "Overlay margin y")
732
+ };
733
+ }
734
+ throw new RangeError("Overlay margin is required");
735
+ }
736
+ function overlayGeometry(node, width, height) {
737
+ const margin = overlayMargins(node.props.margin, width, height);
738
+ const overlayWidth = width - margin.x * 2;
739
+ const overlayHeight = height - margin.y * 2;
740
+ if (overlayWidth < 1 || overlayHeight < 1) {
741
+ throw new RangeError("Overlay margin leaves no renderable area");
742
+ }
743
+ return {
744
+ x: margin.x + 1,
745
+ y: margin.y + 1,
746
+ width: overlayWidth,
747
+ height: overlayHeight
748
+ };
749
+ }
750
+ function renderOverlayChildFrame(node, width, height, context) {
751
+ const geometry = overlayGeometry(node, width, height);
752
+ let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height });
753
+ frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
754
+ if (node.props.id && isFocusable(node)) {
755
+ frame = addFocusableHitbox(frame, node);
756
+ }
757
+ return { frame, geometry };
758
+ }
759
+ function applyDirectOverlays(base, overlays, context) {
760
+ let frame = base;
761
+ const width = Math.max(1, getFrameWidth(base));
762
+ const height = Math.max(1, getFrameHeight(base));
763
+ for (const overlay of overlays) {
764
+ const rendered = renderOverlayChildFrame(overlay, width, height, context);
765
+ frame = overlayFrame(frame, rendered.frame, rendered.geometry);
766
+ }
767
+ return frame;
768
+ }
769
+ function renderScreenFrame(node, context) {
770
+ const { baseChildren, overlays } = splitOverlayChildren(node.children);
771
+ let base;
772
+ if (hasDirectFixedChildren(node)) {
773
+ if (!context) {
774
+ throw new RangeError("Screen with direct Fixed children requires exact render context dimensions");
775
+ }
776
+ if (node.props.title) {
777
+ const title = constrainFrame(createFrame([plainText(node.props.title)]), { width: context.cols, height: 1 });
778
+ const remainingRows = context.rows - 1;
779
+ base = remainingRows > 0
780
+ ? constrainFrame(mergeVertical([title, renderFixedCompositionFrame(node, context.cols, remainingRows)]), { width: context.cols, height: context.rows })
781
+ : constrainFrame(title, { width: context.cols, height: context.rows });
782
+ }
783
+ else {
784
+ base = renderFixedCompositionFrame(node, context.cols, context.rows);
785
+ }
786
+ }
787
+ else {
788
+ const parts = [];
789
+ if (node.props.title) {
790
+ parts.push(createFrame([plainText(node.props.title)]));
791
+ }
792
+ const childContext = context && node.props.title
793
+ ? { cols: context.cols, rows: context.rows - 1, theme: context.theme }
794
+ : context;
795
+ if (!context || !node.props.title || context.rows > 1) {
796
+ parts.push(...baseChildren.map((child) => renderTerminalFrame(child, childContext)));
797
+ }
798
+ base = mergeVertical(parts);
799
+ if (context && overlays.length) {
800
+ base = constrainFrame(base, { width: context.cols, height: context.rows });
801
+ }
802
+ }
803
+ return overlays.length ? applyDirectOverlays(base, overlays, context) : base;
804
+ }
805
+ function renderPaneFrame(node, context) {
806
+ const { baseChildren, overlays } = splitOverlayChildren(node.children);
807
+ const dimensions = resolveBlockLayoutDimensions(node, "Pane", context, { exactHeightFromContext: hasDirectFixedChildren(node) });
808
+ let frame;
809
+ if (hasDirectFixedChildren(node)) {
810
+ const width = decoratedContentDimension(node, "width", dimensions.width ?? positiveInteger(node.props.width, "Pane width"), context);
811
+ const height = decoratedContentDimension(node, "height", dimensions.height ?? positiveInteger(node.props.height, "Pane height"), context);
812
+ frame = renderFixedCompositionFrame(node, width, height);
813
+ frame = decorateContainerFrame(frame, node, { constrain: true, ...dimensions }, context);
814
+ }
815
+ else {
816
+ const childContext = resolveContainerChildContext(node, dimensions, context);
817
+ frame = renderBodyFrame(baseChildren, node.props, childContext);
818
+ frame = decorateContainerFrame(frame, node, { constrain: true, ...dimensions }, context);
819
+ }
820
+ return overlays.length ? applyDirectOverlays(frame, overlays, context) : frame;
821
+ }
822
+ function renderStandaloneOverlayFrame(node, context) {
823
+ if (!context) {
824
+ throw new RangeError("Standalone Overlay requires exact render context dimensions");
825
+ }
826
+ const rendered = renderOverlayChildFrame(node, context.cols, context.rows, context);
827
+ return rendered.frame;
828
+ }
829
+ function renderLogViewFrame(node, context) {
830
+ const dimensions = resolveLayoutDimensions(node, "LogView", context);
831
+ const entries = Array.isArray(node.props.entries) ? node.props.entries : [];
832
+ const lines = [];
833
+ if (entries.length === 0) {
834
+ lines.push(plainText(node.props.emptyText ?? ""));
835
+ }
836
+ else {
837
+ for (let index = 0; index < entries.length; index += 1) {
838
+ const entry = entries[index];
839
+ const value = typeof node.props.renderEntry === "function"
840
+ ? node.props.renderEntry(entry, index)
841
+ : entry?.content ?? "";
842
+ lines.push(...plainText(value).split("\n"));
843
+ }
844
+ }
845
+ const resolved = resolveNodeStyle(node, context);
846
+ const layoutStyle = resolveNodeLayoutStyle(node, context);
847
+ const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, "LogView padding");
848
+ const border = normalizeBorder(layoutStyle?.border);
849
+ const horizontalDecoration = padding.left + padding.right + (border.left ? 1 : 0) + (border.right ? 1 : 0);
850
+ const verticalDecoration = padding.top + padding.bottom + (border.top ? 1 : 0) + (border.bottom ? 1 : 0);
851
+ const innerWidth = typeof dimensions.width === "undefined" ? undefined : dimensions.width - horizontalDecoration;
852
+ const innerHeight = typeof dimensions.height === "undefined" ? undefined : dimensions.height - verticalDecoration;
853
+ let frame = createFrame(lines);
854
+ if ((typeof innerWidth === "number" && innerWidth <= 0) || (typeof innerHeight === "number" && innerHeight <= 0)) {
855
+ frame = createFrame([""]);
856
+ }
857
+ else {
858
+ if (typeof innerHeight !== "undefined") {
859
+ const offset = node.props.followTail === true ? Math.max(0, getFrameHeight(frame) - innerHeight) : 0;
860
+ frame = cropFrame(frame, offset, innerHeight);
861
+ while (getFrameHeight(frame) < innerHeight) {
862
+ frame = createFrame([...frame.lines, ""], frame.hitboxes, frame.cursor, frame.spans);
863
+ }
864
+ }
865
+ if (typeof innerWidth !== "undefined") {
866
+ frame = constrainFrame(frame, { width: innerWidth, height: innerHeight });
867
+ }
868
+ }
869
+ frame = padFrameSides(frame, padding);
870
+ frame = addBorder(frame, border);
871
+ if (typeof dimensions.width !== "undefined") {
872
+ frame = constrainFrame(frame, { width: dimensions.width, height: dimensions.height });
873
+ }
874
+ else if (typeof dimensions.height !== "undefined") {
875
+ frame = cropFrame(frame, 0, dimensions.height);
876
+ while (typeof dimensions.height !== "undefined" && getFrameHeight(frame) < dimensions.height) {
877
+ frame = createFrame([...frame.lines, ""], frame.hitboxes, frame.cursor, frame.spans);
878
+ }
879
+ }
880
+ frame = addContainerStyleSpans(frame, node, resolved);
881
+ if (node.props.id && isFocusable(node)) {
882
+ frame = addFocusableHitbox(frame, node);
883
+ }
884
+ return frame;
885
+ }
85
886
  function renderSeparatedRowFrame(frames, separator = " | ") {
86
887
  if (!frames.length) {
87
888
  return createFrame([""]);
@@ -112,28 +913,33 @@ function renderSeparatedRowFrame(frames, separator = " | ") {
112
913
  }
113
914
  return createFrame(lines, hitboxes, cursor, spans);
114
915
  }
115
- function renderElementFrame(node) {
916
+ function renderElementFrame(node, context) {
917
+ notifyLayoutContextProbe(node, context);
116
918
  switch (node.tag) {
117
919
  case "terminal-screen": {
118
- const parts = [];
119
- if (node.props.title) {
120
- parts.push(createFrame([String(node.props.title)]));
121
- }
122
- parts.push(...node.children.map(renderTerminalFrame));
123
- return mergeVertical(parts);
920
+ return renderScreenFrame(node, context);
124
921
  }
125
922
  case "terminal-box":
126
923
  case "terminal-view":
924
+ case "terminal-pane":
127
925
  case "terminal-scroll": {
128
- const direction = node.props.direction === "row" ? "row" : "column";
129
- const gap = numeric(node.props.gap, 0);
130
- let frame = direction === "row"
131
- ? mergeHorizontal(node.children.map(renderTerminalFrame), { gap })
132
- : mergeVertical(node.children.map(renderTerminalFrame), { gap });
133
- frame = decorateContainerFrame(frame, node);
926
+ let frame;
927
+ if (node.tag === "terminal-pane" && (hasDirectFixedChildren(node) || hasDirectOverlayChildren(node))) {
928
+ return renderPaneFrame(node, context);
929
+ }
930
+ const label = node.tag === "terminal-box" ? "Box" : node.tag === "terminal-view" ? "View" : node.tag === "terminal-scroll" ? "ScrollView" : "Pane";
931
+ const dimensions = node.tag === "terminal-scroll"
932
+ ? resolveLayoutDimensions(node, label, context)
933
+ : resolveBlockLayoutDimensions(node, label, context);
934
+ const childContext = resolveContainerChildContext(node, dimensions, context);
935
+ frame = renderBodyFrame(node.children, node.props, childContext);
936
+ frame = decorateContainerFrame(frame, node, { constrain: node.tag === "terminal-box" || node.tag === "terminal-view" || node.tag === "terminal-pane", ...dimensions }, context);
134
937
  if (node.tag === "terminal-scroll") {
135
938
  const offset = numeric(node.props.__scrollOffset, 0);
136
- const height = numeric(node.props.height, getFrameHeight(frame));
939
+ if (typeof dimensions.width !== "undefined") {
940
+ frame = constrainFrame(frame, { width: dimensions.width });
941
+ }
942
+ const height = numeric(dimensions.height ?? node.props.height, getFrameHeight(frame));
137
943
  frame = cropFrame(frame, offset, height || getFrameHeight(frame));
138
944
  const highlightRows = Array.isArray(node.props.highlightRows) ? node.props.highlightRows.map((value) => Number(value)) : [];
139
945
  const hoveredRow = typeof node.props.__hoveredRow === "number" ? Number(node.props.__hoveredRow) : -1;
@@ -152,77 +958,126 @@ function renderElementFrame(node) {
152
958
  }
153
959
  return frame;
154
960
  }
961
+ case "terminal-split":
962
+ return renderSplitFrame(node, context);
963
+ case "terminal-fixed":
964
+ return renderStandaloneFixedFrame(node, context);
965
+ case "terminal-overlay":
966
+ return renderStandaloneOverlayFrame(node, context);
967
+ case "terminal-log-view":
968
+ return renderLogViewFrame(node, context);
155
969
  case "terminal-list": {
156
970
  const items = Array.isArray(node.props.items) ? node.props.items : [];
157
971
  const selectedIndex = numeric(node.props.__selectedIndex, 0);
158
972
  const hoveredIndex = typeof node.props.__hoveredIndex === "number" ? Number(node.props.__hoveredIndex) : -1;
159
- const lines = items.map((item, index) => {
160
- const label = typeof node.props.renderItem === "function" ? String(node.props.renderItem(item, index)) : String(item);
161
- return index === selectedIndex ? `> ${label}` : ` ${label}`;
162
- });
973
+ const range = listVirtualRange(node, items.length, selectedIndex, context);
974
+ const lines = [];
975
+ for (let index = range.start; index < range.end; index += 1) {
976
+ const item = items[index];
977
+ const label = typeof node.props.renderItem === "function" ? plainText(node.props.renderItem(item, index)) : plainText(item);
978
+ lines.push(label);
979
+ }
980
+ const visibleLines = lines.length ? lines : [""];
981
+ const layoutStyle = resolveLayoutStyle("list.base", node, context);
982
+ const padding = normalizeSpacing(layoutStyle.padding, "List padding");
983
+ const border = normalizeBorder(layoutStyle.border);
984
+ const decorated = addBorder(padFrameSides(createFrame(visibleLines), padding), border);
985
+ const width = Math.max(1, getFrameWidth(decorated));
986
+ const height = Math.max(1, getFrameHeight(decorated));
987
+ const itemY = 1 + (border.top ? 1 : 0) + padding.top;
163
988
  const spans = [];
164
- for (let index = 0; index < lines.length; index += 1) {
165
- const width = lines[index].length + 1;
166
- if (index === selectedIndex) {
167
- spans.push({ kind: "current-row", x1: 1, x2: width, y: index + 1 });
989
+ for (let index = 0; index < visibleLines.length; index += 1) {
990
+ const sourceIndex = range.start + index;
991
+ const y = itemY + index;
992
+ spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
993
+ if (sourceIndex === selectedIndex) {
994
+ spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
168
995
  }
169
- if (index === hoveredIndex) {
170
- spans.push({ kind: "hover", x1: 1, x2: width, y: index + 1 });
996
+ if (sourceIndex === hoveredIndex) {
997
+ spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
171
998
  }
172
999
  }
173
- const frame = createFrame(lines.length ? lines : [""], [], null, spans);
174
- return node.props.id ? addFocusableHitbox(frame, node) : frame;
1000
+ const frame = createFrame(decorated.lines, [], decorated.cursor, spans);
1001
+ const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1002
+ if (!node.props.id) {
1003
+ return styled;
1004
+ }
1005
+ 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);
175
1006
  }
176
1007
  case "terminal-table":
177
- return renderTableFrame(node);
1008
+ return renderTableFrame(node, context);
178
1009
  case "terminal-row":
179
- return renderSeparatedRowFrame(node.children.map(renderTerminalFrame), String(node.props.separator || " | "));
1010
+ return renderSeparatedRowFrame(node.children.map((child) => renderTerminalFrame(child, context)), plainText(node.props.separator || " | "));
180
1011
  case "terminal-td": {
181
- const frame = mergeVertical(node.children.map(renderTerminalFrame));
182
- return node.props.padding ? padFrame(frame, numeric(node.props.padding, 0)) : frame;
1012
+ const frame = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
1013
+ const resolved = resolveNodeStyle(node, context);
1014
+ const layoutStyle = resolveNodeLayoutStyle(node, context);
1015
+ const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, "Td padding");
1016
+ const border = normalizeBorder(layoutStyle?.border);
1017
+ return addFullFrameSpans(addBorder(padFrameSides(frame, padding), border), resolved.spanKinds);
183
1018
  }
184
1019
  case "terminal-text": {
185
- const value = typeof node.props.value !== "undefined" ? String(node.props.value) : node.children.map(textContent).join("");
186
- return createFrame(value.split("\n"));
1020
+ const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : plainText(node.children.map(textContent).join(""));
1021
+ const frame = createFrame(value.split("\n"));
1022
+ return addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
187
1023
  }
188
1024
  case "terminal-input": {
189
1025
  const value = typeof node.props.value !== "undefined" ? node.props.value : node.props.placeholder || "";
190
- const stringValue = String(value);
1026
+ const stringValue = plainText(value);
191
1027
  const displayValue = !node.props.__focused && stringValue.length === 0 && typeof node.props.placeholder !== "undefined"
192
- ? String(node.props.placeholder)
1028
+ ? plainText(node.props.placeholder)
193
1029
  : stringValue;
1030
+ const layoutStyle = resolveLayoutStyle("input.base", node, context);
1031
+ const inputPadding = normalizeSpacing(layoutStyle.padding, "Input padding");
1032
+ const inputBorder = normalizeBorder(layoutStyle.border);
1033
+ const textStartX = (inputBorder.left ? 1 : 0) + inputPadding.left + 1;
194
1034
  if (!node.props.__focused) {
195
- const line = ` ${displayValue}`;
196
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(2, line.length), y1: 1, y2: 1, textStartX: 3, textLength: stringValue.length }] : [];
197
- return createFrame([line], hitboxes, null, []);
1035
+ const line = `${" ".repeat(inputPadding.left)}${displayValue}${" ".repeat(inputPadding.right)}`;
1036
+ const decorated = addBorder(createFrame([line]), inputBorder);
1037
+ const width = Math.max(1, getFrameWidth(decorated));
1038
+ const height = Math.max(1, getFrameHeight(decorated));
1039
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
1040
+ const spans = fullFrameSpans(["input.base"], width, height);
1041
+ return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
198
1042
  }
199
- const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length });
200
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(2, stringValue.length + 2), y1: 1, y2: 1, textStartX: 3, textLength: stringValue.length }] : [];
201
- return createFrame([rendered.line], hitboxes, rendered.cursor, rendered.spans);
1043
+ const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length }, inputPadding);
1044
+ const decorated = addBorder(createFrame([rendered.line], [], rendered.cursor, rendered.spans), inputBorder);
1045
+ const width = Math.max(1, getFrameWidth(decorated));
1046
+ const height = Math.max(1, getFrameHeight(decorated));
1047
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
1048
+ const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
1049
+ return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
202
1050
  }
1051
+ case "terminal-editor":
1052
+ return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
203
1053
  case "terminal-button": {
204
- const label = typeof node.props.label !== "undefined" ? node.props.label : node.children.map(textContent).join("");
205
- const line = node.props.__focused ? `[> ${String(label)} <]` : `[ ${String(label)} ]`;
206
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(1, line.length), y1: 1, y2: 1 }] : [];
207
- const spans = node.props.__focused ? [{ kind: "focus", x1: 4, x2: 4 + String(label).length, y: 1 }] : [];
208
- return createFrame([line], hitboxes, null, spans);
1054
+ const label = typeof node.props.label !== "undefined" ? plainText(node.props.label) : plainText(node.children.map(textContent).join(""));
1055
+ const layoutStyle = resolveLayoutStyle("button.base", node, context);
1056
+ const decorated = decoratedControlFrame([String(label)], layoutStyle);
1057
+ const width = Math.max(1, getFrameWidth(decorated));
1058
+ const height = Math.max(1, getFrameHeight(decorated));
1059
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height }] : [];
1060
+ const kinds = ["button.base", ...nodeStates(node).map((state) => `button.${state}`)];
1061
+ const spans = fullFrameSpans(kinds, width, height);
1062
+ return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
209
1063
  }
210
1064
  default:
211
- return mergeVertical(node.children.map(renderTerminalFrame));
1065
+ return mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
212
1066
  }
213
1067
  }
214
- export function renderTerminalFrame(node) {
1068
+ export function renderTerminalFrame(node, context) {
215
1069
  if (node.type === "text") {
216
- return createFrame([node.value]);
1070
+ return createFrame([plainText(node.value)]);
217
1071
  }
218
- return renderElementFrame(node);
1072
+ return renderElementFrame(node, context);
219
1073
  }
220
- export function renderTerminalNode(node) {
221
- return renderTerminalFrame(node).lines.join("\n");
1074
+ export function renderTerminalNode(node, context) {
1075
+ return renderTerminalFrame(node, context).lines.join("\n");
222
1076
  }
223
- export function renderTerminal(input) {
224
- return resolveRoot(input)
225
- .map(renderTerminalNode)
1077
+ export function renderTerminal(input, context) {
1078
+ const renderContext = validateRenderContext(context);
1079
+ return renderValyrianTerminal(input)
1080
+ .map((node) => renderTerminalNode(node, renderContext))
226
1081
  .filter(Boolean)
227
1082
  .join("\n")
228
1083
  .trimEnd();