@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/dist/src/components/Markdown.d.ts +3 -3
- package/dist/src/components/Markdown.d.ts.map +1 -1
- package/dist/src/components/Markdown.js +16 -15
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/hosts/text.d.ts +1 -1
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +40 -11
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/Markdown.tsx +20 -17
- package/src/hosts/text.ts +37 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
-
/**
|
|
56
|
-
|
|
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}
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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.
|
|
127
|
+
if (line.trimEnd()) result.push(line.trimEnd())
|
|
101
128
|
}
|
|
102
129
|
return result.length > 0 ? result : [""]
|
|
103
130
|
}
|