@zakmandhro/bunti 0.1.4 → 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 (203) hide show
  1. package/AGENTS.md +65 -0
  2. package/README.md +145 -34
  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 +144 -0
  8. package/dist/colors.d.ts.map +1 -0
  9. package/dist/colors.js +426 -0
  10. package/dist/colors.js.map +1 -0
  11. package/dist/components/Box.d.ts +11 -0
  12. package/dist/components/Box.d.ts.map +1 -0
  13. package/dist/components/Box.js +9 -0
  14. package/dist/components/Box.js.map +1 -0
  15. package/dist/components/Button.d.ts +24 -0
  16. package/dist/components/Button.d.ts.map +1 -0
  17. package/dist/components/Button.js +102 -0
  18. package/dist/components/Button.js.map +1 -0
  19. package/dist/components/Card.d.ts +33 -0
  20. package/dist/components/Card.d.ts.map +1 -0
  21. package/dist/components/Card.js +79 -0
  22. package/dist/components/Card.js.map +1 -0
  23. package/dist/components/Header.d.ts +23 -0
  24. package/dist/components/Header.d.ts.map +1 -0
  25. package/dist/components/Header.js +58 -0
  26. package/dist/components/Header.js.map +1 -0
  27. package/dist/components/Input.d.ts +25 -0
  28. package/dist/components/Input.d.ts.map +1 -0
  29. package/dist/components/Input.js +232 -0
  30. package/dist/components/Input.js.map +1 -0
  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 +29 -0
  34. package/dist/components/Link.js.map +1 -0
  35. package/dist/components/Modal.d.ts +38 -0
  36. package/dist/components/Modal.d.ts.map +1 -0
  37. package/dist/components/Modal.js +87 -0
  38. package/dist/components/Modal.js.map +1 -0
  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 +10 -0
  48. package/dist/components/index.d.ts.map +1 -0
  49. package/dist/components/index.js +10 -0
  50. package/dist/components/index.js.map +1 -0
  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 +104 -0
  72. package/dist/detect.d.ts.map +1 -0
  73. package/dist/detect.js +227 -0
  74. package/dist/detect.js.map +1 -0
  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 +563 -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 +16 -0
  88. package/dist/dsl/interaction.d.ts.map +1 -0
  89. package/dist/dsl/interaction.js +101 -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 +510 -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 +18 -0
  108. package/dist/dsl.d.ts.map +1 -0
  109. package/dist/dsl.js +17 -0
  110. package/dist/dsl.js.map +1 -0
  111. package/dist/easing.d.ts +45 -0
  112. package/dist/easing.d.ts.map +1 -0
  113. package/dist/easing.js +121 -0
  114. package/dist/easing.js.map +1 -0
  115. package/dist/geometry.d.ts +77 -0
  116. package/dist/geometry.d.ts.map +1 -0
  117. package/dist/geometry.js +130 -0
  118. package/dist/geometry.js.map +1 -0
  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 +442 -0
  124. package/dist/icons.d.ts.map +1 -0
  125. package/dist/icons.js +314 -0
  126. package/dist/icons.js.map +1 -0
  127. package/dist/index.d.ts +228 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +46 -0
  130. package/dist/index.js.map +1 -0
  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 +194 -0
  136. package/dist/layout.d.ts.map +1 -0
  137. package/dist/layout.js +673 -0
  138. package/dist/layout.js.map +1 -0
  139. package/dist/render.d.ts +49 -0
  140. package/dist/render.d.ts.map +1 -0
  141. package/dist/render.js +650 -0
  142. package/dist/render.js.map +1 -0
  143. package/dist/state.d.ts +226 -0
  144. package/dist/state.d.ts.map +1 -0
  145. package/dist/state.js +130 -0
  146. package/dist/state.js.map +1 -0
  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 +28 -0
  180. package/dist/utils.d.ts.map +1 -0
  181. package/dist/utils.js +158 -0
  182. package/dist/utils.js.map +1 -0
  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 +52 -11
  189. package/src/colors.ts +0 -255
  190. package/src/components/Button.ts +0 -104
  191. package/src/components/Card.ts +0 -53
  192. package/src/components/Header.ts +0 -65
  193. package/src/components/Input.ts +0 -124
  194. package/src/components/index.ts +0 -4
  195. package/src/data/glyphs.ts +0 -30
  196. package/src/detect.ts +0 -60
  197. package/src/dsl.ts +0 -661
  198. package/src/icons.ts +0 -186
  199. package/src/index.ts +0 -72
  200. package/src/layout.ts +0 -640
  201. package/src/render.ts +0 -302
  202. package/src/state.ts +0 -148
  203. package/src/utils.ts +0 -165
package/dist/layout.js ADDED
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Bunti Functional Layout & Drawing
3
+ * Strictly functional primitives for buffer manipulation and layout generation.
4
+ */
5
+ import { fg as fgColor, isThemeColor, resolveColor, resolveColorToRGB, } from './colors';
6
+ import { colorTier } from './detect';
7
+ import { replaceEmojis } from './icons';
8
+ import { charWidth, truncate, visibleWidth, wrapText } from './utils';
9
+ // --- Functional Primitives (Buffer Manipulation) ---
10
+ /**
11
+ * Writes one cell into the back buffer (bounds-checked). Wide glyphs mark
12
+ * their tail cells as skip so the differ keeps columns aligned.
13
+ */
14
+ export function setCell(state, x, y, cell) {
15
+ if (x >= 0 && x < state.width && y >= 0 && y < state.height) {
16
+ const target = state.backBuffer[y * state.width + x];
17
+ let displayWidth = 1;
18
+ if (cell.char !== undefined) {
19
+ target.char = cell.raw ? cell.char : replaceEmojis(cell.char);
20
+ // Single-ASCII-char cells (fills, borders' spaces, plain text) skip width math.
21
+ displayWidth =
22
+ target.char.length === 1 && target.char.charCodeAt(0) < 0x80
23
+ ? 1
24
+ : charWidth(target.char);
25
+ target.fg = undefined;
26
+ target.fgCode = undefined;
27
+ target.bold = false;
28
+ target.italic = false;
29
+ target.underline = false;
30
+ target.dim = false;
31
+ target.strike = false;
32
+ }
33
+ if (cell.fg !== undefined) {
34
+ target.fg = cell.fg;
35
+ target.fgCode = resolveColor(cell.fg);
36
+ }
37
+ if (cell.bg !== undefined) {
38
+ target.bg = cell.bg;
39
+ target.bgCode = resolveColor(cell.bg);
40
+ }
41
+ if (cell.bold !== undefined)
42
+ target.bold = cell.bold;
43
+ if (cell.italic !== undefined)
44
+ target.italic = cell.italic;
45
+ if (cell.underline !== undefined)
46
+ target.underline = cell.underline;
47
+ if (cell.dim !== undefined)
48
+ target.dim = cell.dim;
49
+ if (cell.strike !== undefined)
50
+ target.strike = cell.strike;
51
+ target.skip = cell.skip ?? false;
52
+ for (let dx = 1; dx < displayWidth; dx++) {
53
+ const skipX = x + dx;
54
+ if (skipX >= state.width)
55
+ break;
56
+ const skip = state.backBuffer[y * state.width + skipX];
57
+ skip.char = '';
58
+ skip.fg = target.fg;
59
+ skip.bg = target.bg;
60
+ skip.fgCode = target.fgCode;
61
+ skip.bgCode = target.bgCode;
62
+ skip.bold = target.bold;
63
+ skip.italic = target.italic;
64
+ skip.underline = target.underline;
65
+ skip.dim = target.dim;
66
+ skip.strike = target.strike;
67
+ skip.skip = true;
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Repaints existing back-buffer cells inside `area` at reduced luminance.
73
+ * Each cell's current fg/bg is resolved to exact RGB (numeric ANSI-256 codes
74
+ * via the xterm table) and scaled toward black by `amount` (0 = unchanged,
75
+ * 1 = fully black). Cells with an undefined bg keep their transparent
76
+ * background (fg-only dim); undefined fg is left alone (there is no color to
77
+ * scale). Wide-glyph tail cells (skip) are dimmed too so they stay in sync
78
+ * with their head cell. No-op on the mono tier — there is no color to dim.
79
+ */
80
+ export function dimRect(state, area, amount) {
81
+ if (colorTier() === 'mono')
82
+ return;
83
+ const t = Math.max(0, Math.min(1, amount));
84
+ if (t === 0)
85
+ return;
86
+ const keep = 1 - t;
87
+ const x0 = Math.max(0, area.x);
88
+ const y0 = Math.max(0, area.y);
89
+ const x1 = Math.min(state.width, area.x + area.width);
90
+ const y1 = Math.min(state.height, area.y + area.height);
91
+ for (let y = y0; y < y1; y++) {
92
+ const rowOffset = y * state.width;
93
+ for (let x = x0; x < x1; x++) {
94
+ const cell = state.backBuffer[rowOffset + x];
95
+ if (cell.fg !== undefined) {
96
+ const rgb = resolveColorToRGB(cell.fg);
97
+ const dimmed = {
98
+ r: Math.round(rgb.r * keep),
99
+ g: Math.round(rgb.g * keep),
100
+ b: Math.round(rgb.b * keep),
101
+ };
102
+ cell.fg = dimmed;
103
+ cell.fgCode = resolveColor(dimmed);
104
+ }
105
+ if (cell.bg !== undefined) {
106
+ const rgb = resolveColorToRGB(cell.bg);
107
+ const dimmed = {
108
+ r: Math.round(rgb.r * keep),
109
+ g: Math.round(rgb.g * keep),
110
+ b: Math.round(rgb.b * keep),
111
+ };
112
+ cell.bg = dimmed;
113
+ cell.bgCode = resolveColor(dimmed);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Fills a w×h cell rectangle at absolute coordinates — bars, panels,
120
+ * charts, backgrounds. Pure-background fills (no `char`) preserve the
121
+ * characters already painted underneath.
122
+ * @example rect(state, 0, 0, state.width, 3, { bg: 'midnight' });
123
+ */
124
+ export function rect(state, x, y, w, h, style) {
125
+ const isGradient = style.bg && typeof style.bg === 'object' && 'colors' in style.bg;
126
+ for (let dy = 0; dy < h; dy++) {
127
+ const row = y + dy;
128
+ if (row < 0 || row >= state.height)
129
+ continue;
130
+ for (let dx = 0; dx < w; dx++) {
131
+ const col = x + dx;
132
+ if (col < 0 || col >= state.width)
133
+ continue;
134
+ let resolvedBg;
135
+ if (isGradient) {
136
+ const grad = style.bg;
137
+ if (grad.direction === 'horizontal') {
138
+ resolvedBg = grad.colors[Math.floor((dx / w) * grad.colors.length)];
139
+ }
140
+ else {
141
+ resolvedBg = grad.colors[Math.floor((dy / h) * grad.colors.length)];
142
+ }
143
+ }
144
+ else {
145
+ resolvedBg =
146
+ style.bg !== undefined
147
+ ? resolveColor(style.bg)
148
+ : undefined;
149
+ }
150
+ const cell = { bg: resolvedBg };
151
+ if (style.char)
152
+ cell.char = style.char;
153
+ if (style.fg)
154
+ cell.fg = resolveColor(style.fg);
155
+ const existing = state.backBuffer[row * state.width + col];
156
+ if (existing.char !== ' ' && !style.char) {
157
+ // Keep existing char/fg if drawing a pure background
158
+ cell.char = existing.char;
159
+ cell.fg = existing.fg;
160
+ }
161
+ setCell(state, col, row, cell);
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Paints a multi-line, ANSI-styled string into the back buffer at absolute
167
+ * cell coordinates, parsing SGR codes into per-cell attributes. Grapheme-
168
+ * aware: emoji and wide glyphs occupy their real column width.
169
+ * @example blit(state, 2, 1, fg('gold', 'FPS 60'));
170
+ */
171
+ export function blit(state, startX, startY, content, style = {}) {
172
+ const lines = content.split('\n');
173
+ for (let row = 0; row < lines.length; row++) {
174
+ const line = lines[row];
175
+ let x = startX;
176
+ let currentFg, currentBg;
177
+ let currentBold = false;
178
+ let currentItalic = false;
179
+ let currentUnderline = false;
180
+ let currentDim = false;
181
+ let currentStrike = false;
182
+ const regex = /\x1B\[([0-9;]*)m|([^\x1B]+)/g;
183
+ let match = null;
184
+ while ((match = regex.exec(line)) !== null) {
185
+ if (match[1] !== undefined) {
186
+ const codes = match[1].split(';');
187
+ for (let i = 0; i < codes.length; i++) {
188
+ const code = parseInt(codes[i] || '0', 10);
189
+ if (code === 0) {
190
+ currentFg = undefined;
191
+ currentBg = undefined;
192
+ currentBold = false;
193
+ currentItalic = false;
194
+ currentUnderline = false;
195
+ currentDim = false;
196
+ currentStrike = false;
197
+ }
198
+ else if (code === 1)
199
+ currentBold = true;
200
+ else if (code === 2)
201
+ currentDim = true;
202
+ else if (code === 3)
203
+ currentItalic = true;
204
+ else if (code === 4)
205
+ currentUnderline = true;
206
+ else if (code === 9)
207
+ currentStrike = true;
208
+ else if (code === 22) {
209
+ // SGR 22 resets intensity: clears BOTH bold and dim.
210
+ currentBold = false;
211
+ currentDim = false;
212
+ }
213
+ else if (code === 23)
214
+ currentItalic = false;
215
+ else if (code === 24)
216
+ currentUnderline = false;
217
+ else if (code === 29)
218
+ currentStrike = false;
219
+ else if (code >= 30 && code <= 37)
220
+ currentFg = (code - 30).toString();
221
+ else if (code >= 40 && code <= 47)
222
+ currentBg = (code - 40).toString();
223
+ else if (code >= 90 && code <= 97)
224
+ currentFg = (code - 90 + 8).toString();
225
+ else if (code >= 100 && code <= 107)
226
+ currentBg = (code - 100 + 8).toString();
227
+ else if (code === 38 && codes[i + 1] === '5') {
228
+ currentFg = parseInt(codes[i + 2], 10);
229
+ i += 2;
230
+ }
231
+ else if (code === 38 && codes[i + 1] === '2') {
232
+ currentFg = {
233
+ r: parseInt(codes[i + 2], 10),
234
+ g: parseInt(codes[i + 3], 10),
235
+ b: parseInt(codes[i + 4], 10),
236
+ };
237
+ i += 4;
238
+ }
239
+ else if (code === 48 && codes[i + 1] === '5') {
240
+ currentBg = parseInt(codes[i + 2], 10);
241
+ i += 2;
242
+ }
243
+ else if (code === 48 && codes[i + 1] === '2') {
244
+ currentBg = {
245
+ r: parseInt(codes[i + 2], 10),
246
+ g: parseInt(codes[i + 3], 10),
247
+ b: parseInt(codes[i + 4], 10),
248
+ };
249
+ i += 4;
250
+ }
251
+ else if (code === 39)
252
+ currentFg = undefined;
253
+ else if (code === 49)
254
+ currentBg = undefined;
255
+ }
256
+ }
257
+ else if (match[2]) {
258
+ const processedText = replaceEmojis(match[2]);
259
+ const segmenter = new Intl.Segmenter('en', {
260
+ granularity: 'grapheme',
261
+ });
262
+ for (const { segment } of segmenter.segment(processedText)) {
263
+ const w = charWidth(segment);
264
+ if (w === 0)
265
+ continue;
266
+ const cell = { char: segment, ...style };
267
+ // Only overwrite buffer colors if explicitly set in the string or style
268
+ if (currentFg !== undefined)
269
+ cell.fg = currentFg;
270
+ else if (style.fg !== undefined)
271
+ cell.fg = style.fg;
272
+ else if (state.options.defaultFg !== undefined)
273
+ cell.fg = state.options.defaultFg;
274
+ if (currentBg !== undefined)
275
+ cell.bg = currentBg;
276
+ else if (style.bg !== undefined)
277
+ cell.bg = style.bg;
278
+ if (currentBold)
279
+ cell.bold = true;
280
+ if (currentItalic)
281
+ cell.italic = true;
282
+ if (currentUnderline)
283
+ cell.underline = true;
284
+ if (currentDim)
285
+ cell.dim = true;
286
+ if (currentStrike)
287
+ cell.strike = true;
288
+ setCell(state, x, startY + row, cell);
289
+ x += w;
290
+ }
291
+ }
292
+ }
293
+ }
294
+ }
295
+ const BORDERS = {
296
+ default: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
297
+ rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
298
+ double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
299
+ dashed: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '╌', v: '╎' },
300
+ dotted: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '┈', v: '┊' },
301
+ frame: {
302
+ tl: '█',
303
+ tr: '█',
304
+ bl: '█',
305
+ br: '█',
306
+ top: '▀',
307
+ bottom: '▄',
308
+ left: '█',
309
+ right: '█',
310
+ },
311
+ 'thick-frame': { tl: '█', tr: '█', bl: '█', br: '█', h: '█', v: '█' },
312
+ classic: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' },
313
+ };
314
+ /**
315
+ * Resolves a SizeUnit to an absolute integer based on parent dimensions.
316
+ */
317
+ export function resolveSize(unit, parentDim, contentDim) {
318
+ if (unit === undefined || unit === 'auto')
319
+ return contentDim;
320
+ if (typeof unit === 'number')
321
+ return unit;
322
+ if (typeof unit === 'string') {
323
+ if (unit.endsWith('%')) {
324
+ const pct = parseFloat(unit) / 100;
325
+ return Math.floor(parentDim * pct);
326
+ }
327
+ if (unit.endsWith('fr')) {
328
+ return parentDim;
329
+ }
330
+ }
331
+ return contentDim;
332
+ }
333
+ /**
334
+ * Generates a styled box string with perfect alignment and optional
335
+ * wrapping. This is the pure string-building form; ctx.box() adds
336
+ * placement and painting on top of it.
337
+ * @example box('hello', { width: 20, border: 'rounded', padding: [0, 1] });
338
+ */
339
+ export function box(content, options = {}, parentW, parentH) {
340
+ // Ensure we operate on the swapped glyphs for all layout math
341
+ content = replaceEmojis(content);
342
+ const px = options.padding?.[1] ?? 0;
343
+ const py = options.padding?.[0] ?? 0;
344
+ const borderStyle = options.border || 'none';
345
+ const borderOffset = borderStyle === 'none' ? 0 : 2;
346
+ const rawLines = content.split('\n');
347
+ const maxRawW = Math.max(...rawLines.map((l) => visibleWidth(l)), 0);
348
+ const intrinsicW = maxRawW + px * 2 + borderOffset;
349
+ // 1. Resolve Target Width (Outer)
350
+ const resolvedW = resolveSize(options.width, parentW || 0, intrinsicW);
351
+ // 2. Calculate Inner Width (between borders, including padding)
352
+ let targetInnerW = Math.max(0, resolvedW - borderOffset);
353
+ if (options.minWidth)
354
+ targetInnerW = Math.max(targetInnerW, options.minWidth - borderOffset);
355
+ if (options.maxWidth)
356
+ targetInnerW = Math.min(targetInnerW, options.maxWidth - borderOffset);
357
+ let lines = [];
358
+ const contentWidth = Math.max(0, targetInnerW - px * 2);
359
+ if (options.wrap && contentWidth > 0) {
360
+ lines = wrapText(content, contentWidth);
361
+ }
362
+ else {
363
+ lines = content.split('\n').map((l) => truncate(l, contentWidth));
364
+ }
365
+ const contentH = lines.length + py * 2;
366
+ const resolvedH = resolveSize(options.height, parentH || 0, contentH);
367
+ let finalInnerH = resolvedH ? resolvedH - borderOffset : contentH;
368
+ if (options.minHeight)
369
+ finalInnerH = Math.max(finalInnerH, options.minHeight - borderOffset);
370
+ if (options.maxHeight)
371
+ finalInnerH = Math.min(finalInnerH, options.maxHeight - borderOffset);
372
+ const b = borderStyle === 'none'
373
+ ? null
374
+ : BORDERS[borderStyle] || BORDERS.default;
375
+ // Color Resolution
376
+ // ThemeColors are callable, so they must be detected BEFORE the
377
+ // function-style border-wrapper path (brand check via isThemeColor).
378
+ const resolveSide = (color) => isThemeColor(color)
379
+ ? (s) => fgColor(color.rgb, s)
380
+ : typeof color === 'function'
381
+ ? color
382
+ : (s) => fgColor(color, s);
383
+ const wrapBg = (s) => {
384
+ if (!options.bgColor)
385
+ return s;
386
+ if (typeof options.bgColor === 'object' && 'colors' in options.bgColor) {
387
+ // Gradients cannot be wrapped as a single ANSI string because they change per-cell.
388
+ // In string-building mode, we must ignore gradients. They are handled in absolute rect mode.
389
+ return s;
390
+ }
391
+ const code = resolveColor(options.bgColor);
392
+ if (code === undefined)
393
+ return s; // mono tier: no color output
394
+ const codeStr = String(code);
395
+ const prefix = codeStr.startsWith('2;') ? '48' : '48;5';
396
+ const bgOn = `\x1b[${prefix};${codeStr}m`;
397
+ // Apply bg, and if the string contains resets, re-apply the bg right after the reset.
398
+ // We add a final reset at the very end to clean up.
399
+ return `${bgOn + s.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[0m`;
400
+ };
401
+ const bc = options.borderColor;
402
+ const colors = typeof bc === 'object' && !('r' in bc)
403
+ ? {
404
+ top: resolveSide(bc.top ||
405
+ bc.left ||
406
+ bc.right),
407
+ bottom: resolveSide(bc.bottom ||
408
+ bc.left ||
409
+ bc.right),
410
+ left: resolveSide(bc.left || bc.top),
411
+ right: resolveSide(bc.right || bc.top),
412
+ }
413
+ : {
414
+ top: resolveSide(bc || ((s) => s)),
415
+ bottom: resolveSide(bc || ((s) => s)),
416
+ left: resolveSide(bc || ((s) => s)),
417
+ right: resolveSide(bc || ((s) => s)),
418
+ };
419
+ const hTop = b?.top || b?.h || ' ';
420
+ const hBottom = b?.bottom || b?.h || ' ';
421
+ const vLeft = b?.left || b?.v || ' ';
422
+ const vRight = b?.right || b?.v || ' ';
423
+ const vSpace = Math.max(0, finalInnerH - lines.length - py * 2);
424
+ let topS = 0, bottomS = vSpace;
425
+ if (options.valign === 'middle') {
426
+ topS = Math.floor(vSpace / 2);
427
+ bottomS = Math.ceil(vSpace / 2);
428
+ }
429
+ else if (options.valign === 'bottom') {
430
+ topS = vSpace;
431
+ bottomS = 0;
432
+ }
433
+ const out = [];
434
+ // Top Border
435
+ if (b) {
436
+ const title = options.title && targetInnerW > 2
437
+ ? truncate(` ${options.title.trim()} `, targetInnerW)
438
+ : '';
439
+ const styledTitle = options.titleStyle ? options.titleStyle(title) : title;
440
+ const titleW = visibleWidth(title);
441
+ const beforeTitle = title ? 1 : 0;
442
+ const afterTitle = Math.max(0, targetInnerW - beforeTitle - titleW);
443
+ out.push(wrapBg(colors.top(b.tl) +
444
+ colors.top(hTop.repeat(beforeTitle)) +
445
+ styledTitle +
446
+ colors.top(hTop.repeat(afterTitle)) +
447
+ colors.top(b.tr)));
448
+ }
449
+ // Top Padding
450
+ for (let i = 0; i < py + topS; i++) {
451
+ let row = b ? colors.left(vLeft) : '';
452
+ row += wrapBg(' '.repeat(targetInnerW));
453
+ if (b)
454
+ row += colors.right(vRight);
455
+ out.push(row);
456
+ }
457
+ // Content Lines
458
+ for (let i = 0; i < lines.length; i++) {
459
+ const line = lines[i].trimEnd();
460
+ const lineW = visibleWidth(line);
461
+ const extra = Math.max(0, targetInnerW - lineW - px * 2);
462
+ let left = 0, right = 0;
463
+ const align = options.align || 'left';
464
+ if (align === 'center') {
465
+ left = px + Math.floor(extra / 2);
466
+ right = px + Math.ceil(extra / 2);
467
+ }
468
+ else if (align === 'right') {
469
+ left = px + extra;
470
+ right = px;
471
+ }
472
+ else {
473
+ left = px;
474
+ right = px + extra;
475
+ }
476
+ let row = b ? colors.left(vLeft) : '';
477
+ row += wrapBg(' '.repeat(Math.max(0, left)) + line + ' '.repeat(Math.max(0, right)));
478
+ if (b)
479
+ row += colors.right(vRight);
480
+ out.push(row);
481
+ }
482
+ // Bottom Padding
483
+ for (let i = 0; i < py + bottomS; i++) {
484
+ let row = b ? colors.left(vLeft) : '';
485
+ row += wrapBg(' '.repeat(targetInnerW));
486
+ if (b)
487
+ row += colors.right(vRight);
488
+ out.push(row);
489
+ }
490
+ // Bottom Border
491
+ if (b) {
492
+ out.push(wrapBg(colors.bottom(b.bl) +
493
+ colors.bottom(hBottom.repeat(targetInnerW)) +
494
+ colors.bottom(b.br)));
495
+ }
496
+ return out.join('\n');
497
+ }
498
+ /** Joins rendered blocks side-by-side, padding each to its own width. */
499
+ export function joinHorizontal(...blocks) {
500
+ const parsed = blocks.map((b) => b.split('\n'));
501
+ const maxH = Math.max(...parsed.map((p) => p.length));
502
+ const widths = parsed.map((p) => Math.max(...p.map(visibleWidth)));
503
+ const out = [];
504
+ for (let i = 0; i < maxH; i++) {
505
+ let row = '';
506
+ for (let j = 0; j < parsed.length; j++) {
507
+ const block = parsed[j];
508
+ const targetW = widths[j];
509
+ if (block[i] !== undefined) {
510
+ row +=
511
+ block[i] +
512
+ ' '.repeat(Math.max(0, targetW - visibleWidth(block[i])));
513
+ }
514
+ else {
515
+ row += ' '.repeat(targetW);
516
+ }
517
+ }
518
+ out.push(row);
519
+ }
520
+ return out.join('\n');
521
+ }
522
+ /** Stacks rendered blocks vertically (skips empty strings). */
523
+ export function joinVertical(...blocks) {
524
+ return blocks.filter(Boolean).join('\n');
525
+ }
526
+ /** Returns a box() partially applied with default StyleOptions. */
527
+ export function createStyle(defaults) {
528
+ return (content, overrides = {}) => {
529
+ return box(content, { ...defaults, ...overrides });
530
+ };
531
+ }
532
+ /** Uppercased ` TEXT ` chip, wrapped by the given styler. */
533
+ export function badge(text, colorFn = (s) => s) {
534
+ return colorFn(` ${text.toUpperCase()} `);
535
+ }
536
+ /**
537
+ * Renders a data table with perfectly aligned columns and shared borders.
538
+ */
539
+ export function table(rows, options = {}, parentW) {
540
+ const borderStyle = options.border || 'default';
541
+ const _px = options.padding?.[1] ?? 1;
542
+ const _py = options.padding?.[0] ?? 0;
543
+ const colCount = rows[0]?.length || 0;
544
+ if (colCount === 0)
545
+ return '';
546
+ // 1. Resolve Column Widths
547
+ const resolvedWidth = resolveSize(options.width, parentW || 0, 80);
548
+ const gutterW = 1;
549
+ const colWidth = Math.floor((resolvedWidth - (colCount - 1) * gutterW) / colCount);
550
+ // 2. Render each cell as a rigid block
551
+ const renderedRows = rows.map((row) => {
552
+ const cells = row.map((content, i) => {
553
+ // Explicitly pad empty content to ensure it occupies the full column width
554
+ const safeContent = content || ' '.repeat(colWidth);
555
+ const cellAlign = options.columns?.[i]?.align
556
+ ? options.columns[i].align
557
+ : 'left';
558
+ return box(safeContent, {
559
+ width: colWidth,
560
+ border: 'none',
561
+ // Strict internal zero-padding; let the gutter handle spacing
562
+ padding: [0, 0],
563
+ align: cellAlign,
564
+ }, colWidth, 0);
565
+ });
566
+ // Join with gutter
567
+ return joinHorizontal(...cells.flatMap((c, i) => i < cells.length - 1 ? [c, ' '.repeat(gutterW)] : [c]));
568
+ });
569
+ return box(joinVertical(...renderedRows), {
570
+ border: borderStyle,
571
+ padding: [0, 0],
572
+ width: resolvedWidth,
573
+ align: 'left',
574
+ });
575
+ }
576
+ /**
577
+ * Computes the scroll window that keeps `selectedIndex` visible in
578
+ * `maxVisible` rows (used by list windowing).
579
+ */
580
+ export function getWindow(items, selectedIndex, maxVisible) {
581
+ const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), items.length - maxVisible));
582
+ const visible = items.slice(start, start + maxVisible);
583
+ return {
584
+ visible,
585
+ start,
586
+ hasMoreAbove: start > 0,
587
+ hasMoreBelow: start + maxVisible < items.length,
588
+ };
589
+ }
590
+ /**
591
+ * Renders items as list rows with selection/hover washes and optional
592
+ * maxVisible windowing. Pure string form; ctx.list() adds the keyboard
593
+ * and mouse behavior on top.
594
+ */
595
+ export function list(items, options = {}, parentW) {
596
+ const bullet = options.bullet || '';
597
+ const indent = ' '.repeat(options.indent || 0);
598
+ const resolvedW = resolveSize(options.width, parentW || 0, 0);
599
+ const wrapLineBg = (line, bg) => {
600
+ const code = resolveColor(bg);
601
+ if (code === undefined)
602
+ return line; // mono tier: no color output
603
+ const codeStr = String(code);
604
+ const prefix = codeStr.startsWith('2;') ? '48' : '48;5';
605
+ const bgOn = `\x1b[${prefix};${codeStr}m`;
606
+ return `${bgOn + line.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[0m`;
607
+ };
608
+ const padToWidth = (line) => resolvedW > 0
609
+ ? line + ' '.repeat(Math.max(0, resolvedW - visibleWidth(line)))
610
+ : line;
611
+ let targetItems = items, offset = 0, hasMoreAbove = false, hasMoreBelow = false;
612
+ if (options.maxVisible && items.length > options.maxVisible) {
613
+ const win = getWindow(items, options.focusedIndex || 0, options.maxVisible);
614
+ targetItems = win.visible;
615
+ offset = win.start;
616
+ hasMoreAbove = win.hasMoreAbove;
617
+ hasMoreBelow = win.hasMoreBelow;
618
+ }
619
+ const rendered = targetItems
620
+ .map((item, idx) => {
621
+ const actualIdx = offset + idx;
622
+ const line = `${indent}${bullet}${item}`;
623
+ if (options.focusedIndex === actualIdx) {
624
+ const styledLine = options.focusStyle ? options.focusStyle(line) : line;
625
+ if (!options.selectedBg)
626
+ return styledLine;
627
+ return wrapLineBg(padToWidth(styledLine), options.selectedBg);
628
+ }
629
+ // Hover wash (selection wins: checked above).
630
+ if (options.hoveredIndex === actualIdx && options.hoverBg) {
631
+ return wrapLineBg(padToWidth(line), options.hoverBg);
632
+ }
633
+ return line;
634
+ })
635
+ .join('\n');
636
+ let out = rendered;
637
+ if (hasMoreAbove)
638
+ out = `${indent} ↑ more…\n${out}`;
639
+ if (hasMoreBelow)
640
+ out = `${out}\n${indent} ↓ more…`;
641
+ return out;
642
+ }
643
+ /**
644
+ * Crops a multi-line string to a width×height window starting at
645
+ * `scrollY` — the primitive for scrollable content.
646
+ */
647
+ export function viewport(content, width, height, scrollY = 0) {
648
+ const lines = content.split('\n');
649
+ const visibleLines = lines.slice(scrollY, scrollY + height);
650
+ return visibleLines
651
+ .map((line) => {
652
+ return truncate(line, width, '');
653
+ })
654
+ .join('\n');
655
+ }
656
+ /** Fills the whole screen background with a solid color. */
657
+ export function wallpaper(state, options) {
658
+ const { bg } = options;
659
+ rect(state, 0, 0, state.width, state.height, { char: ' ', bg });
660
+ }
661
+ /** Fills the whole screen background with a multi-stop gradient. */
662
+ export function gradient(state, colors, options = {}) {
663
+ const { direction = 'vertical' } = options;
664
+ rect(state, 0, 0, state.width, state.height, {
665
+ char: ' ',
666
+ bg: {
667
+ colors,
668
+ direction,
669
+ steps: direction === 'vertical' ? state.height : state.width,
670
+ },
671
+ });
672
+ }
673
+ //# sourceMappingURL=layout.js.map