@silvery/term 0.3.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 (92) hide show
  1. package/package.json +54 -0
  2. package/src/adapters/canvas-adapter.ts +356 -0
  3. package/src/adapters/dom-adapter.ts +452 -0
  4. package/src/adapters/flexily-zero-adapter.ts +368 -0
  5. package/src/adapters/terminal-adapter.ts +305 -0
  6. package/src/adapters/yoga-adapter.ts +370 -0
  7. package/src/ansi/ansi.ts +251 -0
  8. package/src/ansi/constants.ts +76 -0
  9. package/src/ansi/detection.ts +441 -0
  10. package/src/ansi/hyperlink.ts +38 -0
  11. package/src/ansi/index.ts +201 -0
  12. package/src/ansi/patch-console.ts +159 -0
  13. package/src/ansi/sgr-codes.ts +34 -0
  14. package/src/ansi/storybook.ts +209 -0
  15. package/src/ansi/term.ts +724 -0
  16. package/src/ansi/types.ts +202 -0
  17. package/src/ansi/underline.ts +156 -0
  18. package/src/ansi/utils.ts +65 -0
  19. package/src/ansi-sanitize.ts +509 -0
  20. package/src/app.ts +571 -0
  21. package/src/bound-term.ts +94 -0
  22. package/src/bracketed-paste.ts +75 -0
  23. package/src/browser-renderer.ts +174 -0
  24. package/src/buffer.ts +1984 -0
  25. package/src/clipboard.ts +74 -0
  26. package/src/cursor-query.ts +85 -0
  27. package/src/device-attrs.ts +228 -0
  28. package/src/devtools.ts +123 -0
  29. package/src/dom/index.ts +194 -0
  30. package/src/errors.ts +39 -0
  31. package/src/focus-reporting.ts +48 -0
  32. package/src/hit-registry-core.ts +228 -0
  33. package/src/hit-registry.ts +176 -0
  34. package/src/index.ts +458 -0
  35. package/src/input.ts +119 -0
  36. package/src/inspector.ts +155 -0
  37. package/src/kitty-detect.ts +95 -0
  38. package/src/kitty-manager.ts +160 -0
  39. package/src/layout-engine.ts +296 -0
  40. package/src/layout.ts +26 -0
  41. package/src/measurer.ts +74 -0
  42. package/src/mode-query.ts +106 -0
  43. package/src/mouse-events.ts +419 -0
  44. package/src/mouse.ts +83 -0
  45. package/src/non-tty.ts +223 -0
  46. package/src/osc-markers.ts +32 -0
  47. package/src/osc-palette.ts +169 -0
  48. package/src/output.ts +406 -0
  49. package/src/pane-manager.ts +248 -0
  50. package/src/pipeline/CLAUDE.md +587 -0
  51. package/src/pipeline/content-phase-adapter.ts +976 -0
  52. package/src/pipeline/content-phase.ts +1765 -0
  53. package/src/pipeline/helpers.ts +42 -0
  54. package/src/pipeline/index.ts +416 -0
  55. package/src/pipeline/layout-phase.ts +686 -0
  56. package/src/pipeline/measure-phase.ts +198 -0
  57. package/src/pipeline/measure-stats.ts +21 -0
  58. package/src/pipeline/output-phase.ts +2593 -0
  59. package/src/pipeline/render-box.ts +343 -0
  60. package/src/pipeline/render-helpers.ts +243 -0
  61. package/src/pipeline/render-text.ts +1255 -0
  62. package/src/pipeline/types.ts +161 -0
  63. package/src/pipeline.ts +29 -0
  64. package/src/pixel-size.ts +119 -0
  65. package/src/render-adapter.ts +179 -0
  66. package/src/renderer.ts +1330 -0
  67. package/src/runtime/create-app.tsx +1845 -0
  68. package/src/runtime/create-buffer.ts +18 -0
  69. package/src/runtime/create-runtime.ts +325 -0
  70. package/src/runtime/diff.ts +56 -0
  71. package/src/runtime/event-handlers.ts +254 -0
  72. package/src/runtime/index.ts +119 -0
  73. package/src/runtime/keys.ts +8 -0
  74. package/src/runtime/layout.ts +164 -0
  75. package/src/runtime/run.tsx +318 -0
  76. package/src/runtime/term-provider.ts +399 -0
  77. package/src/runtime/terminal-lifecycle.ts +246 -0
  78. package/src/runtime/tick.ts +219 -0
  79. package/src/runtime/types.ts +210 -0
  80. package/src/scheduler.ts +723 -0
  81. package/src/screenshot.ts +57 -0
  82. package/src/scroll-region.ts +69 -0
  83. package/src/scroll-utils.ts +97 -0
  84. package/src/term-def.ts +267 -0
  85. package/src/terminal-caps.ts +5 -0
  86. package/src/terminal-colors.ts +216 -0
  87. package/src/termtest.ts +224 -0
  88. package/src/text-sizing.ts +109 -0
  89. package/src/toolbelt/index.ts +72 -0
  90. package/src/unicode.ts +1763 -0
  91. package/src/xterm/index.ts +491 -0
  92. package/src/xterm/xterm-provider.ts +204 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * OSC 52 Clipboard Support
3
+ *
4
+ * Provides clipboard access via the OSC 52 terminal protocol.
5
+ * This works across SSH sessions and in terminals that support it.
6
+ *
7
+ * Protocol: OSC 52
8
+ * - Copy: ESC ] 52 ; c ; <base64> BEL
9
+ * - Query: ESC ] 52 ; c ; ? BEL
10
+ * - Response: ESC ] 52 ; c ; <base64> BEL (or ST terminator)
11
+ *
12
+ * Supported by: Ghostty, Kitty, WezTerm, iTerm2, xterm, foot, tmux
13
+ */
14
+
15
+ const ESC = "\x1b"
16
+ const BEL = "\x07"
17
+
18
+ // ============================================================================
19
+ // Clipboard Operations
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Copy text to the system clipboard via OSC 52.
24
+ * Encodes the text as base64 and writes the OSC 52 sequence to stdout.
25
+ */
26
+ export function copyToClipboard(stdout: NodeJS.WriteStream, text: string): void {
27
+ const base64 = Buffer.from(text).toString("base64")
28
+ stdout.write(`${ESC}]52;c;${base64}${BEL}`)
29
+ }
30
+
31
+ /**
32
+ * Request clipboard contents via OSC 52.
33
+ * Writes the OSC 52 query sequence. The terminal will respond with
34
+ * an OSC 52 response containing the clipboard contents as base64.
35
+ * Use parseClipboardResponse() to decode the response.
36
+ */
37
+ export function requestClipboard(stdout: NodeJS.WriteStream): void {
38
+ stdout.write(`${ESC}]52;c;?${BEL}`)
39
+ }
40
+
41
+ // ============================================================================
42
+ // Response Parsing
43
+ // ============================================================================
44
+
45
+ /** OSC 52 response prefix */
46
+ const OSC52_PREFIX = `${ESC}]52;c;`
47
+
48
+ /**
49
+ * Parse an OSC 52 clipboard response and decode the base64 content.
50
+ *
51
+ * Returns the decoded clipboard text, or null if the input is not
52
+ * an OSC 52 clipboard response.
53
+ *
54
+ * Handles both BEL (\x07) and ST (ESC \) terminators.
55
+ */
56
+ export function parseClipboardResponse(input: string): string | null {
57
+ const prefixIdx = input.indexOf(OSC52_PREFIX)
58
+ if (prefixIdx === -1) return null
59
+
60
+ const contentStart = prefixIdx + OSC52_PREFIX.length
61
+
62
+ // Reject the query marker — it's not a response
63
+ if (input[contentStart] === "?") return null
64
+
65
+ // Find terminator: BEL (\x07) or ST (ESC \)
66
+ let contentEnd = input.indexOf(BEL, contentStart)
67
+ if (contentEnd === -1) {
68
+ contentEnd = input.indexOf(`${ESC}\\`, contentStart)
69
+ }
70
+ if (contentEnd === -1) return null
71
+
72
+ const base64 = input.slice(contentStart, contentEnd)
73
+ return Buffer.from(base64, "base64").toString("utf-8")
74
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * CSI 6n Cursor Position Query
3
+ *
4
+ * Queries the terminal for the current cursor position using the standard
5
+ * Device Status Report (DSR) mechanism.
6
+ *
7
+ * Protocol:
8
+ * - Query: CSI 6 n (\x1b[6n)
9
+ * - Response: CSI {row} ; {col} R (\x1b[{row};{col}R)
10
+ *
11
+ * Row and column are 1-indexed in the protocol response.
12
+ *
13
+ * Supported by: virtually all terminals (VT100+)
14
+ */
15
+
16
+ /** Regex to match a CPR response: CSI row ; col R */
17
+ const CPR_RESPONSE_RE = /\x1b\[(\d+);(\d+)R/
18
+
19
+ /**
20
+ * Query the terminal cursor position.
21
+ *
22
+ * Sends CSI 6n and parses the CPR response.
23
+ * Returns 1-indexed row and column.
24
+ *
25
+ * @param write Function to write to stdout
26
+ * @param read Function to read a chunk from stdin (resolves with data or null on timeout)
27
+ * @param timeoutMs How long to wait for response (default: 200ms)
28
+ */
29
+ export async function queryCursorPosition(
30
+ write: (data: string) => void,
31
+ read: (timeoutMs: number) => Promise<string | null>,
32
+ timeoutMs = 200,
33
+ ): Promise<{ row: number; col: number } | null> {
34
+ write("\x1b[6n")
35
+
36
+ const data = await read(timeoutMs)
37
+ if (data == null) return null
38
+
39
+ const match = CPR_RESPONSE_RE.exec(data)
40
+ if (!match) return null
41
+
42
+ return {
43
+ row: parseInt(match[1]!, 10),
44
+ col: parseInt(match[2]!, 10),
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Query cursor position using real stdin/stdout.
50
+ * Convenience wrapper around queryCursorPosition.
51
+ */
52
+ export async function queryCursorFromStdio(
53
+ stdout: { write: (s: string) => boolean | void },
54
+ stdin: NodeJS.ReadStream,
55
+ timeoutMs = 200,
56
+ ): Promise<{ row: number; col: number } | null> {
57
+ const wasRaw = stdin.isRaw
58
+ if (!wasRaw) stdin.setRawMode(true)
59
+
60
+ try {
61
+ const write = (s: string) => {
62
+ stdout.write(s)
63
+ }
64
+
65
+ const read = (ms: number): Promise<string | null> =>
66
+ new Promise((resolve) => {
67
+ const timer = setTimeout(() => {
68
+ stdin.removeListener("data", onData)
69
+ resolve(null)
70
+ }, ms)
71
+
72
+ function onData(chunk: Buffer) {
73
+ clearTimeout(timer)
74
+ stdin.removeListener("data", onData)
75
+ resolve(chunk.toString())
76
+ }
77
+
78
+ stdin.on("data", onData)
79
+ })
80
+
81
+ return await queryCursorPosition(write, read, timeoutMs)
82
+ } finally {
83
+ if (!wasRaw) stdin.setRawMode(false)
84
+ }
85
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Device Attributes (DA1/DA2/DA3) + XTVERSION Queries
3
+ *
4
+ * Provides functions to query terminal identity and capabilities using
5
+ * the standard VT device attribute escape sequences.
6
+ *
7
+ * Protocols:
8
+ *
9
+ * DA1 (Primary Device Attributes):
10
+ * Query: CSI c (\x1b[c)
11
+ * Response: CSI ? Ps ; Ps ; ... c
12
+ *
13
+ * DA2 (Secondary Device Attributes):
14
+ * Query: CSI > c (\x1b[>c)
15
+ * Response: CSI > Pt ; Pv ; Pc c
16
+ * Where Pt=terminal type, Pv=firmware version, Pc=ROM cartridge id
17
+ *
18
+ * DA3 (Tertiary Device Attributes):
19
+ * Query: CSI = c (\x1b[=c)
20
+ * Response: DCS ! | hex-encoded-id ST (\x1bP!|{hex}\x1b\\)
21
+ *
22
+ * XTVERSION (Terminal Name + Version):
23
+ * Query: CSI > 0 q (\x1b[>0q)
24
+ * Response: DCS > | name(version) ST (\x1bP>|{text}\x1b\\)
25
+ *
26
+ * Supported by: xterm, Ghostty, Kitty, WezTerm, foot, VTE-based terminals
27
+ */
28
+
29
+ /** Regex for DA1 response: CSI ? params c */
30
+ const DA1_RESPONSE_RE = /\x1b\[\?([\d;]+)c/
31
+
32
+ /** Regex for DA2 response: CSI > params c */
33
+ const DA2_RESPONSE_RE = /\x1b\[>([\d;]+)c/
34
+
35
+ /** Regex for DA3 response: DCS ! | hex ST */
36
+ const DA3_RESPONSE_RE = /\x1bP!\|([0-9a-fA-F]*)\x1b\\/
37
+
38
+ /** Regex for XTVERSION response: DCS > | text ST */
39
+ const XTVERSION_RESPONSE_RE = /\x1bP>\|([^\x1b]*)\x1b\\/
40
+
41
+ // ============================================================================
42
+ // DA1 — Primary Device Attributes
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Query primary device attributes (DA1).
47
+ *
48
+ * Returns the list of attribute parameters the terminal reports.
49
+ * Common params: 1=132-cols, 4=sixel, 6=selective-erase, 22=ANSI-color
50
+ *
51
+ * @param write Function to write to stdout
52
+ * @param read Function to read a chunk from stdin
53
+ * @param timeoutMs How long to wait for response (default: 200ms)
54
+ */
55
+ export async function queryPrimaryDA(
56
+ write: (data: string) => void,
57
+ read: (timeoutMs: number) => Promise<string | null>,
58
+ timeoutMs = 200,
59
+ ): Promise<{ params: number[] } | null> {
60
+ write("\x1b[c")
61
+
62
+ const data = await read(timeoutMs)
63
+ if (data == null) return null
64
+
65
+ const match = DA1_RESPONSE_RE.exec(data)
66
+ if (!match) return null
67
+
68
+ const params = match[1]!.split(";").map((s) => parseInt(s, 10))
69
+ return { params }
70
+ }
71
+
72
+ // ============================================================================
73
+ // DA2 — Secondary Device Attributes
74
+ // ============================================================================
75
+
76
+ /**
77
+ * Query secondary device attributes (DA2).
78
+ *
79
+ * Returns terminal type, firmware version, and ROM cartridge id.
80
+ * Common type values: 0=VT100, 1=VT220, 41=xterm, 65=VT500
81
+ *
82
+ * @param write Function to write to stdout
83
+ * @param read Function to read a chunk from stdin
84
+ * @param timeoutMs How long to wait for response (default: 200ms)
85
+ */
86
+ export async function querySecondaryDA(
87
+ write: (data: string) => void,
88
+ read: (timeoutMs: number) => Promise<string | null>,
89
+ timeoutMs = 200,
90
+ ): Promise<{ type: number; version: number; id: number } | null> {
91
+ write("\x1b[>c")
92
+
93
+ const data = await read(timeoutMs)
94
+ if (data == null) return null
95
+
96
+ const match = DA2_RESPONSE_RE.exec(data)
97
+ if (!match) return null
98
+
99
+ const parts = match[1]!.split(";")
100
+ if (parts.length < 3) return null
101
+
102
+ return {
103
+ type: parseInt(parts[0]!, 10),
104
+ version: parseInt(parts[1]!, 10),
105
+ id: parseInt(parts[2]!, 10),
106
+ }
107
+ }
108
+
109
+ // ============================================================================
110
+ // DA3 — Tertiary Device Attributes
111
+ // ============================================================================
112
+
113
+ /**
114
+ * Query tertiary device attributes (DA3).
115
+ *
116
+ * Returns a hex-encoded unit ID string. Decode with Buffer.from(hex, 'hex').
117
+ *
118
+ * @param write Function to write to stdout
119
+ * @param read Function to read a chunk from stdin
120
+ * @param timeoutMs How long to wait for response (default: 200ms)
121
+ */
122
+ export async function queryTertiaryDA(
123
+ write: (data: string) => void,
124
+ read: (timeoutMs: number) => Promise<string | null>,
125
+ timeoutMs = 200,
126
+ ): Promise<string | null> {
127
+ write("\x1b[=c")
128
+
129
+ const data = await read(timeoutMs)
130
+ if (data == null) return null
131
+
132
+ const match = DA3_RESPONSE_RE.exec(data)
133
+ if (!match) return null
134
+
135
+ return match[1]!
136
+ }
137
+
138
+ // ============================================================================
139
+ // XTVERSION — Terminal Name + Version
140
+ // ============================================================================
141
+
142
+ /**
143
+ * Query the terminal name and version via XTVERSION.
144
+ *
145
+ * Returns the version string as reported by the terminal, e.g.:
146
+ * - "xterm(388)"
147
+ * - "tmux 3.4"
148
+ * - "WezTerm 20230712-072601-f4abf8fd"
149
+ *
150
+ * @param write Function to write to stdout
151
+ * @param read Function to read a chunk from stdin
152
+ * @param timeoutMs How long to wait for response (default: 200ms)
153
+ */
154
+ export async function queryTerminalVersion(
155
+ write: (data: string) => void,
156
+ read: (timeoutMs: number) => Promise<string | null>,
157
+ timeoutMs = 200,
158
+ ): Promise<string | null> {
159
+ write("\x1b[>0q")
160
+
161
+ const data = await read(timeoutMs)
162
+ if (data == null) return null
163
+
164
+ const match = XTVERSION_RESPONSE_RE.exec(data)
165
+ if (!match) return null
166
+
167
+ return match[1]!
168
+ }
169
+
170
+ // ============================================================================
171
+ // Combined Query
172
+ // ============================================================================
173
+
174
+ /** Combined device attributes result. */
175
+ export interface DeviceAttributes {
176
+ da1: { params: number[] } | null
177
+ da2: { type: number; version: number; id: number } | null
178
+ version: string | null
179
+ }
180
+
181
+ /**
182
+ * Query all device attributes: DA1, DA2, and XTVERSION.
183
+ *
184
+ * Convenience wrapper that queries all three sequentially.
185
+ * DA3 is omitted from the combined query as it's rarely needed.
186
+ *
187
+ * @param stdout Writable stream (e.g., process.stdout)
188
+ * @param stdin Readable stream (e.g., process.stdin)
189
+ * @param timeoutMs Per-query timeout (default: 200ms)
190
+ */
191
+ export async function queryDeviceAttributes(
192
+ stdout: { write: (s: string) => boolean | void },
193
+ stdin: NodeJS.ReadStream,
194
+ timeoutMs = 200,
195
+ ): Promise<DeviceAttributes> {
196
+ const wasRaw = stdin.isRaw
197
+ if (!wasRaw) stdin.setRawMode(true)
198
+
199
+ try {
200
+ const write = (s: string) => {
201
+ stdout.write(s)
202
+ }
203
+
204
+ const read = (ms: number): Promise<string | null> =>
205
+ new Promise((resolve) => {
206
+ const timer = setTimeout(() => {
207
+ stdin.removeListener("data", onData)
208
+ resolve(null)
209
+ }, ms)
210
+
211
+ function onData(chunk: Buffer) {
212
+ clearTimeout(timer)
213
+ stdin.removeListener("data", onData)
214
+ resolve(chunk.toString())
215
+ }
216
+
217
+ stdin.on("data", onData)
218
+ })
219
+
220
+ const da1 = await queryPrimaryDA(write, read, timeoutMs)
221
+ const da2 = await querySecondaryDA(write, read, timeoutMs)
222
+ const version = await queryTerminalVersion(write, read, timeoutMs)
223
+
224
+ return { da1, da2, version }
225
+ } finally {
226
+ if (!wasRaw) stdin.setRawMode(false)
227
+ }
228
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * React DevTools integration for silvery.
3
+ *
4
+ * Provides optional connection to React DevTools standalone app for
5
+ * debugging TUI component trees. Requires `react-devtools-core` to be
6
+ * installed (optional peer dependency).
7
+ *
8
+ * Usage:
9
+ * 1. Install: `bun add -d react-devtools-core`
10
+ * 2. Run devtools: `npx react-devtools`
11
+ * 3. Launch app with: `DEBUG_DEVTOOLS=1 bun run app.ts`
12
+ *
13
+ * Or call `connectDevTools()` manually from your app code.
14
+ *
15
+ * @module
16
+ */
17
+
18
+ import { reconciler } from "@silvery/react/reconciler"
19
+
20
+ let connected = false
21
+
22
+ /**
23
+ * Connect to React DevTools standalone app.
24
+ *
25
+ * This lazy-loads `react-devtools-core` so it has zero impact on
26
+ * production bundles. The connection is established via WebSocket
27
+ * to the devtools electron app (default: ws://localhost:8097).
28
+ *
29
+ * Safe to call multiple times -- subsequent calls are no-ops.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { connectDevTools } from '@silvery/react';
34
+ * await connectDevTools();
35
+ * // Now open React DevTools standalone to inspect the component tree
36
+ * ```
37
+ */
38
+ export async function connectDevTools(): Promise<boolean> {
39
+ if (connected) return true
40
+
41
+ try {
42
+ // Polyfill WebSocket for Node.js environments (required by react-devtools-core)
43
+ if (typeof globalThis.WebSocket === "undefined") {
44
+ try {
45
+ // @ts-expect-error -- ws is an optional peer dependency
46
+ const ws = await import("ws")
47
+ globalThis.WebSocket = ws.default ?? ws
48
+ } catch {
49
+ // ws not available -- devtools won't be able to connect
50
+ console.warn(
51
+ "silvery devtools: WebSocket polyfill (ws) not available. " +
52
+ "Install ws for DevTools support: bun add -d ws",
53
+ )
54
+ return false
55
+ }
56
+ }
57
+
58
+ // Ensure window/self exist for react-devtools-core internals
59
+ if (typeof globalThis.window === "undefined") {
60
+ // @ts-expect-error -- polyfill for devtools
61
+ globalThis.window = globalThis
62
+ }
63
+
64
+ // Configure component filters to hide silvery internals from the DevTools tree.
65
+ // Filter types from react-devtools-shared/src/types.js:
66
+ // 1 = ComponentFilterElementType, value 7 = HostComponent
67
+ // 2 = ComponentFilterDisplayName (regex on displayName)
68
+ if (!globalThis.__REACT_DEVTOOLS_COMPONENT_FILTERS__) {
69
+ globalThis.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [
70
+ { type: 1, value: 7, isEnabled: true },
71
+ { type: 2, value: "SilveryApp", isEnabled: true, isValid: true },
72
+ ]
73
+ }
74
+
75
+ // @ts-expect-error -- react-devtools-core has no type declarations
76
+ const devtools = await import("react-devtools-core")
77
+ devtools.initialize()
78
+ devtools.connectToDevTools()
79
+
80
+ // Inject renderer info so DevTools can identify silvery.
81
+ // rendererPackageName and rendererVersion are read from the host config
82
+ // passed to Reconciler() -- see reconciler/host-config.ts.
83
+ reconciler.injectIntoDevTools()
84
+
85
+ connected = true
86
+ return true
87
+ } catch (error: unknown) {
88
+ const message = error instanceof Error ? error.message : String(error)
89
+ console.warn(
90
+ `silvery devtools: Failed to connect to React DevTools. ` +
91
+ `Install react-devtools-core: bun add -d react-devtools-core\n` +
92
+ ` Error: ${message}`,
93
+ )
94
+ return false
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if DevTools are currently connected.
100
+ */
101
+ export function isDevToolsConnected(): boolean {
102
+ return connected
103
+ }
104
+
105
+ /**
106
+ * Auto-connect to DevTools if DEBUG_DEVTOOLS=1 environment variable is set.
107
+ * Called internally during render initialization.
108
+ */
109
+ export async function autoConnectDevTools(): Promise<void> {
110
+ if (process.env.DEBUG_DEVTOOLS === "1" || process.env.DEBUG_DEVTOOLS === "true") {
111
+ await connectDevTools()
112
+ }
113
+ }
114
+
115
+ // Global type augmentation for devtools polyfills
116
+ declare global {
117
+ var __REACT_DEVTOOLS_COMPONENT_FILTERS__: Array<{
118
+ type: number
119
+ value: number | string
120
+ isEnabled: boolean
121
+ isValid?: boolean
122
+ }>
123
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * DOM Entry Point
3
+ *
4
+ * Provides a browser-friendly API for rendering silvery components to DOM elements.
5
+ * This module sets up the DOM adapter and provides render functions.
6
+ *
7
+ * Advantages over Canvas:
8
+ * - Native text selection and copying
9
+ * - Screen reader accessibility
10
+ * - Browser font rendering
11
+ * - CSS integration
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { renderToDOM, Box, Text, useContentRect } from '@silvery/term/dom';
16
+ *
17
+ * function App() {
18
+ * const { width, height } = useContentRect();
19
+ * return (
20
+ * <Box flexDirection="column">
21
+ * <Text>Container size: {width}px × {height}px</Text>
22
+ * </Box>
23
+ * );
24
+ * }
25
+ *
26
+ * const container = document.getElementById('app');
27
+ * renderToDOM(<App />, container);
28
+ * ```
29
+ */
30
+
31
+ import type { ReactElement } from "react"
32
+ import { type DOMAdapterConfig, DOMRenderBuffer, createDOMAdapter, injectDOMStyles } from "../adapters/dom-adapter"
33
+ import { createBrowserRenderer, initBrowserRenderer, renderOnce } from "../browser-renderer"
34
+ import type { RenderBuffer } from "../render-adapter"
35
+
36
+ // Re-export components and hooks for convenience
37
+ export { Box, type BoxProps } from "@silvery/react/components/Box"
38
+ export { Text, type TextProps } from "@silvery/react/components/Text"
39
+ export { useContentRect, useScreenRect } from "@silvery/react/hooks/useLayout"
40
+ export { useApp } from "@silvery/react/hooks/useApp"
41
+
42
+ // Re-export adapter utilities
43
+ export { createDOMAdapter, DOMRenderBuffer, injectDOMStyles, type DOMAdapterConfig } from "../adapters/dom-adapter"
44
+
45
+ // ============================================================================
46
+ // Types
47
+ // ============================================================================
48
+
49
+ export interface DOMRenderOptions extends DOMAdapterConfig {
50
+ /** Width of the container (default: container.clientWidth or 800) */
51
+ width?: number
52
+ /** Height of the container (default: container.clientHeight or 600) */
53
+ height?: number
54
+ /** Inject global CSS styles (default: true) */
55
+ injectStyles?: boolean
56
+ }
57
+
58
+ export interface DOMInstance {
59
+ /** Re-render with a new element */
60
+ rerender: (element: ReactElement) => void
61
+ /** Unmount and clean up */
62
+ unmount: () => void
63
+ /** Dispose (alias for unmount) — enables `using` */
64
+ [Symbol.dispose](): void
65
+ /** Get the current buffer */
66
+ getBuffer: () => RenderBuffer | null
67
+ /** Force a re-render */
68
+ refresh: () => void
69
+ /** Get the container element */
70
+ getContainer: () => HTMLElement
71
+ }
72
+
73
+ // ============================================================================
74
+ // Initialization
75
+ // ============================================================================
76
+
77
+ const domAdapterFactory = { createAdapter: (config: DOMAdapterConfig) => createDOMAdapter(config) }
78
+
79
+ /**
80
+ * Initialize the DOM rendering system.
81
+ * Called automatically by renderToDOM, but can be called manually.
82
+ */
83
+ export function initDOMRenderer(config: DOMAdapterConfig = {}): void {
84
+ initBrowserRenderer(domAdapterFactory, config)
85
+ }
86
+
87
+ // ============================================================================
88
+ // Render Functions
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Render a React element to a DOM container.
93
+ *
94
+ * @param element - React element to render
95
+ * @param container - Target DOM element
96
+ * @param options - Render options (font size, colors, etc.)
97
+ * @returns DOMInstance for controlling the render
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * const container = document.getElementById('app');
102
+ * const instance = renderToDOM(<App />, container, { fontSize: 16 });
103
+ *
104
+ * // Later: update the component
105
+ * instance.rerender(<App newProps />);
106
+ *
107
+ * // Clean up
108
+ * instance.unmount();
109
+ * ```
110
+ */
111
+ export function renderToDOM(
112
+ element: ReactElement,
113
+ container: HTMLElement,
114
+ options: DOMRenderOptions = {},
115
+ ): DOMInstance {
116
+ const { injectStyles = true, ...adapterConfig } = options
117
+
118
+ if (injectStyles) {
119
+ injectDOMStyles(adapterConfig.classPrefix)
120
+ }
121
+
122
+ initDOMRenderer(adapterConfig)
123
+
124
+ const pixelWidth = options.width ?? (container.clientWidth || 800)
125
+ const pixelHeight = options.height ?? (container.clientHeight || 600)
126
+
127
+ // Convert pixel dimensions to cell dimensions for the layout engine.
128
+ // The layout engine operates in cell units (columns x rows), not pixels.
129
+ // We estimate cell size from font metrics: charWidth ~ fontSize * 0.6, lineHeight ~ fontSize * lineHeight.
130
+ const fontSize = adapterConfig.fontSize ?? 14
131
+ const lineHeightMultiplier = adapterConfig.lineHeight ?? 1.2
132
+ const charWidth = fontSize * 0.6
133
+ const lineHeight = fontSize * lineHeightMultiplier
134
+ const cols = Math.floor(pixelWidth / charWidth)
135
+ const rows = Math.floor(pixelHeight / lineHeight)
136
+
137
+ const base = createBrowserRenderer<DOMRenderBuffer>(
138
+ element,
139
+ cols,
140
+ rows,
141
+ (buffer) => {
142
+ buffer.setContainer(container)
143
+ buffer.render()
144
+ },
145
+ () => {
146
+ container.innerHTML = ""
147
+ },
148
+ )
149
+
150
+ return {
151
+ ...base,
152
+ getContainer(): HTMLElement {
153
+ return container
154
+ },
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Render a React element to DOM once and return the HTML string.
160
+ * Useful for server-side rendering or static generation.
161
+ *
162
+ * @param element - React element to render
163
+ * @param width - Container width in pixels
164
+ * @param height - Container height in pixels
165
+ * @param options - Render options
166
+ * @returns HTML string representation
167
+ */
168
+ export function renderDOMOnce(
169
+ element: ReactElement,
170
+ width: number,
171
+ height: number,
172
+ options: DOMAdapterConfig = {},
173
+ ): string {
174
+ initDOMRenderer(options)
175
+
176
+ // Convert pixel dimensions to cell dimensions for the layout engine
177
+ const fontSize = options.fontSize ?? 14
178
+ const lineHeightMultiplier = options.lineHeight ?? 1.2
179
+ const charWidth = fontSize * 0.6
180
+ const lineHeight = fontSize * lineHeightMultiplier
181
+ const cols = Math.floor(width / charWidth)
182
+ const rows = Math.floor(height / lineHeight)
183
+
184
+ const buffer = renderOnce<DOMRenderBuffer>(element, cols, rows)
185
+
186
+ if (typeof document !== "undefined") {
187
+ const tempContainer = document.createElement("div")
188
+ buffer.setContainer(tempContainer)
189
+ buffer.render()
190
+ return tempContainer.innerHTML
191
+ }
192
+
193
+ return "<!-- DOM rendering requires browser environment -->"
194
+ }