take4-console 0.15.1 → 0.25.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 (63) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/dist/Screen/InterfaceBuilder.d.mts +15 -4
  3. package/dist/Screen/InterfaceBuilder.d.mts.map +1 -1
  4. package/dist/Screen/InterfaceBuilder.mjs +104 -8
  5. package/dist/Screen/InterfaceBuilder.mjs.map +1 -1
  6. package/dist/Screen/Pos.d.mts +12 -0
  7. package/dist/Screen/Pos.d.mts.map +1 -1
  8. package/dist/Screen/Pos.mjs +23 -1
  9. package/dist/Screen/Pos.mjs.map +1 -1
  10. package/dist/Screen/Screen.d.mts +77 -3
  11. package/dist/Screen/Screen.d.mts.map +1 -1
  12. package/dist/Screen/Screen.mjs +168 -3
  13. package/dist/Screen/Screen.mjs.map +1 -1
  14. package/dist/Screen/Size.d.mts +49 -6
  15. package/dist/Screen/Size.d.mts.map +1 -1
  16. package/dist/Screen/Size.mjs +81 -7
  17. package/dist/Screen/Size.mjs.map +1 -1
  18. package/dist/Screen/Window.d.mts +131 -20
  19. package/dist/Screen/Window.d.mts.map +1 -1
  20. package/dist/Screen/Window.mjs +474 -57
  21. package/dist/Screen/Window.mjs.map +1 -1
  22. package/dist/Screen/WindowManager.d.mts +85 -5
  23. package/dist/Screen/WindowManager.d.mts.map +1 -1
  24. package/dist/Screen/WindowManager.mjs +279 -26
  25. package/dist/Screen/WindowManager.mjs.map +1 -1
  26. package/dist/Screen/controls/ListBox.d.mts +34 -12
  27. package/dist/Screen/controls/ListBox.d.mts.map +1 -1
  28. package/dist/Screen/controls/ListBox.mjs +127 -25
  29. package/dist/Screen/controls/ListBox.mjs.map +1 -1
  30. package/dist/Screen/controls/TextArea.d.mts +15 -1
  31. package/dist/Screen/controls/TextArea.d.mts.map +1 -1
  32. package/dist/Screen/controls/TextArea.mjs +74 -1
  33. package/dist/Screen/controls/TextArea.mjs.map +1 -1
  34. package/dist/Screen/controls/TextBox.d.mts +13 -1
  35. package/dist/Screen/controls/TextBox.d.mts.map +1 -1
  36. package/dist/Screen/controls/TextBox.mjs +36 -1
  37. package/dist/Screen/controls/TextBox.mjs.map +1 -1
  38. package/dist/Screen/textWidth.d.mts +13 -0
  39. package/dist/Screen/textWidth.d.mts.map +1 -0
  40. package/dist/Screen/textWidth.mjs +188 -0
  41. package/dist/Screen/textWidth.mjs.map +1 -0
  42. package/dist/Screen/types.d.mts +336 -20
  43. package/dist/Screen/types.d.mts.map +1 -1
  44. package/dist/Screen/types.mjs.map +1 -1
  45. package/dist/index.d.mts +3 -2
  46. package/dist/index.d.mts.map +1 -1
  47. package/dist/index.mjs +3 -1
  48. package/dist/index.mjs.map +1 -1
  49. package/package.json +1 -1
  50. package/src/Screen/InterfaceBuilder.mts +116 -20
  51. package/src/Screen/Pos.mts +24 -1
  52. package/src/Screen/Screen.mts +192 -4
  53. package/src/Screen/Size.mts +97 -12
  54. package/src/Screen/Window.mts +463 -63
  55. package/src/Screen/WindowManager.mts +301 -29
  56. package/src/Screen/controls/ListBox.mts +151 -32
  57. package/src/Screen/controls/TextArea.mts +82 -1
  58. package/src/Screen/controls/TextBox.mts +40 -1
  59. package/src/Screen/textWidth.mts +186 -0
  60. package/src/Screen/types.mts +328 -23
  61. package/src/demo.mts +232 -20
  62. package/src/index.mts +23 -3
  63. package/src/layout.yaml +56 -24
package/src/demo.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import { join, dirname } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { Screen } from './Screen/Screen.mjs';
4
5
  import { WindowManager } from './Screen/WindowManager.mjs';
5
6
  import { InterfaceBuilder } from './Screen/InterfaceBuilder.mjs';
@@ -12,15 +13,53 @@ import { Spinner } from './Screen/controls/Spinner.mjs';
12
13
  import { Sparkline } from './Screen/controls/Sparkline.mjs';
13
14
  import { ListBox } from './Screen/controls/ListBox.mjs';
14
15
  import { Tabs } from './Screen/controls/Tabs.mjs';
16
+ import { Window } from './Screen/Window.mjs';
17
+ import type { ListBoxRowSegments, WindowProperties, StyleId } from './Screen/types.mjs';
18
+
19
+ /** Custom Window subclass registered with InterfaceBuilder via
20
+ * `registerType('badge', …)` to demonstrate P0-10 — the factory resolves
21
+ * `props.text` / `props.color` from the YAML node and stashes them so the
22
+ * overridden render() can repaint the label every frame after the size is
23
+ * resolved by the parent layout pass. */
24
+ class Badge extends Window {
25
+ private msg: string;
26
+ private styleId: StyleId;
27
+
28
+ /** Constructs a Badge with its visible text and pre-registered style id. */
29
+ public constructor(wp: WindowProperties, msg: string, styleId: StyleId) {
30
+ super(wp);
31
+ this.msg = msg;
32
+ this.styleId = styleId;
33
+ }
34
+
35
+ /** Repaints the badge text before delegating to the default compositing pipeline. */
36
+ public override render(): void {
37
+ this.clear();
38
+ this.writeText(this.msg, { x: 0, y: 0, style: this.styleId });
39
+ super.render();
40
+ }
41
+ }
15
42
 
16
43
  type LedState = 'ok' | 'warn' | 'error' | 'off';
44
+ type EventLevel = 'ok' | 'warn' | 'error';
45
+
46
+ interface EventRow {
47
+ timestamp: string;
48
+ level: EventLevel;
49
+ message: string;
50
+ count: number;
51
+ }
17
52
 
18
53
  // ── Entry point ───────────────────────────────────────────────────────────────
19
54
 
20
55
  /** Renders the controls demo. */
21
56
  const main = async (): Promise<void> => {
22
- const screen = new Screen();
23
- const { width, height } = screen.getSize();
57
+ // 0.19.0: Screen owns the alt-screen + cursor toggles, so WindowManager won't
58
+ // re-enter them. dispose() runs on exit (via process.on('exit', …)) so the
59
+ // terminal state is restored even when the user kills the process with a
60
+ // signal that bypasses WindowManager.stop().
61
+ const screen = new Screen({ altScreen: true, hideCursor: true });
62
+ let { width, height } = screen.getSize();
24
63
 
25
64
  screen.fill(' ', screen.registerStyle({ background: 234 }));
26
65
 
@@ -30,26 +69,123 @@ const main = async (): Promise<void> => {
30
69
  exitKeys: ['q', '\x03'],
31
70
  onExit: () => {
32
71
  if (demoTimer) clearInterval(demoTimer);
72
+ screen.dispose();
33
73
  process.exit(0);
34
74
  },
35
75
  mouse: true,
36
76
  });
37
77
 
78
+ // 0.19.0: SIGWINCH autoresize. The Screen reflows percentage-based children
79
+ // for us; the demo only needs to redraw the current-size label in the
80
+ // status bar so the new geometry is visible at runtime.
81
+ screen.on('resize', size => {
82
+ width = size.width;
83
+ height = size.height;
84
+ redrawStatusBar();
85
+ screen.render();
86
+ });
87
+
38
88
  const layoutPath = join(dirname(fileURLToPath(import.meta.url)), 'layout.yaml');
39
- const result = await new InterfaceBuilder().buildFromFile(layoutPath, screen, wm);
89
+ const ib = new InterfaceBuilder();
90
+
91
+ // P0-10 demo: register a user-defined 'badge' control so the header can
92
+ // host a yellow "P0-10" label built from a class that never needed to be
93
+ // baked into InterfaceBuilder's built-in switch.
94
+ ib.registerType('badge', (node, ctx) => {
95
+ const props = (node.props ?? {}) as { text?: string; color?: number };
96
+ const styleId = screen.registerStyle({
97
+ background: 24,
98
+ foreground: props.color ?? 220,
99
+ bold: true,
100
+ });
101
+ return new Badge(ctx.wp, props.text ?? '', styleId);
102
+ });
103
+
104
+ // P0-6 demo: when the user presses Enter in the username field, push an
105
+ // event into the log so the new onSubmit callback is visible at runtime.
106
+ ib.registerCallback('usernameSubmitted', (...args: unknown[]) => {
107
+ const value = String(args[0] ?? '');
108
+ const list = result.get('eventsList') as ListBox<EventRow>;
109
+ const entry: EventRow = {
110
+ timestamp: timestamp(),
111
+ level: 'ok',
112
+ message: `user submit: ${value}`,
113
+ count: 1,
114
+ };
115
+ list.setItems([entry, ...list.getItems()].slice(0, 20));
116
+ screen.render();
117
+ });
118
+
119
+ const result = await ib.buildFromFile(layoutPath, screen, wm);
40
120
 
41
121
  // ── Header & status bar ───────────────────────────────────────────────────
42
- const headerStyle = screen.registerStyle({ background: 24, foreground: 255, bold: true });
43
- result.get('header')!.writeText(' take4_console │ controls demo ', { style: headerStyle });
44
-
45
- const statusStyle = screen.registerStyle({ background: 24, foreground: 250 });
46
- result.get('statusBar')!.writeText(
47
- ` ${width}×${height} │ `
48
- + 'Button Checkbox Radio TextBox '
49
- + 'StatusLED ProgressBar ProgressBarV LineChart BarChart',
50
- { style: statusStyle },
122
+ // Header is rendered via writeMarkup() so the named styles in the registry drive
123
+ // the per-word colouring. `hdr:*` names are registered for the demo and then
124
+ // referenced from the template string — no manual StyleId threading.
125
+ screen.setBuiltinStyle('hdr:app', { background: 24, foreground: 231, bold: true });
126
+ screen.setBuiltinStyle('hdr:mode', { background: 24, foreground: 153 });
127
+ screen.setBuiltinStyle('hdr:sep', { background: 24, foreground: 110 });
128
+ const headerBase = screen.registerStyle({ background: 24 });
129
+ result.get('header')!.writeMarkup(
130
+ ' {hdr:app}take4_console{/} {hdr:sep}│{/} {hdr:mode}controls demo{/} ',
131
+ { style: headerBase },
51
132
  );
52
133
 
134
+ // Status bar demonstrates the new segmented writeText() — each shortcut label is
135
+ // drawn in a distinct style while the cursor flows across segments without a
136
+ // separate writeText() call per piece.
137
+ const statusBase = screen.registerStyle({ background: 24, foreground: 250 });
138
+ const shortcutId = screen.registerStyle({ background: 24, foreground: 220, bold: true });
139
+ const sepId = screen.registerStyle({ background: 24, foreground: 110 });
140
+ const dimId = screen.registerStyle({ background: 24, foreground: 244, dim: true });
141
+ const helpId = screen.registerStyle({ background: 24, foreground: 118, bold: true });
142
+ const statusBar = result.get('statusBar')!;
143
+
144
+ // Toggled by the '?' global shortcut to flip the status bar into "help mode".
145
+ let helpMode = false;
146
+
147
+ /** Repaints the status bar so the live width × height label tracks SIGWINCH. */
148
+ const redrawStatusBar = (): void => {
149
+ statusBar.clear();
150
+ if (helpMode) {
151
+ statusBar.writeText([
152
+ { text: ' HELP ', style: helpId },
153
+ { text: '│', style: sepId },
154
+ { text: ' ' },
155
+ { text: '?', style: shortcutId }, { text: ' toggle help ' },
156
+ { text: 'Ctrl+R', style: shortcutId }, { text: ' random jiggle ' },
157
+ { text: 'Ctrl+E', style: shortcutId }, { text: ' pause TUI ' },
158
+ { text: 'v', style: shortcutId }, { text: ' hide charts ' },
159
+ { text: 'q', style: shortcutId }, { text: ' quit ' },
160
+ ], { style: statusBase });
161
+ return;
162
+ }
163
+ statusBar.writeText([
164
+ { text: ` ${width}×${height} ` },
165
+ { text: '│', style: sepId },
166
+ { text: ' ' },
167
+ { text: 'Tab', style: shortcutId }, { text: ' focus ' },
168
+ { text: '←/→', style: shortcutId }, { text: ' tabs ' },
169
+ { text: 'Space', style: shortcutId }, { text: ' toggle ' },
170
+ { text: 'v', style: shortcutId }, { text: ' charts ' },
171
+ { text: 'Ctrl+E', style: shortcutId }, { text: ' pause ' },
172
+ { text: '?', style: shortcutId }, { text: ' help ' },
173
+ { text: 'q', style: shortcutId }, { text: ' quit ' },
174
+ { text: ' · emoji 🚀 CJK 日本語 · ', style: dimId },
175
+ ], { style: statusBase });
176
+ };
177
+ redrawStatusBar();
178
+
179
+ // P0-4 demo: '?' toggles the help line in the status bar (consumed, so it
180
+ // never reaches the focused control). Ctrl+R re-jiggles the chart data
181
+ // without waiting for the timer — shows that bindKey co-exists with the
182
+ // regular key dispatch path as long as handlers return true.
183
+ wm.bindKey('?', () => {
184
+ helpMode = !helpMode;
185
+ redrawStatusBar();
186
+ return true;
187
+ });
188
+
53
189
  // ── Resource labels in monitorPanel ──────────────────────────────────────
54
190
  const labelStyle = screen.registerStyle({ foreground: 245 });
55
191
  const monitor = result.get('monitorPanel')!;
@@ -86,12 +222,47 @@ const main = async (): Promise<void> => {
86
222
  tabs.writeText('MEM', { x: 1, y: 3, style: sparkLblStyle });
87
223
  tabs.writeText('DSK', { x: 1, y: 4, style: sparkLblStyle });
88
224
 
89
- // ── Events list (tab 2) ──────────────────────────────────────────────────
90
- const events = result.get('eventsList') as ListBox;
91
- const EVENT_TEMPLATES = [
92
- 'user logged in', 'cache warmed', 'metric collected', 'job completed',
93
- 'config reloaded', 'heartbeat ok', 'worker spawned', 'session expired',
94
- 'gc finished', 'index rebuilt',
225
+ // ── Events list (with custom per-row renderer: icon + dimmed timestamp + right-aligned count)
226
+ const events = result.get('eventsList') as ListBox<EventRow>;
227
+
228
+ // Register the accent styles used by the custom row renderer.
229
+ const iconOkStyle = screen.registerStyle({ foreground: 82, bold: true });
230
+ const iconWarnStyle = screen.registerStyle({ foreground: 214, bold: true });
231
+ const iconErrorStyle = screen.registerStyle({ foreground: 196, bold: true });
232
+ const tsStyle = screen.registerStyle({ foreground: 244, dim: true });
233
+ const countStyle = screen.registerStyle({ foreground: 220 });
234
+
235
+ const levelIcon = (lvl: EventLevel): { glyph: string; style: number } => {
236
+ if (lvl === 'ok') return { glyph: '✓', style: iconOkStyle };
237
+ if (lvl === 'warn') return { glyph: '⚠', style: iconWarnStyle };
238
+ return { glyph: '✖', style: iconErrorStyle };
239
+ };
240
+
241
+ events.setRenderItem((row): ListBoxRowSegments => {
242
+ const { glyph, style: iconSt } = levelIcon(row.level);
243
+ return [
244
+ { text: `${glyph} `, align: 'left', style: iconSt },
245
+ { text: `[${row.timestamp}] `, align: 'left', style: tsStyle },
246
+ { text: row.message, align: 'left' },
247
+ { text: `×${row.count}`, align: 'right', style: countStyle },
248
+ ];
249
+ });
250
+
251
+ // A few entries embed double-width emoji (UTF-16 surrogate pairs) so the
252
+ // East-Asian-width-aware writeText() introduced in 0.17.0 is exercised
253
+ // at runtime — wide glyphs occupy two consecutive buffer cells with a ''
254
+ // continuation sentinel that Screen.render() skips.
255
+ const EVENT_TEMPLATES: Array<{ level: EventLevel; message: string }> = [
256
+ { level: 'ok', message: 'user logged in' },
257
+ { level: 'ok', message: 'cache warmed 🚀' },
258
+ { level: 'ok', message: 'metric collected' },
259
+ { level: 'ok', message: 'job completed 💾' },
260
+ { level: 'warn', message: 'config reloaded' },
261
+ { level: 'ok', message: 'heartbeat ok' },
262
+ { level: 'ok', message: 'worker spawned' },
263
+ { level: 'warn', message: 'session expired' },
264
+ { level: 'ok', message: 'gc finished' },
265
+ { level: 'error', message: 'index rebuild fail' },
95
266
  ];
96
267
 
97
268
  // ── Live-updating handles ─────────────────────────────────────────────────
@@ -156,8 +327,14 @@ const main = async (): Promise<void> => {
156
327
  headerSpinner.step();
157
328
 
158
329
  // prepend a new event to the log; keep the list bounded
159
- const msg = EVENT_TEMPLATES[rand(EVENT_TEMPLATES.length)]!;
160
- const next = [`[${timestamp()}] ${msg}`, ...events.getItems()];
330
+ const tpl = EVENT_TEMPLATES[rand(EVENT_TEMPLATES.length)]!;
331
+ const entry: EventRow = {
332
+ timestamp: timestamp(),
333
+ level: tpl.level,
334
+ message: tpl.message,
335
+ count: 1 + rand(9),
336
+ };
337
+ const next = [entry, ...events.getItems()];
161
338
  events.setItems(next.slice(0, 20));
162
339
 
163
340
  screen.render();
@@ -165,6 +342,41 @@ const main = async (): Promise<void> => {
165
342
 
166
343
  demoTimer = setInterval(tick, 1000);
167
344
 
345
+ // P0-4 demo (cont'd): Ctrl+R force-refreshes the charts out-of-band so the
346
+ // key handler ordering is easy to observe — the TextBox in the layout never
347
+ // receives a '\x12' control code.
348
+ wm.bindKey('ctrl+r', () => {
349
+ tick();
350
+ return true;
351
+ });
352
+
353
+ // P0-8 demo: 'v' toggles the visibility of the charts panel. Hidden children
354
+ // are fully skipped by render() (neither painted nor blitted), so the panel
355
+ // vanishes leaving the dialog background in its place — and reappears
356
+ // unchanged when shown again. The Tab focus cycle also skips focusables
357
+ // inside a hidden subtree.
358
+ const chartsPanel = result.get('chartsPanel') as Tabs;
359
+ wm.bindKey('v', () => {
360
+ chartsPanel.setVisible(!chartsPanel.isVisible());
361
+ screen.render();
362
+ return true;
363
+ });
364
+
365
+ // P0-5 demo: Ctrl+E pauses the WindowManager (drops the TUI), exits the
366
+ // alternate screen buffer, and spawns a short inline shell so you can see
367
+ // that the terminal is fully reusable for an external process. Pressing
368
+ // Enter at the prompt returns to the TUI — resume() re-enters the alt
369
+ // buffer, re-hides the cursor, re-enables mouse tracking, and re-renders
370
+ // the frame. The focus / dialog stack / key bindings stay intact across
371
+ // the cycle.
372
+ wm.bindKey('ctrl+e', () => {
373
+ wm.pause({ leaveAltScreen: true });
374
+ process.stdout.write('\n--- paused take4_console TUI. Press Enter to return ---\n');
375
+ spawnSync('bash', ['-c', 'read -r _'], { stdio: 'inherit' });
376
+ wm.resume();
377
+ return true;
378
+ });
379
+
168
380
  wm.run();
169
381
  };
170
382
 
package/src/index.mts CHANGED
@@ -17,9 +17,10 @@ export { StyleRegistry } from './Screen/StyleRegistry.mjs';
17
17
  export { WindowManager } from './Screen/WindowManager.mjs';
18
18
 
19
19
  // ── Geometry ──────────────────────────────────────────────────────────────────
20
- export { Pos } from './Screen/Pos.mjs';
21
- export { Pct, pct } from './Screen/Pos.mjs';
22
- export { Size } from './Screen/Size.mjs';
20
+ export { Pos } from './Screen/Pos.mjs';
21
+ export { Pct, pct } from './Screen/Pos.mjs';
22
+ export { Size, FlexDim, ContentDim,
23
+ flex, content } from './Screen/Size.mjs';
23
24
 
24
25
  // ── Interactive controls ──────────────────────────────────────────────────────
25
26
  export { Button } from './Screen/controls/Button.mjs';
@@ -42,6 +43,9 @@ export { Spinner } from './Screen/controls/Spinner.mjs';
42
43
  // ── YAML layout builder ───────────────────────────────────────────────────────
43
44
  export { InterfaceBuilder } from './Screen/InterfaceBuilder.mjs';
44
45
 
46
+ // ── Unicode text width helpers ────────────────────────────────────────────────
47
+ export { charWidth, stringWidth, setPuaWidth, getPuaWidth } from './Screen/textWidth.mjs';
48
+
45
49
  // ── Built-in style name constants ─────────────────────────────────────────────
46
50
  export {
47
51
  BUILTIN_WINDOW_BG,
@@ -64,14 +68,25 @@ export type {
64
68
  Cell,
65
69
  CellAttributes,
66
70
  TerminalSize,
71
+ ScreenOptions,
72
+ ScreenFrameStats,
67
73
  AxisSpec,
68
74
  DimSpec,
75
+ FlexBasis,
76
+ LayoutMode,
77
+ AlignItems,
78
+ JustifyContent,
79
+ Padding,
80
+ PaddingSpec,
69
81
 
70
82
  // Window / border
71
83
  BorderStyle,
84
+ BorderChars,
72
85
  WindowBorder,
73
86
  WindowProperties,
74
87
  WriteTextOptions,
88
+ WriteTextSegment,
89
+ WriteTextInput,
75
90
 
76
91
  // Control properties
77
92
  ButtonProperties,
@@ -85,6 +100,9 @@ export type {
85
100
  LineChartProperties,
86
101
  BarChartProperties,
87
102
  ListBoxProperties,
103
+ ListBoxRenderContext,
104
+ ListBoxRowSegment,
105
+ ListBoxRowSegments,
88
106
  TabsProperties,
89
107
  SparklineProperties,
90
108
  SpinnerProperties,
@@ -102,4 +120,6 @@ export type {
102
120
  YamlStyleDef,
103
121
  YamlWindowDef,
104
122
  YamlLayout,
123
+ CustomTypeContext,
124
+ CustomTypeFactory,
105
125
  } from './Screen/types.mjs';
package/src/layout.yaml CHANGED
@@ -10,6 +10,16 @@ windows:
10
10
  spinnerStyle: braille
11
11
  label: "LIVE"
12
12
  chartColor: 231
13
+ # P0-10: custom control registered in demo.mts via
14
+ # InterfaceBuilder.registerType('badge', …). Fields under `props:` are
15
+ # forwarded to the factory untouched.
16
+ - id: buildBadge
17
+ type: badge
18
+ pos: { x: 18, y: 0 }
19
+ size: { width: 10, height: 1 }
20
+ props:
21
+ text: "P0-10 "
22
+ color: 220
13
23
 
14
24
  - id: statusBar
15
25
  pos: { preset: bottom }
@@ -35,7 +45,10 @@ windows:
35
45
  pos: { x: 1, y: 2 }
36
46
  size: { width: "46%", height: "43%" }
37
47
  background: 235
38
- border: { top: true, right: true, bottom: true, left: true, style: single, color: 238 }
48
+ # Demonstrates the per-character override: the corners are swapped for diamond glyphs
49
+ # while the rest of the 'single' style is kept untouched.
50
+ border: { top: true, right: true, bottom: true, left: true, style: single, color: 238,
51
+ chars: { topLeft: '◆', topRight: '◆' } }
39
52
  content: "Personal info"
40
53
  children:
41
54
  - id: tbUsername
@@ -45,6 +58,9 @@ windows:
45
58
  value: "jan_kowalski"
46
59
  placeholder: "username"
47
60
  focused: true
61
+ # P0-6: YAML-wired onSubmit — pressing Enter while this TextBox is
62
+ # focused fires `usernameSubmitted`, registered in demo.mts.
63
+ onSubmit: usernameSubmitted
48
64
  - id: tbEmail
49
65
  type: textbox
50
66
  pos: { x: 0, y: 5 }
@@ -124,7 +140,8 @@ windows:
124
140
  pos: { x: 1, y: "46%" }
125
141
  size: { width: "46%", height: "43%" }
126
142
  background: 235
127
- border: { top: true, right: true, bottom: true, left: true, style: single, color: 238 }
143
+ # Demonstrates the new 'thick' (heavy) box-drawing style.
144
+ border: { top: true, right: true, bottom: true, left: true, style: thick, color: 38 }
128
145
  content: "Resources"
129
146
  children:
130
147
  - id: pbCpu
@@ -204,33 +221,48 @@ windows:
204
221
  pos: { x: "81%", y: "46%" }
205
222
  size: { width: "17%", height: "43%" }
206
223
  background: 235
207
- border: { top: true, right: true, bottom: true, left: true, style: single, color: 238 }
224
+ # Demonstrates the new 'dashed' style.
225
+ border: { top: true, right: true, bottom: true, left: true, style: dashed, color: 33 }
208
226
  content: "Events"
209
227
  children:
210
228
  - id: eventsList
211
229
  type: listbox
212
230
  pos: { x: 0, y: 1 }
213
231
  size: { width: "100%", height: "85%" }
214
- items:
215
- - "[12:04:21] user logged in"
216
- - "[12:04:33] cache warmed"
217
- - "[12:05:01] metric collected"
218
- - "[12:05:18] job completed"
232
+ # Items populated at runtime via ListBox.setItems() with structured EventRow objects;
233
+ # a demo.mts renderItem() draws icon + dimmed timestamp + right-aligned count.
219
234
 
220
235
  # ── Buttons ───────────────────────────────────────────────────────────────
221
- - id: btnDisabled
222
- type: button
223
- pos: { x: 1, y: -4 }
224
- size: { width: 11, height: 3 }
225
- label: "Delete"
226
- disabled: true
227
- - id: btnCancel
228
- type: button
229
- pos: { x: -15, y: -4 }
230
- size: { width: 11, height: 3 }
231
- label: "Cancel"
232
- - id: btnSave
233
- type: button
234
- pos: { x: -27, y: -4 }
235
- size: { width: 11, height: 3 }
236
- label: " Save"
236
+ # Bottom action bar — demonstrates P0-3 flex layout. The container uses
237
+ # `layout: row` with `gap: 1`; the middle `spacer` child has `size: flex`
238
+ # (grow=1 by default), so it soaks up leftover inner width between the
239
+ # left-aligned "Delete" and the right-aligned "Cancel" / " Save" group.
240
+ # alignItems: stretch pins children to the row height. This is the same
241
+ # visual outcome as the previous absolute positioning but authored
242
+ # declaratively, and it adapts automatically if the dialog is resized.
243
+ - id: buttonRow
244
+ pos: { x: 0, y: -4 }
245
+ size: { fillWidth: 3 }
246
+ layout: row
247
+ gap: 1
248
+ alignItems: stretch
249
+ children:
250
+ - id: btnDisabled
251
+ type: button
252
+ pos: flex
253
+ size: { width: 11, height: 3 }
254
+ label: "Delete"
255
+ disabled: true
256
+ - id: buttonSpacer
257
+ pos: flex
258
+ size: flex
259
+ - id: btnCancel
260
+ type: button
261
+ pos: flex
262
+ size: { width: 11, height: 3 }
263
+ label: "Cancel"
264
+ - id: btnSave
265
+ type: button
266
+ pos: flex
267
+ size: { width: 11, height: 3 }
268
+ label: " Save"