1ch 0.1.0 → 0.2.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 +87 -185
- package/dist/index.d.ts +17 -3
- package/dist/index.js +46 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
**Terminal UI components for React.** For those who'd rather be in the terminal.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The smallest unit is one character cell. A button is `[ OK ]`. A progress bar is `████░░░░`. A table border is `│` and `─` and `┼`. The whole thing is a grid.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install 1ch react react-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it looks like
|
|
8
14
|
|
|
9
15
|
```tsx
|
|
10
16
|
import { TermUI, TBox, TVStack, TTable, TBar } from "1ch";
|
|
@@ -19,9 +25,9 @@ function Dashboard() {
|
|
|
19
25
|
<TBar label="MEM" value={4.2} max={8} />
|
|
20
26
|
<TTable
|
|
21
27
|
columns={[
|
|
22
|
-
{ key: "service",
|
|
23
|
-
{ key: "status",
|
|
24
|
-
{ key: "uptime",
|
|
28
|
+
{ key: "service", width: 20 },
|
|
29
|
+
{ key: "status", width: 10 },
|
|
30
|
+
{ key: "uptime", width: 12 },
|
|
25
31
|
]}
|
|
26
32
|
data={[
|
|
27
33
|
{ service: "api-gateway", status: "UP", uptime: "14d 3h" },
|
|
@@ -36,70 +42,93 @@ function Dashboard() {
|
|
|
36
42
|
}
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
<!-- TODO: Add screenshot of the above rendering -->
|
|
40
46
|
|
|
41
|
-
|
|
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.
|
|
47
|
+
## Why this exists
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
Most UI components are styled `<div>`s pretending to be something else. Terminal UIs skip the pretense - every character is either content or structure, and the grid makes boundaries obvious.
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
npm install 1ch react react-dom
|
|
51
|
-
```
|
|
51
|
+
Under the hood, 1ch uses a custom React reconciler that turns your component tree into character grids. React handles lifecycle, hooks, state. The reconciler handles layout. Resize the container and everything reflows.
|
|
52
52
|
|
|
53
53
|
## Components
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
**Layout** - `TVStack`, `THStack`, `TBox`, `TSeparator`, `TBlank`, `TLine`, `TSpan`
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
**Data** - `TTable`, `TTree`, `TList`, `TCode`, `TDiff`, `TBar`, `TSpark`
|
|
58
58
|
|
|
59
|
-
|
|
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 |
|
|
59
|
+
**Interactive** - `TTabs`, `TButton`, `TStatusbar`
|
|
68
60
|
|
|
69
|
-
|
|
61
|
+
**Documents** - `TMarkdown`, `THtml`, `TJson`
|
|
70
62
|
|
|
71
|
-
|
|
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 (`▁▂▃▄▅▆▇█`) |
|
|
63
|
+
The document components accept raw strings. Feed `TMarkdown` a markdown string, `THtml` an HTML string, or `TJson` a JSON object, and you get terminal-styled output back.
|
|
80
64
|
|
|
81
|
-
|
|
65
|
+
## A more complete example
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
| `TButton` | Clickable styled button |
|
|
87
|
-
| `TStatusbar` | Footer bar with left/right content |
|
|
67
|
+
```tsx
|
|
68
|
+
import { TermUI, TBox, TCode, TTabs, TTab, TTable, TBar, TVStack } from "1ch";
|
|
69
|
+
import "1ch/style.css";
|
|
88
70
|
|
|
89
|
-
|
|
71
|
+
function App() {
|
|
72
|
+
const [tab, setTab] = useState(0);
|
|
90
73
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
return (
|
|
75
|
+
<TermUI width={80}>
|
|
76
|
+
<TTabs active={tab} onSelect={setTab}>
|
|
77
|
+
<TTab name="Overview">
|
|
78
|
+
<TBox title="Status">
|
|
79
|
+
<TVStack>
|
|
80
|
+
<TBar label="CPU" value={73} max={100} />
|
|
81
|
+
<TBar label="MEM" value={4.2} max={8} />
|
|
82
|
+
</TVStack>
|
|
83
|
+
</TBox>
|
|
84
|
+
</TTab>
|
|
96
85
|
|
|
97
|
-
|
|
86
|
+
<TTab name="Logs">
|
|
87
|
+
<TTable
|
|
88
|
+
columns={[
|
|
89
|
+
{ key: "time", width: 12 },
|
|
90
|
+
{ key: "level", width: 8 },
|
|
91
|
+
{ key: "message", width: 40 },
|
|
92
|
+
]}
|
|
93
|
+
data={[
|
|
94
|
+
{
|
|
95
|
+
time: "14:03:21",
|
|
96
|
+
level: "INFO",
|
|
97
|
+
message: "Server started on :3000",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
time: "14:03:22",
|
|
101
|
+
level: "INFO",
|
|
102
|
+
message: "Connected to database",
|
|
103
|
+
},
|
|
104
|
+
{ time: "14:05:01", level: "WARN", message: "Slow query: 2.3s" },
|
|
105
|
+
]}
|
|
106
|
+
/>
|
|
107
|
+
</TTab>
|
|
98
108
|
|
|
99
|
-
|
|
109
|
+
<TTab name="Config">
|
|
110
|
+
<TCode
|
|
111
|
+
code={`export default {
|
|
112
|
+
port: 3000,
|
|
113
|
+
database: "postgres://localhost/app",
|
|
114
|
+
cache: { ttl: 300, max: 1000 },
|
|
115
|
+
};`}
|
|
116
|
+
language="typescript"
|
|
117
|
+
title="config.ts"
|
|
118
|
+
/>
|
|
119
|
+
</TTab>
|
|
120
|
+
</TTabs>
|
|
121
|
+
</TermUI>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Don't want React? Skip it.
|
|
127
|
+
|
|
128
|
+
Every component has an imperative equivalent that returns a layout function. Call it with a width and get a character grid back.
|
|
100
129
|
|
|
101
130
|
```typescript
|
|
102
|
-
import { box, vstack, table, code, bar
|
|
131
|
+
import { box, vstack, table, code, bar } from "1ch";
|
|
103
132
|
|
|
104
133
|
const layout = vstack(
|
|
105
134
|
box(
|
|
@@ -121,57 +150,12 @@ const layout = vstack(
|
|
|
121
150
|
code(`console.log("hello")`, { language: "javascript" })
|
|
122
151
|
);
|
|
123
152
|
|
|
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
153
|
const block = layout(80);
|
|
162
154
|
```
|
|
163
155
|
|
|
164
|
-
|
|
156
|
+
## Theming
|
|
165
157
|
|
|
166
|
-
|
|
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.
|
|
158
|
+
Dark and light themes built in, and are switchable at runtime. You can override everything via JSON - palette, semantic colors, syntax highlighting, markdown rendering, component tokens. Use `parseThemeSpec()` to check the JSON before applying it.
|
|
175
159
|
|
|
176
160
|
```tsx
|
|
177
161
|
import {
|
|
@@ -184,96 +168,14 @@ import {
|
|
|
184
168
|
const parsed = parseThemeSpec(userThemeJson);
|
|
185
169
|
const spec = parsed.ok ? parsed.theme : defaultThemeSpec;
|
|
186
170
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
171
|
+
<TermThemeProvider initialTheme={spec} initialMode="system">
|
|
172
|
+
<TermUI width={80}>{/* components inherit the theme */}</TermUI>
|
|
173
|
+
</TermThemeProvider>;
|
|
223
174
|
```
|
|
224
175
|
|
|
225
|
-
##
|
|
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>
|
|
176
|
+
## Docs
|
|
260
177
|
|
|
261
|
-
|
|
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
|
-
```
|
|
178
|
+
Full API reference, theme spec details, hooks (`useSpinner`, `useTick`, `useTermWidth`, `useStreamingText`), and the document pipeline - all in the [docs](1ch.app) (coming soon).
|
|
277
179
|
|
|
278
180
|
## License
|
|
279
181
|
|
package/dist/index.d.ts
CHANGED
|
@@ -131,10 +131,16 @@ declare function bar(val: number, max: number, w: number): string;
|
|
|
131
131
|
declare function spark(data: number[], w: number): string;
|
|
132
132
|
declare function padLine(line: Line, w: number): Line;
|
|
133
133
|
|
|
134
|
+
type BoxAction = {
|
|
135
|
+
label: string;
|
|
136
|
+
onClick: () => void;
|
|
137
|
+
color?: string;
|
|
138
|
+
};
|
|
134
139
|
declare function lines(content: Line[] | ((w: number) => Line[])): LayoutFn;
|
|
135
140
|
declare function box(content: LayoutFn, opts?: {
|
|
136
141
|
title?: string;
|
|
137
142
|
borderColor?: string;
|
|
143
|
+
action?: BoxAction;
|
|
138
144
|
}): LayoutFn;
|
|
139
145
|
declare function hstack(children: LayoutFn[], opts?: {
|
|
140
146
|
gap?: number;
|
|
@@ -263,6 +269,8 @@ declare function code(source: string, opts?: {
|
|
|
263
269
|
title?: string;
|
|
264
270
|
borderColor?: string;
|
|
265
271
|
theme?: Theme;
|
|
272
|
+
copyable?: boolean;
|
|
273
|
+
action?: BoxAction;
|
|
266
274
|
}): LayoutFn;
|
|
267
275
|
declare function diff(value: string, opts?: {
|
|
268
276
|
addColor?: string;
|
|
@@ -397,6 +405,7 @@ type TBoxProps = BaseProps & {
|
|
|
397
405
|
title?: string;
|
|
398
406
|
borderColor?: string;
|
|
399
407
|
gap?: number;
|
|
408
|
+
action?: BoxAction;
|
|
400
409
|
};
|
|
401
410
|
type TSeparatorProps = {
|
|
402
411
|
color?: string;
|
|
@@ -478,6 +487,8 @@ type TCodeProps = {
|
|
|
478
487
|
language?: string;
|
|
479
488
|
title?: string;
|
|
480
489
|
borderColor?: string;
|
|
490
|
+
action?: BoxAction;
|
|
491
|
+
copyable?: boolean;
|
|
481
492
|
};
|
|
482
493
|
type TDiffProps = {
|
|
483
494
|
value: string;
|
|
@@ -520,10 +531,11 @@ declare function THStack({ children, gap, widths }: THStackProps): react.ReactEl
|
|
|
520
531
|
gap: number | undefined;
|
|
521
532
|
widths: number[] | undefined;
|
|
522
533
|
}, string | react.JSXElementConstructor<any>>;
|
|
523
|
-
declare function TBox({ children, title, borderColor, gap }: TBoxProps): react.ReactElement<{
|
|
534
|
+
declare function TBox({ children, title, borderColor, gap, action }: TBoxProps): react.ReactElement<{
|
|
524
535
|
title: string | undefined;
|
|
525
536
|
borderColor: string | undefined;
|
|
526
537
|
gap: number | undefined;
|
|
538
|
+
action: BoxAction | undefined;
|
|
527
539
|
}, string | react.JSXElementConstructor<any>>;
|
|
528
540
|
declare function TSeparator({ color }: TSeparatorProps): react.ReactElement<{
|
|
529
541
|
color: string | undefined;
|
|
@@ -595,11 +607,13 @@ declare function TTree({ data, color, branchColor, leafColor }: TTreeProps): rea
|
|
|
595
607
|
branchColor: string | undefined;
|
|
596
608
|
leafColor: string | undefined;
|
|
597
609
|
}, string | react.JSXElementConstructor<any>>;
|
|
598
|
-
declare function TCode({ code, language, title, borderColor }: TCodeProps): react.ReactElement<{
|
|
610
|
+
declare function TCode({ code, language, title, borderColor, action, copyable }: TCodeProps): react.ReactElement<{
|
|
599
611
|
code: string;
|
|
600
612
|
language: string | undefined;
|
|
601
613
|
title: string | undefined;
|
|
602
614
|
borderColor: string | undefined;
|
|
615
|
+
action: BoxAction | undefined;
|
|
616
|
+
copyable: boolean | undefined;
|
|
603
617
|
}, string | react.JSXElementConstructor<any>>;
|
|
604
618
|
declare function TDiff({ value, addColor, removeColor, metaColor }: TDiffProps): react.ReactElement<{
|
|
605
619
|
value: string;
|
|
@@ -636,4 +650,4 @@ declare function useTick(ms?: number): number;
|
|
|
636
650
|
declare function useTermWidth(ref: RefObject<HTMLElement | null>, fallback?: number): number;
|
|
637
651
|
declare function useStreamingText(text: string, lerp?: number): string;
|
|
638
652
|
|
|
639
|
-
export { type Block, type Column, type DocumentSource, type JsonNode, type LayoutFn, type Line, type MarkdownThemeOverrides, type RenderTermOptions, type Segment, type Style, TBar, type TBarProps, TBlank, TBox, type TBoxProps, TButton, type TButtonProps, TCode, type TCodeProps, TDiff, type TDiffProps, THStack, type THStackProps, THtml, type THtmlProps, TJson, type TJsonProps, TLayout, type TLayoutProps, TLine, type TLineProps, TList, type TListProps, TMarkdown, type TMarkdownProps, TRaw, type TRawProps, TSeparator, type TSeparatorProps, TSpan, type TSpanProps, TSpark, type TSparkProps, TStatusbar, type TStatusbarProps, TTab, type TTabProps, TTable, type TTableProps, TTabs, type TTabsProps, TTree, type TTreeProps, TVStack, type TVStackProps, TermDocument, type TermDocumentProps, type TermThemeContextValue, TermThemeProvider, type TermThemeProviderProps, TermUI, type TermUIProps, type Theme, type ThemeComponents, type ThemeMarkdown, type ThemeMode, type ThemeModeTokens, type ThemePalette, type ThemePreference, type ThemeSemantic, type ThemeSpec, type ThemeSyntax, type ThemeValidationIssue, type ThemeValidationResult, type Token, type TokenType, type TreeNode, badge, bar, blank, box, builtinThemes, code, dark, defaultThemeSpec, diff, fromDocument, fromHtml, fromJson, fromJsonNode, fromMarkdown, hl, hstack, isTableLine, light, lines, list, pad, padLine, parseThemeSpec, preserveTableTag, renderTermReact, resolveTheme, resolveThemeMode, separator, spark, statusbar, table, tabs, themeToCssVars, tokenColor, tokenize, tree, useSpinner, useStreamingText, useTermTheme, useTermWidth, useTick, validateThemeSpec, vstack };
|
|
653
|
+
export { type Block, type BoxAction, type Column, type DocumentSource, type JsonNode, type LayoutFn, type Line, type MarkdownThemeOverrides, type RenderTermOptions, type Segment, type Style, TBar, type TBarProps, TBlank, TBox, type TBoxProps, TButton, type TButtonProps, TCode, type TCodeProps, TDiff, type TDiffProps, THStack, type THStackProps, THtml, type THtmlProps, TJson, type TJsonProps, TLayout, type TLayoutProps, TLine, type TLineProps, TList, type TListProps, TMarkdown, type TMarkdownProps, TRaw, type TRawProps, TSeparator, type TSeparatorProps, TSpan, type TSpanProps, TSpark, type TSparkProps, TStatusbar, type TStatusbarProps, TTab, type TTabProps, TTable, type TTableProps, TTabs, type TTabsProps, TTree, type TTreeProps, TVStack, type TVStackProps, TermDocument, type TermDocumentProps, type TermThemeContextValue, TermThemeProvider, type TermThemeProviderProps, TermUI, type TermUIProps, type Theme, type ThemeComponents, type ThemeMarkdown, type ThemeMode, type ThemeModeTokens, type ThemePalette, type ThemePreference, type ThemeSemantic, type ThemeSpec, type ThemeSyntax, type ThemeValidationIssue, type ThemeValidationResult, type Token, type TokenType, type TreeNode, badge, bar, blank, box, builtinThemes, code, dark, defaultThemeSpec, diff, fromDocument, fromHtml, fromJson, fromJsonNode, fromMarkdown, hl, hstack, isTableLine, light, lines, list, pad, padLine, parseThemeSpec, preserveTableTag, renderTermReact, resolveTheme, resolveThemeMode, separator, spark, statusbar, table, tabs, themeToCssVars, tokenColor, tokenize, tree, useSpinner, useStreamingText, useTermTheme, useTermWidth, useTick, validateThemeSpec, vstack };
|
package/dist/index.js
CHANGED
|
@@ -113,20 +113,27 @@ function wrapText(text, w) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// src/termui/layout.ts
|
|
116
|
-
function borderTop(w,
|
|
116
|
+
function borderTop(w, opts) {
|
|
117
117
|
const width = normalizedWidth(w);
|
|
118
118
|
if (width <= 0) return [];
|
|
119
|
-
if (width === 1) return [{ text: "\u250C", color, noSelect: true }];
|
|
119
|
+
if (width === 1) return [{ text: "\u250C", color: opts?.color, noSelect: true }];
|
|
120
|
+
const { title, color, action } = opts ?? {};
|
|
121
|
+
const innerWidth = width - 2;
|
|
122
|
+
const titleBudget = innerWidth - (action ? action.label.length + 2 : 0) - 4;
|
|
123
|
+
let titleStr = "";
|
|
120
124
|
if (title) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const titleStr = "\u2500 " + safeTitle + " ";
|
|
124
|
-
const remaining = Math.max(0, width - 2 - titleStr.length);
|
|
125
|
-
return [
|
|
126
|
-
{ text: "\u250C" + titleStr + hl(remaining) + "\u2510", color, noSelect: true }
|
|
127
|
-
];
|
|
125
|
+
const safeTitle = title.length > titleBudget ? title.slice(0, Math.max(0, titleBudget)) : title;
|
|
126
|
+
titleStr = safeTitle.length > 0 ? "\u2500 " + safeTitle + " " : "";
|
|
128
127
|
}
|
|
129
|
-
|
|
128
|
+
const fillWidth = Math.max(0, innerWidth - titleStr.length - (action ? action.label.length + 2 : 0));
|
|
129
|
+
if (!action) {
|
|
130
|
+
return [{ text: "\u250C" + titleStr + hl(fillWidth) + "\u2510", color, noSelect: true }];
|
|
131
|
+
}
|
|
132
|
+
return [
|
|
133
|
+
{ text: "\u250C" + titleStr + hl(fillWidth), color, noSelect: true },
|
|
134
|
+
{ text: " " + action.label + " ", color: action.color ?? color, onClick: action.onClick, noSelect: true },
|
|
135
|
+
{ text: "\u2510", color, noSelect: true }
|
|
136
|
+
];
|
|
130
137
|
}
|
|
131
138
|
function borderBottom(w, color) {
|
|
132
139
|
const width = normalizedWidth(w);
|
|
@@ -148,7 +155,7 @@ function box(content, opts) {
|
|
|
148
155
|
const inner = width - 2;
|
|
149
156
|
const contentBlock = content(inner);
|
|
150
157
|
const result = [];
|
|
151
|
-
result.push(borderTop(width, opts?.title, bc));
|
|
158
|
+
result.push(borderTop(width, { title: opts?.title, color: bc, action: opts?.action }));
|
|
152
159
|
for (const line of contentBlock) {
|
|
153
160
|
result.push([
|
|
154
161
|
{ text: "\u2502", color: bc, noSelect: true },
|
|
@@ -1023,6 +1030,7 @@ function tokenColor(type, theme) {
|
|
|
1023
1030
|
function code(source, opts) {
|
|
1024
1031
|
const t = opts?.theme ?? dark;
|
|
1025
1032
|
const tokens = tokenize(source, opts?.language);
|
|
1033
|
+
const copyAction = opts?.copyable !== false ? { label: "cp", onClick: () => navigator.clipboard.writeText(source), color: t.semantic.frameMuted } : void 0;
|
|
1026
1034
|
return box(
|
|
1027
1035
|
lines(
|
|
1028
1036
|
tokens.length > 0 ? tokens.map(
|
|
@@ -1031,7 +1039,8 @@ function code(source, opts) {
|
|
|
1031
1039
|
),
|
|
1032
1040
|
{
|
|
1033
1041
|
title: opts?.title ?? (opts?.language ? `code:${opts.language}` : "code"),
|
|
1034
|
-
borderColor: opts?.borderColor ?? t.markdown.codeBorder
|
|
1042
|
+
borderColor: opts?.borderColor ?? t.markdown.codeBorder,
|
|
1043
|
+
action: opts?.action ?? copyAction
|
|
1035
1044
|
}
|
|
1036
1045
|
);
|
|
1037
1046
|
}
|
|
@@ -1386,16 +1395,23 @@ function renderMarkdownBlock(block, theme, overrides) {
|
|
|
1386
1395
|
};
|
|
1387
1396
|
case "separator":
|
|
1388
1397
|
return separator(overrides?.separator ?? md.separator);
|
|
1389
|
-
case "code":
|
|
1398
|
+
case "code": {
|
|
1399
|
+
const codeSource = block.lines.join("\n");
|
|
1390
1400
|
return box(
|
|
1391
1401
|
lines(
|
|
1392
1402
|
block.lines.length > 0 ? block.lines.map((line) => [{ text: line || " ", color: overrides?.codeText ?? md.codeText }]) : [[{ text: " ", color: overrides?.codeEmpty ?? md.codeEmpty, dim: true }]]
|
|
1393
1403
|
),
|
|
1394
1404
|
{
|
|
1395
1405
|
title: block.language ? `code:${block.language}` : "code",
|
|
1396
|
-
borderColor: overrides?.codeBorder ?? md.codeBorder
|
|
1406
|
+
borderColor: overrides?.codeBorder ?? md.codeBorder,
|
|
1407
|
+
action: {
|
|
1408
|
+
label: "cp",
|
|
1409
|
+
onClick: () => navigator.clipboard.writeText(codeSource),
|
|
1410
|
+
color: theme.semantic.frameMuted
|
|
1411
|
+
}
|
|
1397
1412
|
}
|
|
1398
1413
|
);
|
|
1414
|
+
}
|
|
1399
1415
|
}
|
|
1400
1416
|
}
|
|
1401
1417
|
function fromMarkdown(markdown, opts) {
|
|
@@ -2068,6 +2084,13 @@ function asRecordArray(value) {
|
|
|
2068
2084
|
}
|
|
2069
2085
|
return out;
|
|
2070
2086
|
}
|
|
2087
|
+
function asBoxAction(value) {
|
|
2088
|
+
if (!isRecord4(value)) return void 0;
|
|
2089
|
+
const label = asString(value.label);
|
|
2090
|
+
const onClick = typeof value.onClick === "function" ? value.onClick : void 0;
|
|
2091
|
+
if (!label || !onClick) return void 0;
|
|
2092
|
+
return { label, onClick, color: asString(value.color) };
|
|
2093
|
+
}
|
|
2071
2094
|
function isMarkdownOverrides(value) {
|
|
2072
2095
|
if (!isRecord4(value)) return false;
|
|
2073
2096
|
if (value.headingMarker !== void 0 && typeof value.headingMarker !== "string") return false;
|
|
@@ -2368,7 +2391,8 @@ function compileTermNode(node, theme, mdOverrides) {
|
|
|
2368
2391
|
const gap = asNumber(p.gap) ?? 0;
|
|
2369
2392
|
return box(stackWithGap(ch, gap), {
|
|
2370
2393
|
title: asString(p.title),
|
|
2371
|
-
borderColor: asString(p.borderColor) ?? theme.semantic.border
|
|
2394
|
+
borderColor: asString(p.borderColor) ?? theme.semantic.border,
|
|
2395
|
+
action: asBoxAction(p.action)
|
|
2372
2396
|
});
|
|
2373
2397
|
}
|
|
2374
2398
|
case "t-separator":
|
|
@@ -2447,7 +2471,9 @@ function compileTermNode(node, theme, mdOverrides) {
|
|
|
2447
2471
|
language: asString(p.language),
|
|
2448
2472
|
title: asString(p.title),
|
|
2449
2473
|
borderColor: asString(p.borderColor) ?? theme.markdown.codeBorder,
|
|
2450
|
-
theme
|
|
2474
|
+
theme,
|
|
2475
|
+
copyable: asBoolean(p.copyable),
|
|
2476
|
+
action: asBoxAction(p.action)
|
|
2451
2477
|
});
|
|
2452
2478
|
case "t-diff":
|
|
2453
2479
|
return diff(asString(p.value) ?? "", {
|
|
@@ -3021,8 +3047,8 @@ function TVStack({ children, gap }) {
|
|
|
3021
3047
|
function THStack({ children, gap, widths }) {
|
|
3022
3048
|
return createElement("t-hstack", { gap, widths }, children);
|
|
3023
3049
|
}
|
|
3024
|
-
function TBox({ children, title, borderColor, gap }) {
|
|
3025
|
-
return createElement("t-box", { title, borderColor, gap }, children);
|
|
3050
|
+
function TBox({ children, title, borderColor, gap, action }) {
|
|
3051
|
+
return createElement("t-box", { title, borderColor, gap, action }, children);
|
|
3026
3052
|
}
|
|
3027
3053
|
function TSeparator({ color }) {
|
|
3028
3054
|
return createElement("t-separator", { color });
|
|
@@ -3103,8 +3129,8 @@ function TLayout({ layout }) {
|
|
|
3103
3129
|
function TTree({ data, color, branchColor, leafColor }) {
|
|
3104
3130
|
return createElement("t-tree", { data, color, branchColor, leafColor });
|
|
3105
3131
|
}
|
|
3106
|
-
function TCode({ code: code2, language, title, borderColor }) {
|
|
3107
|
-
return createElement("t-code", { code: code2, language, title, borderColor });
|
|
3132
|
+
function TCode({ code: code2, language, title, borderColor, action, copyable }) {
|
|
3133
|
+
return createElement("t-code", { code: code2, language, title, borderColor, action, copyable });
|
|
3108
3134
|
}
|
|
3109
3135
|
function TDiff({ value, addColor, removeColor, metaColor }) {
|
|
3110
3136
|
return createElement("t-diff", { value, addColor, removeColor, metaColor });
|