@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.
Files changed (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/index.d.ts +12 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +11 -0
  6. package/dist/app/app.d.ts +200 -0
  7. package/dist/app/app.d.ts.map +1 -0
  8. package/dist/app/app.js +980 -0
  9. package/dist/core/animation.d.ts +34 -0
  10. package/dist/core/animation.d.ts.map +1 -0
  11. package/dist/core/animation.js +125 -0
  12. package/dist/core/clipboard.d.ts +29 -0
  13. package/dist/core/clipboard.d.ts.map +1 -0
  14. package/dist/core/clipboard.js +30 -0
  15. package/dist/core/commands.d.ts +42 -0
  16. package/dist/core/commands.d.ts.map +1 -0
  17. package/dist/core/commands.js +146 -0
  18. package/dist/core/components.d.ts +27 -0
  19. package/dist/core/components.d.ts.map +1 -0
  20. package/dist/core/components.js +78 -0
  21. package/dist/core/events.d.ts +23 -0
  22. package/dist/core/events.d.ts.map +1 -0
  23. package/dist/core/events.js +77 -0
  24. package/dist/core/focus.d.ts +69 -0
  25. package/dist/core/focus.d.ts.map +1 -0
  26. package/dist/core/focus.js +336 -0
  27. package/dist/core/i18n.d.ts +27 -0
  28. package/dist/core/i18n.d.ts.map +1 -0
  29. package/dist/core/i18n.js +88 -0
  30. package/dist/core/keybindings.d.ts +53 -0
  31. package/dist/core/keybindings.d.ts.map +1 -0
  32. package/dist/core/keybindings.js +163 -0
  33. package/dist/core/layers.d.ts +28 -0
  34. package/dist/core/layers.d.ts.map +1 -0
  35. package/dist/core/layers.js +84 -0
  36. package/dist/core/manifest.d.ts +22 -0
  37. package/dist/core/manifest.d.ts.map +1 -0
  38. package/dist/core/manifest.js +85 -0
  39. package/dist/core/navigation.d.ts +47 -0
  40. package/dist/core/navigation.d.ts.map +1 -0
  41. package/dist/core/navigation.js +110 -0
  42. package/dist/core/resources.d.ts +115 -0
  43. package/dist/core/resources.d.ts.map +1 -0
  44. package/dist/core/resources.js +321 -0
  45. package/dist/core/services.d.ts +21 -0
  46. package/dist/core/services.d.ts.map +1 -0
  47. package/dist/core/services.js +63 -0
  48. package/dist/core/store.d.ts +60 -0
  49. package/dist/core/store.d.ts.map +1 -0
  50. package/dist/core/store.js +593 -0
  51. package/dist/core/surfaces.d.ts +81 -0
  52. package/dist/core/surfaces.d.ts.map +1 -0
  53. package/dist/core/surfaces.js +237 -0
  54. package/dist/core/syntax.d.ts +49 -0
  55. package/dist/core/syntax.d.ts.map +1 -0
  56. package/dist/core/syntax.js +172 -0
  57. package/dist/core/when.d.ts +15 -0
  58. package/dist/core/when.d.ts.map +1 -0
  59. package/dist/core/when.js +224 -0
  60. package/dist/index.d.ts +49 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +54 -0
  63. package/dist/jsx/factory.d.ts +27 -0
  64. package/dist/jsx/factory.d.ts.map +1 -0
  65. package/dist/jsx/factory.js +102 -0
  66. package/dist/jsx/intrinsics.d.ts +96 -0
  67. package/dist/jsx/intrinsics.d.ts.map +1 -0
  68. package/dist/jsx/intrinsics.js +1 -0
  69. package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
  70. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
  71. package/dist/jsx/jsx-dev-runtime.js +12 -0
  72. package/dist/jsx/jsx-runtime.d.ts +42 -0
  73. package/dist/jsx/jsx-runtime.d.ts.map +1 -0
  74. package/dist/jsx/jsx-runtime.js +16 -0
  75. package/dist/render/buffer.d.ts +77 -0
  76. package/dist/render/buffer.d.ts.map +1 -0
  77. package/dist/render/buffer.js +275 -0
  78. package/dist/render/color.d.ts +35 -0
  79. package/dist/render/color.d.ts.map +1 -0
  80. package/dist/render/color.js +160 -0
  81. package/dist/render/diff.d.ts +37 -0
  82. package/dist/render/diff.d.ts.map +1 -0
  83. package/dist/render/diff.js +66 -0
  84. package/dist/render/layout.d.ts +72 -0
  85. package/dist/render/layout.d.ts.map +1 -0
  86. package/dist/render/layout.js +633 -0
  87. package/dist/render/static.d.ts +63 -0
  88. package/dist/render/static.d.ts.map +1 -0
  89. package/dist/render/static.js +209 -0
  90. package/dist/runtime/bindings.d.ts +46 -0
  91. package/dist/runtime/bindings.d.ts.map +1 -0
  92. package/dist/runtime/bindings.js +116 -0
  93. package/dist/runtime/hooks.d.ts +284 -0
  94. package/dist/runtime/hooks.d.ts.map +1 -0
  95. package/dist/runtime/hooks.js +846 -0
  96. package/dist/runtime/instance.d.ts +106 -0
  97. package/dist/runtime/instance.d.ts.map +1 -0
  98. package/dist/runtime/instance.js +181 -0
  99. package/dist/runtime/paint.d.ts +27 -0
  100. package/dist/runtime/paint.d.ts.map +1 -0
  101. package/dist/runtime/paint.js +567 -0
  102. package/dist/runtime/reconcile.d.ts +23 -0
  103. package/dist/runtime/reconcile.d.ts.map +1 -0
  104. package/dist/runtime/reconcile.js +260 -0
  105. package/dist/runtime/runtime.d.ts +42 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -0
  107. package/dist/runtime/runtime.js +1 -0
  108. package/dist/runtime/style.d.ts +55 -0
  109. package/dist/runtime/style.d.ts.map +1 -0
  110. package/dist/runtime/style.js +143 -0
  111. package/dist/themes/borders.d.ts +10 -0
  112. package/dist/themes/borders.d.ts.map +1 -0
  113. package/dist/themes/borders.js +88 -0
  114. package/dist/themes/builtin.d.ts +31 -0
  115. package/dist/themes/builtin.d.ts.map +1 -0
  116. package/dist/themes/builtin.js +291 -0
  117. package/dist/themes/glyphs.d.ts +15 -0
  118. package/dist/themes/glyphs.d.ts.map +1 -0
  119. package/dist/themes/glyphs.js +97 -0
  120. package/dist/themes/index.d.ts +5 -0
  121. package/dist/themes/index.d.ts.map +1 -0
  122. package/dist/themes/index.js +4 -0
  123. package/dist/themes/registry.d.ts +20 -0
  124. package/dist/themes/registry.d.ts.map +1 -0
  125. package/dist/themes/registry.js +204 -0
  126. package/dist/types/adapter.d.ts +38 -0
  127. package/dist/types/adapter.d.ts.map +1 -0
  128. package/dist/types/adapter.js +1 -0
  129. package/dist/types/animation.d.ts +31 -0
  130. package/dist/types/animation.d.ts.map +1 -0
  131. package/dist/types/animation.js +1 -0
  132. package/dist/types/app.d.ts +148 -0
  133. package/dist/types/app.d.ts.map +1 -0
  134. package/dist/types/app.js +1 -0
  135. package/dist/types/async.d.ts +30 -0
  136. package/dist/types/async.d.ts.map +1 -0
  137. package/dist/types/async.js +1 -0
  138. package/dist/types/capabilities.d.ts +44 -0
  139. package/dist/types/capabilities.d.ts.map +1 -0
  140. package/dist/types/capabilities.js +33 -0
  141. package/dist/types/cells.d.ts +72 -0
  142. package/dist/types/cells.d.ts.map +1 -0
  143. package/dist/types/cells.js +10 -0
  144. package/dist/types/command.d.ts +127 -0
  145. package/dist/types/command.d.ts.map +1 -0
  146. package/dist/types/command.js +1 -0
  147. package/dist/types/component-registry.d.ts +77 -0
  148. package/dist/types/component-registry.d.ts.map +1 -0
  149. package/dist/types/component-registry.js +1 -0
  150. package/dist/types/disposable.d.ts +8 -0
  151. package/dist/types/disposable.d.ts.map +1 -0
  152. package/dist/types/disposable.js +1 -0
  153. package/dist/types/focus.d.ts +72 -0
  154. package/dist/types/focus.d.ts.map +1 -0
  155. package/dist/types/focus.js +1 -0
  156. package/dist/types/geometry.d.ts +28 -0
  157. package/dist/types/geometry.d.ts.map +1 -0
  158. package/dist/types/geometry.js +14 -0
  159. package/dist/types/graph.d.ts +129 -0
  160. package/dist/types/graph.d.ts.map +1 -0
  161. package/dist/types/graph.js +31 -0
  162. package/dist/types/i18n.d.ts +23 -0
  163. package/dist/types/i18n.d.ts.map +1 -0
  164. package/dist/types/i18n.js +1 -0
  165. package/dist/types/index.d.ts +32 -0
  166. package/dist/types/index.d.ts.map +1 -0
  167. package/dist/types/index.js +31 -0
  168. package/dist/types/input.d.ts +65 -0
  169. package/dist/types/input.d.ts.map +1 -0
  170. package/dist/types/input.js +1 -0
  171. package/dist/types/keybinding.d.ts +49 -0
  172. package/dist/types/keybinding.d.ts.map +1 -0
  173. package/dist/types/keybinding.js +1 -0
  174. package/dist/types/layer.d.ts +54 -0
  175. package/dist/types/layer.d.ts.map +1 -0
  176. package/dist/types/layer.js +1 -0
  177. package/dist/types/manifest.d.ts +114 -0
  178. package/dist/types/manifest.d.ts.map +1 -0
  179. package/dist/types/manifest.js +1 -0
  180. package/dist/types/markdown.d.ts +58 -0
  181. package/dist/types/markdown.d.ts.map +1 -0
  182. package/dist/types/markdown.js +1 -0
  183. package/dist/types/navigation.d.ts +40 -0
  184. package/dist/types/navigation.d.ts.map +1 -0
  185. package/dist/types/navigation.js +1 -0
  186. package/dist/types/render.d.ts +72 -0
  187. package/dist/types/render.d.ts.map +1 -0
  188. package/dist/types/render.js +1 -0
  189. package/dist/types/resource.d.ts +170 -0
  190. package/dist/types/resource.d.ts.map +1 -0
  191. package/dist/types/resource.js +1 -0
  192. package/dist/types/services.d.ts +24 -0
  193. package/dist/types/services.d.ts.map +1 -0
  194. package/dist/types/services.js +3 -0
  195. package/dist/types/shell.d.ts +32 -0
  196. package/dist/types/shell.d.ts.map +1 -0
  197. package/dist/types/shell.js +1 -0
  198. package/dist/types/store.d.ts +124 -0
  199. package/dist/types/store.d.ts.map +1 -0
  200. package/dist/types/store.js +1 -0
  201. package/dist/types/stream.d.ts +18 -0
  202. package/dist/types/stream.d.ts.map +1 -0
  203. package/dist/types/stream.js +1 -0
  204. package/dist/types/style.d.ts +166 -0
  205. package/dist/types/style.d.ts.map +1 -0
  206. package/dist/types/style.js +1 -0
  207. package/dist/types/surface.d.ts +97 -0
  208. package/dist/types/surface.d.ts.map +1 -0
  209. package/dist/types/surface.js +1 -0
  210. package/dist/types/syntax.d.ts +68 -0
  211. package/dist/types/syntax.d.ts.map +1 -0
  212. package/dist/types/syntax.js +5 -0
  213. package/dist/types/terminal.d.ts +52 -0
  214. package/dist/types/terminal.d.ts.map +1 -0
  215. package/dist/types/terminal.js +1 -0
  216. package/dist/types/theme.d.ts +117 -0
  217. package/dist/types/theme.d.ts.map +1 -0
  218. package/dist/types/theme.js +1 -0
  219. package/dist/types/when.d.ts +19 -0
  220. package/dist/types/when.d.ts.map +1 -0
  221. package/dist/types/when.js +1 -0
  222. package/dist/ui/primitives.d.ts +56 -0
  223. package/dist/ui/primitives.d.ts.map +1 -0
  224. package/dist/ui/primitives.js +108 -0
  225. package/dist/ui/screen.d.ts +23 -0
  226. package/dist/ui/screen.d.ts.map +1 -0
  227. package/dist/ui/screen.js +18 -0
  228. package/dist/util/disposable.d.ts +6 -0
  229. package/dist/util/disposable.d.ts.map +1 -0
  230. package/dist/util/disposable.js +49 -0
  231. package/dist/util/markdown.d.ts +24 -0
  232. package/dist/util/markdown.d.ts.map +1 -0
  233. package/dist/util/markdown.js +220 -0
  234. package/dist/util/paths.d.ts +47 -0
  235. package/dist/util/paths.d.ts.map +1 -0
  236. package/dist/util/paths.js +134 -0
  237. package/dist/util/stream.d.ts +32 -0
  238. package/dist/util/stream.d.ts.map +1 -0
  239. package/dist/util/stream.js +206 -0
  240. package/dist/util/text.d.ts +65 -0
  241. package/dist/util/text.d.ts.map +1 -0
  242. package/dist/util/text.js +419 -0
  243. package/package.json +70 -0
  244. package/src/adapters/index.ts +11 -0
  245. package/src/app/app.ts +1096 -0
  246. package/src/core/animation.ts +144 -0
  247. package/src/core/clipboard.ts +40 -0
  248. package/src/core/commands.ts +169 -0
  249. package/src/core/components.ts +91 -0
  250. package/src/core/events.ts +95 -0
  251. package/src/core/focus.ts +358 -0
  252. package/src/core/i18n.ts +107 -0
  253. package/src/core/keybindings.ts +184 -0
  254. package/src/core/layers.ts +94 -0
  255. package/src/core/manifest.ts +84 -0
  256. package/src/core/navigation.ts +135 -0
  257. package/src/core/resources.ts +362 -0
  258. package/src/core/services.ts +69 -0
  259. package/src/core/store.ts +640 -0
  260. package/src/core/surfaces.ts +292 -0
  261. package/src/core/syntax.ts +200 -0
  262. package/src/core/when.ts +238 -0
  263. package/src/index.ts +76 -0
  264. package/src/jsx/factory.ts +124 -0
  265. package/src/jsx/intrinsics.ts +99 -0
  266. package/src/jsx/jsx-dev-runtime.ts +23 -0
  267. package/src/jsx/jsx-runtime.ts +76 -0
  268. package/src/render/buffer.ts +318 -0
  269. package/src/render/color.ts +180 -0
  270. package/src/render/diff.ts +99 -0
  271. package/src/render/layout.ts +764 -0
  272. package/src/render/static.ts +290 -0
  273. package/src/runtime/bindings.ts +150 -0
  274. package/src/runtime/hooks.ts +1057 -0
  275. package/src/runtime/instance.ts +284 -0
  276. package/src/runtime/paint.ts +737 -0
  277. package/src/runtime/reconcile.ts +335 -0
  278. package/src/runtime/runtime.ts +47 -0
  279. package/src/runtime/style.ts +205 -0
  280. package/src/themes/borders.ts +95 -0
  281. package/src/themes/builtin.ts +301 -0
  282. package/src/themes/glyphs.ts +101 -0
  283. package/src/themes/index.ts +4 -0
  284. package/src/themes/registry.ts +224 -0
  285. package/src/types/adapter.ts +41 -0
  286. package/src/types/animation.ts +34 -0
  287. package/src/types/app.ts +149 -0
  288. package/src/types/async.ts +35 -0
  289. package/src/types/capabilities.ts +79 -0
  290. package/src/types/cells.ts +79 -0
  291. package/src/types/command.ts +139 -0
  292. package/src/types/component-registry.ts +82 -0
  293. package/src/types/disposable.ts +8 -0
  294. package/src/types/focus.ts +77 -0
  295. package/src/types/geometry.ts +50 -0
  296. package/src/types/graph.ts +174 -0
  297. package/src/types/i18n.ts +25 -0
  298. package/src/types/index.ts +31 -0
  299. package/src/types/input.ts +81 -0
  300. package/src/types/keybinding.ts +51 -0
  301. package/src/types/layer.ts +46 -0
  302. package/src/types/manifest.ts +101 -0
  303. package/src/types/markdown.ts +47 -0
  304. package/src/types/navigation.ts +42 -0
  305. package/src/types/render.ts +86 -0
  306. package/src/types/resource.ts +185 -0
  307. package/src/types/services.ts +28 -0
  308. package/src/types/shell.ts +30 -0
  309. package/src/types/store.ts +148 -0
  310. package/src/types/stream.ts +24 -0
  311. package/src/types/style.ts +205 -0
  312. package/src/types/surface.ts +119 -0
  313. package/src/types/syntax.ts +93 -0
  314. package/src/types/terminal.ts +58 -0
  315. package/src/types/theme.ts +121 -0
  316. package/src/types/when.ts +21 -0
  317. package/src/ui/primitives.ts +118 -0
  318. package/src/ui/screen.ts +41 -0
  319. package/src/util/disposable.ts +49 -0
  320. package/src/util/markdown.ts +225 -0
  321. package/src/util/paths.ts +138 -0
  322. package/src/util/stream.ts +213 -0
  323. 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
+ ];
@@ -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
+ }