@buntui/extensions 0.1.0-alpha.1 → 0.1.0-alpha.3

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.
@@ -0,0 +1,2 @@
1
+ export { mountHmrErrorOverlay, type HmrErrorOverlayHandle } from './widgets/hmr-error-overlay/HmrErrorOverlayWidget';
2
+ export { mountHmrErrorOverlay as default } from './widgets/hmr-error-overlay/HmrErrorOverlayWidget';
@@ -0,0 +1,2 @@
1
+ export { mountHmrErrorOverlay } from './widgets/hmr-error-overlay/HmrErrorOverlayWidget';
2
+ export { mountHmrErrorOverlay as default } from './widgets/hmr-error-overlay/HmrErrorOverlayWidget';
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export * from './framerate';
3
3
  export * from './snake';
4
4
  export * from './videoplayer';
5
5
  export * from './logger';
6
+ export * from './hmr-error-overlay';
6
7
  export { EXTENSION_REGISTRY } from './registry';
package/dist/index.js CHANGED
@@ -5,4 +5,5 @@ export * from './framerate';
5
5
  export * from './snake';
6
6
  export * from './videoplayer';
7
7
  export * from './logger';
8
+ export * from './hmr-error-overlay';
8
9
  export { EXTENSION_REGISTRY } from './registry';
@@ -1,5 +1,4 @@
1
- import { TUI_CONTEXT_INSTANCE, TuiWidgetEntity, getTheme, parseColor, } from '@buntui/core';
2
- const FPS_BG = 0x1E_1E_2E_DD;
1
+ import { TUI_CONTEXT_INSTANCE, TuiWidgetEntity, getTheme, onThemeChange, parseColor, } from '@buntui/core';
3
2
  export class FrameRateWatcher extends TuiWidgetEntity {
4
3
  #x;
5
4
  #y;
@@ -15,8 +14,21 @@ export class FrameRateWatcher extends TuiWidgetEntity {
15
14
  this.#y = rect.y;
16
15
  const theme = getTheme();
17
16
  this.#colorFg = parseColor(options.colorFg ?? theme.colors.text);
18
- this.#colorBg = parseColor(options.colorBg ?? FPS_BG);
17
+ this.#colorBg = parseColor(options.colorBg ?? theme.colors.surface);
19
18
  this.setDraggable(true);
19
+ if (options.colorFg === undefined || options.colorBg === undefined) {
20
+ const trackFg = options.colorFg === undefined;
21
+ const trackBg = options.colorBg === undefined;
22
+ const unsub = onThemeChange(t => {
23
+ if (trackFg) {
24
+ this.#colorFg = parseColor(t.colors.text);
25
+ }
26
+ if (trackBg) {
27
+ this.#colorBg = parseColor(t.colors.surface);
28
+ }
29
+ });
30
+ this.addCleanup(unsub);
31
+ }
20
32
  }
21
33
  get zIndex() {
22
34
  return 999;
@@ -0,0 +1,19 @@
1
+ import { type DrawListBuffer, type TuiWidgetRect, TuiWidgetEntity } from '@buntui/core';
2
+ type Mountable = {
3
+ mount(widget: TuiWidgetEntity): unknown;
4
+ unmount(widget: TuiWidgetEntity): unknown;
5
+ };
6
+ export type HmrErrorOverlayHandle = {
7
+ dismiss(): void;
8
+ };
9
+ export declare function mountHmrErrorOverlay(scene: Mountable, error: Error): HmrErrorOverlayHandle;
10
+ declare class HmrErrorOverlayWidget extends TuiWidgetEntity {
11
+ #private;
12
+ constructor(error: Error);
13
+ get zIndex(): number;
14
+ get rect(): TuiWidgetRect;
15
+ containsPoint(x: number, y: number): boolean;
16
+ update(): void;
17
+ emitDrawCommands(buf: DrawListBuffer): void;
18
+ }
19
+ export default HmrErrorOverlayWidget;
@@ -0,0 +1,167 @@
1
+ import nodeBuffer from 'node:buffer';
2
+ import nodeProcess from 'node:process';
3
+ import { TUI_CONTEXT_INSTANCE, TuiWidgetEntity, } from '@buntui/core';
4
+ const COLOR_BG = 0x18_10_10_F0;
5
+ const COLOR_BORDER = 0xF3_86_AB_FF;
6
+ const COLOR_TITLE = 0xF3_86_AB_FF;
7
+ const COLOR_TEXT = 0xBA_BA_BA_FF;
8
+ const COLOR_FILE = 0x89_B4_FA_FF;
9
+ const COLOR_BTN_BG = 0x33_22_33_F0;
10
+ const COLOR_BTN_BG_HOVER = 0x4A_30_4A_F0;
11
+ const COLOR_BTN_FG = 0xCD_D6_F4_FF;
12
+ const COLOR_BTN_FG_COPIED = 0xA6_E3_A1_FF;
13
+ const BORDER_SIDES_ALL = 0b1111;
14
+ const BORDER_STYLE_DOUBLE = 3;
15
+ const COPY_LABEL = ' Copy to clipboard ';
16
+ const COPIED_LABEL = ' Copied! ';
17
+ const BUTTON_HEIGHT = 1;
18
+ const FEEDBACK_FRAMES = 90;
19
+ export function mountHmrErrorOverlay(scene, error) {
20
+ const overlay = new HmrErrorOverlayWidget(error);
21
+ scene.mount(overlay);
22
+ return {
23
+ dismiss() {
24
+ scene.unmount(overlay);
25
+ },
26
+ };
27
+ }
28
+ function writeToClipboard(text) {
29
+ const encoded = nodeBuffer.Buffer.from(text, 'utf-8').toString('base64');
30
+ nodeProcess.stdout.write(`\u001B]52;c;${encoded}\u0007`);
31
+ }
32
+ class HmrErrorOverlayWidget extends TuiWidgetEntity {
33
+ #lines;
34
+ #titleLine;
35
+ #clipboardText;
36
+ #hoveringButton = false;
37
+ #copiedFrames = 0;
38
+ constructor(error) {
39
+ super();
40
+ const message = error.message ?? String(error);
41
+ const stack = error.stack?.replace(`${error.name}: ${error.message}`, '').trim() ?? '';
42
+ const messageLines = wrapText(message, 80);
43
+ const stackLines = stack
44
+ ? stack.split('\n').map(l => l.trim()).filter(Boolean).slice(0, 8)
45
+ : [];
46
+ this.#titleLine = '[HMR Error]';
47
+ this.#lines = [
48
+ ...messageLines,
49
+ ...stackLines.length > 0 ? ['', ...stackLines] : [],
50
+ ];
51
+ this.#clipboardText = [
52
+ `${error.name}: ${message}`,
53
+ stack,
54
+ ].filter(Boolean).join('\n');
55
+ this.on('mouseover', data => {
56
+ if (this.#isButtonRow(data.y)) {
57
+ this.#hoveringButton = true;
58
+ }
59
+ });
60
+ this.on('mouseout', () => {
61
+ this.#hoveringButton = false;
62
+ });
63
+ this.on('mousemove', data => {
64
+ this.#hoveringButton = this.#isButtonRow(data.y);
65
+ });
66
+ this.on('click', data => {
67
+ if (this.#isButtonRow(data.y)) {
68
+ writeToClipboard(this.#clipboardText);
69
+ this.#copiedFrames = FEEDBACK_FRAMES;
70
+ }
71
+ });
72
+ }
73
+ get zIndex() {
74
+ return this.getZIndexOverride() ?? 9999;
75
+ }
76
+ get rect() {
77
+ const { rows, cols } = TUI_CONTEXT_INSTANCE;
78
+ const maxW = Math.min(cols, 90);
79
+ const maxH = Math.min(rows, this.#lines.length + 4 + BUTTON_HEIGHT + 3);
80
+ const x = Math.max(0, Math.floor((cols - maxW) / 2));
81
+ const y = Math.max(0, Math.floor((rows - maxH) / 2));
82
+ return {
83
+ x, y, width: maxW, height: maxH,
84
+ };
85
+ }
86
+ containsPoint(x, y) {
87
+ const { x: rx, y: ry, width, height } = this.rect;
88
+ return x >= rx && x < rx + width && y >= ry && y < ry + height;
89
+ }
90
+ update() {
91
+ if (this.#copiedFrames > 0) {
92
+ this.#copiedFrames--;
93
+ }
94
+ }
95
+ emitDrawCommands(buf) {
96
+ const { x: rx, y: ry, width, height } = this.rect;
97
+ const innerX = rx + 2;
98
+ const innerW = width - 4;
99
+ const maxLines = height - 4 - BUTTON_HEIGHT - 1;
100
+ const btnY = ry + height - 2;
101
+ buf.pushClip(rx, ry, width, height);
102
+ buf.drawRect({
103
+ x: rx, y: ry, width, height, bgRgba: COLOR_BG,
104
+ });
105
+ buf.drawBorder({
106
+ x: rx, y: ry, width, height,
107
+ colorRgba: COLOR_BORDER,
108
+ style: BORDER_STYLE_DOUBLE,
109
+ sides: BORDER_SIDES_ALL,
110
+ });
111
+ buf.drawText({
112
+ x: innerX, y: ry + 1,
113
+ text: truncate(this.#titleLine, innerW),
114
+ fgRgba: COLOR_TITLE,
115
+ bgRgba: COLOR_BG,
116
+ });
117
+ for (let i = 0; i < Math.min(this.#lines.length, maxLines); i++) {
118
+ const line = this.#lines[i];
119
+ const isStack = line.startsWith('at ');
120
+ buf.drawText({
121
+ x: innerX,
122
+ y: ry + 3 + i,
123
+ text: truncate(isStack ? ` ${line}` : line, innerW),
124
+ fgRgba: isStack ? COLOR_FILE : COLOR_TEXT,
125
+ bgRgba: COLOR_BG,
126
+ });
127
+ }
128
+ const copied = this.#copiedFrames > 0;
129
+ const label = copied ? COPIED_LABEL : COPY_LABEL;
130
+ const btnBg = copied ? COLOR_BG : (this.#hoveringButton ? COLOR_BTN_BG_HOVER : COLOR_BTN_BG);
131
+ const btnFg = copied ? COLOR_BTN_FG_COPIED : COLOR_BTN_FG;
132
+ const btnTextX = rx + Math.floor((width - label.length) / 2);
133
+ buf.drawRect({
134
+ x: rx + 1, y: btnY, width: width - 2, height: BUTTON_HEIGHT, bgRgba: btnBg,
135
+ });
136
+ buf.drawText({
137
+ x: btnTextX, y: btnY,
138
+ text: truncate(label, innerW),
139
+ fgRgba: btnFg,
140
+ bgRgba: btnBg,
141
+ });
142
+ buf.popClip();
143
+ }
144
+ #isButtonRow(mouseY) {
145
+ const { y: ry, height } = this.rect;
146
+ return mouseY - 1 === ry + height - 2;
147
+ }
148
+ }
149
+ function truncate(text, maxLength) {
150
+ if (text.length <= maxLength) {
151
+ return text;
152
+ }
153
+ return `${text.slice(0, maxLength - 1)}\u2026`;
154
+ }
155
+ function wrapText(text, maxLength) {
156
+ const lines = [];
157
+ for (const rawLine of text.split('\n')) {
158
+ let remaining = rawLine;
159
+ while (remaining.length > maxLength) {
160
+ lines.push(remaining.slice(0, maxLength));
161
+ remaining = remaining.slice(maxLength);
162
+ }
163
+ lines.push(remaining);
164
+ }
165
+ return lines;
166
+ }
167
+ export default HmrErrorOverlayWidget;
@@ -9,7 +9,6 @@ export type LoggerWidgetOptions = {
9
9
  colorFg?: TuiColor;
10
10
  colorBg?: TuiColor;
11
11
  label?: string;
12
- hijack?: boolean;
13
12
  };
14
13
  export declare class LoggerWidget extends TuiWidgetEntity {
15
14
  #private;
@@ -19,12 +18,6 @@ export declare class LoggerWidget extends TuiWidgetEntity {
19
18
  toggle(): void;
20
19
  get messages(): readonly string[];
21
20
  get isOpen(): boolean;
22
- /**
23
- * Replace console.log with a wrapper that sends output to this logger.
24
- * Call restoreConsole() to undo.
25
- */
26
- hijackConsole(): void;
27
- restoreConsole(): void;
28
21
  get zIndex(): number;
29
22
  containsPoint(x: number, y: number): boolean;
30
23
  emitDrawCommands(buf: Parameters<TuiWidgetEntity['emitDrawCommands']>[0]): void;
@@ -11,7 +11,6 @@ export class LoggerWidget extends TuiWidgetEntity {
11
11
  #panelHeight;
12
12
  #panelVisible = false;
13
13
  #scrollPending = false;
14
- #restoreConsole = null;
15
14
  constructor(options = {}) {
16
15
  super();
17
16
  const theme = getTheme();
@@ -63,9 +62,6 @@ export class LoggerWidget extends TuiWidgetEntity {
63
62
  this.#toggleBtn.on('click', () => {
64
63
  this.toggle();
65
64
  });
66
- if (options.hijack) {
67
- this.hijackConsole();
68
- }
69
65
  }
70
66
  // -- Public API --
71
67
  log(message) {
@@ -108,24 +104,6 @@ export class LoggerWidget extends TuiWidgetEntity {
108
104
  get isOpen() {
109
105
  return this.#panelVisible;
110
106
  }
111
- /**
112
- * Replace console.log with a wrapper that sends output to this logger.
113
- * Call restoreConsole() to undo.
114
- */
115
- hijackConsole() {
116
- const original = console.log;
117
- this.#restoreConsole = () => {
118
- console.log = original;
119
- };
120
- console.log = (...args) => {
121
- this.log(args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' '));
122
- };
123
- }
124
- restoreConsole() {
125
- const restore = this.#restoreConsole;
126
- this.#restoreConsole = null;
127
- restore?.();
128
- }
129
107
  // -- Overrides --
130
108
  // -- Overrides --
131
109
  get zIndex() {
@@ -2,7 +2,7 @@ import { rgbToRgba } from '@buntui/core';
2
2
  export const DEFAULT_MATRIX_COLOR_SCHEME = {
3
3
  leadRgba: rgbToRgba(0x57, 0xFF, 0x57), // Bright green
4
4
  trailRgba: rgbToRgba(0x00, 0x8F, 0x11), // Medium green
5
- bgRgba: rgbToRgba(0x00, 0x00, 0x00), // Black background
5
+ bgRgba: 0x00_00_00_00,
6
6
  };
7
7
  export const DEFAULT_MATRIX_OPTIONS = {
8
8
  x: 0,
@@ -6,7 +6,7 @@ export declare class SnakeWidget extends widgets.InteractiveWidget {
6
6
  updateRect(rect: Partial<TuiWidgetRect>): void;
7
7
  containsPoint(x: number, y: number): boolean;
8
8
  get rect(): TuiWidgetRect;
9
- handleKey(event: KeyboardEvent): void;
9
+ handleActiveKey(event: KeyboardEvent): void;
10
10
  update(dt: number): void;
11
11
  emitDrawCommands(buffer: DrawListBuffer): void;
12
12
  }
@@ -75,11 +75,8 @@ export class SnakeWidget extends widgets.InteractiveWidget {
75
75
  x: this.#x, y: this.#y, width: this.#width, height: this.#height,
76
76
  };
77
77
  }
78
- handleKey(event) {
79
- const { key } = event;
80
- if (!key) {
81
- return;
82
- }
78
+ handleActiveKey(event) {
79
+ const key = event.key;
83
80
  if (this.#state === 'idle') {
84
81
  if (key === ' ') {
85
82
  this.#startGame();
@@ -94,7 +91,6 @@ export class SnakeWidget extends widgets.InteractiveWidget {
94
91
  this.#handleDirection(key);
95
92
  return;
96
93
  }
97
- // Gameover
98
94
  if (key === ' ') {
99
95
  this.#startGame();
100
96
  }
@@ -8,7 +8,7 @@ export declare class VideoPlayerWidget extends widgets.InteractiveWidget {
8
8
  updateRect(rect: Partial<TuiWidgetRect>): void;
9
9
  containsPoint(x: number, y: number): boolean;
10
10
  get rect(): TuiWidgetRect;
11
- handleKey(event: KeyboardEvent): void;
11
+ handleActiveKey(event: KeyboardEvent): void;
12
12
  update(dt: number): void;
13
13
  emitDrawCommands(buffer: DrawListBuffer): void;
14
14
  }
@@ -102,11 +102,8 @@ export class VideoPlayerWidget extends widgets.InteractiveWidget {
102
102
  x: this.#x, y: this.#y, width: this.#width, height: this.#height,
103
103
  };
104
104
  }
105
- handleKey(event) {
106
- const { key } = event;
107
- if (!key) {
108
- return;
109
- }
105
+ handleActiveKey(event) {
106
+ const key = event.key;
110
107
  if (key === ' ') {
111
108
  switch (this.#playerState) {
112
109
  case 'ready':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buntui/extensions",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AlphaFoxz/buntui.git",
@@ -32,6 +32,10 @@
32
32
  "./logger": {
33
33
  "types": "./dist/logger.d.ts",
34
34
  "default": "./dist/logger.js"
35
+ },
36
+ "./hmr-error-overlay": {
37
+ "types": "./dist/hmr-error-overlay.d.ts",
38
+ "default": "./dist/hmr-error-overlay.js"
35
39
  }
36
40
  },
37
41
  "files": [
@@ -42,7 +46,7 @@
42
46
  "build": "tsc --project ./tsconfig.json"
43
47
  },
44
48
  "dependencies": {
45
- "@buntui/core": "^0.1.0-alpha.1"
49
+ "@buntui/core": "^0.1.0-alpha.3"
46
50
  },
47
51
  "devDependencies": {
48
52
  "@types/bun": "^1.3.14"