@textui/core 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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/app/app.d.ts +200 -0
- package/dist/app/app.d.ts.map +1 -0
- package/dist/app/app.js +980 -0
- package/dist/core/animation.d.ts +34 -0
- package/dist/core/animation.d.ts.map +1 -0
- package/dist/core/animation.js +125 -0
- package/dist/core/clipboard.d.ts +29 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/clipboard.js +30 -0
- package/dist/core/commands.d.ts +42 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +146 -0
- package/dist/core/components.d.ts +27 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +78 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +77 -0
- package/dist/core/focus.d.ts +69 -0
- package/dist/core/focus.d.ts.map +1 -0
- package/dist/core/focus.js +336 -0
- package/dist/core/i18n.d.ts +27 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +88 -0
- package/dist/core/keybindings.d.ts +53 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +163 -0
- package/dist/core/layers.d.ts +28 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +84 -0
- package/dist/core/manifest.d.ts +22 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +85 -0
- package/dist/core/navigation.d.ts +47 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +110 -0
- package/dist/core/resources.d.ts +115 -0
- package/dist/core/resources.d.ts.map +1 -0
- package/dist/core/resources.js +321 -0
- package/dist/core/services.d.ts +21 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +63 -0
- package/dist/core/store.d.ts +60 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +593 -0
- package/dist/core/surfaces.d.ts +81 -0
- package/dist/core/surfaces.d.ts.map +1 -0
- package/dist/core/surfaces.js +237 -0
- package/dist/core/syntax.d.ts +49 -0
- package/dist/core/syntax.d.ts.map +1 -0
- package/dist/core/syntax.js +172 -0
- package/dist/core/when.d.ts +15 -0
- package/dist/core/when.d.ts.map +1 -0
- package/dist/core/when.js +224 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/jsx/factory.d.ts +27 -0
- package/dist/jsx/factory.d.ts.map +1 -0
- package/dist/jsx/factory.js +102 -0
- package/dist/jsx/intrinsics.d.ts +96 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -0
- package/dist/jsx/intrinsics.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime.js +12 -0
- package/dist/jsx/jsx-runtime.d.ts +42 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +16 -0
- package/dist/render/buffer.d.ts +77 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +275 -0
- package/dist/render/color.d.ts +35 -0
- package/dist/render/color.d.ts.map +1 -0
- package/dist/render/color.js +160 -0
- package/dist/render/diff.d.ts +37 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +66 -0
- package/dist/render/layout.d.ts +72 -0
- package/dist/render/layout.d.ts.map +1 -0
- package/dist/render/layout.js +633 -0
- package/dist/render/static.d.ts +63 -0
- package/dist/render/static.d.ts.map +1 -0
- package/dist/render/static.js +209 -0
- package/dist/runtime/bindings.d.ts +46 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +116 -0
- package/dist/runtime/hooks.d.ts +284 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +846 -0
- package/dist/runtime/instance.d.ts +106 -0
- package/dist/runtime/instance.d.ts.map +1 -0
- package/dist/runtime/instance.js +181 -0
- package/dist/runtime/paint.d.ts +27 -0
- package/dist/runtime/paint.d.ts.map +1 -0
- package/dist/runtime/paint.js +567 -0
- package/dist/runtime/reconcile.d.ts +23 -0
- package/dist/runtime/reconcile.d.ts.map +1 -0
- package/dist/runtime/reconcile.js +260 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/style.d.ts +55 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/style.js +143 -0
- package/dist/themes/borders.d.ts +10 -0
- package/dist/themes/borders.d.ts.map +1 -0
- package/dist/themes/borders.js +88 -0
- package/dist/themes/builtin.d.ts +31 -0
- package/dist/themes/builtin.d.ts.map +1 -0
- package/dist/themes/builtin.js +291 -0
- package/dist/themes/glyphs.d.ts +15 -0
- package/dist/themes/glyphs.d.ts.map +1 -0
- package/dist/themes/glyphs.js +97 -0
- package/dist/themes/index.d.ts +5 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/registry.d.ts +20 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +204 -0
- package/dist/types/adapter.d.ts +38 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/animation.d.ts +31 -0
- package/dist/types/animation.d.ts.map +1 -0
- package/dist/types/animation.js +1 -0
- package/dist/types/app.d.ts +148 -0
- package/dist/types/app.d.ts.map +1 -0
- package/dist/types/app.js +1 -0
- package/dist/types/async.d.ts +30 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/async.js +1 -0
- package/dist/types/capabilities.d.ts +44 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +33 -0
- package/dist/types/cells.d.ts +72 -0
- package/dist/types/cells.d.ts.map +1 -0
- package/dist/types/cells.js +10 -0
- package/dist/types/command.d.ts +127 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +1 -0
- package/dist/types/component-registry.d.ts +77 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/component-registry.js +1 -0
- package/dist/types/disposable.d.ts +8 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/focus.d.ts +72 -0
- package/dist/types/focus.d.ts.map +1 -0
- package/dist/types/focus.js +1 -0
- package/dist/types/geometry.d.ts +28 -0
- package/dist/types/geometry.d.ts.map +1 -0
- package/dist/types/geometry.js +14 -0
- package/dist/types/graph.d.ts +129 -0
- package/dist/types/graph.d.ts.map +1 -0
- package/dist/types/graph.js +31 -0
- package/dist/types/i18n.d.ts +23 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +31 -0
- package/dist/types/input.d.ts +65 -0
- package/dist/types/input.d.ts.map +1 -0
- package/dist/types/input.js +1 -0
- package/dist/types/keybinding.d.ts +49 -0
- package/dist/types/keybinding.d.ts.map +1 -0
- package/dist/types/keybinding.js +1 -0
- package/dist/types/layer.d.ts +54 -0
- package/dist/types/layer.d.ts.map +1 -0
- package/dist/types/layer.js +1 -0
- package/dist/types/manifest.d.ts +114 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/markdown.d.ts +58 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/markdown.js +1 -0
- package/dist/types/navigation.d.ts +40 -0
- package/dist/types/navigation.d.ts.map +1 -0
- package/dist/types/navigation.js +1 -0
- package/dist/types/render.d.ts +72 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +1 -0
- package/dist/types/resource.d.ts +170 -0
- package/dist/types/resource.d.ts.map +1 -0
- package/dist/types/resource.js +1 -0
- package/dist/types/services.d.ts +24 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +3 -0
- package/dist/types/shell.d.ts +32 -0
- package/dist/types/shell.d.ts.map +1 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/store.d.ts +124 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/store.js +1 -0
- package/dist/types/stream.d.ts +18 -0
- package/dist/types/stream.d.ts.map +1 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/style.d.ts +166 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/style.js +1 -0
- package/dist/types/surface.d.ts +97 -0
- package/dist/types/surface.d.ts.map +1 -0
- package/dist/types/surface.js +1 -0
- package/dist/types/syntax.d.ts +68 -0
- package/dist/types/syntax.d.ts.map +1 -0
- package/dist/types/syntax.js +5 -0
- package/dist/types/terminal.d.ts +52 -0
- package/dist/types/terminal.d.ts.map +1 -0
- package/dist/types/terminal.js +1 -0
- package/dist/types/theme.d.ts +117 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/when.d.ts +19 -0
- package/dist/types/when.d.ts.map +1 -0
- package/dist/types/when.js +1 -0
- package/dist/ui/primitives.d.ts +56 -0
- package/dist/ui/primitives.d.ts.map +1 -0
- package/dist/ui/primitives.js +108 -0
- package/dist/ui/screen.d.ts +23 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +18 -0
- package/dist/util/disposable.d.ts +6 -0
- package/dist/util/disposable.d.ts.map +1 -0
- package/dist/util/disposable.js +49 -0
- package/dist/util/markdown.d.ts +24 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +220 -0
- package/dist/util/paths.d.ts +47 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +134 -0
- package/dist/util/stream.d.ts +32 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +206 -0
- package/dist/util/text.d.ts +65 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +419 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +11 -0
- package/src/app/app.ts +1096 -0
- package/src/core/animation.ts +144 -0
- package/src/core/clipboard.ts +40 -0
- package/src/core/commands.ts +169 -0
- package/src/core/components.ts +91 -0
- package/src/core/events.ts +95 -0
- package/src/core/focus.ts +358 -0
- package/src/core/i18n.ts +107 -0
- package/src/core/keybindings.ts +184 -0
- package/src/core/layers.ts +94 -0
- package/src/core/manifest.ts +84 -0
- package/src/core/navigation.ts +135 -0
- package/src/core/resources.ts +362 -0
- package/src/core/services.ts +69 -0
- package/src/core/store.ts +640 -0
- package/src/core/surfaces.ts +292 -0
- package/src/core/syntax.ts +200 -0
- package/src/core/when.ts +238 -0
- package/src/index.ts +76 -0
- package/src/jsx/factory.ts +124 -0
- package/src/jsx/intrinsics.ts +99 -0
- package/src/jsx/jsx-dev-runtime.ts +23 -0
- package/src/jsx/jsx-runtime.ts +76 -0
- package/src/render/buffer.ts +318 -0
- package/src/render/color.ts +180 -0
- package/src/render/diff.ts +99 -0
- package/src/render/layout.ts +764 -0
- package/src/render/static.ts +290 -0
- package/src/runtime/bindings.ts +150 -0
- package/src/runtime/hooks.ts +1057 -0
- package/src/runtime/instance.ts +284 -0
- package/src/runtime/paint.ts +737 -0
- package/src/runtime/reconcile.ts +335 -0
- package/src/runtime/runtime.ts +47 -0
- package/src/runtime/style.ts +205 -0
- package/src/themes/borders.ts +95 -0
- package/src/themes/builtin.ts +301 -0
- package/src/themes/glyphs.ts +101 -0
- package/src/themes/index.ts +4 -0
- package/src/themes/registry.ts +224 -0
- package/src/types/adapter.ts +41 -0
- package/src/types/animation.ts +34 -0
- package/src/types/app.ts +149 -0
- package/src/types/async.ts +35 -0
- package/src/types/capabilities.ts +79 -0
- package/src/types/cells.ts +79 -0
- package/src/types/command.ts +139 -0
- package/src/types/component-registry.ts +82 -0
- package/src/types/disposable.ts +8 -0
- package/src/types/focus.ts +77 -0
- package/src/types/geometry.ts +50 -0
- package/src/types/graph.ts +174 -0
- package/src/types/i18n.ts +25 -0
- package/src/types/index.ts +31 -0
- package/src/types/input.ts +81 -0
- package/src/types/keybinding.ts +51 -0
- package/src/types/layer.ts +46 -0
- package/src/types/manifest.ts +101 -0
- package/src/types/markdown.ts +47 -0
- package/src/types/navigation.ts +42 -0
- package/src/types/render.ts +86 -0
- package/src/types/resource.ts +185 -0
- package/src/types/services.ts +28 -0
- package/src/types/shell.ts +30 -0
- package/src/types/store.ts +148 -0
- package/src/types/stream.ts +24 -0
- package/src/types/style.ts +205 -0
- package/src/types/surface.ts +119 -0
- package/src/types/syntax.ts +93 -0
- package/src/types/terminal.ts +58 -0
- package/src/types/theme.ts +121 -0
- package/src/types/when.ts +21 -0
- package/src/ui/primitives.ts +118 -0
- package/src/ui/screen.ts +41 -0
- package/src/util/disposable.ts +49 -0
- package/src/util/markdown.ts +225 -0
- package/src/util/paths.ts +138 -0
- package/src/util/stream.ts +213 -0
- package/src/util/text.ts +428 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ComponentDefinition } from '../types/component-registry.js';
|
|
2
|
+
import type { HostComponent } from '../types/render.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The host primitives.
|
|
6
|
+
*
|
|
7
|
+
* There are four, and there is a reason not to add a fifth: the layout engine
|
|
8
|
+
* and the painter only reason about these shapes, so every component in the
|
|
9
|
+
* catalog composing them means layout has one implementation rather than
|
|
10
|
+
* dozens. Painting for `box`, `text` and `canvas` lives in the painter, which
|
|
11
|
+
* is why these carry no `paint` of their own.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The primitives under Capitalized names, for JSX.
|
|
16
|
+
*
|
|
17
|
+
* These are the strings, not wrappers around them. JSX resolves a Capitalized
|
|
18
|
+
* tag to the value in scope, and a value whose type is a string literal is an
|
|
19
|
+
* intrinsic - so `<Box margin={2}/>` compiles to `h('box', { margin: 2 })`,
|
|
20
|
+
* the identical node. No function to call, no extra depth in the tree, and
|
|
21
|
+
* `BoxProps` still checks the props: `<Box notAProp/>` is the same error
|
|
22
|
+
* `<box notAProp/>` is.
|
|
23
|
+
*
|
|
24
|
+
* They exist so a screen can be written in one case. Every other component is
|
|
25
|
+
* Capitalized already - it has to be, that is how JSX tells a registry lookup
|
|
26
|
+
* from a host tag - and the four primitives being the exception meant mixing
|
|
27
|
+
* `<Row>` and `<box>` in one file. The lowercase names remain what the node
|
|
28
|
+
* actually holds, and stay the spelling for a screen written as data.
|
|
29
|
+
*
|
|
30
|
+
* Each carries its own doc below rather than sharing this one, because a
|
|
31
|
+
* comment above a group attaches to the first declaration and leaves the rest
|
|
32
|
+
* hovering as nothing - and going to the definition of a name that turns out
|
|
33
|
+
* to be an undocumented string is worse than not going anywhere.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The container. Flex layout, background, border, title and footer.
|
|
38
|
+
*
|
|
39
|
+
* Props: `BoxProps`. The same node as `<box/>` - this constant is the string,
|
|
40
|
+
* so JSX resolves the tag as an intrinsic rather than as a component.
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <Box border="round" padding={1} direction="column" gap={1}>
|
|
44
|
+
* <Text bold>Title</Text>
|
|
45
|
+
* </Box>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const Box = 'box' as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A run of text. Wraps, truncates and aligns within its box.
|
|
52
|
+
*
|
|
53
|
+
* Props: `TextProps` - `content`, or children as a shorthand, plus `truncate`
|
|
54
|
+
* and every style key. The same node as `<text/>`.
|
|
55
|
+
*
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <Text bold fg="success">{count} tests passed</Text>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const Text = 'text' as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Direct cell painting. The escape hatch charts and gauges use.
|
|
64
|
+
*
|
|
65
|
+
* Props: `CanvasProps` - a `draw` callback handed a surface and a render
|
|
66
|
+
* context, and an `intrinsic` size for when the style does not fix one. Prefer
|
|
67
|
+
* composing `Box` and `Text` where you can: the layout engine can reason about
|
|
68
|
+
* those and cannot reason about this. The same node as `<canvas/>`.
|
|
69
|
+
*/
|
|
70
|
+
export const Canvas = 'canvas' as const;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Empty space. Greedy by default, or a fixed number of cells.
|
|
74
|
+
*
|
|
75
|
+
* Props: `SpacerProps` - `size` in cells; leave it unset and it takes whatever
|
|
76
|
+
* is left, which is how two things end up at opposite ends of a row. The same
|
|
77
|
+
* node as `<spacer/>`.
|
|
78
|
+
*
|
|
79
|
+
* Between *every* child, `gap` on the container is shorter and does not need a
|
|
80
|
+
* node per space.
|
|
81
|
+
*/
|
|
82
|
+
export const Spacer = 'spacer' as const;
|
|
83
|
+
|
|
84
|
+
const box: HostComponent = { name: 'box', container: true };
|
|
85
|
+
const text: HostComponent = { name: 'text', leaf: true };
|
|
86
|
+
const canvas: HostComponent = { name: 'canvas', leaf: true };
|
|
87
|
+
const spacer: HostComponent = { name: 'spacer', leaf: true };
|
|
88
|
+
|
|
89
|
+
export const PRIMITIVES: ComponentDefinition[] = [
|
|
90
|
+
{
|
|
91
|
+
component: 'box',
|
|
92
|
+
category: 'layout',
|
|
93
|
+
description: 'The container. Flex layout, background, border, title and footer.',
|
|
94
|
+
renderer: { kind: 'host', host: box },
|
|
95
|
+
role: 'presentation',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
component: 'text',
|
|
99
|
+
category: 'display',
|
|
100
|
+
description: 'A run of text. Wraps, truncates and aligns within its box.',
|
|
101
|
+
renderer: { kind: 'host', host: text },
|
|
102
|
+
role: 'presentation',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
component: 'canvas',
|
|
106
|
+
category: 'display',
|
|
107
|
+
description: 'Direct cell painting. The escape hatch charts and gauges use.',
|
|
108
|
+
renderer: { kind: 'host', host: canvas },
|
|
109
|
+
role: 'presentation',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
component: 'spacer',
|
|
113
|
+
category: 'layout',
|
|
114
|
+
description: 'Empty space. Sized, or greedy when given flex.',
|
|
115
|
+
renderer: { kind: 'host', host: spacer },
|
|
116
|
+
role: 'presentation',
|
|
117
|
+
},
|
|
118
|
+
];
|
package/src/ui/screen.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BoxProps } from '../jsx/intrinsics.js';
|
|
2
|
+
import type { ComponentDefinition } from '../types/component-registry.js';
|
|
3
|
+
import type { RenderOutput } from '../types/render.js';
|
|
4
|
+
import { defineComponent, h } from '../jsx/factory.js';
|
|
5
|
+
import { useFocusScope } from '../runtime/hooks.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the navigator mounts.
|
|
9
|
+
*
|
|
10
|
+
* A screen is not a special kind of node - it is a mount in a surface, so the
|
|
11
|
+
* shell arranges it, the layouts apply to it, and everything that reads the
|
|
12
|
+
* surface registry sees it. What this adds is the one thing a screen needs
|
|
13
|
+
* that a mount does not have: a focus scope of its own.
|
|
14
|
+
*
|
|
15
|
+
* That scope is what makes the stack behave. Tab order belongs to what is on
|
|
16
|
+
* screen rather than to the application, and `pop` can put focus back where it
|
|
17
|
+
* was because "where it was" is a question about this scope and not about
|
|
18
|
+
* every focusable that ever registered.
|
|
19
|
+
*/
|
|
20
|
+
export interface ScreenProps extends BoxProps {
|
|
21
|
+
screenId: string;
|
|
22
|
+
children?: RenderOutput;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Screen = defineComponent<ScreenProps>('Screen', (props) => {
|
|
26
|
+
const { screenId, children, ...rest } = props;
|
|
27
|
+
// A screen takes focus when it arrives. Pushing one unmounts whatever held
|
|
28
|
+
// focus, so without this the new screen is one no key reaches - and what it
|
|
29
|
+
// is showing cannot even be scrolled.
|
|
30
|
+
useFocusScope({ id: `screen:${screenId}`, restore: true, autoFocus: true });
|
|
31
|
+
return h('box', { direction: 'column', flex: 1, ...rest }, children as never);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const SCREEN_COMPONENTS: ComponentDefinition[] = [
|
|
35
|
+
{
|
|
36
|
+
component: 'Screen',
|
|
37
|
+
category: 'layout',
|
|
38
|
+
renderer: { kind: 'function', render: Screen },
|
|
39
|
+
description: 'The navigator’s mount: one screen, in its own focus scope.',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Disposable, DisposableBag } from '../types/disposable.js';
|
|
2
|
+
|
|
3
|
+
export const NOOP_DISPOSABLE: Disposable = { dispose() {} };
|
|
4
|
+
|
|
5
|
+
export function toDisposable(fn: () => void): Disposable {
|
|
6
|
+
let done = false;
|
|
7
|
+
return {
|
|
8
|
+
dispose() {
|
|
9
|
+
if (done) return;
|
|
10
|
+
done = true;
|
|
11
|
+
fn();
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function disposeAll(items: Iterable<Disposable>): void {
|
|
17
|
+
const errors: unknown[] = [];
|
|
18
|
+
for (const d of items) {
|
|
19
|
+
try {
|
|
20
|
+
d.dispose();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
errors.push(err);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (errors.length === 1) throw errors[0];
|
|
26
|
+
if (errors.length > 1) throw new AggregateError(errors, 'dispose failed');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createBag(): DisposableBag {
|
|
30
|
+
const items = new Set<Disposable>();
|
|
31
|
+
let disposed = false;
|
|
32
|
+
return {
|
|
33
|
+
add<T extends Disposable>(d: T): T {
|
|
34
|
+
if (disposed) {
|
|
35
|
+
d.dispose();
|
|
36
|
+
return d;
|
|
37
|
+
}
|
|
38
|
+
items.add(d);
|
|
39
|
+
return d;
|
|
40
|
+
},
|
|
41
|
+
dispose() {
|
|
42
|
+
if (disposed) return;
|
|
43
|
+
disposed = true;
|
|
44
|
+
const copy = [...items];
|
|
45
|
+
items.clear();
|
|
46
|
+
disposeAll(copy);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { MarkdownLayoutOptions, MarkdownRow, MarkdownRun } from '../types/markdown.js';
|
|
2
|
+
import type { StyleColor } from '../types/style.js';
|
|
3
|
+
import { graphemes, stringWidth, wrapText } from './text.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Markdown, laid out.
|
|
7
|
+
*
|
|
8
|
+
* Not a parser - a formatter for the subset that appears in READMEs, notes and
|
|
9
|
+
* anything an agent or a service writes for a person: headings, lists, rules,
|
|
10
|
+
* fences, block quotes, and inline emphasis, code and links.
|
|
11
|
+
*
|
|
12
|
+
* Pure, and measured in cells. It takes the glyphs it needs rather than
|
|
13
|
+
* reading a theme, so it can be called from a test, from a viewer that windows
|
|
14
|
+
* the rows, and from a component that draws all of them.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The delimiters, guarded.
|
|
19
|
+
*
|
|
20
|
+
* The guards are the whole difficulty. A single `*` beside a double one is not
|
|
21
|
+
* an opener - `2 * 3 and **half a bold` italicised " 3 and " and ate a
|
|
22
|
+
* character, because the lone asterisk paired with the first half of the
|
|
23
|
+
* unclosed pair. A delimiter with a space just inside it is arithmetic or a
|
|
24
|
+
* footnote, not emphasis. And an underscore needs a word boundary as well, or
|
|
25
|
+
* `snake_case_name` goes italic in the middle.
|
|
26
|
+
*/
|
|
27
|
+
const PATTERNS = [
|
|
28
|
+
String.raw`\*\*(?!\s)[^*]+(?<!\s)\*\*`,
|
|
29
|
+
String.raw`(?<![\w_])__(?!\s)[^_]+(?<!\s)__(?![\w_])`,
|
|
30
|
+
String.raw`\x60[^\x60]+\x60`,
|
|
31
|
+
String.raw`(?<!\*)\*(?!\s)[^*]+(?<!\s)\*(?!\*)`,
|
|
32
|
+
String.raw`(?<![\w_])_(?!\s)[^_]+(?<!\s)_(?![\w_])`,
|
|
33
|
+
String.raw`\[[^\]]+\]\([^)]+\)`,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** One capturing group, so `split` keeps the delimited pieces. */
|
|
37
|
+
const INLINE = new RegExp(`(${PATTERNS.join('|')})`);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Emphasis, code and links, as styled runs.
|
|
41
|
+
*
|
|
42
|
+
* Unmatched markers stay literal: a lone asterisk in prose is an asterisk, and
|
|
43
|
+
* a document that reformats itself as the closing one arrives is worse than
|
|
44
|
+
* one that never tried.
|
|
45
|
+
*/
|
|
46
|
+
export function inlineRuns(text: string): MarkdownRun[] {
|
|
47
|
+
const runs: MarkdownRun[] = [];
|
|
48
|
+
for (const piece of text.split(INLINE)) {
|
|
49
|
+
if (piece === '') continue;
|
|
50
|
+
if (/^\*\*[^*]+\*\*$/.test(piece) || /^__[^_]+__$/.test(piece)) {
|
|
51
|
+
runs.push({ text: piece.slice(2, -2), bold: true });
|
|
52
|
+
} else if (/^`[^`]+`$/.test(piece)) {
|
|
53
|
+
runs.push({ text: piece.slice(1, -1), code: true });
|
|
54
|
+
} else if (/^\*[^*]+\*$/.test(piece) || /^_[^_]+_$/.test(piece)) {
|
|
55
|
+
runs.push({ text: piece.slice(1, -1), italic: true });
|
|
56
|
+
} else {
|
|
57
|
+
const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(piece);
|
|
58
|
+
if (link) runs.push({ text: link[1] ?? '', link: link[2] ?? '' });
|
|
59
|
+
else runs.push({ text: piece });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return runs.length > 0 ? runs : [{ text: '' }];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fold styled runs into lines that fit.
|
|
67
|
+
*
|
|
68
|
+
* `wrapText` wraps a string, and a string cannot carry which half of it was
|
|
69
|
+
* bold - so the wrap happens over runs, on cell widths rather than character
|
|
70
|
+
* counts. A word longer than the whole width is broken rather than allowed to
|
|
71
|
+
* decide the width of the pane it is in.
|
|
72
|
+
*/
|
|
73
|
+
export function wrapRuns(runs: MarkdownRun[], width: number): MarkdownRun[][] {
|
|
74
|
+
if (width <= 0) return [runs];
|
|
75
|
+
const lines: MarkdownRun[][] = [];
|
|
76
|
+
let line: MarkdownRun[] = [];
|
|
77
|
+
let used = 0;
|
|
78
|
+
|
|
79
|
+
const push = (run: MarkdownRun, text: string): void => {
|
|
80
|
+
const last = line[line.length - 1];
|
|
81
|
+
if (last && sameStyle(last, run)) last.text += text;
|
|
82
|
+
else line.push({ ...run, text });
|
|
83
|
+
used += stringWidth(text);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
for (const run of runs) {
|
|
87
|
+
for (const word of run.text.split(/(\s+)/)) {
|
|
88
|
+
if (word === '') continue;
|
|
89
|
+
const w = stringWidth(word);
|
|
90
|
+
|
|
91
|
+
if (/^\s+$/.test(word)) {
|
|
92
|
+
// A space at the start of a line is a space nobody typed.
|
|
93
|
+
if (used > 0 && used + w <= width) push(run, word);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (used + w > width && used > 0) { lines.push(line); line = []; used = 0; }
|
|
98
|
+
|
|
99
|
+
if (w > width) {
|
|
100
|
+
let rest = graphemes(word);
|
|
101
|
+
while (rest.length > 0) {
|
|
102
|
+
const room = Math.max(1, width - used);
|
|
103
|
+
push(run, rest.slice(0, room).join(''));
|
|
104
|
+
rest = rest.slice(room);
|
|
105
|
+
if (rest.length > 0) { lines.push(line); line = []; used = 0; }
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
push(run, word);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (line.length > 0) lines.push(line);
|
|
115
|
+
return lines.length > 0 ? lines : [[{ text: '' }]];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sameStyle(a: MarkdownRun, b: MarkdownRun): boolean {
|
|
119
|
+
return !!a.bold === !!b.bold && !!a.italic === !!b.italic
|
|
120
|
+
&& !!a.code === !!b.code && a.link === b.link;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Wrapped rows behind a gutter: the marker on the first, its width on the rest. */
|
|
124
|
+
function withPrefix(
|
|
125
|
+
runs: MarkdownRun[],
|
|
126
|
+
width: number,
|
|
127
|
+
prefix: string,
|
|
128
|
+
options: { continued?: string; prefixFg?: StyleColor } = {},
|
|
129
|
+
): MarkdownRow[] {
|
|
130
|
+
const gutter = stringWidth(prefix);
|
|
131
|
+
const rest = options.continued ?? ' '.repeat(gutter);
|
|
132
|
+
return wrapRuns(runs, width <= 0 ? 0 : Math.max(1, width - gutter - 1)).map((line, index) => ({
|
|
133
|
+
kind: 'text' as const,
|
|
134
|
+
runs: line,
|
|
135
|
+
prefix: index === 0 ? prefix : rest,
|
|
136
|
+
...(options.prefixFg ? { prefixFg: options.prefixFg } : {}),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function layoutMarkdown(
|
|
141
|
+
source: string | string[],
|
|
142
|
+
options: MarkdownLayoutOptions,
|
|
143
|
+
): MarkdownRow[] {
|
|
144
|
+
const { width, bullet = '-', quoteBar = '|', ruled = true } = options;
|
|
145
|
+
const lines = Array.isArray(source) ? source : source.replace(/\r\n/g, '\n').split('\n');
|
|
146
|
+
const rows: MarkdownRow[] = [];
|
|
147
|
+
let fence = 0;
|
|
148
|
+
let inFence = false;
|
|
149
|
+
let language = '';
|
|
150
|
+
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const marker = /^\s*```(\S*)\s*$/.exec(line);
|
|
153
|
+
if (marker) {
|
|
154
|
+
// An open fence is laid out as a fence from the moment it opens. A turn
|
|
155
|
+
// is streamed, so the closing marker may simply not have been said yet -
|
|
156
|
+
// and a block that flickers between prose and code as the words land is
|
|
157
|
+
// worse than one shown open.
|
|
158
|
+
if (!inFence) language = marker[1] ?? '';
|
|
159
|
+
if (ruled) rows.push({ kind: 'fence', fence, part: inFence ? 'close' : 'open', ...(language ? { language } : {}) });
|
|
160
|
+
if (inFence) fence++;
|
|
161
|
+
inFence = !inFence;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (inFence) {
|
|
165
|
+
rows.push({ kind: 'fence', fence, part: 'code', text: line, ...(language ? { language } : {}) });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
170
|
+
if (heading) {
|
|
171
|
+
const level = (heading[1] as string).length;
|
|
172
|
+
for (const runs of wrapRuns(inlineRuns(heading[2] ?? ''), width)) {
|
|
173
|
+
rows.push({ kind: 'heading', runs, level });
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (/^\s*([-*_])\1{2,}\s*$/.test(line)) { rows.push({ kind: 'rule' }); continue; }
|
|
179
|
+
|
|
180
|
+
const item = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
181
|
+
if (item) {
|
|
182
|
+
rows.push(...withPrefix(inlineRuns(item[2] ?? ''), width, `${item[1]}${bullet}`));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const ordered = /^(\s*)(\d+)[.)]\s+(.*)$/.exec(line);
|
|
187
|
+
if (ordered) {
|
|
188
|
+
rows.push(...withPrefix(inlineRuns(ordered[3] ?? ''), width, `${ordered[1]}${ordered[2]}.`));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
193
|
+
if (quote) {
|
|
194
|
+
// The bar repeats down every row of the quote: a rule that stopped after
|
|
195
|
+
// the first wrapped row would read as one quoted line and some prose.
|
|
196
|
+
rows.push(...withPrefix(inlineRuns(quote[1] ?? ''), width, quoteBar, {
|
|
197
|
+
continued: quoteBar,
|
|
198
|
+
prefixFg: 'borderSubtle',
|
|
199
|
+
}));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (line.trim() === '') { rows.push({ kind: 'text', runs: [{ text: '' }] }); continue; }
|
|
204
|
+
|
|
205
|
+
for (const runs of wrapRuns(inlineRuns(line), width)) rows.push({ kind: 'text', runs });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// An unterminated fence still gets its closing rule, so the box reads as a
|
|
209
|
+
// box rather than as something the renderer forgot to finish.
|
|
210
|
+
if (inFence && ruled) rows.push({ kind: 'fence', fence, part: 'close' });
|
|
211
|
+
|
|
212
|
+
return rows;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Every run's text, for a measurement or a test that does not care how it looks. */
|
|
216
|
+
export function runsToText(runs: MarkdownRun[]): string {
|
|
217
|
+
return runs.map((run) => run.text).join('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Wrap plain text the way a row does, for callers that have no runs. */
|
|
221
|
+
export function wrapAt(text: string, width: number): string[] {
|
|
222
|
+
if (width <= 0) return [text];
|
|
223
|
+
const wrapped = wrapText(text, width);
|
|
224
|
+
return wrapped.length > 0 ? wrapped : [''];
|
|
225
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { BindingPath } from '../types/graph.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Store paths are JSON-Pointer-shaped.
|
|
5
|
+
*
|
|
6
|
+
* `$/scope/a/b` absolute; the first segment is the scope
|
|
7
|
+
* `/a/b` relative to the surrounding data context
|
|
8
|
+
* `..` forbidden - escape to the root with `$/` instead, so a
|
|
9
|
+
* node's meaning never depends on where it was pasted
|
|
10
|
+
* `*` wildcard segment; legal in subscriptions, never in writes
|
|
11
|
+
*
|
|
12
|
+
* Internally a path is its segment list. The canonical key is the segments
|
|
13
|
+
* joined by `/` with no sigil, which is what every map here is keyed by.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class PathError extends Error {
|
|
17
|
+
constructor(message: string, readonly path: string) {
|
|
18
|
+
super(`${message}: ${path}`);
|
|
19
|
+
this.name = 'PathError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unescapeSegment(seg: string): string {
|
|
24
|
+
return seg.includes('~') ? seg.replace(/~1/g, '/').replace(/~0/g, '~') : seg;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function escapeSegment(seg: string): string {
|
|
28
|
+
return seg.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isAbsolute(path: string): path is `$/${string}` {
|
|
32
|
+
return path.startsWith('$/');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isRelative(path: string): boolean {
|
|
36
|
+
return path.startsWith('/');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Split into segments. Does not resolve relatives - use `resolvePath`. */
|
|
40
|
+
export function segments(path: string): string[] {
|
|
41
|
+
const body = isAbsolute(path) ? path.slice(2) : path.startsWith('/') ? path.slice(1) : path;
|
|
42
|
+
if (body === '') return [];
|
|
43
|
+
const parts = body.split('/');
|
|
44
|
+
for (const p of parts) {
|
|
45
|
+
if (p === '..') throw new PathError('".." is forbidden in a store path', path);
|
|
46
|
+
}
|
|
47
|
+
return parts.map(unescapeSegment);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Canonical map key for a path: segments joined, no sigil.
|
|
52
|
+
*
|
|
53
|
+
* The segments are re-escaped on the way out. Everything downstream - the
|
|
54
|
+
* store's own walk, `ancestorKeys`, `matchKey` - splits a key on `/`, so a
|
|
55
|
+
* segment that legitimately contains one (a URI used as a key, a filename with
|
|
56
|
+
* a slash) has to stay escaped or it silently becomes several segments and the
|
|
57
|
+
* value lands somewhere nobody looks.
|
|
58
|
+
*/
|
|
59
|
+
export function pathKey(path: string): string {
|
|
60
|
+
return segments(path).map(escapeSegment).join('/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function joinPath(...parts: (string | number)[]): BindingPath {
|
|
64
|
+
const segs: string[] = [];
|
|
65
|
+
for (const p of parts) {
|
|
66
|
+
const s = String(p);
|
|
67
|
+
for (const seg of s.replace(/^\$\//, '').replace(/^\//, '').split('/')) {
|
|
68
|
+
if (seg !== '') segs.push(seg);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return `$/${segs.join('/')}` as BindingPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a possibly-relative path against a data context.
|
|
76
|
+
* A relative path with no context is an error a caller should never reach.
|
|
77
|
+
*/
|
|
78
|
+
export function resolvePath(path: string, dataContext?: string): BindingPath {
|
|
79
|
+
if (isAbsolute(path)) return path as BindingPath;
|
|
80
|
+
if (!path.startsWith('/')) {
|
|
81
|
+
throw new PathError('a path must start with "$/" or "/"', path);
|
|
82
|
+
}
|
|
83
|
+
if (!dataContext) {
|
|
84
|
+
throw new PathError('relative path used with no data context', path);
|
|
85
|
+
}
|
|
86
|
+
const base = segments(dataContext);
|
|
87
|
+
const rest = segments(path);
|
|
88
|
+
return `$/${[...base, ...rest].join('/')}` as BindingPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function scopeOf(path: string): string {
|
|
92
|
+
const segs = segments(path);
|
|
93
|
+
return segs[0] ?? '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parentKey(key: string): string | null {
|
|
97
|
+
const i = key.lastIndexOf('/');
|
|
98
|
+
return i === -1 ? (key === '' ? null : '') : key.slice(0, i);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every ancestor key of `key`, closest first, including `''` for the root. */
|
|
102
|
+
export function ancestorKeys(key: string): string[] {
|
|
103
|
+
const out: string[] = [];
|
|
104
|
+
let cur = key;
|
|
105
|
+
for (;;) {
|
|
106
|
+
const p = parentKey(cur);
|
|
107
|
+
if (p === null) break;
|
|
108
|
+
out.push(p);
|
|
109
|
+
cur = p;
|
|
110
|
+
if (p === '') break;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isDescendantKey(key: string, ancestor: string): boolean {
|
|
116
|
+
if (ancestor === '') return key !== '';
|
|
117
|
+
return key.startsWith(ancestor + '/');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function hasWildcard(path: string): boolean {
|
|
121
|
+
return path.includes('*');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Match a concrete key against a pattern key that may contain `*`. */
|
|
125
|
+
export function matchKey(key: string, pattern: string, subtree = false): boolean {
|
|
126
|
+
if (!pattern.includes('*')) {
|
|
127
|
+
return key === pattern || (subtree && isDescendantKey(key, pattern));
|
|
128
|
+
}
|
|
129
|
+
const p = pattern.split('/');
|
|
130
|
+
const k = key.split('/');
|
|
131
|
+
if (!subtree && p.length !== k.length) return false;
|
|
132
|
+
if (subtree && k.length < p.length) return false;
|
|
133
|
+
for (let i = 0; i < p.length; i++) {
|
|
134
|
+
if (p[i] === '*') continue;
|
|
135
|
+
if (p[i] !== k[i]) return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|