@zakmandhro/bunti 0.2.1 → 0.4.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 (121) hide show
  1. package/README.md +6 -5
  2. package/dist/cli.d.ts +2 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +33 -9
  5. package/dist/cli.js.map +1 -1
  6. package/dist/colors.d.ts +2 -2
  7. package/dist/colors.js +2 -2
  8. package/dist/components/Box.d.ts +1 -1
  9. package/dist/components/Button.d.ts +1 -1
  10. package/dist/components/Button.js +2 -2
  11. package/dist/components/Card.d.ts +1 -1
  12. package/dist/components/Card.js +3 -3
  13. package/dist/components/Header.d.ts +1 -1
  14. package/dist/components/Header.js +2 -2
  15. package/dist/components/Input.d.ts +2 -2
  16. package/dist/components/Input.js +1 -1
  17. package/dist/components/Link.d.ts +2 -2
  18. package/dist/components/Link.js +2 -2
  19. package/dist/components/Modal.d.ts +1 -1
  20. package/dist/components/Modal.js +2 -2
  21. package/dist/components/Progress.d.ts +2 -2
  22. package/dist/components/Progress.js +2 -2
  23. package/dist/components/Spinner.d.ts +2 -2
  24. package/dist/components/index.d.ts +9 -9
  25. package/dist/components/index.js +9 -9
  26. package/dist/data/nf-glyphs.d.ts +2 -2
  27. package/dist/demo-registry.d.ts.map +1 -1
  28. package/dist/demo-registry.js +6 -1
  29. package/dist/demo-registry.js.map +1 -1
  30. package/dist/demos/2048.js +559 -0
  31. package/dist/demos/2048.ts +1 -1
  32. package/dist/demos/animation.js +177 -0
  33. package/dist/demos/animation.ts +1 -1
  34. package/dist/demos/demo-layout.js +85 -0
  35. package/dist/demos/demo-layout.ts +6 -1
  36. package/dist/demos/engine.js +244 -0
  37. package/dist/demos/interaction.js +74 -0
  38. package/dist/demos/interaction.ts +1 -1
  39. package/dist/demos/login.js +175 -0
  40. package/dist/demos/login.ts +1 -1
  41. package/dist/demos/mission-control.js +427 -0
  42. package/dist/demos/platformer-physics.js +122 -0
  43. package/dist/demos/platformer-physics.ts +284 -0
  44. package/dist/demos/platformer.js +519 -0
  45. package/dist/demos/platformer.ts +761 -0
  46. package/dist/demos/showcase.js +181 -0
  47. package/dist/demos/showcase.ts +1 -1
  48. package/dist/detect.d.ts +24 -7
  49. package/dist/detect.d.ts.map +1 -1
  50. package/dist/detect.js +73 -12
  51. package/dist/detect.js.map +1 -1
  52. package/dist/diagnostics.d.ts +8 -2
  53. package/dist/diagnostics.d.ts.map +1 -1
  54. package/dist/diagnostics.js +11 -0
  55. package/dist/diagnostics.js.map +1 -1
  56. package/dist/dsl/context.d.ts +2 -2
  57. package/dist/dsl/context.d.ts.map +1 -1
  58. package/dist/dsl/context.js +14 -14
  59. package/dist/dsl/context.js.map +1 -1
  60. package/dist/dsl/hooks.d.ts +2 -2
  61. package/dist/dsl/hooks.js +1 -1
  62. package/dist/dsl/interaction.d.ts +3 -3
  63. package/dist/dsl/layers.d.ts +2 -2
  64. package/dist/dsl/motion.d.ts +2 -2
  65. package/dist/dsl/motion.js +2 -2
  66. package/dist/dsl/render.d.ts +2 -2
  67. package/dist/dsl/render.d.ts.map +1 -1
  68. package/dist/dsl/render.js +4 -8
  69. package/dist/dsl/render.js.map +1 -1
  70. package/dist/dsl/types.d.ts +11 -10
  71. package/dist/dsl/types.d.ts.map +1 -1
  72. package/dist/dsl/types.js.map +1 -1
  73. package/dist/dsl.d.ts +4 -4
  74. package/dist/dsl.js +3 -3
  75. package/dist/easing.d.ts +1 -1
  76. package/dist/geometry.d.ts +1 -1
  77. package/dist/geometry.js +1 -1
  78. package/dist/icons-full.d.ts +2 -2
  79. package/dist/icons-full.js +3 -3
  80. package/dist/icons.d.ts +4 -4
  81. package/dist/icons.js +3 -3
  82. package/dist/index.d.ts +45 -36
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +29 -24
  85. package/dist/index.js.map +1 -1
  86. package/dist/layout.d.ts +18 -6
  87. package/dist/layout.d.ts.map +1 -1
  88. package/dist/layout.js +15 -6
  89. package/dist/layout.js.map +1 -1
  90. package/dist/pixels.d.ts +51 -0
  91. package/dist/pixels.d.ts.map +1 -0
  92. package/dist/pixels.js +82 -0
  93. package/dist/pixels.js.map +1 -0
  94. package/dist/render.d.ts +1 -1
  95. package/dist/render.d.ts.map +1 -1
  96. package/dist/render.js +51 -11
  97. package/dist/render.js.map +1 -1
  98. package/dist/state.d.ts +4 -4
  99. package/dist/state.js +1 -1
  100. package/dist/theme.d.ts +2 -2
  101. package/dist/theme.js +1 -1
  102. package/dist/themes/catppuccin-mocha.d.ts +1 -1
  103. package/dist/themes/catppuccin-mocha.js +1 -1
  104. package/dist/themes/dracula.d.ts +1 -1
  105. package/dist/themes/dracula.js +1 -1
  106. package/dist/themes/github-light.d.ts +1 -1
  107. package/dist/themes/github-light.js +1 -1
  108. package/dist/themes/index.d.ts +7 -7
  109. package/dist/themes/index.js +6 -6
  110. package/dist/themes/nord.d.ts +1 -1
  111. package/dist/themes/nord.js +1 -1
  112. package/dist/themes/one-dark-pro.d.ts +1 -1
  113. package/dist/themes/one-dark-pro.js +1 -1
  114. package/dist/themes/tokyo-night.d.ts +1 -1
  115. package/dist/themes/tokyo-night.js +1 -1
  116. package/dist/utils.d.ts +6 -1
  117. package/dist/utils.d.ts.map +1 -1
  118. package/dist/utils.js +68 -3
  119. package/dist/utils.js.map +1 -1
  120. package/llms.txt +4 -2
  121. package/package.json +8 -4
@@ -0,0 +1,177 @@
1
+ import { Box } from "../components/index.js";
2
+ import { easeOutBack, easeOutCubic, lerp } from "../index.js";
3
+ import { demo } from "./demo-layout.js";
4
+ const LOGO = [
5
+ " ┏┓ ┳ ┳ ┏┓┓ ┏┳┓ ┳ ",
6
+ " ┣┻┓ ┃ ┃ ┃┃┃ ┃ ┃ ",
7
+ " ┗━┛ ┗━┛ ┛┗┛ ┻ ┻ "
8
+ ];
9
+ const TIMELINE_IDS = [
10
+ "container",
11
+ "progress",
12
+ "bunti-logo",
13
+ "boot-copy",
14
+ "restart-hint"
15
+ ];
16
+ function gradientLogoLine(ctx, line, progress, muted) {
17
+ return Array.from(line).map((char, index) => {
18
+ if (char === " ")
19
+ return char;
20
+ const phase = index / Math.max(1, line.length - 1);
21
+ const target = phase < 0.5 ? ctx.fade("#39d7ff", "#bd5cff", phase * 2) : ctx.fade("#bd5cff", "#ffe66d", (phase - 0.5) * 2);
22
+ return ctx.color.fg(ctx.fade(muted, target, progress), char);
23
+ }).join("");
24
+ }
25
+ demo("Animation Sequence", (ctx, bounds) => {
26
+ const { color, elapsedTime, lastKey, requestStop } = ctx;
27
+ if (lastKey === "q")
28
+ requestStop();
29
+ if (lastKey === "enter") {
30
+ for (const id of TIMELINE_IDS)
31
+ ctx.restartAnimation(id);
32
+ }
33
+ const [showStatus, setShowStatus] = ctx.useState("show-status", false);
34
+ if (lastKey === " ")
35
+ setShowStatus(!showStatus);
36
+ const panelWidth = Math.min(Math.max(bounds.w - 4, 28), 76);
37
+ const panel = bounds.place({
38
+ y: Math.max(1, Math.floor(bounds.h / 2) - 7),
39
+ width: panelWidth,
40
+ height: Math.min(Math.max(bounds.h - 2, 12), 15)
41
+ });
42
+ const innerX = panel.x + 2;
43
+ const innerY = panel.y + 1;
44
+ const innerW = Math.max(1, panel.width - 4);
45
+ const innerH = Math.max(1, panel.height - 2);
46
+ const containerProgress = ctx.animate(650, {
47
+ id: "container",
48
+ easing: easeOutCubic
49
+ });
50
+ const progress = ctx.animate(1800, {
51
+ id: "progress",
52
+ loop: true,
53
+ delay: 650
54
+ });
55
+ const logoProgress = ctx.animate(900, { id: "bunti-logo", delay: 650 });
56
+ const hintProgress = ctx.animate(650, {
57
+ id: "restart-hint",
58
+ delay: 2850,
59
+ easing: easeOutCubic
60
+ });
61
+ const logoReady = logoProgress >= 1;
62
+ const showLogo = logoReady || ctx.flicker(0.72, { id: "logo-boot" });
63
+ const pulse = ctx.animate(440, { loop: "yoyo" });
64
+ const amber = ctx.rgb(255, Math.floor(170 + pulse * 70), 40);
65
+ const muted = { r: 92, g: 98, b: 112 };
66
+ const panelBg = ctx.fade("#0a0a0b", "#101118", containerProgress);
67
+ const innerBg = ctx.fade("#0a0a0b", "#151724", containerProgress);
68
+ const trackBg = ctx.fade("#0a0a0b", "#202338", containerProgress);
69
+ const borderColor = ctx.fade("#0a0a0b", logoReady ? "mint" : "gray", containerProgress);
70
+ const typed = ctx.typewriter("Bunti features terminal animations", {
71
+ id: "boot-copy",
72
+ cps: 28,
73
+ delay: 1550,
74
+ cursor: "█"
75
+ });
76
+ ctx.rect(panel.x, panel.y, panel.width, panel.height, {
77
+ char: " ",
78
+ bg: panelBg
79
+ });
80
+ ctx.rect(innerX, innerY, innerW, innerH, {
81
+ char: " ",
82
+ bg: innerBg
83
+ });
84
+ ctx.rect(innerX, panel.y + panel.height - 3, innerW, 1, {
85
+ char: " ",
86
+ bg: trackBg
87
+ });
88
+ for (let x = 0;x < innerW; x++) {
89
+ const phase = (x / innerW + progress) % 1;
90
+ const bg = phase < 0.33 ? ctx.fade("#39d7ff", "#bd5cff", phase / 0.33) : phase < 0.66 ? ctx.fade("#bd5cff", "#ffe66d", (phase - 0.33) / 0.33) : ctx.fade("#ffe66d", "#39d7ff", (phase - 0.66) / 0.34);
91
+ ctx.rect(innerX + x, panel.y + panel.height - 3, 1, 1, {
92
+ char: " ",
93
+ bg
94
+ });
95
+ }
96
+ Box(ctx, {
97
+ x: panel.x,
98
+ y: panel.y,
99
+ width: panel.width,
100
+ height: panel.height,
101
+ border: "rounded",
102
+ borderColor,
103
+ padding: [1, 2],
104
+ align: "center",
105
+ valign: "middle"
106
+ }, (sub) => {
107
+ sub.text(color.cyan(color.bold("BOOT SEQUENCE")));
108
+ sub.text(`
109
+
110
+ `);
111
+ LOGO.forEach((line, index) => {
112
+ const lineProgress = ctx.stagger(index + 4, {
113
+ id: "bunti-logo",
114
+ delay: 160,
115
+ duration: 520,
116
+ easing: easeOutCubic
117
+ });
118
+ sub.text(showLogo ? gradientLogoLine(ctx, line, lineProgress, muted) : " ".repeat(line.length));
119
+ sub.text(`
120
+ `);
121
+ });
122
+ sub.text(`
123
+ `);
124
+ sub.text(logoReady ? color.fg(amber, ">>> SYSTEMS READY <<<") : color.fg(muted, "Booting systems..."));
125
+ sub.text(`
126
+ `);
127
+ sub.text(color.fg(typed.done ? "mint" : "silver", typed.text));
128
+ sub.text(color.fg(amber, typed.cursor));
129
+ });
130
+ const status = ctx.transition("status-overlay", showStatus, {
131
+ duration: 320,
132
+ exitDuration: 220,
133
+ easing: easeOutBack
134
+ });
135
+ if (status.mounted) {
136
+ const overlayW = Math.min(34, Math.max(24, panel.width - 12));
137
+ const overlayX = panel.x + Math.floor((panel.width - overlayW) / 2);
138
+ const overlayY = Math.round(lerp(panel.y + panel.height + 2, panel.y + 3, status.progress));
139
+ const overlayBorder = ctx.fade("#0a0a0b", "#39d7ff", status.progress);
140
+ const overlayText = ctx.fade("#0a0a0b", "#e8ecf4", status.progress);
141
+ ctx.layer(10, (layer) => {
142
+ layer.box({
143
+ x: overlayX,
144
+ y: overlayY,
145
+ width: overlayW,
146
+ height: 6,
147
+ border: "rounded",
148
+ borderColor: overlayBorder,
149
+ bgColor: "#101426",
150
+ padding: [0, 2],
151
+ align: "left",
152
+ title: "STATUS"
153
+ }, (sub) => {
154
+ sub.text(color.fg(overlayText, `frame ${ctx.frame}`));
155
+ sub.text(`
156
+ `);
157
+ sub.text(color.fg(overlayText, `dt ${ctx.dt}ms`));
158
+ sub.text(`
159
+ `);
160
+ sub.text(color.fg(overlayText, `elapsed ${Math.round(elapsedTime)}ms`));
161
+ sub.text(`
162
+ `);
163
+ sub.text(color.fg(overlayText, "SPACE to dismiss"));
164
+ });
165
+ });
166
+ }
167
+ const hint = "ENTER restart | SPACE status | q quit";
168
+ ctx.blit(panel.x + Math.max(0, Math.floor((panel.width - hint.length) / 2)), panel.y + panel.height + 1, color.fg(ctx.fade("#0a0a0b", { r: 96, g: 102, b: 118 }, hintProgress), hint));
169
+ }, {
170
+ fps: 20,
171
+ alternateBuffer: true,
172
+ hideCursor: true,
173
+ nerdFont: true,
174
+ keyboard: true,
175
+ mouse: true,
176
+ footer: false
177
+ });
@@ -10,7 +10,7 @@
10
10
  import { Box } from '../components/index.js';
11
11
  import type { BuntiContext } from '../dsl/index.js';
12
12
  import { easeOutBack, easeOutCubic, lerp } from '../index.js';
13
- import { demo } from './demo-layout';
13
+ import { demo } from './demo-layout.js';
14
14
 
15
15
  const LOGO = [
16
16
  ' ┏┓ ┳ ┳ ┏┓┓ ┏┳┓ ┳ ',
@@ -0,0 +1,85 @@
1
+ import { Box, Header } from "../components/index.js";
2
+ import {
3
+ bunti,
4
+ resolvePlacedRect,
5
+ splitRect
6
+ } from "../index.js";
7
+ const DEFAULT_DEMO_OPTIONS = {
8
+ fps: 10,
9
+ alternateBuffer: true,
10
+ hideCursor: true,
11
+ nerdFont: true,
12
+ mouse: true,
13
+ keyboard: true,
14
+ defaultFg: "silver",
15
+ header: true,
16
+ footer: "telemetry",
17
+ wallpaper: "#0a0a0b",
18
+ exitHint: "q",
19
+ padding: 0
20
+ };
21
+ function viewportPadding(padding) {
22
+ if (Array.isArray(padding))
23
+ return { x: padding[0], y: padding[1] };
24
+ return { x: padding ?? 0, y: padding ?? 0 };
25
+ }
26
+ export async function demo(title, contentRender, options = {}) {
27
+ await bunti.init({ nerdFont: true });
28
+ const once = process.argv.includes("--once");
29
+ const config = {
30
+ ...DEFAULT_DEMO_OPTIONS,
31
+ ...options,
32
+ ...once ? { once: true } : {}
33
+ };
34
+ const {
35
+ header,
36
+ footer,
37
+ wallpaper: wallpaperColor,
38
+ exitHint,
39
+ padding,
40
+ ...screenOptions
41
+ } = config;
42
+ bunti.render((ctx) => {
43
+ const { color, width, height, wallpaper, icon } = ctx;
44
+ if (ctx.lastKey === "q")
45
+ ctx.requestStop();
46
+ wallpaper(wallpaperColor);
47
+ if (header) {
48
+ Header(ctx, {
49
+ title: title.toUpperCase(),
50
+ leftIcon: icon("bunti"),
51
+ rightLabel: exitHint
52
+ });
53
+ }
54
+ if (footer) {
55
+ Box(ctx, {
56
+ anchor: "bottom",
57
+ width: "100%",
58
+ align: "center",
59
+ border: "none",
60
+ padding: [0, 0]
61
+ }, ({ text }) => {
62
+ text(color.dim(` \uDB80\uDDC4 Screen: ${width}x${height} | Mouse: ${ctx.mouseX},${ctx.mouseY} | Logic: Stateless `));
63
+ });
64
+ }
65
+ const chromeTop = header ? 2 : 0;
66
+ const chromeBottom = footer ? 2 : 0;
67
+ const pad = viewportPadding(padding);
68
+ const rect = {
69
+ x: pad.x,
70
+ y: chromeTop + pad.y,
71
+ width: Math.max(0, width - pad.x * 2),
72
+ height: Math.max(0, height - chromeTop - chromeBottom - pad.y * 2)
73
+ };
74
+ const bounds = {
75
+ x: rect.x,
76
+ y: rect.y,
77
+ w: rect.width,
78
+ h: rect.height,
79
+ rect,
80
+ place: (input) => resolvePlacedRect(rect, input),
81
+ split: (options) => splitRect(rect, options)
82
+ };
83
+ contentRender(ctx, bounds);
84
+ }, screenOptions);
85
+ }
@@ -56,7 +56,12 @@ export async function demo(
56
56
  options: DemoOptions = {},
57
57
  ) {
58
58
  await bunti.init({ nerdFont: true });
59
- const config = { ...DEFAULT_DEMO_OPTIONS, ...options };
59
+ const once = process.argv.includes('--once');
60
+ const config = {
61
+ ...DEFAULT_DEMO_OPTIONS,
62
+ ...options,
63
+ ...(once ? { once: true } : {}),
64
+ };
60
65
  const {
61
66
  header,
62
67
  footer,
@@ -0,0 +1,244 @@
1
+ import {
2
+ bunti,
3
+ innerRect,
4
+ setCell,
5
+ splitRect,
6
+ visibleWidth
7
+ } from "../index.js";
8
+ let frameCount = 0;
9
+ const mix = (a, b, t) => ({
10
+ r: Math.round(a.r + (b.r - a.r) * Math.max(0, Math.min(1, t))),
11
+ g: Math.round(a.g + (b.g - a.g) * Math.max(0, Math.min(1, t))),
12
+ b: Math.round(a.b + (b.b - a.b) * Math.max(0, Math.min(1, t)))
13
+ });
14
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
15
+ const addGlow = (base, color, amount, cx, cy, x, y, rx, ry) => {
16
+ const dx = (x - cx) / rx;
17
+ const dy = (y - cy) / ry;
18
+ const strength = clamp01(1 - dx * dx - dy * dy) * amount;
19
+ return mix(base, color, strength);
20
+ };
21
+ function vividTextureColor(nx, ny) {
22
+ let color = mix({ r: 207, g: 74, b: 236 }, { r: 255, g: 184, b: 72 }, clamp01((nx + ny * 0.78) / 1.55));
23
+ color = addGlow(color, { r: 113, g: 230, b: 248 }, 0.74, 0.5, 0.12, nx, ny, 0.48, 0.36);
24
+ color = addGlow(color, { r: 245, g: 72, b: 218 }, 0.44, 0.1, 0.28, nx, ny, 0.34, 0.54);
25
+ color = addGlow(color, { r: 255, g: 236, b: 98 }, 0.58, 0.82, 0.78, nx, ny, 0.32, 0.36);
26
+ color = addGlow(color, { r: 255, g: 246, b: 222 }, 0.22, 0.56, 0.42, nx, ny, 0.62, 0.42);
27
+ return {
28
+ r: Math.round(color.r),
29
+ g: Math.round(color.g),
30
+ b: Math.round(color.b)
31
+ };
32
+ }
33
+ function drawEngineField(state, width, height) {
34
+ for (let y = 0;y < height; y++) {
35
+ for (let x = 0;x < width; x++) {
36
+ const nx = x / Math.max(1, width - 1);
37
+ const ny = y / Math.max(1, height - 1);
38
+ const gradientColor = vividTextureColor(nx, ny);
39
+ setCell(state, x, y, {
40
+ char: "█",
41
+ fg: gradientColor,
42
+ bg: gradientColor
43
+ });
44
+ }
45
+ }
46
+ }
47
+ function metric(label, value, valueColor, labelColor) {
48
+ return `${labelColor(label.padEnd(13))} ${valueColor(value)}`;
49
+ }
50
+ function wrapWords(text, width) {
51
+ const lines = [];
52
+ let line = "";
53
+ for (const word of text.split(/\s+/)) {
54
+ const next = line ? `${line} ${word}` : word;
55
+ if (visibleWidth(next) > width && line) {
56
+ lines.push(line);
57
+ line = word;
58
+ } else {
59
+ line = next;
60
+ }
61
+ }
62
+ if (line)
63
+ lines.push(line);
64
+ return lines;
65
+ }
66
+ bunti.render((ctx) => {
67
+ const {
68
+ state,
69
+ box,
70
+ color,
71
+ width,
72
+ height,
73
+ mouseX,
74
+ mouseY,
75
+ lastKey,
76
+ requestStop,
77
+ resolveLocalRect,
78
+ hitbox,
79
+ blit
80
+ } = ctx;
81
+ if (lastKey === "q")
82
+ requestStop();
83
+ drawEngineField(state, width, height);
84
+ const frame = frameCount++;
85
+ const textFg = { r: 205, g: 212, b: 224 };
86
+ const softTextFg = { r: 145, g: 152, b: 166 };
87
+ const panelText = (s) => color.fg(textFg, s);
88
+ const softText = (s) => color.fg(softTextFg, s);
89
+ const shell = innerRect({ x: 0, y: 0, width, height }, 2);
90
+ const compact = width < 92;
91
+ const stageTop = Math.min(6, Math.max(3, Math.floor(height * 0.12)));
92
+ const title = " \uD83C\uDF6D BUNTI RENDER ENGINE ";
93
+ const titleArea = resolveLocalRect({
94
+ x: 2,
95
+ y: Math.max(1, Math.floor((shell.y + stageTop) / 2)),
96
+ width: Math.max(0, width - 4),
97
+ height: 1
98
+ }, { defaultX: "left", defaultY: "top" });
99
+ box({
100
+ x: titleArea.x,
101
+ y: titleArea.y,
102
+ width: titleArea.width,
103
+ height: titleArea.height,
104
+ border: "none",
105
+ align: "center"
106
+ }, ({ text }) => text(color.bold(color.fg({ r: 255, g: 255, b: 255 }, title))));
107
+ const stage = innerRect(shell, {
108
+ top: stageTop,
109
+ left: compact ? 1 : 4,
110
+ right: compact ? 1 : 4,
111
+ bottom: 2
112
+ });
113
+ const [top, bottom] = splitRect(stage, {
114
+ direction: "vertical",
115
+ constraints: [compact ? "52%" : "50%", "1fr"],
116
+ gap: 1
117
+ });
118
+ const [telemetry, clipping] = splitRect(top, {
119
+ direction: compact ? "vertical" : "horizontal",
120
+ constraints: ["1fr", "1fr"],
121
+ gap: 2
122
+ });
123
+ const [hitTargets, colorStack] = splitRect(bottom, {
124
+ direction: compact ? "vertical" : "horizontal",
125
+ constraints: ["1fr", compact ? 7 : "1fr"],
126
+ gap: 2
127
+ });
128
+ box({
129
+ x: telemetry.x,
130
+ y: telemetry.y,
131
+ width: telemetry.width,
132
+ height: telemetry.height,
133
+ border: "none",
134
+ bgColor: { r: 8, g: 10, b: 14 },
135
+ padding: [1, 2]
136
+ }, ({ text }) => {
137
+ text([
138
+ color.cyan("CORE TELEMETRY"),
139
+ "",
140
+ metric("TICK", frame.toString().padStart(6, "0"), color.yellow, panelText),
141
+ metric("RESOLUTION", `${width}x${height}`, color.green, panelText),
142
+ metric("MOUSE", `${mouseX},${mouseY}`, color.magenta, panelText),
143
+ metric("MODE", compact ? "STACKED RECTS" : "SPLIT RECTS", color.cyan, panelText),
144
+ "",
145
+ `${color.gray("q")} ${softText("quits")} ${color.gray("resize")} ${softText("reflows")} ${color.gray("mouse")} ${softText("lights hitboxes")}`
146
+ ].join(`
147
+ `));
148
+ });
149
+ const clippingText = "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.";
150
+ const clippingTextWidth = Math.max(12, clipping.width - 4);
151
+ const wrappedClipping = wrapWords(clippingText, clippingTextWidth).map(panelText).join(`
152
+ `);
153
+ const truncatedClipping = wrapWords(clippingText.repeat(2), clippingTextWidth).slice(0, Math.max(1, clipping.height - 10)).map((line) => color.fg({ r: 160, g: 160, b: 170 }, line)).join(`
154
+ `);
155
+ box({
156
+ x: clipping.x,
157
+ y: clipping.y,
158
+ width: clipping.width,
159
+ height: clipping.height,
160
+ border: "none",
161
+ bgColor: { r: 8, g: 8, b: 12 },
162
+ padding: [1, 2]
163
+ }, ({ text }) => {
164
+ text(`${color.yellow("CLIPPING + WRAP")}
165
+
166
+ `);
167
+ text(`${color.fg(textFg, color.bold("WRAPPED"))}
168
+ ${wrappedClipping}
169
+
170
+ `);
171
+ text(`${color.fg(textFg, color.bold("TRUNCATED"))}
172
+ `);
173
+ text(truncatedClipping);
174
+ });
175
+ box({
176
+ x: hitTargets.x,
177
+ y: hitTargets.y,
178
+ width: hitTargets.width,
179
+ height: hitTargets.height,
180
+ border: "none",
181
+ bgColor: { r: 7, g: 11, b: 9 },
182
+ padding: [1, 2]
183
+ }, ({ text }) => {
184
+ text(`${color.green("HITBOX MAP")}
185
+
186
+ `);
187
+ text(`${softText("Move the mouse over the bars.")}
188
+
189
+ `);
190
+ });
191
+ const targetInner = innerRect(hitTargets, {
192
+ top: 5,
193
+ right: 3,
194
+ bottom: 2,
195
+ left: 3
196
+ });
197
+ const targetRows = splitRect(targetInner, {
198
+ direction: "vertical",
199
+ constraints: [1, 1, 1],
200
+ gap: 1
201
+ });
202
+ const labels = ["layout rect", "input zone", "paint layer"];
203
+ const fills = [
204
+ { r: 70, g: 190, b: 255 },
205
+ { r: 255, g: 190, b: 75 },
206
+ { r: 195, g: 110, b: 255 }
207
+ ];
208
+ for (const [index, row] of targetRows.entries()) {
209
+ const id = `engine-demo-target-${index}`;
210
+ const target = hitbox(id, row);
211
+ const fg = target.hovered ? { r: 255, g: 255, b: 255 } : { r: 20, g: 24, b: 28 };
212
+ const bg = target.hovered ? fills[index] : mix(fills[index], { r: 0, g: 0, b: 0 }, 0.45);
213
+ ctx.rect(row.x, row.y, row.width, row.height, {
214
+ char: " ",
215
+ bg
216
+ });
217
+ blit(row.x + 2, row.y, `${target.hovered ? ">" : " "} ${labels[index].padEnd(14)} ${target.box.x},${target.box.y},${target.box.width},${target.box.height}`, { fg, bg });
218
+ }
219
+ box({
220
+ x: colorStack.x,
221
+ y: colorStack.y,
222
+ width: colorStack.width,
223
+ height: colorStack.height,
224
+ border: "none",
225
+ bgColor: { r: 11, g: 8, b: 16 },
226
+ padding: [1, 2]
227
+ }, ({ text }) => {
228
+ text([
229
+ color.magenta("COLOR ISOLATION"),
230
+ "",
231
+ `${color.bgBlue(color.white(" nested bg "))} ${color.red("red")} ${color.green("green")} ${color.yellow("yellow")}`,
232
+ panelText("ANSI resets stay contained inside composed rows."),
233
+ "",
234
+ `${color.magenta("████")}${color.cyan("████")}${color.magenta("████")}`
235
+ ].join(`
236
+ `));
237
+ });
238
+ }, {
239
+ fps: 8,
240
+ keyboard: true,
241
+ mouse: true,
242
+ alternateBuffer: true,
243
+ hideCursor: true
244
+ });
@@ -0,0 +1,74 @@
1
+ import { Box, Button, Input } from "../components/index.js";
2
+ import { demo } from "./demo-layout.js";
3
+ demo("INTERACTIVE COMPONENTS", (ctx, bounds) => {
4
+ const { color, icon } = ctx;
5
+ const W = Math.min(bounds.w - 8, 72);
6
+ const panel = bounds.place({
7
+ y: 1,
8
+ width: W,
9
+ height: Math.min(bounds.h - 1, 13)
10
+ });
11
+ const compact = W < 60;
12
+ Box(ctx, {
13
+ x: panel.x,
14
+ y: panel.y,
15
+ width: panel.width,
16
+ height: panel.height,
17
+ border: "rounded",
18
+ borderColor: "bunti-blue",
19
+ padding: [1, compact ? 1 : 3],
20
+ align: "left"
21
+ }, (sub) => {
22
+ const ghostW = 5;
23
+ const primaryW = compact ? 14 : 15;
24
+ const actionGap = 2;
25
+ const actionRowW = ghostW + actionGap + primaryW;
26
+ const actionRow = sub.resolveLocalRect({
27
+ y: 8,
28
+ width: actionRowW,
29
+ height: 1
30
+ }, { defaultX: "right", defaultY: "top" });
31
+ const actionY = 8;
32
+ sub.text(color.cyan(color.bold("TACTICAL CONTROLS")));
33
+ sub.text(`
34
+ `);
35
+ sub.text(color.dim("Keyboard, mouse, focus, and rerender states."));
36
+ sub.text(`
37
+
38
+ `);
39
+ Input(sub, {
40
+ id: "input-mission",
41
+ label: "MISSION:",
42
+ placeholder: "Enter mission name...",
43
+ width: compact ? W - 8 : 48
44
+ });
45
+ sub.text(`
46
+ `);
47
+ sub.text(color.dim(`Last key: ${ctx.lastKey || "none"}`));
48
+ sub.text(color.dim(` Mouse: ${ctx.mouseX},${ctx.mouseY}`));
49
+ sub.text(`
50
+
51
+ `);
52
+ const abort = Button(sub, {
53
+ id: "btn-abort",
54
+ label: "Abort",
55
+ variant: "ghost",
56
+ width: ghostW,
57
+ x: actionRow.x,
58
+ y: actionY,
59
+ detach: true
60
+ });
61
+ const deploy = Button(sub, {
62
+ id: "btn-deploy",
63
+ label: "Deploy",
64
+ icon: icon("rocket"),
65
+ variant: "primary",
66
+ width: primaryW,
67
+ x: actionRow.x + ghostW + actionGap,
68
+ y: actionY,
69
+ detach: true,
70
+ onClick: () => {}
71
+ });
72
+ sub.text(`${" ".repeat(actionRow.x)}${abort}${" ".repeat(actionGap)}${deploy}`);
73
+ });
74
+ });
@@ -1,5 +1,5 @@
1
1
  import { Box, Button, Input } from '../components/index.js';
2
- import { demo } from './demo-layout';
2
+ import { demo } from './demo-layout.js';
3
3
 
4
4
  /**
5
5
  * Interactive Components Showcase