@effect-tui/react 0.3.1 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-tui/react",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
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.3.1",
86
+ "@effect-tui/core": "^0.4.1",
87
87
  "@effect/platform": "^0.94.0",
88
88
  "@effect/platform-bun": "^0.87.0",
89
89
  "@effect/rpc": "^0.73.0",
@@ -52,8 +52,8 @@ export interface MarkdownProps {
52
52
  theme?: MarkdownTheme
53
53
  /** Code block theme for syntax highlighting */
54
54
  codeTheme?: BundledTheme
55
- /** Maximum width for text wrapping (default: no wrap) */
56
- maxWidth?: number
55
+ /** Enable text wrapping (default: false, text is truncated) */
56
+ wrap?: boolean
57
57
  }
58
58
 
59
59
  // Parsed markdown elements
@@ -226,39 +226,42 @@ function parseMarkdown(content: string): MdElement[] {
226
226
  }
227
227
 
228
228
  /**
229
- * Render inline spans as text elements
229
+ * Render inline spans as text elements.
230
+ * Uses <text wrap> for host-level wrapping when wrap=true.
230
231
  */
231
- function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme>) {
232
+ function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme>, wrap = false) {
232
233
  return spans.map((span, i) => {
233
234
  switch (span.type) {
234
235
  case "text":
235
236
  return (
236
- <text key={i} fg={theme.text}>
237
+ <text key={i} fg={theme.text} wrap={wrap}>
237
238
  {span.text}
238
239
  </text>
239
240
  )
240
241
  case "bold":
241
242
  return (
242
- <text key={i} fg={theme.bold} bold>
243
+ <text key={i} fg={theme.bold} bold wrap={wrap}>
243
244
  {span.text}
244
245
  </text>
245
246
  )
246
247
  case "italic":
247
248
  return (
248
- <text key={i} fg={theme.italic} italic>
249
+ <text key={i} fg={theme.italic} italic wrap={wrap}>
249
250
  {span.text}
250
251
  </text>
251
252
  )
252
253
  case "code":
253
254
  return (
254
- <text key={i} fg={theme.code} bg={theme.codeBg}>
255
+ <text key={i} fg={theme.code} bg={theme.codeBg} wrap={wrap}>
255
256
  {span.text}
256
257
  </text>
257
258
  )
258
259
  case "link":
259
260
  return (
260
261
  <hstack key={i}>
261
- <text fg={theme.link}>{span.text}</text>
262
+ <text fg={theme.link} wrap={wrap}>
263
+ {span.text}
264
+ </text>
262
265
  <text fg={theme.linkUrl}>{" ("}</text>
263
266
  <text fg={theme.linkUrl}>{span.url}</text>
264
267
  <text fg={theme.linkUrl}>{")"}</text>
@@ -294,7 +297,7 @@ function renderSpans(spans: MdSpan[], theme: Required<MarkdownTheme>) {
294
297
  * `} />
295
298
  * ```
296
299
  */
297
- export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }: MarkdownProps) {
300
+ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord", wrap = false }: MarkdownProps) {
298
301
  const theme = { ...defaultTheme, ...themeOverrides }
299
302
  const elements = parseMarkdown(content)
300
303
 
@@ -304,27 +307,27 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
304
307
  switch (el.type) {
305
308
  case "h1":
306
309
  return (
307
- <text key={i} fg={theme.h1} bold>
310
+ <text key={i} fg={theme.h1} bold wrap={wrap}>
308
311
  {"# "}
309
312
  {el.text}
310
313
  </text>
311
314
  )
312
315
  case "h2":
313
316
  return (
314
- <text key={i} fg={theme.h2} bold>
317
+ <text key={i} fg={theme.h2} bold wrap={wrap}>
315
318
  {"## "}
316
319
  {el.text}
317
320
  </text>
318
321
  )
319
322
  case "h3":
320
323
  return (
321
- <text key={i} fg={theme.h3} bold>
324
+ <text key={i} fg={theme.h3} bold wrap={wrap}>
322
325
  {"### "}
323
326
  {el.text}
324
327
  </text>
325
328
  )
326
329
  case "paragraph":
327
- return <hstack key={i}>{renderSpans(el.spans, theme)}</hstack>
330
+ return <hstack key={i}>{renderSpans(el.spans, theme, wrap)}</hstack>
328
331
  case "code":
329
332
  return (
330
333
  <CodeBlock
@@ -340,7 +343,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
340
343
  return (
341
344
  <hstack key={i}>
342
345
  <text fg={theme.quoteBorder}>{"│ "}</text>
343
- {renderSpans(el.spans, theme)}
346
+ {renderSpans(el.spans, theme, wrap)}
344
347
  </hstack>
345
348
  )
346
349
  case "ul":
@@ -349,7 +352,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
349
352
  {el.items.map((item, j) => (
350
353
  <hstack key={j}>
351
354
  <text fg={theme.listMarker}>{" • "}</text>
352
- {renderSpans(item, theme)}
355
+ {renderSpans(item, theme, wrap)}
353
356
  </hstack>
354
357
  ))}
355
358
  </vstack>
@@ -360,7 +363,7 @@ export function Markdown({ content, theme: themeOverrides, codeTheme = "nord" }:
360
363
  {el.items.map((item, j) => (
361
364
  <hstack key={j}>
362
365
  <text fg={theme.listMarker}>{` ${el.start + j}. `}</text>
363
- {renderSpans(item, theme)}
366
+ {renderSpans(item, theme, wrap)}
364
367
  </hstack>
365
368
  ))}
366
369
  </vstack>
package/src/hosts/text.ts CHANGED
@@ -76,7 +76,7 @@ export class TextHost extends BaseHost {
76
76
  return { w, h }
77
77
  }
78
78
 
79
- /** Wrap text to fit within maxWidth */
79
+ /** Wrap text to fit within maxWidth, preferring word boundaries */
80
80
  private wrapText(text: string, maxWidth: number): string[] {
81
81
  const result: string[] = []
82
82
  for (const rawLine of text.split("\n")) {
@@ -84,20 +84,47 @@ export class TextHost extends BaseHost {
84
84
  result.push("")
85
85
  continue
86
86
  }
87
+
88
+ // Split into words (keeping whitespace as separate tokens)
89
+ const tokens = rawLine.split(/(\s+)/)
87
90
  let line = ""
88
91
  let lineW = 0
89
- for (const ch of rawLine) {
90
- const w = displayWidth(ch)
91
- if (lineW + w > maxWidth && line.length > 0) {
92
- result.push(line)
93
- line = ch
94
- lineW = w
92
+
93
+ for (const token of tokens) {
94
+ const tokenW = displayWidth(token)
95
+ const isWhitespace = /^\s+$/.test(token)
96
+
97
+ if (lineW + tokenW <= maxWidth) {
98
+ // Token fits on current line
99
+ line += token
100
+ lineW += tokenW
101
+ } else if (isWhitespace) {
102
+ // Whitespace doesn't fit - just skip it (don't start new line with space)
103
+ continue
104
+ } else if (tokenW <= maxWidth) {
105
+ // Word doesn't fit but is smaller than maxWidth - start new line
106
+ if (line.trimEnd()) result.push(line.trimEnd())
107
+ line = token
108
+ lineW = tokenW
95
109
  } else {
96
- line += ch
97
- lineW += w
110
+ // Word is longer than maxWidth - break it character by character
111
+ if (line.trimEnd()) result.push(line.trimEnd())
112
+ line = ""
113
+ lineW = 0
114
+ for (const ch of token) {
115
+ const chW = displayWidth(ch)
116
+ if (lineW + chW > maxWidth && line.length > 0) {
117
+ result.push(line)
118
+ line = ch
119
+ lineW = chW
120
+ } else {
121
+ line += ch
122
+ lineW += chW
123
+ }
124
+ }
98
125
  }
99
126
  }
100
- if (line.length > 0) result.push(line)
127
+ if (line.trimEnd()) result.push(line.trimEnd())
101
128
  }
102
129
  return result.length > 0 ? result : [""]
103
130
  }