@nick-skriabin/glyph 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/README.md +545 -0
  2. package/package.json +11 -3
package/README.md ADDED
@@ -0,0 +1,545 @@
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
+ <img src="https://img.shields.io/badge/React-18%2B-61dafb?logo=react&logoColor=white" alt="React 18+">
22
+ <img src="https://img.shields.io/badge/Yoga-Flexbox-mediumpurple?logo=meta&logoColor=white" alt="Yoga Flexbox">
23
+ <img src="https://img.shields.io/badge/TypeScript-First-3178c6?logo=typescript&logoColor=white" alt="TypeScript">
24
+ <img src="https://img.shields.io/badge/License-MIT-blue" alt="MIT License">
25
+ </p>
26
+
27
+ ---
28
+
29
+ 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.
30
+
31
+ | | | |
32
+ |---|---|---|
33
+ | ![Glyph](./screehshots/glyph-main.jpg) | ![Glyph List](./screehshots/glyph-list.jpg) |![Glyph Edit](./screehshots/glyph-edit.jpg) |
34
+
35
+
36
+ ### Features
37
+
38
+ | | |
39
+ |---|---|
40
+ | **Flexbox Layout** | Full CSS-like flexbox via Yoga &mdash; rows, columns, wrapping, alignment, gaps, padding |
41
+ | **Rich Components** | Box, Text, Input, Button, Checkbox, Radio, Select, ScrollView, List, Menu, Progress, Spinner, Toasts, Portal |
42
+ | **Focus System** | Tab navigation, focus scopes, focus trapping for modals |
43
+ | **Keyboard Input** | `useInput` hook, declarative `<Keybind>` component, vim-style bindings |
44
+ | **Smart Rendering** | Double-buffered framebuffer with character-level diffing &mdash; only changed cells are written |
45
+ | **True Colors** | Named colors, hex, RGB, 256-palette. Auto-contrast text on colored backgrounds |
46
+ | **Borders** | Single, double, rounded, and ASCII border styles |
47
+ | **TypeScript** | Full type coverage. Every prop, style, and hook is typed |
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ # npm
55
+ npm install @nick-skriabin/glyph react
56
+
57
+ # pnpm
58
+ pnpm add @nick-skriabin/glyph react
59
+
60
+ # bun
61
+ bun add @nick-skriabin/glyph react
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Quick Start
67
+
68
+ ```tsx
69
+ import React from "react";
70
+ import { render, Box, Text, Keybind, useApp } from "@nick-skriabin/glyph";
71
+
72
+ function App() {
73
+ const { exit } = useApp();
74
+
75
+ return (
76
+ <Box style={{ border: "round", borderColor: "cyan", padding: 1 }}>
77
+ <Text style={{ bold: true, color: "green" }}>Hello, Glyph!</Text>
78
+ <Keybind keypress="q" onPress={() => exit()} />
79
+ </Box>
80
+ );
81
+ }
82
+
83
+ render(<App />);
84
+ ```
85
+
86
+ Run it:
87
+
88
+ ```bash
89
+ npx tsx app.tsx
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Components
95
+
96
+ ### `<Box>`
97
+
98
+ Flexbox container. The fundamental building block.
99
+
100
+ ```tsx
101
+ <Box style={{ flexDirection: "row", gap: 2, border: "single", padding: 1 }}>
102
+ <Box style={{ flexGrow: 1, bg: "blue" }}>
103
+ <Text>Left</Text>
104
+ </Box>
105
+ <Box style={{ flexGrow: 1, bg: "red" }}>
106
+ <Text>Right</Text>
107
+ </Box>
108
+ </Box>
109
+ ```
110
+
111
+ ### `<Text>`
112
+
113
+ Styled text content. Supports wrapping, alignment, bold, dim, italic, underline.
114
+
115
+ ```tsx
116
+ <Text style={{ color: "yellowBright", bold: true, textAlign: "center" }}>
117
+ Warning: something happened
118
+ </Text>
119
+ ```
120
+
121
+ ### `<Input>`
122
+
123
+ Text input field with cursor and placeholder support.
124
+
125
+ ```tsx
126
+ <Input
127
+ value={text}
128
+ onChange={setText}
129
+ placeholder="Type here..."
130
+ style={{ bg: "blackBright", paddingX: 1 }}
131
+ focusedStyle={{ bg: "white", color: "black" }}
132
+ />
133
+ ```
134
+
135
+ Supports `multiline` for multi-line editing. The cursor is always visible when focused, with inverted colors for clarity.
136
+
137
+ ### `<Button>`
138
+
139
+ Focusable button with press handling and visual feedback.
140
+
141
+ ```tsx
142
+ <Button
143
+ onPress={() => console.log("clicked")}
144
+ style={{ border: "single", borderColor: "cyan", paddingX: 2 }}
145
+ focusedStyle={{ borderColor: "yellowBright", bold: true }}
146
+ >
147
+ <Text>Submit</Text>
148
+ </Button>
149
+ ```
150
+
151
+ Buttons participate in the focus system automatically. Press `Enter` or `Space` to activate.
152
+
153
+ ### `<Checkbox>`
154
+
155
+ Toggle checkbox with label support.
156
+
157
+ ```tsx
158
+ const [agreed, setAgreed] = useState(false);
159
+
160
+ <Checkbox
161
+ checked={agreed}
162
+ onChange={setAgreed}
163
+ label="I agree to the terms"
164
+ focusedStyle={{ color: "cyan" }}
165
+ />
166
+ ```
167
+
168
+ Focusable. Press `Enter` or `Space` to toggle. Supports custom `checkedChar` and `uncheckedChar` props.
169
+
170
+ ### `<Radio>`
171
+
172
+ Radio button group for single selection from multiple options.
173
+
174
+ ```tsx
175
+ const [theme, setTheme] = useState<string>("dark");
176
+
177
+ <Radio
178
+ items={[
179
+ { label: "Light", value: "light" },
180
+ { label: "Dark", value: "dark" },
181
+ { label: "System", value: "system" },
182
+ ]}
183
+ value={theme}
184
+ onChange={setTheme}
185
+ focusedItemStyle={{ color: "cyan" }}
186
+ selectedItemStyle={{ bold: true }}
187
+ />
188
+ ```
189
+
190
+ Focusable. Navigate with `Up`/`Down`/`Left`/`Right`/`Tab`/`Shift+Tab`, select with `Enter`/`Space`. Supports `direction` prop (`"column"` or `"row"`), custom `selectedChar` and `unselectedChar`.
191
+
192
+ ### `<ScrollView>`
193
+
194
+ Scrollable container with keyboard navigation and clipping.
195
+
196
+ ```tsx
197
+ <ScrollView style={{ flexGrow: 1, border: "single" }}>
198
+ {items.map((item, i) => (
199
+ <Box key={i}>
200
+ <Text>{item}</Text>
201
+ </Box>
202
+ ))}
203
+ </ScrollView>
204
+ ```
205
+
206
+ **Keyboard:** `PageUp`/`PageDown`, `Ctrl+d`/`Ctrl+u` (half-page), `Ctrl+f`/`Ctrl+b` (full page).
207
+
208
+ Shows a scrollbar when content exceeds viewport (disable with `showScrollbar={false}`). Supports controlled mode with `scrollOffset` and `onScroll` props.
209
+
210
+ ### `<List>`
211
+
212
+ Keyboard-navigable selection list with a render callback.
213
+
214
+ ```tsx
215
+ <List
216
+ count={items.length}
217
+ onSelect={(index) => handleSelect(items[index])}
218
+ disabledIndices={new Set([2, 5])}
219
+ renderItem={({ index, selected, focused }) => (
220
+ <Box style={selected && focused ? { bg: "cyan" } : {}}>
221
+ <Text style={selected ? { bold: true } : {}}>
222
+ {selected ? "> " : " "}{items[index]}
223
+ </Text>
224
+ </Box>
225
+ )}
226
+ />
227
+ ```
228
+
229
+ Focusable. `Up`/`Down`/`j`/`k` to navigate, `G` to jump to bottom, `gg` to jump to top, `Enter` to select. Disabled indices are skipped.
230
+
231
+ ### `<Menu>`
232
+
233
+ Styled menu built on `<List>`. Accepts structured items with labels, values, and disabled state.
234
+
235
+ ```tsx
236
+ <Menu
237
+ items={[
238
+ { label: "New File", value: "new" },
239
+ { label: "Open File", value: "open" },
240
+ { label: "Export", value: "export", disabled: true },
241
+ { label: "Quit", value: "quit" },
242
+ ]}
243
+ onSelect={(value) => handleAction(value)}
244
+ highlightColor="yellow"
245
+ />
246
+ ```
247
+
248
+ ### `<Select>`
249
+
250
+ Dropdown select with keyboard navigation and type-to-filter search.
251
+
252
+ ```tsx
253
+ const [lang, setLang] = useState<string | undefined>();
254
+
255
+ <Select
256
+ items={[
257
+ { label: "TypeScript", value: "ts" },
258
+ { label: "JavaScript", value: "js" },
259
+ { label: "Rust", value: "rust" },
260
+ { label: "Go", value: "go" },
261
+ { label: "COBOL", value: "cobol", disabled: true },
262
+ ]}
263
+ value={lang}
264
+ onChange={setLang}
265
+ placeholder="Pick a language..."
266
+ maxVisible={6}
267
+ highlightColor="yellow"
268
+ />
269
+ ```
270
+
271
+ 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.
272
+
273
+ Props: `items`, `value`, `onChange`, `placeholder`, `maxVisible`, `highlightColor`, `searchable`, `style`, `focusedStyle`, `dropdownStyle`, `disabled`.
274
+
275
+ ### `<FocusScope>`
276
+
277
+ Focus trapping for modals and overlays.
278
+
279
+ ```tsx
280
+ <FocusScope trap>
281
+ <Input value={v} onChange={setV} />
282
+ <Button onPress={submit}>
283
+ <Text>OK</Text>
284
+ </Button>
285
+ </FocusScope>
286
+ ```
287
+
288
+ ### `<Portal>`
289
+
290
+ Renders children in a fullscreen absolute overlay. Useful for modals and dialogs.
291
+
292
+ ```tsx
293
+ <Portal>
294
+ <Box style={{ width: "100%", height: "100%", justifyContent: "center", alignItems: "center" }}>
295
+ <Box style={{ width: 40, border: "double", bg: "black", padding: 1 }}>
296
+ <Text>Modal content</Text>
297
+ </Box>
298
+ </Box>
299
+ </Portal>
300
+ ```
301
+
302
+ ### `<Keybind>`
303
+
304
+ Declarative keyboard shortcut. Renders nothing.
305
+
306
+ ```tsx
307
+ <Keybind keypress="ctrl+s" onPress={save} />
308
+ <Keybind keypress="escape" onPress={close} />
309
+ <Keybind keypress="q" onPress={() => exit()} />
310
+ ```
311
+
312
+ ### `<Progress>`
313
+
314
+ Determinate or indeterminate progress bar. Uses `useLayout` to measure actual width and renders block characters.
315
+
316
+ ```tsx
317
+ <Progress value={0.65} showPercent />
318
+ <Progress indeterminate label="Loading" />
319
+ ```
320
+
321
+ Props: `value` (0..1), `indeterminate`, `width`, `label`, `showPercent`, `filled`/`empty` (characters).
322
+
323
+ ### `<Spinner>`
324
+
325
+ Animated spinner with configurable frames. Cleans up timers on unmount.
326
+
327
+ ```tsx
328
+ <Spinner label="Loading..." style={{ color: "green" }} />
329
+ <Spinner frames={["|", "/", "-", "\\"]} intervalMs={100} />
330
+ ```
331
+
332
+ ### `<ToastHost>` + `useToast()`
333
+
334
+ Lightweight toast notifications rendered via Portal. Wrap your app in `<ToastHost>`, then push toasts from anywhere with `useToast()`.
335
+
336
+ ```tsx
337
+ function App() {
338
+ const toast = useToast();
339
+ return <Keybind keypress="t" onPress={() =>
340
+ toast({ message: "Saved!", variant: "success" })
341
+ } />;
342
+ }
343
+
344
+ render(<ToastHost position="top-right"><App /></ToastHost>);
345
+ ```
346
+
347
+ Variants: `"info"`, `"success"`, `"warning"`, `"error"`. Auto-dismiss after `durationMs` (default 3000).
348
+
349
+ ### `<Spacer>`
350
+
351
+ Flexible space filler. Pushes siblings apart.
352
+
353
+ ```tsx
354
+ <Box style={{ flexDirection: "row" }}>
355
+ <Text>Left</Text>
356
+ <Spacer />
357
+ <Text>Right</Text>
358
+ </Box>
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Hooks
364
+
365
+ ### `useInput(handler)`
366
+
367
+ Listen for all keyboard events.
368
+
369
+ ```tsx
370
+ useInput((key) => {
371
+ if (key.name === "escape") close();
372
+ if (key.ctrl && key.name === "s") save();
373
+ });
374
+ ```
375
+
376
+ ### `useFocus(nodeRef)`
377
+
378
+ Get focus state for a node.
379
+
380
+ ```tsx
381
+ const ref = useRef(null);
382
+ const { focused, focus } = useFocus(ref);
383
+
384
+ <Box ref={ref} focusable>
385
+ <Text style={focused ? { color: "cyan" } : {}}>
386
+ {focused ? "* focused *" : "not focused"}
387
+ </Text>
388
+ </Box>
389
+ ```
390
+
391
+ ### `useLayout(nodeRef)`
392
+
393
+ Subscribe to a node's computed layout.
394
+
395
+ ```tsx
396
+ const ref = useRef(null);
397
+ const layout = useLayout(ref);
398
+
399
+ // layout: { x, y, width, height, innerX, innerY, innerWidth, innerHeight }
400
+ ```
401
+
402
+ ### `useApp()`
403
+
404
+ Access app-level utilities.
405
+
406
+ ```tsx
407
+ const { exit, columns, rows } = useApp();
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Styling
413
+
414
+ 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.
415
+
416
+ ### Layout
417
+
418
+ | Property | Type | Description |
419
+ |----------|------|-------------|
420
+ | `width`, `height` | `number \| "${n}%"` | Dimensions |
421
+ | `minWidth`, `minHeight` | `number` | Minimum dimensions |
422
+ | `maxWidth`, `maxHeight` | `number` | Maximum dimensions |
423
+ | `padding` | `number` | Padding on all sides |
424
+ | `paddingX`, `paddingY` | `number` | Horizontal / vertical padding |
425
+ | `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` | `number` | Individual sides |
426
+ | `gap` | `number` | Gap between flex children |
427
+
428
+ ### Flexbox
429
+
430
+ | Property | Type | Default |
431
+ |----------|------|---------|
432
+ | `flexDirection` | `"row" \| "column"` | `"column"` |
433
+ | `flexWrap` | `"nowrap" \| "wrap"` | `"nowrap"` |
434
+ | `justifyContent` | `"flex-start" \| "center" \| "flex-end" \| "space-between" \| "space-around"` | `"flex-start"` |
435
+ | `alignItems` | `"flex-start" \| "center" \| "flex-end" \| "stretch"` | `"stretch"` |
436
+ | `flexGrow` | `number` | `0` |
437
+ | `flexShrink` | `number` | `0` |
438
+
439
+ ### Positioning
440
+
441
+ | Property | Type | Description |
442
+ |----------|------|-------------|
443
+ | `position` | `"relative" \| "absolute"` | Positioning mode |
444
+ | `top`, `right`, `bottom`, `left` | `number \| "${n}%"` | Offsets |
445
+ | `inset` | `number \| "${n}%"` | Shorthand for all four edges |
446
+ | `zIndex` | `number` | Stacking order |
447
+
448
+ ### Visual
449
+
450
+ | Property | Type | Description |
451
+ |----------|------|-------------|
452
+ | `bg` | `Color` | Background color |
453
+ | `border` | `"none" \| "single" \| "double" \| "round" \| "ascii"` | Border style |
454
+ | `borderColor` | `Color` | Border color |
455
+ | `clip` | `boolean` | Clip overflowing children |
456
+
457
+ ### Text
458
+
459
+ | Property | Type | Description |
460
+ |----------|------|-------------|
461
+ | `color` | `Color` | Text color |
462
+ | `bold` | `boolean` | Bold text |
463
+ | `dim` | `boolean` | Dimmed text |
464
+ | `italic` | `boolean` | Italic text |
465
+ | `underline` | `boolean` | Underlined text |
466
+ | `wrap` | `"wrap" \| "truncate" \| "ellipsis" \| "none"` | Text wrapping mode |
467
+ | `textAlign` | `"left" \| "center" \| "right"` | Text alignment |
468
+
469
+ ### Colors
470
+
471
+ Colors can be specified as:
472
+
473
+ - **Named:** `"red"`, `"green"`, `"blueBright"`, `"whiteBright"`, etc.
474
+ - **Hex:** `"#ff0000"`, `"#1a1a2e"`
475
+ - **RGB:** `{ r: 255, g: 0, b: 0 }`
476
+ - **256-palette:** `0`&ndash;`255`
477
+
478
+ Text on colored backgrounds automatically picks black or white for contrast when no explicit color is set.
479
+
480
+ ---
481
+
482
+ ## `render(element, options?)`
483
+
484
+ Mount a React element to the terminal.
485
+
486
+ ```tsx
487
+ const app = render(<App />, {
488
+ stdout: process.stdout,
489
+ stdin: process.stdin,
490
+ debug: false,
491
+ });
492
+
493
+ app.unmount(); // Tear down
494
+ app.exit(); // Unmount and exit process
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Examples
500
+
501
+ ```bash
502
+ # Clone and install
503
+ git clone <repo-url> && cd glyph
504
+ pnpm install && pnpm build
505
+
506
+ # Run examples
507
+ pnpm --filter basic-layout dev # Flexbox layout demo
508
+ pnpm --filter modal-input dev # Modal, input, focus trapping
509
+ pnpm --filter scrollview-demo dev # Scrollable content
510
+ pnpm --filter list-demo dev # Keyboard-navigable list
511
+ pnpm --filter menu-demo dev # Styled menu
512
+ pnpm --filter select-demo dev # Dropdown select with search
513
+ pnpm --filter forms-demo dev # Checkbox and Radio inputs
514
+ pnpm --filter dashboard dev # Full task manager (all components)
515
+ pnpm --filter showcase dev # Progress, Spinner, Toasts
516
+ ```
517
+
518
+ ---
519
+
520
+ ## Architecture
521
+
522
+ ```
523
+ src/
524
+ ├── reconciler/ React reconciler (host config + GlyphNode tree)
525
+ ├── layout/ Yoga-based flexbox + text measurement
526
+ ├── paint/ Framebuffer, character diffing, borders, colors
527
+ ├── runtime/ Terminal raw mode, key parsing, OSC handling
528
+ ├── components/ Box, Text, Input, Button, ScrollView, List, Menu, ...
529
+ ├── hooks/ useInput, useFocus, useLayout, useApp
530
+ └── render.ts Entry point tying it all together
531
+ ```
532
+
533
+ **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.
534
+
535
+ ---
536
+
537
+ ## License
538
+
539
+ MIT
540
+
541
+ ---
542
+
543
+ <p align="center">
544
+ <sub>Built with React &bull; Yoga &bull; a lot of ANSI escape codes</sub>
545
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nick-skriabin/glyph",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "A React renderer for terminal UIs with flexbox layout",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -18,7 +18,9 @@
18
18
  }
19
19
  }
20
20
  },
21
- "files": ["dist"],
21
+ "files": [
22
+ "dist"
23
+ ],
22
24
  "scripts": {
23
25
  "build": "tsup",
24
26
  "dev": "tsup --watch",
@@ -40,7 +42,13 @@
40
42
  "tsup": "^8",
41
43
  "typescript": "^5.5"
42
44
  },
43
- "keywords": ["react", "terminal", "tui", "cli", "flexbox"],
45
+ "keywords": [
46
+ "react",
47
+ "terminal",
48
+ "tui",
49
+ "cli",
50
+ "flexbox"
51
+ ],
44
52
  "license": "MIT",
45
53
  "repository": {
46
54
  "type": "git",