@zakmandhro/bunti 0.1.5 → 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 (192) hide show
  1. package/AGENTS.md +65 -0
  2. package/README.md +86 -11
  3. package/dist/cli.d.ts +44 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +147 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/colors.d.ts +77 -7
  8. package/dist/colors.d.ts.map +1 -1
  9. package/dist/colors.js +238 -39
  10. package/dist/colors.js.map +1 -1
  11. package/dist/components/Box.d.ts +6 -0
  12. package/dist/components/Box.d.ts.map +1 -1
  13. package/dist/components/Box.js +5 -0
  14. package/dist/components/Box.js.map +1 -1
  15. package/dist/components/Button.d.ts +15 -2
  16. package/dist/components/Button.d.ts.map +1 -1
  17. package/dist/components/Button.js +45 -25
  18. package/dist/components/Button.js.map +1 -1
  19. package/dist/components/Card.d.ts +24 -2
  20. package/dist/components/Card.d.ts.map +1 -1
  21. package/dist/components/Card.js +56 -14
  22. package/dist/components/Card.js.map +1 -1
  23. package/dist/components/Header.d.ts +11 -1
  24. package/dist/components/Header.d.ts.map +1 -1
  25. package/dist/components/Header.js +12 -8
  26. package/dist/components/Header.js.map +1 -1
  27. package/dist/components/Input.d.ts +10 -1
  28. package/dist/components/Input.d.ts.map +1 -1
  29. package/dist/components/Input.js +176 -41
  30. package/dist/components/Input.js.map +1 -1
  31. package/dist/components/Link.d.ts +21 -0
  32. package/dist/components/Link.d.ts.map +1 -0
  33. package/dist/components/Link.js +38 -0
  34. package/dist/components/Link.js.map +1 -0
  35. package/dist/components/Modal.d.ts +25 -0
  36. package/dist/components/Modal.d.ts.map +1 -1
  37. package/dist/components/Modal.js +88 -24
  38. package/dist/components/Modal.js.map +1 -1
  39. package/dist/components/Progress.d.ts +27 -0
  40. package/dist/components/Progress.d.ts.map +1 -0
  41. package/dist/components/Progress.js +39 -0
  42. package/dist/components/Progress.js.map +1 -0
  43. package/dist/components/Spinner.d.ts +19 -0
  44. package/dist/components/Spinner.d.ts.map +1 -0
  45. package/dist/components/Spinner.js +27 -0
  46. package/dist/components/Spinner.js.map +1 -0
  47. package/dist/components/index.d.ts +3 -0
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/index.js +3 -0
  50. package/dist/components/index.js.map +1 -1
  51. package/dist/data/nf-glyphs.d.ts +11 -0
  52. package/dist/data/nf-glyphs.d.ts.map +1 -0
  53. package/dist/data/nf-glyphs.js +535 -0
  54. package/dist/data/nf-glyphs.js.map +1 -0
  55. package/dist/data/nf-names.d.ts +8 -0
  56. package/dist/data/nf-names.d.ts.map +1 -0
  57. package/dist/data/nf-names.js +2 -0
  58. package/dist/data/nf-names.js.map +1 -0
  59. package/dist/demo-registry.d.ts +27 -0
  60. package/dist/demo-registry.d.ts.map +1 -0
  61. package/dist/demo-registry.js +57 -0
  62. package/dist/demo-registry.js.map +1 -0
  63. package/dist/demos/2048.ts +743 -0
  64. package/dist/demos/animation.ts +246 -0
  65. package/dist/demos/demo-layout.ts +124 -0
  66. package/dist/demos/engine.ts +374 -0
  67. package/dist/demos/interaction.ts +91 -0
  68. package/dist/demos/login.ts +203 -0
  69. package/dist/demos/mission-control.ts +558 -0
  70. package/dist/demos/showcase.ts +224 -0
  71. package/dist/detect.d.ts +92 -3
  72. package/dist/detect.d.ts.map +1 -1
  73. package/dist/detect.js +218 -40
  74. package/dist/detect.js.map +1 -1
  75. package/dist/diagnostics.d.ts +73 -0
  76. package/dist/diagnostics.d.ts.map +1 -0
  77. package/dist/diagnostics.js +225 -0
  78. package/dist/diagnostics.js.map +1 -0
  79. package/dist/dsl/context.d.ts +11 -0
  80. package/dist/dsl/context.d.ts.map +1 -0
  81. package/dist/dsl/context.js +658 -0
  82. package/dist/dsl/context.js.map +1 -0
  83. package/dist/dsl/hooks.d.ts +12 -0
  84. package/dist/dsl/hooks.d.ts.map +1 -0
  85. package/dist/dsl/hooks.js +148 -0
  86. package/dist/dsl/hooks.js.map +1 -0
  87. package/dist/dsl/interaction.d.ts +31 -0
  88. package/dist/dsl/interaction.d.ts.map +1 -0
  89. package/dist/dsl/interaction.js +150 -0
  90. package/dist/dsl/interaction.js.map +1 -0
  91. package/dist/dsl/layers.d.ts +14 -0
  92. package/dist/dsl/layers.d.ts.map +1 -0
  93. package/dist/dsl/layers.js +87 -0
  94. package/dist/dsl/layers.js.map +1 -0
  95. package/dist/dsl/motion.d.ts +10 -0
  96. package/dist/dsl/motion.d.ts.map +1 -0
  97. package/dist/dsl/motion.js +150 -0
  98. package/dist/dsl/motion.js.map +1 -0
  99. package/dist/dsl/render.d.ts +22 -0
  100. package/dist/dsl/render.d.ts.map +1 -0
  101. package/dist/dsl/render.js +69 -0
  102. package/dist/dsl/render.js.map +1 -0
  103. package/dist/dsl/types.d.ts +555 -0
  104. package/dist/dsl/types.d.ts.map +1 -0
  105. package/dist/dsl/types.js +33 -0
  106. package/dist/dsl/types.js.map +1 -0
  107. package/dist/dsl.d.ts +13 -154
  108. package/dist/dsl.d.ts.map +1 -1
  109. package/dist/dsl.js +12 -658
  110. package/dist/dsl.js.map +1 -1
  111. package/dist/easing.d.ts +41 -1
  112. package/dist/easing.d.ts.map +1 -1
  113. package/dist/easing.js +105 -2
  114. package/dist/easing.js.map +1 -1
  115. package/dist/geometry.d.ts +39 -0
  116. package/dist/geometry.d.ts.map +1 -1
  117. package/dist/geometry.js +20 -0
  118. package/dist/geometry.js.map +1 -1
  119. package/dist/icons-full.d.ts +5 -0
  120. package/dist/icons-full.d.ts.map +1 -0
  121. package/dist/icons-full.js +31 -0
  122. package/dist/icons-full.js.map +1 -0
  123. package/dist/icons.d.ts +426 -5
  124. package/dist/icons.d.ts.map +1 -1
  125. package/dist/icons.js +144 -15
  126. package/dist/icons.js.map +1 -1
  127. package/dist/index.d.ts +95 -23
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +17 -7
  130. package/dist/index.js.map +1 -1
  131. package/dist/input.d.ts +177 -0
  132. package/dist/input.d.ts.map +1 -0
  133. package/dist/input.js +533 -0
  134. package/dist/input.js.map +1 -0
  135. package/dist/layout.d.ts +153 -7
  136. package/dist/layout.d.ts.map +1 -1
  137. package/dist/layout.js +221 -34
  138. package/dist/layout.js.map +1 -1
  139. package/dist/render.d.ts +33 -1
  140. package/dist/render.d.ts.map +1 -1
  141. package/dist/render.js +431 -116
  142. package/dist/render.js.map +1 -1
  143. package/dist/state.d.ts +156 -3
  144. package/dist/state.d.ts.map +1 -1
  145. package/dist/state.js +25 -0
  146. package/dist/state.js.map +1 -1
  147. package/dist/theme.d.ts +129 -0
  148. package/dist/theme.d.ts.map +1 -0
  149. package/dist/theme.js +167 -0
  150. package/dist/theme.js.map +1 -0
  151. package/dist/themes/catppuccin-mocha.d.ts +19 -0
  152. package/dist/themes/catppuccin-mocha.d.ts.map +1 -0
  153. package/dist/themes/catppuccin-mocha.js +36 -0
  154. package/dist/themes/catppuccin-mocha.js.map +1 -0
  155. package/dist/themes/dracula.d.ts +21 -0
  156. package/dist/themes/dracula.d.ts.map +1 -0
  157. package/dist/themes/dracula.js +37 -0
  158. package/dist/themes/dracula.js.map +1 -0
  159. package/dist/themes/github-light.d.ts +21 -0
  160. package/dist/themes/github-light.d.ts.map +1 -0
  161. package/dist/themes/github-light.js +36 -0
  162. package/dist/themes/github-light.js.map +1 -0
  163. package/dist/themes/index.d.ts +19 -0
  164. package/dist/themes/index.d.ts.map +1 -0
  165. package/dist/themes/index.js +17 -0
  166. package/dist/themes/index.js.map +1 -0
  167. package/dist/themes/nord.d.ts +23 -0
  168. package/dist/themes/nord.d.ts.map +1 -0
  169. package/dist/themes/nord.js +38 -0
  170. package/dist/themes/nord.js.map +1 -0
  171. package/dist/themes/one-dark-pro.d.ts +21 -0
  172. package/dist/themes/one-dark-pro.d.ts.map +1 -0
  173. package/dist/themes/one-dark-pro.js +36 -0
  174. package/dist/themes/one-dark-pro.js.map +1 -0
  175. package/dist/themes/tokyo-night.d.ts +20 -0
  176. package/dist/themes/tokyo-night.d.ts.map +1 -0
  177. package/dist/themes/tokyo-night.js +37 -0
  178. package/dist/themes/tokyo-night.js.map +1 -0
  179. package/dist/utils.d.ts +1 -0
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +1 -0
  182. package/dist/utils.js.map +1 -1
  183. package/dist/vendor/colors.d.ts +45 -0
  184. package/dist/vendor/colors.d.ts.map +1 -0
  185. package/dist/vendor/colors.js +72 -0
  186. package/dist/vendor/colors.js.map +1 -0
  187. package/llms.txt +79 -0
  188. package/package.json +23 -7
  189. package/dist/data/glyphs.d.ts +0 -5
  190. package/dist/data/glyphs.d.ts.map +0 -1
  191. package/dist/data/glyphs.js +0 -29
  192. package/dist/data/glyphs.js.map +0 -1
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Bunti DSL context factories: the common per-closure context, the direct
3
+ * (screen-buffer) box renderer, and the top-level screen context.
4
+ */
5
+ import { bg, contrastText, createGradient, darken, fade, fg, lighten, rgb, } from '../colors';
6
+ import { identifyTerminal } from '../detect';
7
+ import { beginHookFrame, hintInputReadWithoutKeyboard, hintOverlappingBoxes, } from '../diagnostics';
8
+ import { resolveRect as resolveGeometryRect, resolvePlacedRect, splitRect as splitGeometryRect, } from '../geometry';
9
+ import { icon, replaceEmojis } from '../icons';
10
+ import { dimRect, getWindow, joinHorizontal, joinVertical, blit as layoutBlit, box as layoutBox, gradient as layoutGradient, list as layoutList, table as layoutTable, viewport as layoutViewport, wallpaper as layoutWallpaper, measureBox, rect, resolveSize, } from '../layout';
11
+ import { darkTheme, resolveTheme, } from '../theme';
12
+ import { visibleWidth } from '../utils';
13
+ import { colors } from '../vendor/colors';
14
+ import { createHooks } from './hooks';
15
+ import { createInteraction } from './interaction';
16
+ import { compositeLayer, createLayerScreenState, layerContentBounds, } from './layers';
17
+ import { createMotion } from './motion';
18
+ import { KEYS, } from './types';
19
+ /** Luminance scale applied by layer drop shadows at composite time. */
20
+ const LAYER_SHADOW_DIM = 0.45;
21
+ function pushHitboxFrame(dslState) {
22
+ const frame = { entries: [] };
23
+ (dslState.hitboxFrames ??= []).push(frame);
24
+ return frame;
25
+ }
26
+ function popHitboxFrame(dslState) {
27
+ dslState.hitboxFrames?.pop();
28
+ }
29
+ /**
30
+ * Re-places hitboxes registered inside a direct box once its painted rect
31
+ * is known. Flow-anchored entries land exactly where their content line
32
+ * painted (per-line alignment included, via the SAME measure the painter
33
+ * used); area-anchored entries shift by the delta between the assumed and
34
+ * painted inner origins. Pass `measure` as undefined when the box wraps —
35
+ * wrapping reflows line indices, so flow entries fall back to the delta.
36
+ */
37
+ function fixupHitboxFrame(frame, measure, boxX, boxY, dx, dy) {
38
+ for (const entry of frame.entries) {
39
+ const flow = entry.flow;
40
+ if (flow && measure && flow.line >= 0 && flow.line < measure.lines.length) {
41
+ entry.box.x = boxX + measure.contentLeft(flow.line) + flow.col;
42
+ entry.box.y = boxY + measure.contentTop(flow.line);
43
+ }
44
+ else {
45
+ entry.box.x += dx;
46
+ entry.box.y += dy;
47
+ }
48
+ }
49
+ }
50
+ /**
51
+ * Converts flow-anchored entries captured during a nested closure back to
52
+ * area anchoring. Nested flow boxes and spans re-position their content
53
+ * within the parent flow in ways the direct-box fixup does not model, so
54
+ * entries registered inside them keep their eagerly resolved rects.
55
+ */
56
+ function dropFlowAnchors(dslState, mark) {
57
+ const frames = dslState.hitboxFrames;
58
+ const frame = frames?.[frames.length - 1];
59
+ if (!frame)
60
+ return;
61
+ for (let i = mark; i < frame.entries.length; i++) {
62
+ frame.entries[i].flow = undefined;
63
+ }
64
+ }
65
+ /** Entry count of the innermost hitbox frame (for dropFlowAnchors marks). */
66
+ function hitboxFrameMark(dslState) {
67
+ const frames = dslState.hitboxFrames;
68
+ return frames?.[frames.length - 1]?.entries.length ?? 0;
69
+ }
70
+ function boxBorderInset(options) {
71
+ return options.border === 'none' || !options.border ? 0 : 1;
72
+ }
73
+ /**
74
+ * Resolves the effective foreground for a box's `color` option. 'blank'
75
+ * auto-picks pure black or pure white text by WCAG luminance of the box
76
+ * background (gradients sample their middle stop).
77
+ */
78
+ function resolveBoxFg(options) {
79
+ if (options.color !== 'blank')
80
+ return options.color;
81
+ const bgColor = options.bgColor;
82
+ if (bgColor === undefined)
83
+ return undefined;
84
+ if (typeof bgColor === 'object' && 'colors' in bgColor) {
85
+ const stops = bgColor.colors;
86
+ if (stops.length === 0)
87
+ return undefined;
88
+ return contrastText(stops[Math.floor(stops.length / 2)]);
89
+ }
90
+ return contrastText(bgColor);
91
+ }
92
+ function activeTheme(dslState, state) {
93
+ // options is shared by reference with layer screen-state copies, so
94
+ // options.theme always reflects the latest setTheme() call.
95
+ return (dslState.themeStack[dslState.themeStack.length - 1] ??
96
+ state.options.theme ??
97
+ state.theme ??
98
+ darkTheme);
99
+ }
100
+ function resolveBoxArea(parent, options, contentWidth, contentHeight) {
101
+ return resolvePlacedRect(parent, {
102
+ x: options.x,
103
+ y: options.y,
104
+ width: options.anchor === 'top' || options.anchor === 'bottom'
105
+ ? parent.width
106
+ : options.width,
107
+ height: options.height,
108
+ anchor: options.anchor,
109
+ contentWidth,
110
+ contentHeight,
111
+ });
112
+ }
113
+ function resolveBoxInnerArea(area, options) {
114
+ const borderInset = boxBorderInset(options);
115
+ const px = options.padding?.[1] ?? 0;
116
+ const py = options.padding?.[0] ?? 0;
117
+ return {
118
+ x: area.x + borderInset + px,
119
+ y: area.y + borderInset + py,
120
+ width: Math.max(0, area.width - borderInset * 2 - px * 2),
121
+ height: Math.max(0, area.height - borderInset * 2 - py * 2),
122
+ };
123
+ }
124
+ /**
125
+ * Redefines ctx.lastKey / ctx.keys as live getters that also feed the
126
+ * "input read without { keyboard: true }" dev hint. Non-enumerable so the
127
+ * screen-context spread of the base context can never trigger a spurious
128
+ * read — only real user reads do.
129
+ */
130
+ function defineInputAccessors(ctx, state) {
131
+ Object.defineProperty(ctx, 'lastKey', {
132
+ configurable: true,
133
+ enumerable: false,
134
+ get: () => {
135
+ hintInputReadWithoutKeyboard(state.options);
136
+ return state.lastKey;
137
+ },
138
+ });
139
+ Object.defineProperty(ctx, 'keys', {
140
+ configurable: true,
141
+ enumerable: false,
142
+ get: () => {
143
+ hintInputReadWithoutKeyboard(state.options);
144
+ return state.keys ?? [];
145
+ },
146
+ });
147
+ }
148
+ const buntiColor = {
149
+ ...colors,
150
+ darken,
151
+ lighten,
152
+ rgb,
153
+ fg,
154
+ bg,
155
+ dim: (text) => fg({ r: 96, g: 102, b: 118 }, text),
156
+ };
157
+ /**
158
+ * Common Context Factory: Provided to every closure.
159
+ */
160
+ function createDSLContext(state, dslState, availableW, availableH, offsetX = 0, offsetY = 0, isRoot = false) {
161
+ const hooks = createHooks(state);
162
+ const motion = createMotion(state, hooks.useState);
163
+ const interaction = createInteraction(state, (bounds) => ctx.resolveRect(bounds),
164
+ // Hitboxes registered while a direct box renders are captured so the
165
+ // renderer can re-place them once the painted rect is known.
166
+ (box, flow) => {
167
+ const frames = dslState.hitboxFrames;
168
+ frames?.[frames.length - 1]?.entries.push({ box, flow });
169
+ });
170
+ const ctx = {
171
+ color: buntiColor,
172
+ state,
173
+ width: availableW,
174
+ height: availableH,
175
+ area: { x: offsetX, y: offsetY, width: availableW, height: availableH },
176
+ isRoot,
177
+ offsetX,
178
+ offsetY,
179
+ get cursorX() {
180
+ const currentFlow = dslState.activeContents.join('');
181
+ const lines = currentFlow.split('\n');
182
+ return visibleWidth(lines[lines.length - 1]);
183
+ },
184
+ get cursorY() {
185
+ const currentFlow = dslState.activeContents.join('');
186
+ return Math.max(0, currentFlow.split('\n').length - 1);
187
+ },
188
+ mouseX: state.mouseX,
189
+ mouseY: state.mouseY,
190
+ mouseButton: state.mouseButton,
191
+ isMouseDown: state.isMouseDown,
192
+ lastKey: state.lastKey,
193
+ keys: state.keys ?? [],
194
+ focusedId: state.focusedId,
195
+ elapsedTime: Date.now() - state.startTime,
196
+ terminal: (state.terminal ??= identifyTerminal()),
197
+ get theme() {
198
+ return activeTheme(dslState, state);
199
+ },
200
+ setTheme(theme) {
201
+ const resolved = resolveTheme(theme);
202
+ state.theme = resolved;
203
+ state.options.theme = resolved;
204
+ state.requestTick?.();
205
+ },
206
+ themed(theme, callback) {
207
+ const next = resolveTheme(theme, activeTheme(dslState, state));
208
+ dslState.themeStack.push(next);
209
+ try {
210
+ callback(ctx);
211
+ }
212
+ finally {
213
+ dslState.themeStack.pop();
214
+ }
215
+ return ctx;
216
+ },
217
+ keyPressed(name) {
218
+ hintInputReadWithoutKeyboard(state.options);
219
+ return (state.keys?.some((e) => e.key === name && e.kind !== 'release') ?? false);
220
+ },
221
+ isKeyHeld(name) {
222
+ hintInputReadWithoutKeyboard(state.options);
223
+ return state.heldKeys?.isHeld(name) ?? false;
224
+ },
225
+ text(str) {
226
+ dslState.activeContents.push(replaceEmojis(String(str)));
227
+ return ctx;
228
+ },
229
+ ...motion,
230
+ ...hooks,
231
+ ...interaction,
232
+ resolveRect(bounds) {
233
+ return resolveGeometryRect({ x: offsetX, y: offsetY, width: availableW, height: availableH }, {
234
+ ...bounds,
235
+ y: bounds.y ?? ctx.cursorY,
236
+ });
237
+ },
238
+ resolveLocalRect(bounds, options) {
239
+ return resolvePlacedRect({ x: 0, y: 0, width: availableW, height: availableH }, bounds, options);
240
+ },
241
+ split(options) {
242
+ return splitGeometryRect({ x: 0, y: 0, width: availableW, height: availableH }, options);
243
+ },
244
+ list(id, items, options = {}) {
245
+ const [selectedIndex, setSelectedIndex] = ctx.useState(`${id}_index`, 0);
246
+ const isFocused = ctx.focusable(id);
247
+ if (isFocused && options.interactive !== false) {
248
+ if (state.lastKey === KEYS.UP)
249
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
250
+ if (state.lastKey === KEYS.DOWN)
251
+ setSelectedIndex(Math.min(items.length - 1, selectedIndex + 1));
252
+ }
253
+ // Mouse: each visible row registers a `${id}:row:${index}` hitbox.
254
+ // Clicking a row selects it (and focuses the list so the selection is
255
+ // visible); the hovered row gets a soft selection wash. Keyboard
256
+ // behavior is unchanged.
257
+ let hoveredIndex;
258
+ if (state.options.mouse && options.interactive !== false) {
259
+ const startY = ctx.cursorY;
260
+ let visibleItems = items;
261
+ let offset = 0;
262
+ let hasMoreAbove = false;
263
+ if (options.maxVisible && items.length > options.maxVisible) {
264
+ const win = getWindow(items, selectedIndex, options.maxVisible);
265
+ visibleItems = win.visible;
266
+ offset = win.start;
267
+ hasMoreAbove = win.hasMoreAbove;
268
+ }
269
+ const resolvedW = resolveSize(options.width, availableW, 0);
270
+ const rowW = resolvedW > 0 ? resolvedW : availableW;
271
+ for (let i = 0; i < visibleItems.length; i++) {
272
+ const actualIdx = offset + i;
273
+ const row = ctx.hitbox(`${id}:row:${actualIdx}`, {
274
+ x: 0,
275
+ y: startY + (hasMoreAbove ? 1 : 0) + i,
276
+ width: rowW,
277
+ height: 1,
278
+ });
279
+ if (row.hovered)
280
+ hoveredIndex = actualIdx;
281
+ if (row.clicked) {
282
+ setSelectedIndex(actualIdx);
283
+ ctx.focus(id);
284
+ }
285
+ }
286
+ }
287
+ // Theme-token fallbacks: focused rows highlight with theme.selection /
288
+ // theme.focus, unfocused lists mute their selected row via theme.muted.
289
+ // Explicit options always win.
290
+ const theme = activeTheme(dslState, state);
291
+ const content = layoutList(items, {
292
+ ...options,
293
+ focusedIndex: selectedIndex,
294
+ focusStyle: isFocused
295
+ ? (options.focusStyle ?? ((s) => theme.focus(s)))
296
+ : (s) => theme.muted(s),
297
+ selectedBg: isFocused
298
+ ? (options.selectedBg ?? theme.selection)
299
+ : undefined,
300
+ hoveredIndex: options.hoveredIndex ?? hoveredIndex,
301
+ hoverBg: options.hoverBg ??
302
+ fade(theme.selection.rgb, theme.background.rgb, 0.55),
303
+ }, availableW);
304
+ dslState.activeContents.push(content);
305
+ return ctx;
306
+ },
307
+ table(rows, options = {}) {
308
+ const content = layoutTable(rows, options, availableW);
309
+ dslState.activeContents.push(content);
310
+ return ctx;
311
+ },
312
+ icon(name) {
313
+ return icon(name);
314
+ },
315
+ blit(x, y, content, style = {}) {
316
+ layoutBlit(state, x, y, content, style);
317
+ return ctx;
318
+ },
319
+ rect(x, y, w, h, style) {
320
+ rect(state, x, y, w, h, style);
321
+ return ctx;
322
+ },
323
+ viewport(content, width, height, scrollY = 0) {
324
+ return layoutViewport(content, width, height, scrollY);
325
+ },
326
+ span(options, callback) {
327
+ const subContents = [];
328
+ dslState.stack.push(dslState.activeContents);
329
+ dslState.activeContents = subContents;
330
+ const mark = hitboxFrameMark(dslState);
331
+ callback(ctx);
332
+ dropFlowAnchors(dslState, mark);
333
+ dslState.activeContents = dslState.stack.pop();
334
+ const combined = subContents.join('');
335
+ let styled = combined;
336
+ if (typeof options.color === 'function') {
337
+ styled = options.color(combined);
338
+ }
339
+ else if (options.color !== undefined) {
340
+ styled = fg(options.color, combined);
341
+ }
342
+ dslState.activeContents.push(styled);
343
+ return styled;
344
+ },
345
+ box(options, callback) {
346
+ if (options.zIndex !== undefined) {
347
+ const { zIndex, ...layeredOptions } = options;
348
+ ctx.layer(zIndex, (layerCtx) => {
349
+ layerCtx.box(layeredOptions, callback);
350
+ });
351
+ return '';
352
+ }
353
+ const preContentW = options.width === undefined ? availableW : 0;
354
+ const preContentH = options.height === undefined ? availableH : 0;
355
+ const preArea = resolveBoxArea(ctx.area, options, preContentW, preContentH);
356
+ const innerArea = resolveBoxInnerArea(preArea, options);
357
+ const subContents = [];
358
+ dslState.stack.push(dslState.activeContents);
359
+ dslState.activeContents = subContents;
360
+ const mark = hitboxFrameMark(dslState);
361
+ const subCtx = createDSLContext(state, dslState, innerArea.width, innerArea.height, innerArea.x, innerArea.y);
362
+ callback(subCtx);
363
+ dropFlowAnchors(dslState, mark);
364
+ dslState.activeContents = dslState.stack.pop();
365
+ const innerContent = subContents.join('');
366
+ const styledBox = layoutBox(innerContent, options, availableW, availableH);
367
+ if (!options.detach) {
368
+ dslState.activeContents.push(styledBox);
369
+ }
370
+ return styledBox;
371
+ },
372
+ layer(zIndexOrOptions, maybeCallback) {
373
+ const callback = typeof zIndexOrOptions === 'function' ? zIndexOrOptions : maybeCallback;
374
+ if (!callback)
375
+ return ctx;
376
+ const layerOptions = typeof zIndexOrOptions === 'number'
377
+ ? { zIndex: zIndexOrOptions }
378
+ : typeof zIndexOrOptions === 'object'
379
+ ? zIndexOrOptions
380
+ : {};
381
+ const zIndex = layerOptions.zIndex ?? 0;
382
+ const layerState = createLayerScreenState(state);
383
+ const layerDslState = {
384
+ activeContents: [],
385
+ stack: [],
386
+ layers: dslState.layers,
387
+ layerOrder: dslState.layerOrder,
388
+ themeStack: dslState.themeStack,
389
+ };
390
+ const layerCtx = createDSLContext(layerState, layerDslState, availableW, availableH, offsetX, offsetY, isRoot);
391
+ layerCtx.box = createDirectBoxRenderer(layerState, layerDslState, layerCtx);
392
+ callback(layerCtx);
393
+ const flow = layerDslState.activeContents.join('');
394
+ if (flow)
395
+ layoutBlit(layerState, offsetX, offsetY, flow);
396
+ dslState.layerOrder = layerDslState.layerOrder + 1;
397
+ dslState.layers.push({
398
+ zIndex,
399
+ order: layerDslState.layerOrder,
400
+ buffer: layerState.backBuffer,
401
+ shadow: layerOptions.shadow,
402
+ backdrop: layerOptions.backdrop,
403
+ bounds: layerContentBounds(layerState.backBuffer, state.width),
404
+ });
405
+ return ctx;
406
+ },
407
+ joinHorizontal(...blocks) {
408
+ return joinHorizontal(...blocks);
409
+ },
410
+ joinVertical(...blocks) {
411
+ return joinVertical(...blocks);
412
+ },
413
+ wallpaper(input) {
414
+ if (typeof input === 'object' && 'colors' in input) {
415
+ layoutGradient(state, input.colors, { direction: input.direction });
416
+ }
417
+ else if (Array.isArray(input)) {
418
+ layoutGradient(state, input);
419
+ }
420
+ else if (typeof input === 'object' && 'color' in input) {
421
+ ctx.wallpaper(input.color);
422
+ }
423
+ else {
424
+ layoutWallpaper(state, { bg: input });
425
+ }
426
+ },
427
+ gradient: (opts) => ({
428
+ colors: createGradient(opts.colors, opts.steps || 10),
429
+ direction: opts.direction || 'vertical',
430
+ steps: opts.steps || 10,
431
+ }),
432
+ rgb,
433
+ requestStop() {
434
+ state.requestStop?.();
435
+ },
436
+ flushFlow() { },
437
+ };
438
+ // Live input accessors (also feed the keyboard-not-enabled dev hint).
439
+ defineInputAccessors(ctx, state);
440
+ return ctx;
441
+ }
442
+ function createDirectBoxRenderer(state, dslState, base) {
443
+ // Dev diagnostics: remember where direct boxes painted this frame
444
+ // (root screen context only — layer states never set boxRects).
445
+ // Anchored boxes are exempt: top/bottom bars are deliberate full-width
446
+ // chrome, and their "overlaps" are almost always small-screen clipping
447
+ // rather than missing-layer bugs.
448
+ const trackBoxRect = (area, options) => {
449
+ if (options.anchor !== undefined)
450
+ return;
451
+ const rects = dslState.boxRects;
452
+ if (rects && rects.length < 50)
453
+ rects.push(area);
454
+ };
455
+ return (options, callback) => {
456
+ if (options.zIndex !== undefined) {
457
+ const { zIndex, ...layeredOptions } = options;
458
+ base.layer(zIndex, (layerCtx) => {
459
+ layerCtx.box(layeredOptions, callback);
460
+ });
461
+ return '';
462
+ }
463
+ const boxOptions = options.anchor === 'top' || options.anchor === 'bottom'
464
+ ? { ...options, width: base.width }
465
+ : options;
466
+ const hasFixedSize = boxOptions.width !== undefined &&
467
+ boxOptions.width !== 'auto' &&
468
+ boxOptions.height !== undefined &&
469
+ boxOptions.height !== 'auto';
470
+ // 'blank' resolves to WCAG auto-contrast black/white vs the box bg.
471
+ const boxFg = resolveBoxFg(boxOptions);
472
+ const fgStyle = boxFg !== undefined ? { fg: boxFg } : {};
473
+ // Visual-only slide (entrance animations): paints shift, geometry
474
+ // (sub-context, hitboxes) stays at the settled rect.
475
+ const paintDy = boxOptions.paintOffsetY ?? 0;
476
+ if (hasFixedSize) {
477
+ const preArea = resolveBoxArea(base.area, boxOptions, 0, 0);
478
+ const innerArea = resolveBoxInnerArea(preArea, boxOptions);
479
+ trackBoxRect(preArea, boxOptions);
480
+ if (boxOptions.bgColor || boxOptions.color) {
481
+ rect(state, preArea.x, preArea.y + paintDy, preArea.width, preArea.height, {
482
+ char: ' ',
483
+ bg: boxOptions.bgColor,
484
+ fg: boxFg,
485
+ });
486
+ }
487
+ const styledBox = layoutBox('', boxOptions, base.width, base.height);
488
+ layoutBlit(state, preArea.x, preArea.y + paintDy, styledBox, fgStyle);
489
+ const frame = pushHitboxFrame(dslState);
490
+ const subContents = [];
491
+ dslState.stack.push(dslState.activeContents);
492
+ dslState.activeContents = subContents;
493
+ const subCtx = createDSLContext(state, dslState, innerArea.width, innerArea.height, innerArea.x, innerArea.y);
494
+ callback(subCtx);
495
+ dslState.activeContents = dslState.stack.pop();
496
+ popHitboxFrame(dslState);
497
+ const contentStr = subContents.join('');
498
+ if (contentStr) {
499
+ const innerOptions = {
500
+ width: innerArea.width,
501
+ height: innerArea.height,
502
+ align: boxOptions.align,
503
+ valign: boxOptions.valign,
504
+ wrap: boxOptions.wrap,
505
+ border: 'none',
506
+ padding: [0, 0],
507
+ };
508
+ const measure = measureBox(contentStr, innerOptions, innerArea.width, innerArea.height);
509
+ const styledContent = layoutBox(contentStr, innerOptions, innerArea.width, innerArea.height, measure);
510
+ layoutBlit(state, innerArea.x, innerArea.y + paintDy, styledContent, fgStyle);
511
+ // Flow-anchored hitboxes land where their content line painted
512
+ // (alignment included); area-anchored rects were exact already.
513
+ fixupHitboxFrame(frame, boxOptions.wrap ? undefined : measure, innerArea.x, innerArea.y, 0, 0);
514
+ }
515
+ return styledBox;
516
+ }
517
+ // Auto-sized path: content must render before the painted rect is
518
+ // known. Seed the sub-context with last frame's measured rect for this
519
+ // declaration slot (exact from frame 2 on), fall back to the
520
+ // parent-sized guess on the first frame, then re-place the sub-context
521
+ // and every captured hitbox at the measured rect below — so hitboxes
522
+ // and rendered output share the same resolved rect within one frame.
523
+ const seqRef = state.autoBoxSeq;
524
+ const seq = seqRef ? seqRef.n++ : -1;
525
+ const seeded = seq >= 0 ? state.autoBoxAreas?.get(seq) : undefined;
526
+ const frame = pushHitboxFrame(dslState);
527
+ const subContents = [];
528
+ dslState.stack.push(dslState.activeContents);
529
+ dslState.activeContents = subContents;
530
+ const preAreaTemp = seeded ?? resolveBoxArea(base.area, boxOptions, base.width, base.height);
531
+ const innerAreaTemp = resolveBoxInnerArea(preAreaTemp, boxOptions);
532
+ const subCtx = createDSLContext(state, dslState, innerAreaTemp.width, innerAreaTemp.height, innerAreaTemp.x, innerAreaTemp.y);
533
+ callback(subCtx);
534
+ dslState.activeContents = dslState.stack.pop();
535
+ popHitboxFrame(dslState);
536
+ const contentStr = subContents.join('');
537
+ const measure = measureBox(contentStr, boxOptions, base.width, base.height);
538
+ const styledBox = layoutBox(contentStr, boxOptions, base.width, base.height, measure);
539
+ const boxArea = resolveBoxArea(base.area, boxOptions, measure.width, measure.height);
540
+ trackBoxRect(boxArea, boxOptions);
541
+ if (seq >= 0)
542
+ state.autoBoxAreas?.set(seq, boxArea);
543
+ // Retarget the sub-context to the painted rect so post-render reads
544
+ // (offsetX/offsetY/width/height/area) match the pixels on screen.
545
+ const innerArea = resolveBoxInnerArea(boxArea, boxOptions);
546
+ subCtx.offsetX = innerArea.x;
547
+ subCtx.offsetY = innerArea.y;
548
+ subCtx.width = innerArea.width;
549
+ subCtx.height = innerArea.height;
550
+ subCtx.area = innerArea;
551
+ fixupHitboxFrame(frame, boxOptions.wrap ? undefined : measure, boxArea.x, boxArea.y, innerArea.x - innerAreaTemp.x, innerArea.y - innerAreaTemp.y);
552
+ if (boxOptions.bgColor || boxOptions.color) {
553
+ rect(state, boxArea.x, boxArea.y + paintDy, boxArea.width, boxArea.height, {
554
+ char: ' ',
555
+ bg: boxOptions.bgColor,
556
+ fg: boxFg,
557
+ });
558
+ }
559
+ layoutBlit(state, boxArea.x, boxArea.y + paintDy, styledBox, fgStyle);
560
+ return styledBox;
561
+ };
562
+ }
563
+ /**
564
+ * Top-level Screen Context
565
+ */
566
+ export function createScreenContext(state) {
567
+ state.hookCounter = 0;
568
+ // Dev diagnostics: frame boundary for keyless hook-drift tracking.
569
+ beginHookFrame(state);
570
+ const previousFocusableIds = state.focusableIds;
571
+ if (state.lastKey === KEYS.TAB && previousFocusableIds.length > 0) {
572
+ const idx = previousFocusableIds.indexOf(state.focusedId || '');
573
+ const nextIdx = (idx + 1) % previousFocusableIds.length;
574
+ state.focusedId = previousFocusableIds[nextIdx];
575
+ }
576
+ state.focusableIds = []; // Rebuild from this frame's rendered focusables.
577
+ // Last frame's settled hitboxes drive hover/click evaluation while this
578
+ // frame's registrations are still being placed (see interaction.ts).
579
+ state.prevHitboxes = state.hitboxes;
580
+ state.hitboxes = new Map(); // Rebuild from this frame's interactive geometry.
581
+ state.autoBoxSeq = { n: 0 }; // Re-key auto-box area seeding per frame.
582
+ state.autoBoxAreas ??= new Map();
583
+ // Hover enter/leave events last one frame; hitbox() refills them as this
584
+ // frame's hitboxes register (mutated, not reassigned: layers share them).
585
+ state.hoverEntered?.clear();
586
+ state.hoverLeft?.clear();
587
+ const dslState = {
588
+ activeContents: [],
589
+ stack: [],
590
+ layers: [],
591
+ layerOrder: 0,
592
+ themeStack: [],
593
+ boxRects: [],
594
+ };
595
+ const base = createDSLContext(state, dslState, state.width, state.height, 0, 0, true);
596
+ const flushFlow = () => {
597
+ const flow = dslState.activeContents.join('');
598
+ if (flow)
599
+ layoutBlit(state, 0, 0, flow);
600
+ // Dev diagnostics: direct boxes that painted over each other this
601
+ // frame with nothing controlling the stacking (hints once).
602
+ hintOverlappingBoxes(dslState.boxRects);
603
+ // Layers composite in ascending z-order. Right before each layer lands,
604
+ // its backdrop dims the whole screen and its shadow dims an offset rect
605
+ // (+2 cols, +1 row) under its painted bounds — both darken everything
606
+ // already composited, lower layers included.
607
+ const screenRect = { x: 0, y: 0, width: state.width, height: state.height };
608
+ dslState.layers
609
+ .sort((a, b) => a.zIndex - b.zIndex || a.order - b.order)
610
+ .forEach((layer) => {
611
+ if (layer.backdrop)
612
+ dimRect(state, screenRect, layer.backdrop);
613
+ if (layer.shadow && layer.bounds) {
614
+ dimRect(state, {
615
+ x: layer.bounds.x + 2,
616
+ y: layer.bounds.y + 1,
617
+ width: layer.bounds.width,
618
+ height: layer.bounds.height,
619
+ }, LAYER_SHADOW_DIM);
620
+ }
621
+ compositeLayer(state, layer.buffer);
622
+ });
623
+ };
624
+ const boxOverride = createDirectBoxRenderer(state, dslState, base);
625
+ const screenCtx = {
626
+ ...base,
627
+ box: boxOverride,
628
+ flushFlow,
629
+ requestStop: () => {
630
+ state.requestStop?.();
631
+ },
632
+ // Rebound so callbacks receive the screen context (direct box renderer),
633
+ // not the flow-mode base it was originally defined against.
634
+ themed(theme, callback) {
635
+ const next = resolveTheme(theme, activeTheme(dslState, state));
636
+ dslState.themeStack.push(next);
637
+ try {
638
+ callback(screenCtx);
639
+ }
640
+ finally {
641
+ dslState.themeStack.pop();
642
+ }
643
+ return screenCtx;
644
+ },
645
+ };
646
+ // Spreading `base` snapshots getter values; re-declare theme as a live
647
+ // getter so themed() overrides are visible on the screen context too.
648
+ Object.defineProperty(screenCtx, 'theme', {
649
+ configurable: true,
650
+ enumerable: true,
651
+ get: () => activeTheme(dslState, state),
652
+ });
653
+ // lastKey/keys are non-enumerable on `base` (so the spread above cannot
654
+ // fake an input read); re-declare them on the screen context.
655
+ defineInputAccessors(screenCtx, state);
656
+ return screenCtx;
657
+ }
658
+ //# sourceMappingURL=context.js.map