@effect-tui/react 0.1.0-alpha.1

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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/dist/jsx-dev-runtime.d.ts +3 -0
  4. package/dist/jsx-dev-runtime.d.ts.map +1 -0
  5. package/dist/jsx-dev-runtime.js +3 -0
  6. package/dist/jsx-dev-runtime.js.map +1 -0
  7. package/dist/jsx-runtime.d.ts +47 -0
  8. package/dist/jsx-runtime.d.ts.map +1 -0
  9. package/dist/jsx-runtime.js +6 -0
  10. package/dist/jsx-runtime.js.map +1 -0
  11. package/dist/src/codeblock.d.ts +9 -0
  12. package/dist/src/codeblock.d.ts.map +1 -0
  13. package/dist/src/codeblock.js +24 -0
  14. package/dist/src/codeblock.js.map +1 -0
  15. package/dist/src/constants.d.ts +3 -0
  16. package/dist/src/constants.d.ts.map +1 -0
  17. package/dist/src/constants.js +3 -0
  18. package/dist/src/constants.js.map +1 -0
  19. package/dist/src/debug/DiagnosticsPanel.d.ts +7 -0
  20. package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -0
  21. package/dist/src/debug/DiagnosticsPanel.js +13 -0
  22. package/dist/src/debug/DiagnosticsPanel.js.map +1 -0
  23. package/dist/src/highlight.d.ts +20 -0
  24. package/dist/src/highlight.d.ts.map +1 -0
  25. package/dist/src/highlight.js +51 -0
  26. package/dist/src/highlight.js.map +1 -0
  27. package/dist/src/hooks/index.d.ts +4 -0
  28. package/dist/src/hooks/index.d.ts.map +1 -0
  29. package/dist/src/hooks/index.js +3 -0
  30. package/dist/src/hooks/index.js.map +1 -0
  31. package/dist/src/hooks/use-keyboard.d.ts +18 -0
  32. package/dist/src/hooks/use-keyboard.d.ts.map +1 -0
  33. package/dist/src/hooks/use-keyboard.js +26 -0
  34. package/dist/src/hooks/use-keyboard.js.map +1 -0
  35. package/dist/src/hooks/use-paste.d.ts +5 -0
  36. package/dist/src/hooks/use-paste.d.ts.map +1 -0
  37. package/dist/src/hooks/use-paste.js +14 -0
  38. package/dist/src/hooks/use-paste.js.map +1 -0
  39. package/dist/src/hooks/useFrameStats.d.ts +7 -0
  40. package/dist/src/hooks/useFrameStats.d.ts.map +1 -0
  41. package/dist/src/hooks/useFrameStats.js +28 -0
  42. package/dist/src/hooks/useFrameStats.js.map +1 -0
  43. package/dist/src/hosts/base.d.ts +22 -0
  44. package/dist/src/hosts/base.d.ts.map +1 -0
  45. package/dist/src/hosts/base.js +53 -0
  46. package/dist/src/hosts/base.js.map +1 -0
  47. package/dist/src/hosts/box.d.ts +26 -0
  48. package/dist/src/hosts/box.d.ts.map +1 -0
  49. package/dist/src/hosts/box.js +84 -0
  50. package/dist/src/hosts/box.js.map +1 -0
  51. package/dist/src/hosts/canvas.d.ts +48 -0
  52. package/dist/src/hosts/canvas.d.ts.map +1 -0
  53. package/dist/src/hosts/canvas.js +109 -0
  54. package/dist/src/hosts/canvas.js.map +1 -0
  55. package/dist/src/hosts/codeblock.d.ts +32 -0
  56. package/dist/src/hosts/codeblock.d.ts.map +1 -0
  57. package/dist/src/hosts/codeblock.js +118 -0
  58. package/dist/src/hosts/codeblock.js.map +1 -0
  59. package/dist/src/hosts/hstack.d.ts +18 -0
  60. package/dist/src/hosts/hstack.d.ts.map +1 -0
  61. package/dist/src/hosts/hstack.js +45 -0
  62. package/dist/src/hosts/hstack.js.map +1 -0
  63. package/dist/src/hosts/index.d.ts +16 -0
  64. package/dist/src/hosts/index.d.ts.map +1 -0
  65. package/dist/src/hosts/index.js +40 -0
  66. package/dist/src/hosts/index.js.map +1 -0
  67. package/dist/src/hosts/spacer.d.ts +19 -0
  68. package/dist/src/hosts/spacer.d.ts.map +1 -0
  69. package/dist/src/hosts/spacer.js +28 -0
  70. package/dist/src/hosts/spacer.js.map +1 -0
  71. package/dist/src/hosts/text.d.ts +43 -0
  72. package/dist/src/hosts/text.d.ts.map +1 -0
  73. package/dist/src/hosts/text.js +148 -0
  74. package/dist/src/hosts/text.js.map +1 -0
  75. package/dist/src/hosts/vstack.d.ts +18 -0
  76. package/dist/src/hosts/vstack.d.ts.map +1 -0
  77. package/dist/src/hosts/vstack.js +45 -0
  78. package/dist/src/hosts/vstack.js.map +1 -0
  79. package/dist/src/hosts/zstack.d.ts +20 -0
  80. package/dist/src/hosts/zstack.d.ts.map +1 -0
  81. package/dist/src/hosts/zstack.js +65 -0
  82. package/dist/src/hosts/zstack.js.map +1 -0
  83. package/dist/src/index.d.ts +20 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +20 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/inline/index.d.ts +32 -0
  88. package/dist/src/inline/index.d.ts.map +1 -0
  89. package/dist/src/inline/index.js +111 -0
  90. package/dist/src/inline/index.js.map +1 -0
  91. package/dist/src/jsx.d.ts +2 -0
  92. package/dist/src/jsx.d.ts.map +1 -0
  93. package/dist/src/jsx.js +4 -0
  94. package/dist/src/jsx.js.map +1 -0
  95. package/dist/src/motion/color-motion-value.d.ts +32 -0
  96. package/dist/src/motion/color-motion-value.d.ts.map +1 -0
  97. package/dist/src/motion/color-motion-value.js +80 -0
  98. package/dist/src/motion/color-motion-value.js.map +1 -0
  99. package/dist/src/motion/color.d.ts +30 -0
  100. package/dist/src/motion/color.d.ts.map +1 -0
  101. package/dist/src/motion/color.js +172 -0
  102. package/dist/src/motion/color.js.map +1 -0
  103. package/dist/src/motion/color.test.d.ts +2 -0
  104. package/dist/src/motion/color.test.d.ts.map +1 -0
  105. package/dist/src/motion/color.test.js +97 -0
  106. package/dist/src/motion/color.test.js.map +1 -0
  107. package/dist/src/motion/event-emitter.d.ts +18 -0
  108. package/dist/src/motion/event-emitter.d.ts.map +1 -0
  109. package/dist/src/motion/event-emitter.js +30 -0
  110. package/dist/src/motion/event-emitter.js.map +1 -0
  111. package/dist/src/motion/frame.d.ts +9 -0
  112. package/dist/src/motion/frame.d.ts.map +1 -0
  113. package/dist/src/motion/frame.js +51 -0
  114. package/dist/src/motion/frame.js.map +1 -0
  115. package/dist/src/motion/hooks.d.ts +75 -0
  116. package/dist/src/motion/hooks.d.ts.map +1 -0
  117. package/dist/src/motion/hooks.js +190 -0
  118. package/dist/src/motion/hooks.js.map +1 -0
  119. package/dist/src/motion/index.d.ts +4 -0
  120. package/dist/src/motion/index.d.ts.map +1 -0
  121. package/dist/src/motion/index.js +7 -0
  122. package/dist/src/motion/index.js.map +1 -0
  123. package/dist/src/motion/motion-value.d.ts +40 -0
  124. package/dist/src/motion/motion-value.d.ts.map +1 -0
  125. package/dist/src/motion/motion-value.js +109 -0
  126. package/dist/src/motion/motion-value.js.map +1 -0
  127. package/dist/src/motion/motion-value.test.d.ts +2 -0
  128. package/dist/src/motion/motion-value.test.d.ts.map +1 -0
  129. package/dist/src/motion/motion-value.test.js +177 -0
  130. package/dist/src/motion/motion-value.test.js.map +1 -0
  131. package/dist/src/motion/spring-math.d.ts +28 -0
  132. package/dist/src/motion/spring-math.d.ts.map +1 -0
  133. package/dist/src/motion/spring-math.js +81 -0
  134. package/dist/src/motion/spring-math.js.map +1 -0
  135. package/dist/src/motion/types.d.ts +25 -0
  136. package/dist/src/motion/types.d.ts.map +1 -0
  137. package/dist/src/motion/types.js +13 -0
  138. package/dist/src/motion/types.js.map +1 -0
  139. package/dist/src/output.d.ts +47 -0
  140. package/dist/src/output.d.ts.map +1 -0
  141. package/dist/src/output.js +125 -0
  142. package/dist/src/output.js.map +1 -0
  143. package/dist/src/profiler.d.ts +6 -0
  144. package/dist/src/profiler.d.ts.map +1 -0
  145. package/dist/src/profiler.js +73 -0
  146. package/dist/src/profiler.js.map +1 -0
  147. package/dist/src/reconciler/host-config.d.ts +16 -0
  148. package/dist/src/reconciler/host-config.d.ts.map +1 -0
  149. package/dist/src/reconciler/host-config.js +174 -0
  150. package/dist/src/reconciler/host-config.js.map +1 -0
  151. package/dist/src/reconciler/types.d.ts +52 -0
  152. package/dist/src/reconciler/types.d.ts.map +1 -0
  153. package/dist/src/reconciler/types.js +2 -0
  154. package/dist/src/reconciler/types.js.map +1 -0
  155. package/dist/src/renderer.d.ts +101 -0
  156. package/dist/src/renderer.d.ts.map +1 -0
  157. package/dist/src/renderer.js +509 -0
  158. package/dist/src/renderer.js.map +1 -0
  159. package/dist/src/terminal.d.ts +37 -0
  160. package/dist/src/terminal.d.ts.map +1 -0
  161. package/dist/src/terminal.js +65 -0
  162. package/dist/src/terminal.js.map +1 -0
  163. package/dist/src/test/index.d.ts +3 -0
  164. package/dist/src/test/index.d.ts.map +1 -0
  165. package/dist/src/test/index.js +3 -0
  166. package/dist/src/test/index.js.map +1 -0
  167. package/dist/src/test/mock-streams.d.ts +44 -0
  168. package/dist/src/test/mock-streams.d.ts.map +1 -0
  169. package/dist/src/test/mock-streams.js +136 -0
  170. package/dist/src/test/mock-streams.js.map +1 -0
  171. package/dist/src/test/render-tui.d.ts +47 -0
  172. package/dist/src/test/render-tui.d.ts.map +1 -0
  173. package/dist/src/test/render-tui.js +76 -0
  174. package/dist/src/test/render-tui.js.map +1 -0
  175. package/dist/src/trace/SpanTree.d.ts +10 -0
  176. package/dist/src/trace/SpanTree.d.ts.map +1 -0
  177. package/dist/src/trace/SpanTree.js +104 -0
  178. package/dist/src/trace/SpanTree.js.map +1 -0
  179. package/dist/src/trace/index.d.ts +30 -0
  180. package/dist/src/trace/index.d.ts.map +1 -0
  181. package/dist/src/trace/index.js +142 -0
  182. package/dist/src/trace/index.js.map +1 -0
  183. package/dist/src/trace/location.d.ts +9 -0
  184. package/dist/src/trace/location.d.ts.map +1 -0
  185. package/dist/src/trace/location.js +88 -0
  186. package/dist/src/trace/location.js.map +1 -0
  187. package/dist/src/trace/span-processor.d.ts +16 -0
  188. package/dist/src/trace/span-processor.d.ts.map +1 -0
  189. package/dist/src/trace/span-processor.js +54 -0
  190. package/dist/src/trace/span-processor.js.map +1 -0
  191. package/dist/src/trace/span-state.d.ts +79 -0
  192. package/dist/src/trace/span-state.d.ts.map +1 -0
  193. package/dist/src/trace/span-state.js +229 -0
  194. package/dist/src/trace/span-state.js.map +1 -0
  195. package/dist/src/trace/tui-logger.d.ts +8 -0
  196. package/dist/src/trace/tui-logger.d.ts.map +1 -0
  197. package/dist/src/trace/tui-logger.js +70 -0
  198. package/dist/src/trace/tui-logger.js.map +1 -0
  199. package/dist/src/utils/border.d.ts +31 -0
  200. package/dist/src/utils/border.d.ts.map +1 -0
  201. package/dist/src/utils/border.js +81 -0
  202. package/dist/src/utils/border.js.map +1 -0
  203. package/dist/src/utils/flex-layout.d.ts +20 -0
  204. package/dist/src/utils/flex-layout.d.ts.map +1 -0
  205. package/dist/src/utils/flex-layout.js +85 -0
  206. package/dist/src/utils/flex-layout.js.map +1 -0
  207. package/dist/src/utils/index.d.ts +5 -0
  208. package/dist/src/utils/index.d.ts.map +1 -0
  209. package/dist/src/utils/index.js +5 -0
  210. package/dist/src/utils/index.js.map +1 -0
  211. package/dist/src/utils/padding.d.ts +26 -0
  212. package/dist/src/utils/padding.d.ts.map +1 -0
  213. package/dist/src/utils/padding.js +34 -0
  214. package/dist/src/utils/padding.js.map +1 -0
  215. package/dist/src/utils/styles.d.ts +13 -0
  216. package/dist/src/utils/styles.d.ts.map +1 -0
  217. package/dist/src/utils/styles.js +5 -0
  218. package/dist/src/utils/styles.js.map +1 -0
  219. package/dist/src/visualize/index.d.ts +50 -0
  220. package/dist/src/visualize/index.d.ts.map +1 -0
  221. package/dist/src/visualize/index.js +194 -0
  222. package/dist/src/visualize/index.js.map +1 -0
  223. package/dist/tsconfig.tsbuildinfo +1 -0
  224. package/package.json +94 -0
  225. package/src/codeblock.tsx +47 -0
  226. package/src/constants.ts +2 -0
  227. package/src/debug/DiagnosticsPanel.tsx +38 -0
  228. package/src/highlight.ts +76 -0
  229. package/src/hooks/index.ts +3 -0
  230. package/src/hooks/use-keyboard.ts +37 -0
  231. package/src/hooks/use-paste.ts +14 -0
  232. package/src/hooks/useFrameStats.ts +32 -0
  233. package/src/hosts/base.ts +65 -0
  234. package/src/hosts/box.ts +105 -0
  235. package/src/hosts/canvas.ts +155 -0
  236. package/src/hosts/codeblock.ts +145 -0
  237. package/src/hosts/hstack.ts +64 -0
  238. package/src/hosts/index.ts +45 -0
  239. package/src/hosts/spacer.ts +40 -0
  240. package/src/hosts/text.ts +175 -0
  241. package/src/hosts/vstack.ts +64 -0
  242. package/src/hosts/zstack.ts +77 -0
  243. package/src/index.ts +62 -0
  244. package/src/inline/index.tsx +181 -0
  245. package/src/jsx.ts +3 -0
  246. package/src/motion/color-motion-value.ts +90 -0
  247. package/src/motion/color.test.ts +115 -0
  248. package/src/motion/color.ts +191 -0
  249. package/src/motion/event-emitter.ts +35 -0
  250. package/src/motion/frame.ts +59 -0
  251. package/src/motion/hooks.ts +237 -0
  252. package/src/motion/index.ts +17 -0
  253. package/src/motion/motion-value.test.ts +222 -0
  254. package/src/motion/motion-value.ts +140 -0
  255. package/src/motion/spring-math.ts +114 -0
  256. package/src/motion/types.ts +34 -0
  257. package/src/output.ts +156 -0
  258. package/src/profiler.ts +88 -0
  259. package/src/reconciler/host-config.ts +277 -0
  260. package/src/reconciler/types.ts +66 -0
  261. package/src/renderer.ts +661 -0
  262. package/src/terminal.ts +67 -0
  263. package/src/test/index.ts +8 -0
  264. package/src/test/mock-streams.ts +149 -0
  265. package/src/test/render-tui.ts +118 -0
  266. package/src/trace/SpanTree.tsx +195 -0
  267. package/src/trace/index.tsx +205 -0
  268. package/src/trace/location.ts +90 -0
  269. package/src/trace/span-processor.ts +65 -0
  270. package/src/trace/span-state.ts +286 -0
  271. package/src/trace/tui-logger.ts +72 -0
  272. package/src/utils/border.ts +108 -0
  273. package/src/utils/flex-layout.ts +125 -0
  274. package/src/utils/index.ts +4 -0
  275. package/src/utils/padding.ts +45 -0
  276. package/src/utils/styles.ts +14 -0
  277. package/src/visualize/index.tsx +305 -0
@@ -0,0 +1,145 @@
1
+ import {
2
+ Colors,
3
+ displayWidth,
4
+ parseColor,
5
+ type CellBuffer,
6
+ type ColorLike,
7
+ type Palette,
8
+ type ColorValue,
9
+ } from "@effect-tui/core"
10
+ import type { HighlightLine } from "../highlight.js"
11
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
12
+ import { BaseHost } from "./base.js"
13
+ import { type Padding, type PaddingInput, resolvePadding } from "../utils/index.js"
14
+
15
+ export interface CodeBlockProps extends CommonProps {
16
+ lines: HighlightLine[]
17
+ lineNumbers?: boolean
18
+ padding?: PaddingInput
19
+ background?: ColorLike
20
+ lineNumberColor?: ColorLike
21
+ lineNumberBackground?: ColorLike
22
+ }
23
+
24
+ function normalizeColor(c: unknown): ColorValue | undefined {
25
+ if (c === undefined || c === null) return undefined
26
+ if (typeof c === "number" || typeof c === "object") return c as ColorValue
27
+ return parseColor(c as string)
28
+ }
29
+
30
+ function lineDisplayWidth(line: HighlightLine): number {
31
+ return line.reduce((w, token) => w + displayWidth(token.text), 0)
32
+ }
33
+
34
+ export class CodeBlockHost extends BaseHost {
35
+ lines: HighlightLine[] = [[]]
36
+ lineNumbers = false
37
+ padding: Padding = { top: 0, right: 0, bottom: 0, left: 0 }
38
+ background?: ColorLike
39
+ lineNumberColor?: ColorLike
40
+ lineNumberBackground?: ColorLike
41
+
42
+ private cachedLineWidths: number[] = []
43
+ private gutterWidth = 0
44
+
45
+ constructor(props: CodeBlockProps, ctx: HostContext) {
46
+ super("codeblock", props, ctx)
47
+ this.updateProps(props)
48
+ }
49
+
50
+ private computeGutterWidth(): number {
51
+ if (!this.lineNumbers) return 0
52
+ // Fix width so layout doesn't shift when moving from 1→2 digits.
53
+ const digits = Math.max(2, String(Math.max(1, this.lines.length)).length)
54
+ // digits plus a trailing space
55
+ return digits + 1
56
+ }
57
+
58
+ private get insetX(): number {
59
+ return this.padding.left + this.padding.right + this.gutterWidth
60
+ }
61
+
62
+ private get insetY(): number {
63
+ return this.padding.top + this.padding.bottom
64
+ }
65
+
66
+ measure(maxW: number, maxH: number): Size {
67
+ this.cachedLineWidths = this.lines.map((l) => lineDisplayWidth(l))
68
+ this.gutterWidth = this.computeGutterWidth()
69
+
70
+ const maxLineW = this.cachedLineWidths.reduce((max, w) => (w > max ? w : max), 0)
71
+ const contentW = maxLineW + this.insetX
72
+
73
+ const innerHeight = Math.max(1, this.lines.length) + this.insetY
74
+
75
+ return {
76
+ w: Math.min(maxW, contentW),
77
+ h: Math.min(maxH, innerHeight),
78
+ }
79
+ }
80
+
81
+ override layout(rect: Rect): void {
82
+ super.layout(rect)
83
+ }
84
+
85
+ render(buffer: CellBuffer, palette: Palette): void {
86
+ if (!this.rect) return
87
+
88
+ const { x, y, w, h } = this.rect
89
+ const contentWidth = Math.max(0, w - this.insetX)
90
+ const maxLines = Math.max(0, Math.min(this.lines.length, h - this.insetY))
91
+ const startX = x + this.padding.left + this.gutterWidth
92
+ const startY = y + this.padding.top
93
+
94
+ if (this.background !== undefined && w > 0 && h > 0) {
95
+ const bgStyle = palette.id({ bg: normalizeColor(this.background) })
96
+ buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyle)
97
+ }
98
+
99
+ for (let i = 0; i < maxLines; i++) {
100
+ const lineY = startY + i
101
+ let drawX = startX
102
+
103
+ if (this.lineNumbers) {
104
+ const gutterStyle = palette.id({
105
+ fg: normalizeColor(this.lineNumberColor) ?? Colors.gray(11),
106
+ bg: normalizeColor(this.lineNumberBackground ?? this.background),
107
+ })
108
+ const digits = String(i + 1).padStart(this.gutterWidth - 1, " ")
109
+ buffer.drawText(x + this.padding.left, lineY, `${digits} `, gutterStyle, this.gutterWidth)
110
+ }
111
+
112
+ const line = this.lines[i] ?? []
113
+ for (const token of line) {
114
+ if (contentWidth <= 0) break
115
+ const remaining = x + w - drawX
116
+ if (remaining <= 0) break
117
+
118
+ const style = token.style ?? {}
119
+ const fg = normalizeColor(style.fg)
120
+ const bg = normalizeColor(style.bg ?? this.background)
121
+ const styleId = palette.id({
122
+ fg,
123
+ bg,
124
+ bold: style.bold,
125
+ italic: style.italic,
126
+ underline: style.underline,
127
+ })
128
+
129
+ buffer.drawText(drawX, lineY, token.text, styleId, remaining)
130
+ drawX += displayWidth(token.text)
131
+ if (drawX >= x + w) break
132
+ }
133
+ }
134
+ }
135
+
136
+ override updateProps(props: Record<string, unknown>): void {
137
+ super.updateProps(props)
138
+ if (props.lines !== undefined) this.lines = props.lines as HighlightLine[]
139
+ if (props.lineNumbers !== undefined) this.lineNumbers = !!props.lineNumbers
140
+ if (props.padding !== undefined) this.padding = resolvePadding(props.padding as CodeBlockProps["padding"])
141
+ if (props.background !== undefined) this.background = props.background as ColorLike
142
+ if (props.lineNumberColor !== undefined) this.lineNumberColor = props.lineNumberColor as ColorLike
143
+ if (props.lineNumberBackground !== undefined) this.lineNumberBackground = props.lineNumberBackground as ColorLike
144
+ }
145
+ }
@@ -0,0 +1,64 @@
1
+ import type { CellBuffer, Palette } from "@effect-tui/core"
2
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
3
+ import { BaseHost } from "./base.js"
4
+ import { measureFlex, layoutFlex, type FlexAlignment } from "../utils/index.js"
5
+
6
+ export interface HStackProps extends CommonProps {
7
+ spacing?: number
8
+ alignment?: "top" | "center" | "bottom"
9
+ }
10
+
11
+ // Map HStack alignment names to generic flex alignment
12
+ function toFlexAlignment(alignment: "top" | "center" | "bottom"): FlexAlignment {
13
+ switch (alignment) {
14
+ case "top":
15
+ return "start"
16
+ case "center":
17
+ return "center"
18
+ case "bottom":
19
+ return "end"
20
+ }
21
+ }
22
+
23
+ export class HStackHost extends BaseHost {
24
+ spacing = 0
25
+ alignment: "top" | "center" | "bottom" = "top"
26
+ private cachedSizes: Size[] = []
27
+
28
+ constructor(props: HStackProps, ctx: HostContext) {
29
+ super("hstack", props, ctx)
30
+ this.updateProps(props)
31
+ }
32
+
33
+ measure(maxW: number, maxH: number): Size {
34
+ const result = measureFlex("horizontal", this.children, this.spacing, maxW, maxH)
35
+ this.cachedSizes = result.sizes
36
+ return result.totalSize
37
+ }
38
+
39
+ override layout(rect: Rect): void {
40
+ super.layout(rect)
41
+ const stretchCross = this.alignment === "top"
42
+ layoutFlex(
43
+ "horizontal",
44
+ this.children,
45
+ this.cachedSizes,
46
+ rect,
47
+ this.spacing,
48
+ toFlexAlignment(this.alignment),
49
+ stretchCross,
50
+ )
51
+ }
52
+
53
+ render(buffer: CellBuffer, palette: Palette): void {
54
+ for (const child of this.children) {
55
+ child.render(buffer, palette)
56
+ }
57
+ }
58
+
59
+ override updateProps(props: Record<string, unknown>): void {
60
+ super.updateProps(props)
61
+ if (props.spacing !== undefined) this.spacing = props.spacing as number
62
+ if (props.alignment !== undefined) this.alignment = props.alignment as "top" | "center" | "bottom"
63
+ }
64
+ }
@@ -0,0 +1,45 @@
1
+ import type { HostContext, CommonProps } from "../reconciler/types.js"
2
+ import type { BaseHost } from "./base.js"
3
+ import { TextHost, RawTextHost } from "./text.js"
4
+ import { SpacerHost } from "./spacer.js"
5
+ import { VStackHost } from "./vstack.js"
6
+ import { HStackHost } from "./hstack.js"
7
+ import { ZStackHost } from "./zstack.js"
8
+ import { BoxHost } from "./box.js"
9
+ import { CanvasHost } from "./canvas.js"
10
+ import { CodeBlockHost } from "./codeblock.js"
11
+
12
+ export { BaseHost } from "./base.js"
13
+ export { TextHost, RawTextHost, type TextProps } from "./text.js"
14
+ export { SpacerHost, type SpacerProps } from "./spacer.js"
15
+ export { VStackHost, type VStackProps } from "./vstack.js"
16
+ export { HStackHost, type HStackProps } from "./hstack.js"
17
+ export { ZStackHost, type ZStackProps } from "./zstack.js"
18
+ export { BoxHost, type BoxProps } from "./box.js"
19
+ export { CanvasHost, type CanvasProps, type DrawContext } from "./canvas.js"
20
+ export { CodeBlockHost, type CodeBlockProps } from "./codeblock.js"
21
+
22
+ // Use any to allow specialized props on each host type
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ export const hostRegistry: Record<string, new (props: any, ctx: HostContext) => BaseHost> = {
25
+ text: TextHost,
26
+ spacer: SpacerHost,
27
+ vstack: VStackHost,
28
+ hstack: HStackHost,
29
+ zstack: ZStackHost,
30
+ box: BoxHost,
31
+ canvas: CanvasHost,
32
+ codeblock: CodeBlockHost,
33
+ }
34
+
35
+ export function createHostInstance(type: string, props: CommonProps, ctx: HostContext): BaseHost {
36
+ const Host = hostRegistry[type]
37
+ if (!Host) {
38
+ throw new Error(`Unknown host component type: ${type}`)
39
+ }
40
+ return new Host(props, ctx)
41
+ }
42
+
43
+ export function createTextInstance(text: string, ctx: HostContext): RawTextHost {
44
+ return new RawTextHost(text, ctx)
45
+ }
@@ -0,0 +1,40 @@
1
+ import type { CellBuffer, Palette } from "@effect-tui/core"
2
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
3
+ import { BaseHost } from "./base.js"
4
+
5
+ export interface SpacerProps extends CommonProps {
6
+ /** Minimum width (default 0) */
7
+ minWidth?: number
8
+ /** Minimum height (default 0) */
9
+ minHeight?: number
10
+ }
11
+
12
+ export class SpacerHost extends BaseHost {
13
+ minWidth = 0
14
+ minHeight = 0
15
+
16
+ constructor(props: SpacerProps, ctx: HostContext) {
17
+ // Spacers have flexGrow=1 by default
18
+ super("spacer", { flexGrow: 1, ...props }, ctx)
19
+ this.updateProps(props)
20
+ }
21
+
22
+ measure(_maxW: number, _maxH: number): Size {
23
+ // Spacers have no natural size, they expand via flexGrow
24
+ return { w: this.minWidth, h: this.minHeight }
25
+ }
26
+
27
+ override layout(rect: Rect): void {
28
+ super.layout(rect)
29
+ }
30
+
31
+ render(_buffer: CellBuffer, _palette: Palette): void {
32
+ // Spacers render nothing
33
+ }
34
+
35
+ override updateProps(props: Record<string, unknown>): void {
36
+ super.updateProps(props)
37
+ if (props.minWidth !== undefined) this.minWidth = props.minWidth as number
38
+ if (props.minHeight !== undefined) this.minHeight = props.minHeight as number
39
+ }
40
+ }
@@ -0,0 +1,175 @@
1
+ import { displayWidth, parseColor, type CellBuffer, type Palette, type ColorLike } from "@effect-tui/core"
2
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
3
+ import { BaseHost } from "./base.js"
4
+
5
+ export interface TextProps extends CommonProps {
6
+ fg?: ColorLike
7
+ bg?: ColorLike
8
+ bold?: boolean
9
+ italic?: boolean
10
+ underline?: boolean
11
+ inverse?: boolean
12
+ /** If true, wrap text to multiple lines (default: false, text is truncated) */
13
+ wrap?: boolean
14
+ }
15
+
16
+ export class TextHost extends BaseHost {
17
+ fg?: ColorLike
18
+ bg?: ColorLike
19
+ bold = false
20
+ italic = false
21
+ underline = false
22
+ inverse = false
23
+ wrap = false // Default: truncate (no wrap)
24
+
25
+ // Cache wrapped lines between measure() and render()
26
+ private cachedLines: string[] | null = null
27
+ private cachedWidth = 0
28
+
29
+ constructor(props: TextProps, ctx: HostContext) {
30
+ super("text", props, ctx)
31
+ this.updateProps(props)
32
+ }
33
+
34
+ /** Get text content from RawTextHost children */
35
+ private getContent(): string {
36
+ return this.children
37
+ .filter((c): c is RawTextHost => c instanceof RawTextHost)
38
+ .map((c) => c.content)
39
+ .join("")
40
+ }
41
+
42
+ measure(maxW: number, maxH: number): Size {
43
+ const content = this.getContent()
44
+ const rawLines = content.split("\n")
45
+
46
+ if (this.wrap) {
47
+ // Wrap mode: may span multiple lines. Cache result for render()
48
+ this.cachedLines = rawLines.flatMap((line, idx) =>
49
+ idx < rawLines.length - 1 ? [...this.wrapText(line, maxW), ""] : this.wrapText(line, maxW),
50
+ )
51
+ this.cachedWidth = maxW
52
+ const w = this.cachedLines.reduce((max, line) => Math.max(max, displayWidth(line)), 0)
53
+ return { w, h: Math.min(this.cachedLines.length, maxH) }
54
+ }
55
+
56
+ // Default: respect explicit newlines but do not wrap long words
57
+ this.cachedLines = null
58
+ const widths = rawLines.map((line) => Math.min(displayWidth(line), maxW))
59
+ const w = widths.reduce((max, val) => Math.max(max, val), 0)
60
+ const h = Math.min(rawLines.length, maxH)
61
+ return { w, h }
62
+ }
63
+
64
+ /** Wrap text to fit within maxWidth */
65
+ private wrapText(text: string, maxWidth: number): string[] {
66
+ const result: string[] = []
67
+ for (const rawLine of text.split("\n")) {
68
+ if (rawLine === "") {
69
+ result.push("")
70
+ continue
71
+ }
72
+ let line = ""
73
+ let lineW = 0
74
+ for (const ch of rawLine) {
75
+ const w = displayWidth(ch)
76
+ if (lineW + w > maxWidth && line.length > 0) {
77
+ result.push(line)
78
+ line = ch
79
+ lineW = w
80
+ } else {
81
+ line += ch
82
+ lineW += w
83
+ }
84
+ }
85
+ if (line.length > 0) result.push(line)
86
+ }
87
+ return result.length > 0 ? result : [""]
88
+ }
89
+
90
+ override layout(rect: Rect): void {
91
+ super.layout(rect)
92
+ // Layout children (RawTextHost nodes) at same position
93
+ for (const child of this.children) {
94
+ child.layout(rect)
95
+ }
96
+ }
97
+
98
+ render(buffer: CellBuffer, palette: Palette): void {
99
+ if (!this.rect) return
100
+
101
+ const style: Record<string, unknown> = {}
102
+ if (this.fg !== undefined) style.fg = typeof this.fg === "string" ? parseColor(this.fg) : this.fg
103
+ if (this.bg !== undefined) style.bg = typeof this.bg === "string" ? parseColor(this.bg) : this.bg
104
+ if (this.bold) style.bold = true
105
+ if (this.italic) style.italic = true
106
+ if (this.underline) style.underline = true
107
+ if (this.inverse) style.inverse = true
108
+
109
+ const styleId = palette.id(style)
110
+ const content = this.getContent()
111
+ const rawLines = content.split("\n")
112
+
113
+ if (this.wrap) {
114
+ // Wrap mode: use cached lines if width matches, otherwise rewrap
115
+ const rectW = this.rect.w
116
+ const lines =
117
+ this.cachedLines && this.cachedWidth === rectW
118
+ ? this.cachedLines
119
+ : rawLines.flatMap((line, idx) =>
120
+ idx < rawLines.length - 1 ? [...this.wrapText(line, rectW), ""] : this.wrapText(line, rectW),
121
+ )
122
+ for (let i = 0; i < lines.length && i < this.rect.h; i++) {
123
+ buffer.drawText(this.rect.x, this.rect.y + i, lines[i], styleId, this.rect.w)
124
+ }
125
+ return
126
+ }
127
+
128
+ // Default: render explicit newlines, clip to width/height
129
+ const maxLines = Math.min(this.rect.h, rawLines.length)
130
+ for (let i = 0; i < maxLines; i++) {
131
+ buffer.drawText(this.rect.x, this.rect.y + i, rawLines[i], styleId, this.rect.w)
132
+ }
133
+ }
134
+
135
+ override updateProps(props: Record<string, unknown>): void {
136
+ super.updateProps(props)
137
+ // Always assign; props may be undefined when attribute is removed
138
+ this.fg = props.fg as ColorLike | undefined
139
+ this.bg = props.bg as ColorLike | undefined
140
+ this.bold = Boolean(props.bold)
141
+ this.italic = Boolean(props.italic)
142
+ this.underline = Boolean(props.underline)
143
+ this.inverse = Boolean(props.inverse)
144
+ this.wrap = Boolean(props.wrap)
145
+ }
146
+ }
147
+
148
+ /** Special host for raw text nodes (React text children) */
149
+ export class RawTextHost extends BaseHost {
150
+ content = ""
151
+
152
+ constructor(text: string, ctx: HostContext) {
153
+ super("rawtext", {}, ctx)
154
+ this.content = text
155
+ }
156
+
157
+ measure(maxW: number, _maxH: number): Size {
158
+ const w = Math.min(displayWidth(this.content), maxW)
159
+ return { w, h: 1 }
160
+ }
161
+
162
+ render(buffer: CellBuffer, palette: Palette): void {
163
+ if (!this.rect) return
164
+ const styleId = palette.id({})
165
+ buffer.drawText(this.rect.x, this.rect.y, this.content, styleId, this.rect.w)
166
+ }
167
+
168
+ updateText(text: string): void {
169
+ this.content = text
170
+ }
171
+
172
+ override updateProps(_props: Record<string, unknown>): void {
173
+ // Raw text has no props
174
+ }
175
+ }
@@ -0,0 +1,64 @@
1
+ import type { CellBuffer, Palette } from "@effect-tui/core"
2
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
3
+ import { BaseHost } from "./base.js"
4
+ import { measureFlex, layoutFlex, type FlexAlignment } from "../utils/index.js"
5
+
6
+ export interface VStackProps extends CommonProps {
7
+ spacing?: number
8
+ alignment?: "leading" | "center" | "trailing"
9
+ }
10
+
11
+ // Map VStack alignment names to generic flex alignment
12
+ function toFlexAlignment(alignment: "leading" | "center" | "trailing"): FlexAlignment {
13
+ switch (alignment) {
14
+ case "leading":
15
+ return "start"
16
+ case "center":
17
+ return "center"
18
+ case "trailing":
19
+ return "end"
20
+ }
21
+ }
22
+
23
+ export class VStackHost extends BaseHost {
24
+ spacing = 0
25
+ alignment: "leading" | "center" | "trailing" = "leading"
26
+ private cachedSizes: Size[] = []
27
+
28
+ constructor(props: VStackProps, ctx: HostContext) {
29
+ super("vstack", props, ctx)
30
+ this.updateProps(props)
31
+ }
32
+
33
+ measure(maxW: number, maxH: number): Size {
34
+ const result = measureFlex("vertical", this.children, this.spacing, maxH, maxW)
35
+ this.cachedSizes = result.sizes
36
+ return result.totalSize
37
+ }
38
+
39
+ override layout(rect: Rect): void {
40
+ super.layout(rect)
41
+ const stretchCross = this.alignment === "leading"
42
+ layoutFlex(
43
+ "vertical",
44
+ this.children,
45
+ this.cachedSizes,
46
+ rect,
47
+ this.spacing,
48
+ toFlexAlignment(this.alignment),
49
+ stretchCross,
50
+ )
51
+ }
52
+
53
+ render(buffer: CellBuffer, palette: Palette): void {
54
+ for (const child of this.children) {
55
+ child.render(buffer, palette)
56
+ }
57
+ }
58
+
59
+ override updateProps(props: Record<string, unknown>): void {
60
+ super.updateProps(props)
61
+ if (props.spacing !== undefined) this.spacing = props.spacing as number
62
+ if (props.alignment !== undefined) this.alignment = props.alignment as "leading" | "center" | "trailing"
63
+ }
64
+ }
@@ -0,0 +1,77 @@
1
+ import type { CellBuffer, Palette } from "@effect-tui/core"
2
+ import type { HostContext, Rect, Size, CommonProps } from "../reconciler/types.js"
3
+ import { BaseHost } from "./base.js"
4
+
5
+ export interface ZStackProps extends CommonProps {
6
+ alignment?: { h?: "leading" | "center" | "trailing"; v?: "top" | "center" | "bottom" }
7
+ }
8
+
9
+ // Overlay children in the same rect, honoring alignment for each child.
10
+ export class ZStackHost extends BaseHost {
11
+ alignmentH: "leading" | "center" | "trailing" = "center"
12
+ alignmentV: "top" | "center" | "bottom" = "center"
13
+ private cachedSizes: Size[] = []
14
+
15
+ constructor(props: ZStackProps, ctx: HostContext) {
16
+ super("zstack", props, ctx)
17
+ this.updateProps(props)
18
+ }
19
+
20
+ measure(maxW: number, maxH: number): Size {
21
+ let maxChildW = 0
22
+ let maxChildH = 0
23
+ this.cachedSizes = []
24
+
25
+ for (const child of this.children) {
26
+ const size = child.measure(maxW, maxH)
27
+ this.cachedSizes.push(size)
28
+ maxChildW = Math.max(maxChildW, size.w)
29
+ maxChildH = Math.max(maxChildH, size.h)
30
+ }
31
+
32
+ return {
33
+ w: Math.min(maxW, maxChildW),
34
+ h: Math.min(maxH, maxChildH),
35
+ }
36
+ }
37
+
38
+ override layout(rect: Rect): void {
39
+ super.layout(rect)
40
+
41
+ for (let i = 0; i < this.children.length; i++) {
42
+ const child = this.children[i]
43
+ const size = this.cachedSizes[i] ?? child.measure(rect.w, rect.h)
44
+
45
+ let x = rect.x
46
+ let y = rect.y
47
+
48
+ if (this.alignmentH === "center") x += Math.floor((rect.w - size.w) / 2)
49
+ else if (this.alignmentH === "trailing") x += Math.max(0, rect.w - size.w)
50
+
51
+ if (this.alignmentV === "center") y += Math.floor((rect.h - size.h) / 2)
52
+ else if (this.alignmentV === "bottom") y += Math.max(0, rect.h - size.h)
53
+
54
+ child.layout({
55
+ x,
56
+ y,
57
+ w: Math.min(rect.w, size.w),
58
+ h: Math.min(rect.h, size.h),
59
+ })
60
+ }
61
+ }
62
+
63
+ render(buffer: CellBuffer, palette: Palette): void {
64
+ for (const child of this.children) {
65
+ child.render(buffer, palette)
66
+ }
67
+ }
68
+
69
+ override updateProps(props: Record<string, unknown>): void {
70
+ super.updateProps(props)
71
+ if (props.alignment !== undefined) {
72
+ const a = props.alignment as ZStackProps["alignment"]
73
+ if (a?.h) this.alignmentH = a.h
74
+ if (a?.v) this.alignmentV = a.v
75
+ }
76
+ }
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ // Convenience: re-export core theming/types so apps can import from one place
2
+ export * from "@effect-tui/core"
3
+
4
+ // Renderer
5
+ export {
6
+ createRenderer,
7
+ createRoot,
8
+ render,
9
+ useRenderer,
10
+ useTerminalSize,
11
+ RendererContext,
12
+ } from "./renderer.js"
13
+ export type { TuiRenderer, RendererOptions, Root, FrameStats, RenderInstance } from "./renderer.js"
14
+
15
+ // Components
16
+ export { CodeBlock } from "./codeblock.js"
17
+ export type { CodeBlockProps } from "./codeblock.js"
18
+
19
+ // Highlight utilities
20
+ export {
21
+ highlightCode,
22
+ toPlainLines,
23
+ type HighlightLine,
24
+ type HighlightToken,
25
+ type HighlightTokenStyle,
26
+ } from "./highlight.js"
27
+
28
+ // Hooks
29
+ export { useKeyboard, usePaste } from "./hooks/index.js"
30
+ export { useFrameStats } from "./hooks/useFrameStats.js"
31
+ export type { UseKeyboardOptions } from "./hooks/index.js"
32
+
33
+ // Motion (spring animations)
34
+ export {
35
+ useMotionValue,
36
+ useSpring,
37
+ useSprings,
38
+ useSpringRenderer,
39
+ useMotionValueEvent,
40
+ useAnimationFrame,
41
+ motionValue,
42
+ // Color springs
43
+ ColorMotionValue,
44
+ useColorMotionValue,
45
+ useColorSpring,
46
+ } from "./motion/index.js"
47
+ export type { SpringOptions, MotionValue, RGBA, ColorInput } from "./motion/index.js"
48
+
49
+ // Types
50
+ export type { HostInstance, HostContext, Rect, Size, CommonProps } from "./reconciler/types.js"
51
+ export type { TextProps } from "./hosts/text.js"
52
+ export type { SpacerProps } from "./hosts/spacer.js"
53
+ export type { VStackProps } from "./hosts/vstack.js"
54
+ export type { HStackProps } from "./hosts/hstack.js"
55
+ export type { BoxProps, BorderKind } from "./hosts/box.js"
56
+ export type { CanvasProps, DrawContext } from "./hosts/canvas.js"
57
+
58
+ // Debug
59
+ export { DiagnosticsPanel } from "./debug/DiagnosticsPanel.js"
60
+
61
+ // JSX types are provided via jsxImportSource: "@effect-tui/react"
62
+ // See jsx-runtime.ts at package root for the JSX namespace