@zakmandhro/bunti 0.1.5 → 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 (192) hide show
  1. package/AGENTS.md +65 -0
  2. package/README.md +81 -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 +12 -2
  16. package/dist/components/Button.d.ts.map +1 -1
  17. package/dist/components/Button.js +35 -24
  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 +172 -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 +29 -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 +83 -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 +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 +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 +93 -22
  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 +111 -6
  136. package/dist/layout.d.ts.map +1 -1
  137. package/dist/layout.js +163 -23
  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 +137 -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,374 @@
1
+ import {
2
+ bunti,
3
+ innerRect,
4
+ type RGB,
5
+ type ScreenState,
6
+ setCell,
7
+ splitRect,
8
+ visibleWidth,
9
+ } from '../index.js';
10
+
11
+ /**
12
+ * Bunti Render Engine Demo
13
+ * Direct buffer animation + Rect layout + clipping + mouse hitboxes.
14
+ */
15
+
16
+ let frameCount = 0;
17
+
18
+ const mix = (a: RGB, b: RGB, t: number): RGB => ({
19
+ r: Math.round(a.r + (b.r - a.r) * Math.max(0, Math.min(1, t))),
20
+ g: Math.round(a.g + (b.g - a.g) * Math.max(0, Math.min(1, t))),
21
+ b: Math.round(a.b + (b.b - a.b) * Math.max(0, Math.min(1, t))),
22
+ });
23
+
24
+ const clamp01 = (value: number) => Math.max(0, Math.min(1, value));
25
+
26
+ const addGlow = (
27
+ base: RGB,
28
+ color: RGB,
29
+ amount: number,
30
+ cx: number,
31
+ cy: number,
32
+ x: number,
33
+ y: number,
34
+ rx: number,
35
+ ry: number,
36
+ ) => {
37
+ const dx = (x - cx) / rx;
38
+ const dy = (y - cy) / ry;
39
+ const strength = clamp01(1 - dx * dx - dy * dy) * amount;
40
+ return mix(base, color, strength);
41
+ };
42
+
43
+ function vividTextureColor(nx: number, ny: number): RGB {
44
+ let color: RGB = mix(
45
+ { r: 207, g: 74, b: 236 },
46
+ { r: 255, g: 184, b: 72 },
47
+ clamp01((nx + ny * 0.78) / 1.55),
48
+ );
49
+
50
+ color = addGlow(
51
+ color,
52
+ { r: 113, g: 230, b: 248 },
53
+ 0.74,
54
+ 0.5,
55
+ 0.12,
56
+ nx,
57
+ ny,
58
+ 0.48,
59
+ 0.36,
60
+ );
61
+ color = addGlow(
62
+ color,
63
+ { r: 245, g: 72, b: 218 },
64
+ 0.44,
65
+ 0.1,
66
+ 0.28,
67
+ nx,
68
+ ny,
69
+ 0.34,
70
+ 0.54,
71
+ );
72
+ color = addGlow(
73
+ color,
74
+ { r: 255, g: 236, b: 98 },
75
+ 0.58,
76
+ 0.82,
77
+ 0.78,
78
+ nx,
79
+ ny,
80
+ 0.32,
81
+ 0.36,
82
+ );
83
+ color = addGlow(
84
+ color,
85
+ { r: 255, g: 246, b: 222 },
86
+ 0.22,
87
+ 0.56,
88
+ 0.42,
89
+ nx,
90
+ ny,
91
+ 0.62,
92
+ 0.42,
93
+ );
94
+
95
+ return {
96
+ r: Math.round(color.r),
97
+ g: Math.round(color.g),
98
+ b: Math.round(color.b),
99
+ };
100
+ }
101
+
102
+ function drawEngineField(state: ScreenState, width: number, height: number) {
103
+ for (let y = 0; y < height; y++) {
104
+ for (let x = 0; x < width; x++) {
105
+ const nx = x / Math.max(1, width - 1);
106
+ const ny = y / Math.max(1, height - 1);
107
+ const gradientColor = vividTextureColor(nx, ny);
108
+ setCell(state, x, y, {
109
+ char: '█',
110
+ fg: gradientColor,
111
+ bg: gradientColor,
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ function metric(
118
+ label: string,
119
+ value: string,
120
+ valueColor: (s: string) => string,
121
+ labelColor: (s: string) => string,
122
+ ) {
123
+ return `${labelColor(label.padEnd(13))} ${valueColor(value)}`;
124
+ }
125
+
126
+ function wrapWords(text: string, width: number) {
127
+ const lines: string[] = [];
128
+ let line = '';
129
+
130
+ for (const word of text.split(/\s+/)) {
131
+ const next = line ? `${line} ${word}` : word;
132
+ if (visibleWidth(next) > width && line) {
133
+ lines.push(line);
134
+ line = word;
135
+ } else {
136
+ line = next;
137
+ }
138
+ }
139
+
140
+ if (line) lines.push(line);
141
+ return lines;
142
+ }
143
+
144
+ bunti.render(
145
+ (ctx) => {
146
+ const {
147
+ state,
148
+ box,
149
+ color,
150
+ width,
151
+ height,
152
+ mouseX,
153
+ mouseY,
154
+ lastKey,
155
+ requestStop,
156
+ resolveLocalRect,
157
+ hitbox,
158
+ blit,
159
+ } = ctx;
160
+
161
+ if (lastKey === 'q') requestStop();
162
+
163
+ drawEngineField(state, width, height);
164
+
165
+ const frame = frameCount++;
166
+ const textFg = { r: 205, g: 212, b: 224 };
167
+ const softTextFg = { r: 145, g: 152, b: 166 };
168
+ const panelText = (s: string) => color.fg(textFg, s);
169
+ const softText = (s: string) => color.fg(softTextFg, s);
170
+ const shell = innerRect({ x: 0, y: 0, width, height }, 2);
171
+ const compact = width < 92;
172
+ const stageTop = Math.min(6, Math.max(3, Math.floor(height * 0.12)));
173
+ const title = ' 🍭 BUNTI RENDER ENGINE ';
174
+ const titleArea = resolveLocalRect(
175
+ {
176
+ x: 2,
177
+ y: Math.max(1, Math.floor((shell.y + stageTop) / 2)),
178
+ width: Math.max(0, width - 4),
179
+ height: 1,
180
+ },
181
+ { defaultX: 'left', defaultY: 'top' },
182
+ );
183
+
184
+ box(
185
+ {
186
+ x: titleArea.x,
187
+ y: titleArea.y,
188
+ width: titleArea.width,
189
+ height: titleArea.height,
190
+ border: 'none',
191
+ align: 'center',
192
+ },
193
+ ({ text }) =>
194
+ text(color.bold(color.fg({ r: 255, g: 255, b: 255 }, title))),
195
+ );
196
+
197
+ const stage = innerRect(shell, {
198
+ top: stageTop,
199
+ left: compact ? 1 : 4,
200
+ right: compact ? 1 : 4,
201
+ bottom: 2,
202
+ });
203
+ const [top, bottom] = splitRect(stage, {
204
+ direction: 'vertical',
205
+ constraints: [compact ? '52%' : '50%', '1fr'],
206
+ gap: 1,
207
+ });
208
+ const [telemetry, clipping] = splitRect(top!, {
209
+ direction: compact ? 'vertical' : 'horizontal',
210
+ constraints: ['1fr', '1fr'],
211
+ gap: 2,
212
+ });
213
+ const [hitTargets, colorStack] = splitRect(bottom!, {
214
+ direction: compact ? 'vertical' : 'horizontal',
215
+ constraints: ['1fr', compact ? 7 : '1fr'],
216
+ gap: 2,
217
+ });
218
+
219
+ box(
220
+ {
221
+ x: telemetry!.x,
222
+ y: telemetry!.y,
223
+ width: telemetry!.width,
224
+ height: telemetry!.height,
225
+ border: 'none',
226
+ bgColor: { r: 8, g: 10, b: 14 },
227
+ padding: [1, 2],
228
+ },
229
+ ({ text }) => {
230
+ text(
231
+ [
232
+ color.cyan('CORE TELEMETRY'),
233
+ '',
234
+ metric(
235
+ 'TICK',
236
+ frame.toString().padStart(6, '0'),
237
+ color.yellow,
238
+ panelText,
239
+ ),
240
+ metric('RESOLUTION', `${width}x${height}`, color.green, panelText),
241
+ metric('MOUSE', `${mouseX},${mouseY}`, color.magenta, panelText),
242
+ metric(
243
+ 'MODE',
244
+ compact ? 'STACKED RECTS' : 'SPLIT RECTS',
245
+ color.cyan,
246
+ panelText,
247
+ ),
248
+ '',
249
+ `${color.gray('q')} ${softText('quits')} ${color.gray('resize')} ${softText('reflows')} ${color.gray('mouse')} ${softText('lights hitboxes')}`,
250
+ ].join('\n'),
251
+ );
252
+ },
253
+ );
254
+
255
+ const clippingText =
256
+ 'This panel proves ANSI-aware clipping and wrapping. The same sentence can fold inside one Rect while the neighbor truncates sharply at its boundary without escape-sequence damage.';
257
+ const clippingTextWidth = Math.max(12, clipping!.width - 4);
258
+ const wrappedClipping = wrapWords(clippingText, clippingTextWidth)
259
+ .map(panelText)
260
+ .join('\n');
261
+ const truncatedClipping = wrapWords(
262
+ clippingText.repeat(2),
263
+ clippingTextWidth,
264
+ )
265
+ .slice(0, Math.max(1, clipping!.height - 10))
266
+ .map((line) => color.fg({ r: 160, g: 160, b: 170 }, line))
267
+ .join('\n');
268
+ box(
269
+ {
270
+ x: clipping!.x,
271
+ y: clipping!.y,
272
+ width: clipping!.width,
273
+ height: clipping!.height,
274
+ border: 'none',
275
+ bgColor: { r: 8, g: 8, b: 12 },
276
+ padding: [1, 2],
277
+ },
278
+ ({ text }) => {
279
+ text(`${color.yellow('CLIPPING + WRAP')}\n\n`);
280
+ text(
281
+ `${color.fg(textFg, color.bold('WRAPPED'))}\n${wrappedClipping}\n\n`,
282
+ );
283
+ text(`${color.fg(textFg, color.bold('TRUNCATED'))}\n`);
284
+ text(truncatedClipping);
285
+ },
286
+ );
287
+
288
+ box(
289
+ {
290
+ x: hitTargets!.x,
291
+ y: hitTargets!.y,
292
+ width: hitTargets!.width,
293
+ height: hitTargets!.height,
294
+ border: 'none',
295
+ bgColor: { r: 7, g: 11, b: 9 },
296
+ padding: [1, 2],
297
+ },
298
+ ({ text }) => {
299
+ text(`${color.green('HITBOX MAP')}\n\n`);
300
+ text(`${softText('Move the mouse over the bars.')}\n\n`);
301
+ },
302
+ );
303
+
304
+ const targetInner = innerRect(hitTargets!, {
305
+ top: 5,
306
+ right: 3,
307
+ bottom: 2,
308
+ left: 3,
309
+ });
310
+ const targetRows = splitRect(targetInner, {
311
+ direction: 'vertical',
312
+ constraints: [1, 1, 1],
313
+ gap: 1,
314
+ });
315
+ const labels = ['layout rect', 'input zone', 'paint layer'];
316
+ const fills = [
317
+ { r: 70, g: 190, b: 255 },
318
+ { r: 255, g: 190, b: 75 },
319
+ { r: 195, g: 110, b: 255 },
320
+ ];
321
+
322
+ for (const [index, row] of targetRows.entries()) {
323
+ const id = `engine-demo-target-${index}`;
324
+ const target = hitbox(id, row!);
325
+ const fg = target.hovered
326
+ ? { r: 255, g: 255, b: 255 }
327
+ : { r: 20, g: 24, b: 28 };
328
+ const bg = target.hovered
329
+ ? fills[index]!
330
+ : mix(fills[index]!, { r: 0, g: 0, b: 0 }, 0.45);
331
+ ctx.rect(row!.x, row!.y, row!.width, row!.height, {
332
+ char: ' ',
333
+ bg,
334
+ });
335
+ blit(
336
+ row!.x + 2,
337
+ row!.y,
338
+ `${target.hovered ? '>' : ' '} ${labels[index]!.padEnd(14)} ${target.box.x},${target.box.y},${target.box.width},${target.box.height}`,
339
+ { fg, bg },
340
+ );
341
+ }
342
+
343
+ box(
344
+ {
345
+ x: colorStack!.x,
346
+ y: colorStack!.y,
347
+ width: colorStack!.width,
348
+ height: colorStack!.height,
349
+ border: 'none',
350
+ bgColor: { r: 11, g: 8, b: 16 },
351
+ padding: [1, 2],
352
+ },
353
+ ({ text }) => {
354
+ text(
355
+ [
356
+ color.magenta('COLOR ISOLATION'),
357
+ '',
358
+ `${color.bgBlue(color.white(' nested bg '))} ${color.red('red')} ${color.green('green')} ${color.yellow('yellow')}`,
359
+ panelText('ANSI resets stay contained inside composed rows.'),
360
+ '',
361
+ `${color.magenta('████')}${color.cyan('████')}${color.magenta('████')}`,
362
+ ].join('\n'),
363
+ );
364
+ },
365
+ );
366
+ },
367
+ {
368
+ fps: 8,
369
+ keyboard: true,
370
+ mouse: true,
371
+ alternateBuffer: true,
372
+ hideCursor: true,
373
+ },
374
+ );
@@ -0,0 +1,91 @@
1
+ import { Box, Button, Input } from '../components/index.js';
2
+ import { demo } from './demo-layout';
3
+
4
+ /**
5
+ * Interactive Components Showcase
6
+ */
7
+
8
+ demo('INTERACTIVE COMPONENTS', (ctx, bounds) => {
9
+ const { color, icon } = ctx;
10
+ const W = Math.min(bounds.w - 8, 72);
11
+ const panel = bounds.place({
12
+ y: 1,
13
+ width: W,
14
+ height: Math.min(bounds.h - 1, 13),
15
+ });
16
+ const compact = W < 60;
17
+
18
+ Box(
19
+ ctx,
20
+ {
21
+ x: panel.x,
22
+ y: panel.y,
23
+ width: panel.width,
24
+ height: panel.height,
25
+ border: 'rounded',
26
+ borderColor: 'bunti-blue',
27
+ padding: [1, compact ? 1 : 3],
28
+ align: 'left',
29
+ },
30
+ (sub) => {
31
+ const ghostW = 5;
32
+ const primaryW = compact ? 14 : 15;
33
+ const actionGap = 2;
34
+ const actionRowW = ghostW + actionGap + primaryW;
35
+ const actionRow = sub.resolveLocalRect(
36
+ {
37
+ y: 8,
38
+ width: actionRowW,
39
+ height: 1,
40
+ },
41
+ { defaultX: 'right', defaultY: 'top' },
42
+ );
43
+ const actionY = 8;
44
+
45
+ sub.text(color.cyan(color.bold('TACTICAL CONTROLS')));
46
+ sub.text('\n');
47
+ sub.text(color.dim('Keyboard, mouse, focus, and rerender states.'));
48
+ sub.text('\n\n');
49
+
50
+ Input(sub, {
51
+ id: 'input-mission',
52
+ label: 'MISSION:',
53
+ placeholder: 'Enter mission name...',
54
+ width: compact ? W - 8 : 48,
55
+ });
56
+
57
+ sub.text('\n');
58
+ sub.text(color.dim(`Last key: ${ctx.lastKey || 'none'}`));
59
+ sub.text(color.dim(` Mouse: ${ctx.mouseX},${ctx.mouseY}`));
60
+ sub.text('\n\n');
61
+
62
+ const abort = Button(sub, {
63
+ id: 'btn-abort',
64
+ label: 'Abort',
65
+ variant: 'ghost',
66
+ width: ghostW,
67
+ x: actionRow.x,
68
+ y: actionY,
69
+ detach: true,
70
+ }) as unknown as string;
71
+
72
+ const deploy = Button(sub, {
73
+ id: 'btn-deploy',
74
+ label: 'Deploy',
75
+ icon: icon('rocket'),
76
+ variant: 'primary',
77
+ width: primaryW,
78
+ x: actionRow.x + ghostW + actionGap,
79
+ y: actionY,
80
+ detach: true,
81
+ onClick: () => {
82
+ /* Action handled here */
83
+ },
84
+ }) as unknown as string;
85
+
86
+ sub.text(
87
+ `${' '.repeat(actionRow.x)}${abort}${' '.repeat(actionGap)}${deploy}`,
88
+ );
89
+ },
90
+ );
91
+ });
@@ -0,0 +1,203 @@
1
+ import { Button, Card, Input, Link, Progress } from '../components/index.js';
2
+ import { demo } from './demo-layout';
3
+
4
+ /**
5
+ * Sign-In Demo — web-app style auth form rendered in the terminal.
6
+ */
7
+
8
+ demo('AUTHENTICATION', (ctx, bounds) => {
9
+ const { color, icon, useState, lastKey, focus, isFocused, requestStop } = ctx;
10
+
11
+ const [email] = useState('input-email', '');
12
+ const [password] = useState('input-password', '');
13
+ const [status, setStatus] = useState('login-status', 'idle');
14
+ const [error, setError] = useState('login-error', '');
15
+ const [progress, setProgress] = useState('login-progress', 0);
16
+ const [forgotNotice, setForgotNotice] = useState('forgot-notice', '');
17
+
18
+ const subtleTitle = { r: 140, g: 140, b: 140 };
19
+ const mutedText = { r: 110, g: 110, b: 110 };
20
+ const linkColor = { r: 37, g: 99, b: 235 }; // bootstrap-ish blue
21
+
22
+ const handleLogin = () => {
23
+ if (!email.includes('@')) {
24
+ setError('Please enter a valid email address.');
25
+ return;
26
+ }
27
+ if (password.length < 4) {
28
+ setError('Password must be at least 4 characters.');
29
+ return;
30
+ }
31
+ setError('');
32
+ setForgotNotice('');
33
+ setStatus('loading');
34
+ setProgress(0);
35
+ };
36
+
37
+ if (lastKey === 'q') requestStop();
38
+
39
+ if (status === 'loading') {
40
+ if (progress < 1) {
41
+ const step = ctx.flicker(0.1) ? 0.05 : 0.01;
42
+ setProgress(Math.min(1, progress + step));
43
+ } else {
44
+ setStatus('success');
45
+ }
46
+ }
47
+
48
+ // Success screen
49
+ if (status === 'success') {
50
+ const W = Math.min(bounds.w - 8, 60);
51
+ const card = bounds.place({
52
+ y: 4,
53
+ width: W,
54
+ height: 12,
55
+ });
56
+
57
+ Card(
58
+ ctx,
59
+ {
60
+ x: card.x,
61
+ y: card.y,
62
+ width: card.width,
63
+ height: card.height,
64
+ title: 'Welcome back',
65
+ variant: 'accent',
66
+ bgColor: { r: 250, g: 249, b: 246 },
67
+ },
68
+ (sub) => {
69
+ sub.text(`\n ${icon('check')} ${color.bold("You're signed in")}\n\n`);
70
+ sub.text(
71
+ ` Hi ${color.bold(email.split('@')[0])}, your session is now active.\n\n`,
72
+ );
73
+
74
+ Button(sub, {
75
+ id: 'btn-logout',
76
+ label: 'Sign out',
77
+ icon: icon('exit'),
78
+ variant: 'danger',
79
+ onClick: () => {
80
+ setStatus('idle');
81
+ setProgress(0);
82
+ setError('');
83
+ },
84
+ });
85
+ },
86
+ );
87
+ return;
88
+ }
89
+
90
+ // Sign-in form
91
+ const W = Math.min(bounds.w - 8, 50);
92
+ const form = bounds.place({
93
+ width: W,
94
+ height: 26,
95
+ });
96
+
97
+ Card(
98
+ ctx,
99
+ {
100
+ id: 'auth-card', // hover: border lifts to theme.focus, bg shifts a step
101
+ x: form.x,
102
+ y: form.y,
103
+ width: form.width,
104
+ height: form.height,
105
+ border: 'rounded',
106
+ variant: error ? 'danger' : 'accent',
107
+ bgColor: { r: 250, g: 249, b: 246 },
108
+ },
109
+ (sub) => {
110
+ // Brand logo — centered, nerd-font glyph + bold "Bunti"
111
+ const brandVisible = 2 + 1 + 5; // glyph(2) + space + "Bunti"(5)
112
+ const logo = sub.resolveLocalRect({ width: brandVisible, height: 1 });
113
+ const logoPad = ' '.repeat(logo.x);
114
+ const purple = { r: 147, g: 51, b: 234 };
115
+ const brand = color.fg(purple, color.bold(`${icon('bunti')} Bunti`));
116
+ sub.text(`${logoPad + brand}\n\n`);
117
+
118
+ sub.text(
119
+ color.fg(mutedText, `Sign in to your Bunti account to continue.\n`),
120
+ );
121
+ sub.text(
122
+ color.fg(
123
+ mutedText,
124
+ `Use ${color.bold('Tab')} or your mouse to move between fields.\n\n`,
125
+ ),
126
+ );
127
+
128
+ // Email — label on its own line, Bootstrap style
129
+ sub.text(`${color.fg(subtleTitle, color.bold('Email address'))}\n`);
130
+ Input(sub, {
131
+ id: 'input-email',
132
+ placeholder: 'you@bunti.net',
133
+ width: '100%',
134
+ });
135
+
136
+ sub.text('\n');
137
+
138
+ // Password — label on its own line
139
+ sub.text(`${color.fg(subtleTitle, color.bold('Password'))}\n`);
140
+ Input(sub, {
141
+ id: 'input-password',
142
+ placeholder: 'Enter your password',
143
+ type: 'password',
144
+ width: '100%',
145
+ });
146
+
147
+ if (isFocused('input-email') && lastKey === 'enter')
148
+ focus('input-password');
149
+ if (isFocused('input-password') && lastKey === 'enter') handleLogin();
150
+
151
+ sub.text('\n\n');
152
+
153
+ if (status === 'loading') {
154
+ const barW = W - 26;
155
+ sub.text(` ${icon('loading')} ${color.dim('Signing in ')} `);
156
+ Progress(sub, {
157
+ value: progress,
158
+ width: barW,
159
+ fillColor: { r: 37, g: 99, b: 235 },
160
+ trackColor: { r: 210, g: 208, b: 200 },
161
+ showPercent: true,
162
+ });
163
+ } else {
164
+ Button(sub, {
165
+ id: 'btn-login',
166
+ label: 'Sign in',
167
+ icon: icon('lock'),
168
+ variant: 'primary',
169
+ width: '100%',
170
+ onClick: handleLogin,
171
+ });
172
+ }
173
+
174
+ sub.text('\n');
175
+
176
+ // "Forgot password?" link — underlined, hover-brightened, clickable
177
+ const linkText = 'Forgot password?';
178
+ const link = sub.resolveLocalRect({
179
+ y: sub.cursorY,
180
+ width: linkText.length,
181
+ height: 1,
182
+ });
183
+ Link(sub, {
184
+ id: 'forgot-password',
185
+ label: linkText,
186
+ color: linkColor,
187
+ x: link.x,
188
+ y: link.y,
189
+ onClick: () => {
190
+ setForgotNotice('Password reset link sent to your email.');
191
+ },
192
+ });
193
+
194
+ if (error) {
195
+ sub.text(`\n\n ${color.fg('error', `${icon('warning')} ${error}`)}`);
196
+ } else if (forgotNotice) {
197
+ sub.text(
198
+ `\n\n ${color.fg(mutedText, `${icon('check')} ${forgotNotice}`)}`,
199
+ );
200
+ }
201
+ },
202
+ );
203
+ });