@effect-tui/react 0.2.2 → 0.2.4

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 (69) hide show
  1. package/dist/src/components/MultilineTextInput.js +1 -1
  2. package/dist/src/components/MultilineTextInput.js.map +1 -1
  3. package/dist/src/components/TextInput.js +1 -1
  4. package/dist/src/components/TextInput.js.map +1 -1
  5. package/dist/src/components/text-editing.js +1 -1
  6. package/dist/src/components/text-editing.js.map +1 -1
  7. package/dist/src/console/ConsoleCapture.d.ts +1 -1
  8. package/dist/src/console/ConsoleCapture.d.ts.map +1 -1
  9. package/dist/src/console/ConsoleCapture.js +1 -1
  10. package/dist/src/console/ConsoleCapture.js.map +1 -1
  11. package/dist/src/console/ConsolePopover.js +1 -1
  12. package/dist/src/console/ConsolePopover.js.map +1 -1
  13. package/dist/src/debug/DebugOverlay.d.ts +2 -2
  14. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  15. package/dist/src/debug/DebugOverlay.js +2 -2
  16. package/dist/src/debug/DebugOverlay.js.map +1 -1
  17. package/dist/src/dev.d.ts.map +1 -1
  18. package/dist/src/dev.js +24 -8
  19. package/dist/src/dev.js.map +1 -1
  20. package/dist/src/highlight.d.ts.map +1 -1
  21. package/dist/src/highlight.js +2 -4
  22. package/dist/src/highlight.js.map +1 -1
  23. package/dist/src/hmr-plugin.d.ts +14 -0
  24. package/dist/src/hmr-plugin.d.ts.map +1 -1
  25. package/dist/src/hmr-plugin.js +1 -1
  26. package/dist/src/hmr-plugin.js.map +1 -1
  27. package/dist/src/hosts/base.d.ts +1 -1
  28. package/dist/src/hosts/base.d.ts.map +1 -1
  29. package/dist/src/hosts/base.js +1 -1
  30. package/dist/src/hosts/base.js.map +1 -1
  31. package/dist/src/hosts/single-child.d.ts +1 -2
  32. package/dist/src/hosts/single-child.d.ts.map +1 -1
  33. package/dist/src/hosts/single-child.js +0 -3
  34. package/dist/src/hosts/single-child.js.map +1 -1
  35. package/dist/src/motion/hooks.d.ts.map +1 -1
  36. package/dist/src/motion/hooks.js +4 -2
  37. package/dist/src/motion/hooks.js.map +1 -1
  38. package/dist/src/renderer/modes/InlineRenderer.js +1 -1
  39. package/dist/src/renderer/modes/InlineRenderer.js.map +1 -1
  40. package/dist/src/renderer/modes/StaticContentRenderer.js +1 -1
  41. package/dist/src/renderer/modes/StaticContentRenderer.js.map +1 -1
  42. package/dist/src/renderer.d.ts.map +1 -1
  43. package/dist/src/renderer.js +3 -1
  44. package/dist/src/renderer.js.map +1 -1
  45. package/dist/src/test/mock-streams.d.ts.map +1 -1
  46. package/dist/src/test/mock-streams.js +2 -1
  47. package/dist/src/test/mock-streams.js.map +1 -1
  48. package/dist/src/utils/flex-layout.d.ts.map +1 -1
  49. package/dist/src/utils/flex-layout.js +2 -4
  50. package/dist/src/utils/flex-layout.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +2 -2
  53. package/src/components/MultilineTextInput.tsx +1 -1
  54. package/src/components/TextInput.tsx +1 -1
  55. package/src/components/text-editing.ts +1 -1
  56. package/src/console/ConsoleCapture.ts +1 -1
  57. package/src/console/ConsolePopover.tsx +2 -2
  58. package/src/debug/DebugOverlay.ts +2 -2
  59. package/src/dev.tsx +29 -11
  60. package/src/highlight.ts +5 -7
  61. package/src/hmr-plugin.ts +2 -1
  62. package/src/hosts/base.ts +1 -1
  63. package/src/hosts/single-child.ts +1 -5
  64. package/src/motion/hooks.ts +5 -2
  65. package/src/renderer/modes/InlineRenderer.ts +1 -1
  66. package/src/renderer/modes/StaticContentRenderer.ts +1 -1
  67. package/src/renderer.ts +3 -1
  68. package/src/test/mock-streams.ts +2 -1
  69. package/src/utils/flex-layout.ts +3 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-tui/react",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "React bindings for @effect-tui/core",
5
5
  "type": "module",
6
6
  "files": [
@@ -83,7 +83,7 @@
83
83
  "prepublishOnly": "bun run typecheck && bun run build"
84
84
  },
85
85
  "dependencies": {
86
- "@effect-tui/core": "^0.2.2",
86
+ "@effect-tui/core": "^0.2.4",
87
87
  "@effect/platform": "^0.94.0",
88
88
  "@effect/platform-bun": "^0.87.0",
89
89
  "@effect/rpc": "^0.73.0",
@@ -335,7 +335,7 @@ export function MultilineTextInput({
335
335
  if (newRow !== cursor.row || newCol !== cursor.col) {
336
336
  setCursor({ row: newRow, col: newCol })
337
337
  }
338
- }, [value, logicalLines, layout.lines, cursor.row, cursor.col])
338
+ }, [logicalLines, layout.lines, cursor.row, cursor.col])
339
339
 
340
340
  // Keep scroll in bounds (visual lines)
341
341
  useEffect(() => {
@@ -352,5 +352,5 @@ export function TextInput({
352
352
  ],
353
353
  )
354
354
 
355
- return <canvas draw={draw} width={width} height={1} inheritBg />
355
+ return <canvas draw={draw} width={width} height={1} />
356
356
  }
@@ -219,7 +219,7 @@ export function deleteWordBackwardMultiline(state: MultilineState): EditResult<M
219
219
  const match = matchPrevWord(prevLine)
220
220
 
221
221
  if (match) {
222
- const killed = match + "\n"
222
+ const killed = `${match}\n`
223
223
  const newPrevLine = prevLine.slice(0, prevLine.length - match.length)
224
224
  const newLines = [...lines]
225
225
  newLines[cursor.row - 1] = newPrevLine + currentLine
@@ -2,9 +2,9 @@
2
2
  // Intercepts console.log/info/warn/error/debug and stores entries for display
3
3
 
4
4
  import { Console } from "node:console"
5
+ import { EventEmitter } from "node:events"
5
6
  import { Writable } from "node:stream"
6
7
  import * as util from "node:util"
7
- import { EventEmitter } from "events"
8
8
 
9
9
  // ─────────────────────────────────────────────────────────────
10
10
  // Types
@@ -451,11 +451,11 @@ export function ConsolePopover({
451
451
  {/* Title bar */}
452
452
  <hstack height={1} bg={TITLE_BG}>
453
453
  <text fg={feedback ? Colors.green : TITLE_FG} bg={TITLE_BG}>
454
- {" " + titleText}
454
+ {` ${titleText}`}
455
455
  </text>
456
456
  <spacer />
457
457
  <text fg={Colors.gray(14)} bg={TITLE_BG}>
458
- {(mode === "inline" ? inlineHints : hints) + " "}
458
+ {`${mode === "inline" ? inlineHints : hints} `}
459
459
  </text>
460
460
  </hstack>
461
461
 
@@ -162,7 +162,7 @@ export class DebugOverlay {
162
162
  // Key handling - returns true if event was consumed
163
163
  // ─────────────────────────────────────────────────────────────
164
164
 
165
- handleKey(key: KeyMsg, width: number, height: number): boolean {
165
+ handleKey(key: KeyMsg, _width: number, height: number): boolean {
166
166
  // Ctrl+Shift+D - toggle overlay (handled even when hidden)
167
167
  if (key.ctrl && key.shift && key.name === "char" && key.text === "d") {
168
168
  this.toggle()
@@ -246,7 +246,7 @@ export class DebugOverlay {
246
246
  // Mouse handling - returns true if event was consumed
247
247
  // ─────────────────────────────────────────────────────────────
248
248
 
249
- handleMouse(mouse: MouseMsg, width: number, height: number): boolean {
249
+ handleMouse(mouse: MouseMsg, _width: number, height: number): boolean {
250
250
  if (!this._visible) return false
251
251
 
252
252
  const popoverHeight = this.getPopoverHeight(height)
package/src/dev.tsx CHANGED
@@ -9,12 +9,12 @@
9
9
  * - Remote control when EFFECT_TUI_REMOTE=1
10
10
  */
11
11
 
12
+ import { readFileSync } from "node:fs"
13
+ import { stat } from "node:fs/promises"
14
+ import { dirname } from "node:path"
12
15
  import { Colors } from "@effect-tui/core"
13
16
  import * as watcher from "@parcel/watcher"
14
17
  import { globalValue } from "effect/GlobalValue"
15
- import { readFileSync } from "fs"
16
- import { stat } from "fs/promises"
17
- import { dirname } from "path"
18
18
  import React from "react"
19
19
  import { Overlay } from "./components/Overlay.js"
20
20
  import { ConsolePopover } from "./console/ConsolePopover.js"
@@ -222,6 +222,20 @@ export interface DevRenderResult {
222
222
  stop: () => Promise<void>
223
223
  }
224
224
 
225
+ /**
226
+ * Clear require.cache for project files to ensure fresh imports.
227
+ * Bun uses require.cache internally even for ESM modules.
228
+ * This is necessary because query-string cache-busting only affects
229
+ * the entry point, not transitive dependencies.
230
+ */
231
+ function clearProjectCache(projectRoot: string): void {
232
+ for (const file of Object.keys(require.cache)) {
233
+ if (file.startsWith(projectRoot) && !file.includes("node_modules")) {
234
+ delete require.cache[file]
235
+ }
236
+ }
237
+ }
238
+
225
239
  /**
226
240
  * Wait for a file to stabilize (no size changes) before proceeding.
227
241
  * This avoids importing partially-written files.
@@ -363,10 +377,10 @@ function DevWrapper({
363
377
  <ToastProvider>
364
378
  <Overlay>
365
379
  {/* Base - determines container size */}
366
- <>
367
- <ScreenshotHandler />
368
- {children}
369
- </>
380
+
381
+ <ScreenshotHandler />
382
+ {children}
383
+
370
384
  {/* Overlays */}
371
385
  {visible && (
372
386
  <Overlay.Item alignment={{ v: "bottom" }}>
@@ -401,11 +415,17 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
401
415
  let version = 0
402
416
  let subscription: watcher.AsyncSubscription | null = null
403
417
 
418
+ // Determine project root for cache clearing
419
+ const projectRoot = options?.watchDirs?.[0] ?? dirname(entryPath)
420
+
404
421
  // Import and render the module
405
422
  const render = async () => {
406
423
  const thisVersion = ++version
407
424
 
408
425
  try {
426
+ // Clear module cache for project files before re-importing
427
+ clearProjectCache(projectRoot)
428
+
409
429
  // Cache-bust by adding query string with version
410
430
  const mod = await import(`${entryPath}?v=${thisVersion}`)
411
431
 
@@ -445,10 +465,8 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
445
465
  await render()
446
466
 
447
467
  // Watch for changes using @parcel/watcher
448
- const watchDir = options?.watchDirs?.[0] ?? dirname(entryPath)
449
-
450
468
  try {
451
- subscription = await watcher.subscribe(watchDir, async (err, events) => {
469
+ subscription = await watcher.subscribe(projectRoot, async (err, events) => {
452
470
  if (err) {
453
471
  options?.onError?.(err)
454
472
  console.error("[devRender] Watcher error:", err)
@@ -472,7 +490,7 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
472
490
  }, debounceMs)
473
491
  })
474
492
  } catch (err) {
475
- console.error(`[devRender] Failed to watch ${watchDir}:`, err)
493
+ console.error(`[devRender] Failed to watch ${projectRoot}:`, err)
476
494
  }
477
495
 
478
496
  const stop = async () => {
package/src/highlight.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Color } from "@effect-tui/core"
2
- import { type BundledLanguage, type BundledTheme, createHighlighter, type Highlighter } from "shiki"
2
+ import { type BundledLanguage, type BundledTheme, createHighlighter, type Highlighter, type ThemedToken } from "shiki"
3
3
 
4
4
  export interface HighlightTokenStyle {
5
5
  fg?: Color
@@ -48,17 +48,15 @@ export async function highlightCode(
48
48
  }
49
49
 
50
50
  const tokensResult = await highlighter.codeToTokens(code, { lang, theme })
51
- const tokenLines = Array.isArray(tokensResult)
52
- ? tokensResult
53
- : // Shiki v3 returns { tokens, theme }
54
- (tokensResult as any).tokens
51
+ // Shiki v3 returns { tokens: ThemedToken[][], ... }
52
+ const tokenLines: ThemedToken[][] = tokensResult.tokens
55
53
 
56
54
  if (!Array.isArray(tokenLines)) return toPlainLines(code)
57
55
 
58
- return tokenLines.map((line: any[]) =>
56
+ return tokenLines.map((line) =>
59
57
  line.map((token) => {
60
58
  const style: HighlightTokenStyle = {}
61
- if (token.color) style.fg = token.color
59
+ if (token.color) style.fg = token.color as Color
62
60
  const fs = token.fontStyle ?? 0
63
61
  // fontStyle bitmask is: 1 = Italic, 2 = Bold, 4 = Underline
64
62
  if (fs & 2) style.bold = true
package/src/hmr-plugin.ts CHANGED
@@ -12,8 +12,9 @@
12
12
  * [run]
13
13
  * preload = ["./node_modules/@effect-tui/react/dist/hmr-plugin.js"]
14
14
  */
15
+
16
+ import { relative } from "node:path"
15
17
  import { plugin } from "bun"
16
- import { relative } from "path"
17
18
 
18
19
  // Only enable in development
19
20
  if (process.env.NODE_ENV !== "production") {
package/src/hosts/base.ts CHANGED
@@ -54,7 +54,7 @@ export abstract class BaseHost implements HostInstance {
54
54
 
55
55
  protected ctx: HostContext
56
56
 
57
- constructor(type: string, props: CommonProps, ctx: HostContext) {
57
+ constructor(type: string, _props: CommonProps, ctx: HostContext) {
58
58
  this.id = `${type}-${idCounter++}`
59
59
  this.type = type
60
60
  this.ctx = ctx
@@ -1,4 +1,4 @@
1
- import type { CommonProps, HostContext, HostInstance } from "../reconciler/types.js"
1
+ import type { HostInstance } from "../reconciler/types.js"
2
2
  import { BaseHost } from "./base.js"
3
3
 
4
4
  /**
@@ -8,10 +8,6 @@ import { BaseHost } from "./base.js"
8
8
  export abstract class SingleChildHost extends BaseHost {
9
9
  private warned = false
10
10
 
11
- constructor(type: string, props: CommonProps, ctx: HostContext) {
12
- super(type, props, ctx)
13
- }
14
-
15
11
  protected get child(): HostInstance | null {
16
12
  return this.children[0] ?? null
17
13
  }
@@ -34,8 +34,11 @@ export function useMotionValue<T>(initial: T): MotionValue<T> {
34
34
  * If no renderer is passed, it will use the nearest RendererContext (useRenderer()).
35
35
  */
36
36
  export function useSpringRenderer(renderer?: { requestRender: () => void }) {
37
- // Prefer explicit renderer; otherwise pull from context
38
- const inferred = renderer ?? useRenderer()
37
+ // Always call useRenderer to satisfy rules of hooks
38
+ const contextRenderer = useRenderer()
39
+ // Prefer explicit renderer; otherwise use context
40
+ const inferred = renderer ?? contextRenderer
41
+
39
42
  useEffect(() => {
40
43
  if (!inferred) return
41
44
  // Store the bound function so we can compare for cleanup
@@ -139,7 +139,7 @@ export class InlineRenderer implements RendererMode {
139
139
 
140
140
  // Clear any extra lines if content shrank
141
141
  for (let screenY = rowCount; screenY < this.previousHeight; screenY++) {
142
- output += palette.sgr(0) + ANSI.line.clear + "\r\n"
142
+ output += `${palette.sgr(0) + ANSI.line.clear}\r\n`
143
143
  this.printedWidths[screenY] = 0
144
144
  }
145
145
 
@@ -45,7 +45,7 @@ export class StaticContentRenderer {
45
45
  for (let y = 0; y < staticSize.h; y++) {
46
46
  const trimmedWidth = rowContentWidth(staticBuffer, y, frameWidth)
47
47
  const line = emitRowWithReset(staticBuffer, this.palette, y, frameWidth, 0, trimmedWidth)
48
- contentLines += line + "\n"
48
+ contentLines += `${line}\n`
49
49
  }
50
50
 
51
51
  // Cache the content for replay on resize
package/src/renderer.ts CHANGED
@@ -176,7 +176,9 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
176
176
  })
177
177
 
178
178
  // Combine static + dynamic output for atomic write
179
- const output = staticOutput + modeOutput + state.palette.sgr(0)
179
+ // Wrap in synchronized output mode (DECSET 2026) to prevent tearing
180
+ // Terminals that don't support it will safely ignore these sequences
181
+ const output = ANSI.sync.begin + staticOutput + modeOutput + state.palette.sgr(0) + ANSI.sync.end
180
182
  contentH = contentHeight
181
183
  const diffAnsiMs = performance.now() - diffStartMs
182
184
  Prof.endPhase("diff+ansi", t)
@@ -134,7 +134,8 @@ export function encodeKey(key: KeyMsg): Buffer {
134
134
  */
135
135
  export function stripAnsi(str: string): string {
136
136
  // eslint-disable-next-line no-control-regex
137
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "")
137
+ // Pattern handles: CSI sequences (including DEC private modes like ?2026h), OSC sequences
138
+ return str.replace(/\x1b\[[\?0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "")
138
139
  }
139
140
 
140
141
  /**
@@ -68,9 +68,8 @@ export function measureFlex(
68
68
 
69
69
  if (greedyWeight > 0) {
70
70
  // Greedy: measure with proportional share of remaining space
71
- const greedyMain = totalGreedyWeight > 0
72
- ? (remainingForGreedy * greedyWeight) / totalGreedyWeight
73
- : remainingForGreedy
71
+ const greedyMain =
72
+ totalGreedyWeight > 0 ? (remainingForGreedy * greedyWeight) / totalGreedyWeight : remainingForGreedy
74
73
  const childMaxW = axis === "vertical" ? maxCross : greedyMain
75
74
  const childMaxH = axis === "vertical" ? greedyMain : maxCross
76
75
  const size = child.measure(childMaxW, childMaxH)
@@ -82,7 +81,7 @@ export function measureFlex(
82
81
 
83
82
  // Calculate total main dimension
84
83
  // Use actual measured sizes, not the constraint
85
- let totalMain = nonGreedyTotal + greedyMeasuredTotal + totalSpacing
84
+ const totalMain = nonGreedyTotal + greedyMeasuredTotal + totalSpacing
86
85
 
87
86
  // Build total size from main/cross dimensions
88
87
  const totalW = axis === "vertical" ? maxChildCross : totalMain