@effect-tui/react 0.14.1 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-tui/react",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "description": "React bindings for @effect-tui/core",
5
5
  "type": "module",
6
6
  "files": [
@@ -83,8 +83,8 @@
83
83
  "prepublishOnly": "bun run typecheck && bun run build"
84
84
  },
85
85
  "dependencies": {
86
- "@effect-tui/core": "^0.14.1",
87
- "@effect/platform": "^0.94.0",
86
+ "@effect-tui/core": "^0.14.3",
87
+ "@effect/platform": "^0.94.1",
88
88
  "@effect/platform-bun": "^0.87.0",
89
89
  "@effect/rpc": "^0.73.0",
90
90
  "@parcel/watcher": "^2.5.1",
@@ -92,8 +92,8 @@
92
92
  "shiki": "^3.20.0"
93
93
  },
94
94
  "peerDependencies": {
95
- "effect": "^3.19.13",
96
- "react": "^19.2.3",
95
+ "effect": "^3.0.0",
96
+ "react": "^19.0.0",
97
97
  "@effect/opentelemetry": "^0.60.0",
98
98
  "@opentelemetry/api": "^1.9.0",
99
99
  "@opentelemetry/sdk-trace-base": "^2.2.0"
@@ -119,7 +119,7 @@
119
119
  "@types/node": "^25.0.3",
120
120
  "@types/react": "^19.0.0",
121
121
  "@types/react-reconciler": "^0.32.3",
122
- "effect": "^3.19.13",
122
+ "effect": "^3.19.14",
123
123
  "react": "^19.2.3",
124
124
  "typescript": "^5.9.3",
125
125
  "vitest": "^4.0.16"
@@ -2,6 +2,7 @@ import { type Color, Colors, displayWidth, graphemes } from "@effect-tui/core"
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react"
3
3
  import { useKeyboard } from "../hooks/use-keyboard.js"
4
4
  import { usePaste } from "../hooks/use-paste.js"
5
+ import { isWhitespace } from "../utils/word-boundaries.js"
5
6
  import type { DrawContext } from "../hosts/canvas.js"
6
7
  import {
7
8
  deleteCharBackwardMultiline,
@@ -97,7 +98,7 @@ function wrapLogicalLine(line: string, logicalRow: number, maxWidth: number): Li
97
98
  }
98
99
 
99
100
  // Find break opportunities (after spaces)
100
- const isBreakable = graphemeList.map((g) => /\s/.test(g))
101
+ const isBreakable = graphemeList.map((g) => isWhitespace(g))
101
102
 
102
103
  const visualLines: VisualLine[] = []
103
104
  let start = 0
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { graphemes } from "@effect-tui/core"
8
+ import { matchNextWord, matchPrevWord } from "../utils/word-boundaries.js"
8
9
 
9
10
  // ============================================================================
10
11
  // Types
@@ -34,15 +35,7 @@ export interface EditResult<T> {
34
35
  // Word boundary helpers (shared by TextInput and MultilineTextInput)
35
36
  // ============================================================================
36
37
 
37
- /** Match the previous word (and trailing whitespace) or whitespace-only chunk. */
38
- export function matchPrevWord(beforeCursor: string): string | null {
39
- return beforeCursor.match(/(\S+\s*|\s+)$/)?.[0] ?? null
40
- }
41
-
42
- /** Match the next word (including leading whitespace). */
43
- export function matchNextWord(afterCursor: string): string | null {
44
- return afterCursor.match(/^\s*\S+/)?.[0] ?? null
45
- }
38
+ export { matchNextWord, matchPrevWord }
46
39
 
47
40
  // ============================================================================
48
41
  // Single-line operations (for TextInput)
package/src/hosts/text.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CellBuffer, type Color, displayWidth, type Palette } from "@effect-tui/core"
2
2
  import type { ColorMotionValue } from "../motion/color-motion-value.js"
3
3
  import type { CommonProps, HostContext, HostInstance, Rect, Size } from "../reconciler/types.js"
4
- import { resolveInheritedBgStyle, styleIdFromProps, wrapSpans } from "../utils/index.js"
4
+ import { isWhitespace, resolveInheritedBgStyle, splitWords, styleIdFromProps, wrapSpans } from "../utils/index.js"
5
5
  import { BaseHost, getInheritedBg } from "./base.js"
6
6
 
7
7
  /** Color prop that can be a static Color or a spring-animated ColorMotionValue */
@@ -230,19 +230,19 @@ export class TextHost extends BaseHost {
230
230
  }
231
231
 
232
232
  // Split into words (keeping whitespace as separate tokens)
233
- const tokens = rawLine.split(/(\s+)/)
233
+ const tokens = splitWords(rawLine)
234
234
  let line = ""
235
235
  let lineW = 0
236
236
 
237
237
  for (const token of tokens) {
238
238
  const tokenW = displayWidth(token)
239
- const isWhitespace = /^\s+$/.test(token)
239
+ const isWs = isWhitespace(token)
240
240
 
241
241
  if (lineW + tokenW <= maxWidth) {
242
242
  // Token fits on current line
243
243
  line += token
244
244
  lineW += tokenW
245
- } else if (isWhitespace) {
245
+ } else if (isWs) {
246
246
  // Whitespace doesn't fit - just skip it (don't start new line with space)
247
247
  continue
248
248
  } else if (tokenW <= maxWidth) {
@@ -21,3 +21,4 @@ export {
21
21
  toColorValue,
22
22
  } from "./styles.js"
23
23
  export { wrapSpans } from "./text-wrap.js"
24
+ export { isWhitespace, matchNextWord, matchPrevWord, splitWords } from "./word-boundaries.js"
@@ -1,4 +1,5 @@
1
1
  import { displayWidth } from "@effect-tui/core"
2
+ import { isWhitespace, splitWords } from "./word-boundaries.js"
2
3
 
3
4
  type SpanLike = { text: string }
4
5
 
@@ -12,18 +13,18 @@ export function wrapSpans<T extends SpanLike>(spans: T[], maxWidth: number): T[]
12
13
 
13
14
  for (const span of spans) {
14
15
  // Split span text into words (keeping whitespace as separate tokens)
15
- const tokens = span.text.split(/(\s+)/)
16
+ const tokens = splitWords(span.text)
16
17
 
17
18
  for (const token of tokens) {
18
19
  if (!token) continue
19
20
  const tokenWidth = displayWidth(token)
20
- const isWhitespace = /^\s+$/.test(token)
21
+ const isWs = isWhitespace(token)
21
22
 
22
23
  if (lineWidth + tokenWidth <= maxWidth) {
23
24
  // Token fits on current line
24
25
  lines[lines.length - 1].push({ ...span, text: token })
25
26
  lineWidth += tokenWidth
26
- } else if (isWhitespace) {
27
+ } else if (isWs) {
27
28
  // Skip whitespace at line break
28
29
  continue
29
30
  } else if (tokenWidth <= maxWidth) {
@@ -0,0 +1,40 @@
1
+ const whitespaceRegex = /^\s+$/u
2
+
3
+ export function isWhitespace(text: string): boolean {
4
+ return whitespaceRegex.test(text)
5
+ }
6
+
7
+ export function splitWords(text: string): string[] {
8
+ if (!text) return []
9
+ return text.split(/(\s+)/u).filter((token) => token.length > 0)
10
+ }
11
+
12
+ /** Match the previous word (and trailing whitespace) or whitespace-only chunk. */
13
+ export function matchPrevWord(beforeCursor: string): string | null {
14
+ const tokens = splitWords(beforeCursor)
15
+ if (tokens.length === 0) return null
16
+ const last = tokens[tokens.length - 1]
17
+ if (isWhitespace(last)) {
18
+ const prev = tokens[tokens.length - 2]
19
+ if (prev && !isWhitespace(prev)) {
20
+ return prev + last
21
+ }
22
+ return last
23
+ }
24
+ return last
25
+ }
26
+
27
+ /** Match the next word (including leading whitespace). */
28
+ export function matchNextWord(afterCursor: string): string | null {
29
+ const tokens = splitWords(afterCursor)
30
+ if (tokens.length === 0) return null
31
+ const first = tokens[0]
32
+ if (isWhitespace(first)) {
33
+ const next = tokens[1]
34
+ if (next && !isWhitespace(next)) {
35
+ return first + next
36
+ }
37
+ return null
38
+ }
39
+ return first
40
+ }