1ch 0.1.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.
- package/README.md +280 -0
- package/dist/index.d.ts +639 -0
- package/dist/index.js +3283 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +55 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# 1ch
|
|
2
|
+
|
|
3
|
+
**Terminal UI components for React.** For those who'd rather be in the terminal.
|
|
4
|
+
|
|
5
|
+
Every UI framework treats the browser as a canvas of infinite resolution. This one doesn't. The smallest unit is one character cell. A button is `[ OK ]`. A progress bar is `████░░░░`. A table border is `│` and `─` and `┼`.
|
|
6
|
+
|
|
7
|
+
The web primitives that make it work are almost embarrassingly simple: a monospace font, `white-space: pre`, and `ch` units. That's the entire rendering engine.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { TermUI, TBox, TVStack, TTable, TBar } from "1ch";
|
|
11
|
+
import "1ch/style.css";
|
|
12
|
+
|
|
13
|
+
function Dashboard() {
|
|
14
|
+
return (
|
|
15
|
+
<TermUI width={60}>
|
|
16
|
+
<TBox title="Server Status">
|
|
17
|
+
<TVStack gap={1}>
|
|
18
|
+
<TBar label="CPU" value={73} max={100} />
|
|
19
|
+
<TBar label="MEM" value={4.2} max={8} />
|
|
20
|
+
<TTable
|
|
21
|
+
columns={[
|
|
22
|
+
{ key: "service", header: "Service", width: 20 },
|
|
23
|
+
{ key: "status", header: "Status", width: 10 },
|
|
24
|
+
{ key: "uptime", header: "Uptime", width: 12 },
|
|
25
|
+
]}
|
|
26
|
+
data={[
|
|
27
|
+
{ service: "api-gateway", status: "UP", uptime: "14d 3h" },
|
|
28
|
+
{ service: "worker-pool", status: "UP", uptime: "14d 3h" },
|
|
29
|
+
{ service: "cache", status: "WARN", uptime: "2h 41m" },
|
|
30
|
+
]}
|
|
31
|
+
/>
|
|
32
|
+
</TVStack>
|
|
33
|
+
</TBox>
|
|
34
|
+
</TermUI>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
- **Dense.** Every character is content or structure. Nothing is decorative. You can always tell where one element ends and another begins because the grid enforces it.
|
|
42
|
+
- **React-native.** Not a wrapper around a terminal emulator. Custom React reconciler that turns a component tree into character grids. React handles lifecycle, hooks, state. The reconciler handles layout.
|
|
43
|
+
- **Responsive.** All layout is lazy - functions of width, not pre-computed blocks. Resize the container and everything reflows.
|
|
44
|
+
- **Themeable.** Full token system: palette, semantic, syntax, markdown, component colors. Light/dark/system. Runtime-switchable. End-user JSON theming with validation.
|
|
45
|
+
- **Three input formats.** Render React components, or feed it markdown, HTML, or JSON and get terminal layouts back.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install 1ch react react-dom
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Components
|
|
54
|
+
|
|
55
|
+
React components that render through a custom reconciler. No DOM elements - they produce `LayoutFn` internally.
|
|
56
|
+
|
|
57
|
+
### Layout
|
|
58
|
+
|
|
59
|
+
| Component | What it does |
|
|
60
|
+
| ------------ | ---------------------------------------------------- |
|
|
61
|
+
| `TVStack` | Vertical stack with optional `gap` |
|
|
62
|
+
| `THStack` | Horizontal stack with optional `gap` and `widths` |
|
|
63
|
+
| `TBox` | Bordered box with optional `title` and `borderColor` |
|
|
64
|
+
| `TSeparator` | Horizontal line |
|
|
65
|
+
| `TBlank` | Empty line |
|
|
66
|
+
| `TLine` | Single-line container |
|
|
67
|
+
| `TSpan` | Inline styled text |
|
|
68
|
+
|
|
69
|
+
### Data
|
|
70
|
+
|
|
71
|
+
| Component | What it does |
|
|
72
|
+
| --------- | ---------------------------------------------------------- |
|
|
73
|
+
| `TTable` | Bordered table with column widths, wrapping, header colors |
|
|
74
|
+
| `TTree` | Nested tree view with `├─` and `└─` branch characters |
|
|
75
|
+
| `TList` | Bulleted or numbered list |
|
|
76
|
+
| `TCode` | Syntax-highlighted code block in a box |
|
|
77
|
+
| `TDiff` | Colored diff output |
|
|
78
|
+
| `TBar` | Progress bar (`████░░░░`) |
|
|
79
|
+
| `TSpark` | Sparkline (`▁▂▃▄▅▆▇█`) |
|
|
80
|
+
|
|
81
|
+
### Interactive
|
|
82
|
+
|
|
83
|
+
| Component | What it does |
|
|
84
|
+
| ------------ | ---------------------------------- |
|
|
85
|
+
| `TTabs` | Clickable tab bar with `onSelect` |
|
|
86
|
+
| `TButton` | Clickable styled button |
|
|
87
|
+
| `TStatusbar` | Footer bar with left/right content |
|
|
88
|
+
|
|
89
|
+
### Documents
|
|
90
|
+
|
|
91
|
+
| Component | What it does |
|
|
92
|
+
| ----------- | ------------------------------------------ |
|
|
93
|
+
| `TMarkdown` | Renders markdown string as terminal layout |
|
|
94
|
+
| `THtml` | Renders HTML string as terminal layout |
|
|
95
|
+
| `TJson` | Renders structured JSON as terminal layout |
|
|
96
|
+
|
|
97
|
+
## Imperative API
|
|
98
|
+
|
|
99
|
+
If you don't want React components, every layout primitive has a function equivalent that returns a `LayoutFn`:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { box, vstack, table, code, bar, hstack } from "1ch";
|
|
103
|
+
|
|
104
|
+
const layout = vstack(
|
|
105
|
+
box(
|
|
106
|
+
vstack(
|
|
107
|
+
bar(73, 100, 40),
|
|
108
|
+
table(
|
|
109
|
+
[
|
|
110
|
+
{ key: "name", header: "Name", width: 20 },
|
|
111
|
+
{ key: "value", header: "Value", width: 10 },
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
{ name: "requests", value: "1.2k/s" },
|
|
115
|
+
{ name: "errors", value: "0.03%" },
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
),
|
|
119
|
+
{ title: "Metrics" }
|
|
120
|
+
),
|
|
121
|
+
code(`console.log("hello")`, { language: "javascript" })
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Materialize at any width
|
|
125
|
+
const block: Block = layout(80);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Available builders
|
|
129
|
+
|
|
130
|
+
**Layout:** `box`, `hstack`, `vstack`, `separator`, `blank`, `lines`
|
|
131
|
+
|
|
132
|
+
**Data:** `table`, `tree`, `list`, `code`, `diff`
|
|
133
|
+
|
|
134
|
+
**Widgets:** `tabs`, `statusbar`, `badge`, `bar`, `spark`
|
|
135
|
+
|
|
136
|
+
**Text:** `pad`, `hl`, `padLine`
|
|
137
|
+
|
|
138
|
+
## Document Pipeline
|
|
139
|
+
|
|
140
|
+
Feed it markdown, HTML, or JSON. Get terminal layouts back.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { TermDocument } from "1ch";
|
|
144
|
+
|
|
145
|
+
// Markdown
|
|
146
|
+
<TermDocument
|
|
147
|
+
source={{ format: "markdown", value: "# Hello\n\nSome **bold** text." }}
|
|
148
|
+
width={60}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
// HTML
|
|
152
|
+
<TermDocument
|
|
153
|
+
source={{ format: "html", value: "<h1>Hello</h1><p>Some text.</p>" }}
|
|
154
|
+
width={60}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
// Or use the functions directly
|
|
158
|
+
import { fromMarkdown, fromHtml, fromJson } from "1ch";
|
|
159
|
+
|
|
160
|
+
const layout = fromMarkdown("# Hello\n\n- one\n- two", { theme });
|
|
161
|
+
const block = layout(80);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### HTML support
|
|
165
|
+
|
|
166
|
+
Supported tags: `h1`-`h6`, `p`, `ul`, `ol`, `li`, `table` (with `thead`/`tbody`/`tr`/`th`/`td`), `blockquote`, `hr`, `pre`, `div`, `section`, `article`, `main`, `header`, `footer`, `nav`.
|
|
167
|
+
|
|
168
|
+
`<term>` acts as an optional root - if present, only its subtree renders.
|
|
169
|
+
|
|
170
|
+
Color attributes: `color`, `data-color`, `marker-color`, `header-color`, `border-color`, `text-color`. Values can be theme keys (`yellow`, `cyan`) or CSS colors (`#56b6c2`).
|
|
171
|
+
|
|
172
|
+
## Theme System
|
|
173
|
+
|
|
174
|
+
Two built-in themes (dark and light), runtime-switchable, fully customizable via JSON.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import {
|
|
178
|
+
TermThemeProvider,
|
|
179
|
+
TermUI,
|
|
180
|
+
parseThemeSpec,
|
|
181
|
+
defaultThemeSpec,
|
|
182
|
+
} from "1ch";
|
|
183
|
+
|
|
184
|
+
const parsed = parseThemeSpec(userThemeJson);
|
|
185
|
+
const spec = parsed.ok ? parsed.theme : defaultThemeSpec;
|
|
186
|
+
|
|
187
|
+
function App() {
|
|
188
|
+
return (
|
|
189
|
+
<TermThemeProvider initialTheme={spec} initialMode="system">
|
|
190
|
+
<TermUI width={80}>{/* components inherit the theme */}</TermUI>
|
|
191
|
+
</TermThemeProvider>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
A `ThemeSpec` defines both light and dark modes. Each mode has:
|
|
197
|
+
|
|
198
|
+
- **Palette** - 10 ANSI-like colors (black, red, green, yellow, blue, magenta, cyan, white, brightBlack, brightWhite)
|
|
199
|
+
- **Semantic** - UI intent (border, info, success, warn, danger, frameBg, frameFg, etc.)
|
|
200
|
+
- **Syntax** - Code highlighting (keyword, string, number, comment, function, type, punctuation)
|
|
201
|
+
- **Markdown** - Document rendering (headings h1-h6, paragraph, lists, tables, quotes, code)
|
|
202
|
+
- **Components** - Widget colors (tabs, statusbar, badge, button)
|
|
203
|
+
|
|
204
|
+
Use `validateThemeSpec()` to check user-provided JSON before applying it.
|
|
205
|
+
|
|
206
|
+
## Hooks
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { useSpinner, useTick, useTermWidth, useStreamingText } from "1ch";
|
|
210
|
+
|
|
211
|
+
// Animated braille spinner
|
|
212
|
+
const spinner = useSpinner(80);
|
|
213
|
+
|
|
214
|
+
// Interval counter for animations
|
|
215
|
+
const tick = useTick(500);
|
|
216
|
+
|
|
217
|
+
// Measure container width in character units (uses ResizeObserver)
|
|
218
|
+
const ref = useRef(null);
|
|
219
|
+
const width = useTermWidth(ref, 80);
|
|
220
|
+
|
|
221
|
+
// Streaming text with lerp (detects appends, doesn't restart)
|
|
222
|
+
const displayed = useStreamingText(streamingText, 0.08);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Putting It Together
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { TermUI, TBox, TCode, TTabs, TTab, TTable, TBar, TVStack } from "1ch";
|
|
229
|
+
import "1ch/style.css";
|
|
230
|
+
|
|
231
|
+
function App() {
|
|
232
|
+
const [tab, setTab] = useState(0);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<TermUI width={80}>
|
|
236
|
+
<TTabs active={tab} onSelect={setTab}>
|
|
237
|
+
<TTab name="Overview">
|
|
238
|
+
<TBox title="Status">
|
|
239
|
+
<TVStack>
|
|
240
|
+
<TBar label="CPU" value={73} max={100} />
|
|
241
|
+
<TBar label="MEM" value={4.2} max={8} />
|
|
242
|
+
</TVStack>
|
|
243
|
+
</TBox>
|
|
244
|
+
</TTab>
|
|
245
|
+
|
|
246
|
+
<TTab name="Logs">
|
|
247
|
+
<TTable
|
|
248
|
+
columns={[
|
|
249
|
+
{ key: "time", header: "Time", width: 12 },
|
|
250
|
+
{ key: "level", header: "Level", width: 8 },
|
|
251
|
+
{ key: "message", header: "Message", width: 40 },
|
|
252
|
+
]}
|
|
253
|
+
data={[
|
|
254
|
+
{ time: "14:03:21", level: "INFO", message: "Server started on :3000" },
|
|
255
|
+
{ time: "14:03:22", level: "INFO", message: "Connected to database" },
|
|
256
|
+
{ time: "14:05:01", level: "WARN", message: "Slow query: 2.3s" },
|
|
257
|
+
]}
|
|
258
|
+
/>
|
|
259
|
+
</TTab>
|
|
260
|
+
|
|
261
|
+
<TTab name="Config">
|
|
262
|
+
<TCode
|
|
263
|
+
code={`export default {
|
|
264
|
+
port: 3000,
|
|
265
|
+
database: "postgres://localhost/app",
|
|
266
|
+
cache: { ttl: 300, max: 1000 },
|
|
267
|
+
};`}
|
|
268
|
+
language="typescript"
|
|
269
|
+
title="config.ts"
|
|
270
|
+
/>
|
|
271
|
+
</TTab>
|
|
272
|
+
</TTabs>
|
|
273
|
+
</TermUI>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|