@nick-skriabin/glyph 0.1.26 → 0.1.27

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 (2) hide show
  1. package/package.json +1 -1
  2. package/README.md +0 -663
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nick-skriabin/glyph",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "A React renderer for terminal UIs with flexbox layout",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/README.md DELETED
@@ -1,663 +0,0 @@
1
- <p align="center">
2
- <img src="https://em-content.zobj.net/source/apple/391/crystal-ball_1f52e.png" width="120" height="120" alt="Glyph">
3
- </p>
4
-
5
- <h1 align="center">Glyph</h1>
6
-
7
- <p align="center">
8
- <strong>React renderer for terminal UIs</strong><br>
9
- <em>Flexbox layout. Keyboard-driven. Zero compromises.</em>
10
- </p>
11
-
12
- <p align="center">
13
- <a href="#quick-start">Quick Start</a> &bull;
14
- <a href="#components">Components</a> &bull;
15
- <a href="#hooks">Hooks</a> &bull;
16
- <a href="#styling">Styling</a> &bull;
17
- <a href="#examples">Examples</a>
18
- </p>
19
-
20
- <p align="center">
21
- <a href="https://www.npmjs.com/package/@nick-skriabin/glyph"><img src="https://img.shields.io/npm/v/@nick-skriabin/glyph?color=crimson&logo=npm" alt="npm version"></a>
22
- <img src="https://img.shields.io/badge/React-18%2B-61dafb?logo=react&logoColor=white" alt="React 18+">
23
- <img src="https://img.shields.io/badge/Yoga-Flexbox-mediumpurple?logo=meta&logoColor=white" alt="Yoga Flexbox">
24
- <img src="https://img.shields.io/badge/TypeScript-First-3178c6?logo=typescript&logoColor=white" alt="TypeScript">
25
- <img src="https://img.shields.io/badge/License-MIT-blue" alt="MIT License">
26
- </p>
27
-
28
- ---
29
-
30
- Build real terminal applications with React. Glyph provides a full component model with flexbox layout (powered by Yoga), focus management, keyboard input, and efficient diff-based rendering. Write TUIs the same way you write web apps.
31
-
32
- | | | |
33
- |---|---|---|
34
- | ![Glyph](./screehshots/glyph-main.jpg) | ![Glyph List](./screehshots/glyph-list.jpg) |![Glyph Edit](./screehshots/glyph-edit.jpg) |
35
-
36
-
37
- ### Features
38
-
39
- | | |
40
- |---|---|
41
- | **Flexbox Layout** | Full CSS-like flexbox via Yoga &mdash; rows, columns, wrapping, alignment, gaps, padding |
42
- | **Rich Components** | Box, Text, Input, Button, Checkbox, Radio, Select, ScrollView, List, Menu, Progress, Spinner, Toasts, Dialogs, Portal |
43
- | **Focus System** | Tab navigation, focus scopes, focus trapping for modals |
44
- | **Keyboard Input** | `useInput` hook, declarative `<Keybind>` component, vim-style bindings |
45
- | **Smart Rendering** | Double-buffered framebuffer with character-level diffing &mdash; only changed cells are written |
46
- | **True Colors** | Named colors, hex, RGB, 256-palette. Auto-contrast text on colored backgrounds |
47
- | **Borders** | Single, double, rounded, and ASCII border styles |
48
- | **TypeScript** | Full type coverage. Every prop, style, and hook is typed |
49
-
50
- ---
51
-
52
- ## Installation
53
-
54
- ```bash
55
- # npm
56
- npm install @nick-skriabin/glyph react
57
-
58
- # pnpm
59
- pnpm add @nick-skriabin/glyph react
60
-
61
- # bun
62
- bun add @nick-skriabin/glyph react
63
- ```
64
-
65
- ---
66
-
67
- ## Quick Start
68
-
69
- ```tsx
70
- import React from "react";
71
- import { render, Box, Text, Keybind, useApp } from "@nick-skriabin/glyph";
72
-
73
- function App() {
74
- const { exit } = useApp();
75
-
76
- return (
77
- <Box style={{ border: "round", borderColor: "cyan", padding: 1 }}>
78
- <Text style={{ bold: true, color: "green" }}>Hello, Glyph!</Text>
79
- <Keybind keypress="q" onPress={() => exit()} />
80
- </Box>
81
- );
82
- }
83
-
84
- render(<App />);
85
- ```
86
-
87
- Run it:
88
-
89
- ```bash
90
- npx tsx app.tsx
91
- ```
92
-
93
- ---
94
-
95
- ## Components
96
-
97
- ### `<Box>`
98
-
99
- Flexbox container. The fundamental building block.
100
-
101
- ```tsx
102
- <Box style={{ flexDirection: "row", gap: 2, border: "single", padding: 1 }}>
103
- <Box style={{ flexGrow: 1, bg: "blue" }}>
104
- <Text>Left</Text>
105
- </Box>
106
- <Box style={{ flexGrow: 1, bg: "red" }}>
107
- <Text>Right</Text>
108
- </Box>
109
- </Box>
110
- ```
111
-
112
- ### `<Text>`
113
-
114
- Styled text content. Supports wrapping, alignment, bold, dim, italic, underline.
115
-
116
- ```tsx
117
- <Text style={{ color: "yellowBright", bold: true, textAlign: "center" }}>
118
- Warning: something happened
119
- </Text>
120
- ```
121
-
122
- ### `<Input>`
123
-
124
- Text input field with cursor and placeholder support.
125
-
126
- ```tsx
127
- <Input
128
- value={text}
129
- onChange={setText}
130
- placeholder="Type here..."
131
- style={{ bg: "blackBright", paddingX: 1 }}
132
- focusedStyle={{ bg: "white", color: "black" }}
133
- />
134
- ```
135
-
136
- Supports `multiline` for multi-line editing, `autoFocus` for automatic focus on mount. The cursor is always visible when focused.
137
-
138
- **Input masking** with `onBeforeChange` for validation/formatting:
139
-
140
- ```tsx
141
- import { createMask, masks } from "@nick-skriabin/glyph";
142
-
143
- // Pre-built masks
144
- <Input onBeforeChange={masks.usPhone} placeholder="(___) ___-____" />
145
- <Input onBeforeChange={masks.creditCard} placeholder="____ ____ ____ ____" />
146
-
147
- // Custom masks: 9=digit, a=letter, *=alphanumeric
148
- const licensePlate = createMask("aaa-9999");
149
- <Input onBeforeChange={licensePlate} placeholder="___-____" />
150
- ```
151
-
152
- Available masks: `usPhone`, `intlPhone`, `creditCard`, `dateUS`, `dateEU`, `dateISO`, `time`, `timeFull`, `ssn`, `zip`, `zipPlus4`, `ipv4`, `mac`.
153
-
154
- ### `<Button>`
155
-
156
- Focusable button with press handling and visual feedback.
157
-
158
- ```tsx
159
- <Button
160
- onPress={() => console.log("clicked")}
161
- style={{ border: "single", borderColor: "cyan", paddingX: 2 }}
162
- focusedStyle={{ borderColor: "yellowBright", bold: true }}
163
- >
164
- <Text>Submit</Text>
165
- </Button>
166
- ```
167
-
168
- Buttons participate in the focus system automatically. Press `Enter` or `Space` to activate.
169
-
170
- ### `<Checkbox>`
171
-
172
- Toggle checkbox with label support.
173
-
174
- ```tsx
175
- const [agreed, setAgreed] = useState(false);
176
-
177
- <Checkbox
178
- checked={agreed}
179
- onChange={setAgreed}
180
- label="I agree to the terms"
181
- focusedStyle={{ color: "cyan" }}
182
- />
183
- ```
184
-
185
- Focusable. Press `Enter` or `Space` to toggle. Supports custom `checkedChar` and `uncheckedChar` props.
186
-
187
- ### `<Radio>`
188
-
189
- Radio button group for single selection from multiple options.
190
-
191
- ```tsx
192
- const [theme, setTheme] = useState<string>("dark");
193
-
194
- <Radio
195
- items={[
196
- { label: "Light", value: "light" },
197
- { label: "Dark", value: "dark" },
198
- { label: "System", value: "system" },
199
- ]}
200
- value={theme}
201
- onChange={setTheme}
202
- focusedItemStyle={{ color: "cyan" }}
203
- selectedItemStyle={{ bold: true }}
204
- />
205
- ```
206
-
207
- Focusable. Navigate with `Up`/`Down`/`Left`/`Right`/`Tab`/`Shift+Tab`, select with `Enter`/`Space`. Supports `direction` prop (`"column"` or `"row"`), custom `selectedChar` and `unselectedChar`.
208
-
209
- ### `<ScrollView>`
210
-
211
- Scrollable container with keyboard navigation and clipping.
212
-
213
- ```tsx
214
- <ScrollView style={{ flexGrow: 1, border: "single" }}>
215
- {items.map((item, i) => (
216
- <Box key={i}>
217
- <Text>{item}</Text>
218
- </Box>
219
- ))}
220
- </ScrollView>
221
- ```
222
-
223
- **Keyboard:** `PageUp`/`PageDown`, `Ctrl+d`/`Ctrl+u` (half-page), `Ctrl+f`/`Ctrl+b` (full page).
224
-
225
- Shows a scrollbar when content exceeds viewport (disable with `showScrollbar={false}`). Supports controlled mode with `scrollOffset` and `onScroll` props.
226
-
227
- **Focus-aware scrolling:** ScrollView is focusable by default and responds to scroll keys when focused (or when it contains the focused element). This prevents multiple ScrollViews from scrolling simultaneously — only the one with focus responds.
228
-
229
- Set `focusable={false}` if you want the ScrollView to only scroll when a child element has focus:
230
-
231
- ```tsx
232
- <ScrollView focusable={false} style={{ flexGrow: 1 }}>
233
- <Input ... /> {/* ScrollView scrolls only when Input is focused */}
234
- </ScrollView>
235
- ```
236
-
237
- ### `<List>`
238
-
239
- Keyboard-navigable selection list with a render callback.
240
-
241
- ```tsx
242
- <List
243
- count={items.length}
244
- onSelect={(index) => handleSelect(items[index])}
245
- disabledIndices={new Set([2, 5])}
246
- renderItem={({ index, selected, focused }) => (
247
- <Box style={selected && focused ? { bg: "cyan" } : {}}>
248
- <Text style={selected ? { bold: true } : {}}>
249
- {selected ? "> " : " "}{items[index]}
250
- </Text>
251
- </Box>
252
- )}
253
- />
254
- ```
255
-
256
- Focusable. `Up`/`Down`/`j`/`k` to navigate, `G` to jump to bottom, `gg` to jump to top, `Enter` to select. Disabled indices are skipped.
257
-
258
- ### `<Menu>`
259
-
260
- Styled menu built on `<List>`. Accepts structured items with labels, values, and disabled state.
261
-
262
- ```tsx
263
- <Menu
264
- items={[
265
- { label: "New File", value: "new" },
266
- { label: "Open File", value: "open" },
267
- { label: "Export", value: "export", disabled: true },
268
- { label: "Quit", value: "quit" },
269
- ]}
270
- onSelect={(value) => handleAction(value)}
271
- highlightColor="yellow"
272
- />
273
- ```
274
-
275
- ### `<Select>`
276
-
277
- Dropdown select with keyboard navigation and type-to-filter search.
278
-
279
- ```tsx
280
- const [lang, setLang] = useState<string | undefined>();
281
-
282
- <Select
283
- items={[
284
- { label: "TypeScript", value: "ts" },
285
- { label: "JavaScript", value: "js" },
286
- { label: "Rust", value: "rust" },
287
- { label: "Go", value: "go" },
288
- { label: "COBOL", value: "cobol", disabled: true },
289
- ]}
290
- value={lang}
291
- onChange={setLang}
292
- placeholder="Pick a language..."
293
- maxVisible={6}
294
- highlightColor="yellow"
295
- />
296
- ```
297
-
298
- Focusable. `Enter`/`Space`/`Down` to open, `Up`/`Down` to navigate, `Enter` to confirm, `Escape` to close. Type characters to filter items when open. Disabled items are skipped.
299
-
300
- Props: `items`, `value`, `onChange`, `placeholder`, `maxVisible`, `highlightColor`, `searchable`, `style`, `focusedStyle`, `dropdownStyle`, `disabled`.
301
-
302
- ### `<FocusScope>`
303
-
304
- Focus trapping for modals and overlays.
305
-
306
- ```tsx
307
- <FocusScope trap>
308
- <Input value={v} onChange={setV} />
309
- <Button onPress={submit}>
310
- <Text>OK</Text>
311
- </Button>
312
- </FocusScope>
313
- ```
314
-
315
- ### `<Portal>`
316
-
317
- Renders children in a fullscreen absolute overlay. Useful for modals and dialogs.
318
-
319
- ```tsx
320
- <Portal>
321
- <Box style={{ width: "100%", height: "100%", justifyContent: "center", alignItems: "center" }}>
322
- <Box style={{ width: 40, border: "double", bg: "black", padding: 1 }}>
323
- <Text>Modal content</Text>
324
- </Box>
325
- </Box>
326
- </Portal>
327
- ```
328
-
329
- ### `<Keybind>`
330
-
331
- Declarative keyboard shortcut. Renders nothing.
332
-
333
- ```tsx
334
- <Keybind keypress="ctrl+s" onPress={save} />
335
- <Keybind keypress="escape" onPress={close} />
336
- <Keybind keypress="q" onPress={() => exit()} />
337
- ```
338
-
339
- ### `<Progress>`
340
-
341
- Determinate or indeterminate progress bar. Uses `useLayout` to measure actual width and renders block characters.
342
-
343
- ```tsx
344
- <Progress value={0.65} showPercent />
345
- <Progress indeterminate label="Loading" />
346
- ```
347
-
348
- Props: `value` (0..1), `indeterminate`, `width`, `label`, `showPercent`, `filled`/`empty` (characters).
349
-
350
- ### `<Spinner>`
351
-
352
- Animated spinner with configurable frames. Cleans up timers on unmount.
353
-
354
- ```tsx
355
- <Spinner label="Loading..." style={{ color: "green" }} />
356
- <Spinner frames={["|", "/", "-", "\\"]} intervalMs={100} />
357
- ```
358
-
359
- ### `<ToastHost>` + `useToast()`
360
-
361
- Lightweight toast notifications rendered via Portal. Wrap your app in `<ToastHost>`, then push toasts from anywhere with `useToast()`.
362
-
363
- ```tsx
364
- function App() {
365
- const toast = useToast();
366
- return <Keybind keypress="t" onPress={() =>
367
- toast({ message: "Saved!", variant: "success" })
368
- } />;
369
- }
370
-
371
- render(<ToastHost position="top-right"><App /></ToastHost>);
372
- ```
373
-
374
- Variants: `"info"`, `"success"`, `"warning"`, `"error"`. Auto-dismiss after `durationMs` (default 3000).
375
-
376
- ### `<DialogHost>` + `useDialog()`
377
-
378
- Imperative `alert()` and `confirm()` dialogs, similar to browser APIs. Wrap your app in `<DialogHost>`, then show dialogs from anywhere.
379
-
380
- ```tsx
381
- function App() {
382
- const { alert, confirm } = useDialog();
383
-
384
- const handleDelete = async () => {
385
- const ok = await confirm("Delete this item?", {
386
- okText: "Delete",
387
- cancelText: "Keep"
388
- });
389
- if (ok) {
390
- // delete the item
391
- }
392
- };
393
-
394
- const handleSave = async () => {
395
- await saveData();
396
- await alert("Saved successfully!");
397
- };
398
-
399
- return <Button onPress={handleDelete}><Text>Delete</Text></Button>;
400
- }
401
-
402
- render(<DialogHost><App /></DialogHost>);
403
- ```
404
-
405
- **Rich content** — pass React elements instead of strings:
406
-
407
- ```tsx
408
- await alert(
409
- <Box style={{ flexDirection: "column" }}>
410
- <Text style={{ bold: true, color: "green" }}>✓ Success!</Text>
411
- <Text>Your changes have been saved.</Text>
412
- </Box>,
413
- { okText: "Got it!" }
414
- );
415
- ```
416
-
417
- **Keyboard:** Tab/Shift+Tab or arrows to switch buttons, Enter/Space to select, Escape to cancel.
418
-
419
- **Chained dialogs** work naturally with async/await — each dialog waits for the previous to close.
420
-
421
- ### `<Spacer>`
422
-
423
- Flexible space filler. Pushes siblings apart.
424
-
425
- ```tsx
426
- <Box style={{ flexDirection: "row" }}>
427
- <Text>Left</Text>
428
- <Spacer />
429
- <Text>Right</Text>
430
- </Box>
431
- ```
432
-
433
- ---
434
-
435
- ## Hooks
436
-
437
- ### `useInput(handler)`
438
-
439
- Listen for all keyboard events.
440
-
441
- ```tsx
442
- useInput((key) => {
443
- if (key.name === "escape") close();
444
- if (key.ctrl && key.name === "s") save();
445
- });
446
- ```
447
-
448
- ### `useFocus(nodeRef)`
449
-
450
- Get focus state for a node.
451
-
452
- ```tsx
453
- const ref = useRef(null);
454
- const { focused, focus } = useFocus(ref);
455
-
456
- <Box ref={ref} focusable>
457
- <Text style={focused ? { color: "cyan" } : {}}>
458
- {focused ? "* focused *" : "not focused"}
459
- </Text>
460
- </Box>
461
- ```
462
-
463
- ### `useLayout(nodeRef)`
464
-
465
- Subscribe to a node's computed layout.
466
-
467
- ```tsx
468
- const ref = useRef(null);
469
- const layout = useLayout(ref);
470
-
471
- // layout: { x, y, width, height, innerX, innerY, innerWidth, innerHeight }
472
- ```
473
-
474
- ### `useApp()`
475
-
476
- Access app-level utilities.
477
-
478
- ```tsx
479
- const { exit, columns, rows } = useApp();
480
- ```
481
-
482
- ---
483
-
484
- ## Styling
485
-
486
- All components accept a `style` prop. Glyph uses Yoga for flexbox layout, so the model is familiar if you've used CSS flexbox or React Native.
487
-
488
- ### Layout
489
-
490
- | Property | Type | Description |
491
- |----------|------|-------------|
492
- | `width`, `height` | `number \| "${n}%"` | Dimensions |
493
- | `minWidth`, `minHeight` | `number` | Minimum dimensions |
494
- | `maxWidth`, `maxHeight` | `number` | Maximum dimensions |
495
- | `padding` | `number` | Padding on all sides |
496
- | `paddingX`, `paddingY` | `number` | Horizontal / vertical padding |
497
- | `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` | `number` | Individual sides |
498
- | `gap` | `number` | Gap between flex children |
499
-
500
- ### Flexbox
501
-
502
- | Property | Type | Default |
503
- |----------|------|---------|
504
- | `flexDirection` | `"row" \| "column"` | `"column"` |
505
- | `flexWrap` | `"nowrap" \| "wrap"` | `"nowrap"` |
506
- | `justifyContent` | `"flex-start" \| "center" \| "flex-end" \| "space-between" \| "space-around"` | `"flex-start"` |
507
- | `alignItems` | `"flex-start" \| "center" \| "flex-end" \| "stretch"` | `"stretch"` |
508
- | `flexGrow` | `number` | `0` |
509
- | `flexShrink` | `number` | `0` |
510
-
511
- ### Positioning
512
-
513
- | Property | Type | Description |
514
- |----------|------|-------------|
515
- | `position` | `"relative" \| "absolute"` | Positioning mode |
516
- | `top`, `right`, `bottom`, `left` | `number \| "${n}%"` | Offsets |
517
- | `inset` | `number \| "${n}%"` | Shorthand for all four edges |
518
- | `zIndex` | `number` | Stacking order |
519
-
520
- ### Visual
521
-
522
- | Property | Type | Description |
523
- |----------|------|-------------|
524
- | `bg` | `Color` | Background color |
525
- | `border` | `"none" \| "single" \| "double" \| "round" \| "ascii"` | Border style |
526
- | `borderColor` | `Color` | Border color |
527
- | `clip` | `boolean` | Clip overflowing children |
528
-
529
- ### Text
530
-
531
- | Property | Type | Description |
532
- |----------|------|-------------|
533
- | `color` | `Color` | Text color |
534
- | `bold` | `boolean` | Bold text |
535
- | `dim` | `boolean` | Dimmed text |
536
- | `italic` | `boolean` | Italic text |
537
- | `underline` | `boolean` | Underlined text |
538
- | `wrap` | `"wrap" \| "truncate" \| "ellipsis" \| "none"` | Text wrapping mode |
539
- | `textAlign` | `"left" \| "center" \| "right"` | Text alignment |
540
-
541
- ### Colors
542
-
543
- Colors can be specified as:
544
-
545
- - **Named:** `"red"`, `"green"`, `"blueBright"`, `"whiteBright"`, etc.
546
- - **Hex:** `"#ff0000"`, `"#1a1a2e"`
547
- - **RGB:** `{ r: 255, g: 0, b: 0 }`
548
- - **256-palette:** `0`&ndash;`255`
549
-
550
- Text on colored backgrounds automatically picks black or white for contrast when no explicit color is set.
551
-
552
- ---
553
-
554
- ## `render(element, options?)`
555
-
556
- Mount a React element to the terminal.
557
-
558
- ```tsx
559
- const app = render(<App />, {
560
- stdout: process.stdout,
561
- stdin: process.stdin,
562
- debug: false,
563
- useNativeCursor: true, // Use terminal's native cursor (default: true)
564
- });
565
-
566
- app.unmount(); // Tear down
567
- app.exit(); // Unmount and exit process
568
- ```
569
-
570
- ### Options
571
-
572
- | Option | Type | Default | Description |
573
- |--------|------|---------|-------------|
574
- | `stdout` | `NodeJS.WriteStream` | `process.stdout` | Output stream |
575
- | `stdin` | `NodeJS.ReadStream` | `process.stdin` | Input stream |
576
- | `debug` | `boolean` | `false` | Enable debug logging |
577
- | `useNativeCursor` | `boolean` | `true` | Use terminal's native cursor instead of simulated one |
578
-
579
- ### Native Cursor
580
-
581
- By default, Glyph uses the terminal's native cursor, which enables:
582
-
583
- - **Cursor shaders** in terminals that support them (e.g., Ghostty)
584
- - **Custom cursor shapes** (block, beam, underline) from terminal settings
585
- - **Cursor animations** and blinking behavior
586
-
587
- The native cursor is automatically shown when an input is focused and hidden otherwise.
588
-
589
- To use the simulated cursor instead (inverted colors, no shader support):
590
-
591
- ```tsx
592
- render(<App />, { useNativeCursor: false });
593
- ```
594
-
595
- ---
596
-
597
- ## Examples
598
-
599
- ```bash
600
- # Clone and install
601
- git clone <repo-url> && cd glyph
602
- pnpm install && pnpm build
603
-
604
- # Run examples
605
- pnpm --filter basic-layout dev # Flexbox layout demo
606
- pnpm --filter modal-input dev # Modal, input, focus trapping
607
- pnpm --filter scrollview-demo dev # Scrollable content
608
- pnpm --filter list-demo dev # Keyboard-navigable list
609
- pnpm --filter menu-demo dev # Styled menu
610
- pnpm --filter select-demo dev # Dropdown select with search
611
- pnpm --filter forms-demo dev # Checkbox and Radio inputs
612
- pnpm --filter masked-input dev # Input masks (phone, credit card, etc.)
613
- pnpm --filter dialog-demo dev # Alert and Confirm dialogs
614
- pnpm --filter dashboard dev # Full task manager (all components)
615
- pnpm --filter showcase dev # Progress, Spinner, Toasts
616
- ```
617
-
618
- ---
619
-
620
- ## Who Uses Glyph
621
-
622
- <table>
623
- <tr>
624
- <td align="center">
625
- <a href="https://github.com/nick-skriabin/aion">
626
- <strong>Aion</strong>
627
- </a>
628
- <br>
629
- <sub>Calendar & time management TUI</sub>
630
- </td>
631
- </tr>
632
- </table>
633
-
634
- <sub>Using Glyph in your project? <a href="https://github.com/nick-skriabin/glyph/issues">Let us know!</a></sub>
635
-
636
- ---
637
-
638
- ## Architecture
639
-
640
- ```
641
- src/
642
- ├── reconciler/ React reconciler (host config + GlyphNode tree)
643
- ├── layout/ Yoga-based flexbox + text measurement
644
- ├── paint/ Framebuffer, character diffing, borders, colors
645
- ├── runtime/ Terminal raw mode, key parsing, OSC handling
646
- ├── components/ Box, Text, Input, Button, ScrollView, List, Menu, ...
647
- ├── hooks/ useInput, useFocus, useLayout, useApp
648
- └── render.ts Entry point tying it all together
649
- ```
650
-
651
- **Render pipeline:** React reconciler builds a GlyphNode tree &rarr; Yoga computes flexbox layout &rarr; painter rasterizes to a framebuffer &rarr; diff engine writes only changed cells to stdout.
652
-
653
- ---
654
-
655
- ## License
656
-
657
- MIT
658
-
659
- ---
660
-
661
- <p align="center">
662
- <sub>Built with React &bull; Yoga &bull; a lot of ANSI escape codes</sub>
663
- </p>