termcast 1.3.36 → 1.3.37

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 (44) hide show
  1. package/dist/components/detail.d.ts.map +1 -1
  2. package/dist/components/detail.js +12 -2
  3. package/dist/components/detail.js.map +1 -1
  4. package/dist/components/list.d.ts.map +1 -1
  5. package/dist/components/list.js +11 -2
  6. package/dist/components/list.js.map +1 -1
  7. package/dist/diagram-parser.d.ts +34 -0
  8. package/dist/diagram-parser.d.ts.map +1 -0
  9. package/dist/diagram-parser.js +114 -0
  10. package/dist/diagram-parser.js.map +1 -0
  11. package/dist/examples/simple-detail-markdown.d.ts +2 -0
  12. package/dist/examples/simple-detail-markdown.d.ts.map +1 -0
  13. package/dist/examples/simple-detail-markdown.js +94 -0
  14. package/dist/examples/simple-detail-markdown.js.map +1 -0
  15. package/dist/internal/scrollbox.d.ts.map +1 -1
  16. package/dist/internal/scrollbox.js +1 -2
  17. package/dist/internal/scrollbox.js.map +1 -1
  18. package/dist/markdown-utils.d.ts +14 -0
  19. package/dist/markdown-utils.d.ts.map +1 -0
  20. package/dist/markdown-utils.js +138 -0
  21. package/dist/markdown-utils.js.map +1 -0
  22. package/dist/theme.d.ts.map +1 -1
  23. package/dist/theme.js +5 -24
  24. package/dist/theme.js.map +1 -1
  25. package/dist/themes/termcast.json +4 -4
  26. package/dist/themes.d.ts +12 -0
  27. package/dist/themes.d.ts.map +1 -1
  28. package/dist/themes.js +79 -0
  29. package/dist/themes.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/components/detail.tsx +16 -2
  32. package/src/components/list.tsx +15 -2
  33. package/src/diagram-parser.tsx +141 -0
  34. package/src/examples/list-with-detail.vitest.tsx +34 -34
  35. package/src/examples/list-with-sections.vitest.tsx +1 -1
  36. package/src/examples/simple-detail-markdown.tsx +96 -0
  37. package/src/examples/simple-detail-markdown.vitest.tsx +156 -0
  38. package/src/examples/simple-grid.vitest.tsx +2 -2
  39. package/src/examples/swift-extension.vitest.tsx +1 -1
  40. package/src/internal/scrollbox.tsx +1 -3
  41. package/src/markdown-utils.tsx +182 -0
  42. package/src/theme.tsx +5 -24
  43. package/src/themes/termcast.json +4 -4
  44. package/src/themes.ts +98 -0
@@ -4,7 +4,7 @@ import {
4
4
  TextAttributes,
5
5
  TextareaRenderable,
6
6
  } from '@opentui/core'
7
- import { useKeyboard, flushSync } from '@opentui/react'
7
+ import { useKeyboard, flushSync, useRenderer } from '@opentui/react'
8
8
  import React, {
9
9
  ReactElement,
10
10
  ReactNode,
@@ -32,6 +32,7 @@ import { Color, resolveColor } from 'termcast/src/colors'
32
32
  import { getIconEmoji, getIconValue } from 'termcast/src/components/icon'
33
33
  import { ActionPanel } from 'termcast/src/components/actions'
34
34
  import { useTheme, markdownSyntaxStyle } from 'termcast/src/theme'
35
+ import { createMarkdownRenderNode } from 'termcast/src/markdown-utils'
35
36
  import { CommonProps } from 'termcast/src/utils'
36
37
 
37
38
  export { Color }
@@ -1412,6 +1413,18 @@ const ListItem: ListItemType = (props) => {
1412
1413
  )
1413
1414
  }
1414
1415
 
1416
+ // Renders markdown with link URL stripping via renderNode for list detail panel.
1417
+ function ListMarkdownContent({ markdown }: { markdown: string }): any {
1418
+ const renderer = useRenderer()
1419
+ const renderNode = React.useMemo(() => {
1420
+ return createMarkdownRenderNode(renderer)
1421
+ }, [renderer])
1422
+
1423
+ return (
1424
+ <markdown content={markdown} syntaxStyle={markdownSyntaxStyle} conceal renderNode={renderNode} />
1425
+ )
1426
+ }
1427
+
1415
1428
  const ListItemDetail: ListItemDetailType = (props) => {
1416
1429
  const theme = useTheme()
1417
1430
  const { isLoading, markdown, metadata } = props
@@ -1441,7 +1454,7 @@ const ListItemDetail: ListItemDetailType = (props) => {
1441
1454
  >
1442
1455
  <box gap={1} style={{ flexDirection: 'column' }}>
1443
1456
  {markdown && (
1444
- <code content={markdown} filetype="markdown" syntaxStyle={markdownSyntaxStyle} drawUnstyledText={false} />
1457
+ <ListMarkdownContent markdown={markdown} />
1445
1458
  )}
1446
1459
  {metadata && (
1447
1460
  <box
@@ -0,0 +1,141 @@
1
+ // ASCII/Unicode diagram parser for syntax highlighting in markdown code blocks.
2
+ // Separates structural characters (box-drawing, arrows) from text content
3
+ // to render diagrams with muted structural elements and highlighted labels.
4
+ // Ported from critique (https://github.com/remorses/critique)
5
+
6
+ /**
7
+ * A segment of text with a specific color type
8
+ */
9
+ export interface DiagramSegment {
10
+ text: string
11
+ type: 'text' | 'muted'
12
+ }
13
+
14
+ /**
15
+ * A parsed line of diagram content
16
+ */
17
+ export interface ParsedDiagramLine {
18
+ segments: DiagramSegment[]
19
+ }
20
+
21
+ // Box drawing characters (Unicode)
22
+ const BOX_DRAWING_CHARS = new Set([
23
+ // Light box drawing
24
+ '┌', '┐', '└', '┘', '─', '│', '├', '┤', '┬', '┴', '┼',
25
+ // Double box drawing
26
+ '╔', '╗', '╚', '╝', '═', '║', '╠', '╣', '╦', '╩', '╬',
27
+ // Heavy box drawing
28
+ '┏', '┓', '┗', '┛', '━', '┃', '┣', '┫', '┳', '┻', '╋',
29
+ // Mixed light/heavy
30
+ '┍', '┎', '┑', '┒', '┕', '┖', '┙', '┚',
31
+ '┝', '┞', '┟', '┠', '┡', '┢', '┥', '┦', '┧', '┨', '┩', '┪',
32
+ '┭', '┮', '┯', '┰', '┱', '┲', '┵', '┶', '┷', '┸', '┹', '┺',
33
+ '┽', '┾', '┿', '╀', '╁', '╂', '╃', '╄', '╅', '╆', '╇', '╈', '╉', '╊',
34
+ // Rounded corners
35
+ '╭', '╮', '╯', '╰',
36
+ ])
37
+
38
+ // Arrow characters
39
+ const ARROW_CHARS = new Set([
40
+ // Unicode arrows
41
+ '▶', '◀', '▼', '▲', '►', '◄', '▾', '▴',
42
+ '→', '←', '↓', '↑', '↔', '↕', '↖', '↗', '↘', '↙',
43
+ '⇒', '⇐', '⇓', '⇑', '⇔', '⇕',
44
+ // Triangle arrows
45
+ '△', '▽', '◁', '▷', '⊳', '⊲', '⊴', '⊵',
46
+ ])
47
+
48
+ // ASCII diagram characters (structural, not text)
49
+ // Note: "v" and "V" are NOT included because they appear in regular text
50
+ // like "Server", "Validate", etc.
51
+ const ASCII_STRUCTURAL_CHARS = new Set(['-', '|', '+', '/', '\\', '<', '>', '^'])
52
+
53
+ /**
54
+ * Check if a character is a diagram structural character (should be muted)
55
+ */
56
+ function isDiagramChar(char: string): boolean {
57
+ return (
58
+ BOX_DRAWING_CHARS.has(char) ||
59
+ ARROW_CHARS.has(char) ||
60
+ ASCII_STRUCTURAL_CHARS.has(char)
61
+ )
62
+ }
63
+
64
+ /**
65
+ * Parse a single line of diagram content into segments
66
+ */
67
+ export function parseDiagramLine(line: string): ParsedDiagramLine {
68
+ if (!line) {
69
+ return { segments: [] }
70
+ }
71
+
72
+ const segments: DiagramSegment[] = []
73
+ let currentText = ''
74
+ let currentType: 'text' | 'muted' | null = null
75
+
76
+ // Iterate through each character (handling Unicode properly)
77
+ for (const char of line) {
78
+ const isMuted = isDiagramChar(char) || char === ' '
79
+ const type = isMuted ? 'muted' : 'text'
80
+
81
+ if (currentType === null) {
82
+ currentType = type
83
+ currentText = char
84
+ } else if (type === currentType) {
85
+ currentText += char
86
+ } else {
87
+ // Type changed, push current segment and start new one
88
+ segments.push({ text: currentText, type: currentType })
89
+ currentText = char
90
+ currentType = type
91
+ }
92
+ }
93
+
94
+ // Push final segment
95
+ if (currentText && currentType !== null) {
96
+ segments.push({ text: currentText, type: currentType })
97
+ }
98
+
99
+ return { segments }
100
+ }
101
+
102
+ /**
103
+ * Parse entire diagram content into lines of segments
104
+ */
105
+ export function parseDiagram(content: string): ParsedDiagramLine[] {
106
+ const lines = content.split('\n')
107
+ return lines.map(parseDiagramLine)
108
+ }
109
+
110
+ /**
111
+ * Convert parsed diagram to a debug string for testing
112
+ * Muted segments are replaced with '*' characters
113
+ */
114
+ export function diagramToDebugString(parsed: ParsedDiagramLine[]): string {
115
+ return parsed
116
+ .map((line) => {
117
+ return line.segments
118
+ .map((segment) => {
119
+ if (segment.type === 'muted') {
120
+ return '*'.repeat([...segment.text].length)
121
+ }
122
+ return segment.text
123
+ })
124
+ .join('')
125
+ })
126
+ .join('\n')
127
+ }
128
+
129
+ /**
130
+ * Convert ASCII diagram characters to Unicode box-drawing equivalents.
131
+ * Eliminates visual gaps between lines.
132
+ * - `|` -> `│` (vertical lines)
133
+ * - `--` or more -> `──` (horizontal lines, but not single hyphens in text)
134
+ */
135
+ export function convertAsciiToUnicode(content: string): string {
136
+ return content
137
+ .replace(/\|/g, '│')
138
+ .replace(/-{2,}/g, (match) => {
139
+ return '─'.repeat(match.length)
140
+ })
141
+ }
@@ -39,25 +39,25 @@ test('list with detail view display and navigation', async () => {
39
39
 
40
40
  ›bulbasaur #001
41
41
  ivysaur #002 │ bulbasaur ▲
42
- charmander #004 │
42
+ charmander #004 │
43
43
  charmeleon #005 │ Illustration
44
44
  squirtle #007 │
45
45
  wartortle #008 │ Types
46
+
46
47
  │ Grass / Poison
47
48
 
48
49
  │ Characteristics
50
+
49
51
  │ - Height: 0.7m
50
52
  │ - Weight: 6.9kg
51
53
 
52
54
  │ Abilities
55
+
53
56
  │ - Chlorophyll
54
57
  │ - Overgrow
55
58
 
56
59
 
57
60
  │ Types
58
-
59
- │ Grass
60
- │ ─────────────────
61
61
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
62
62
 
63
63
  "
@@ -78,25 +78,25 @@ test('list with detail view display and navigation', async () => {
78
78
 
79
79
  bulbasaur #001
80
80
  ›ivysaur #002 │ ivysaur ▲
81
- charmander #004 │
81
+ charmander #004 │
82
82
  charmeleon #005 │ Illustration
83
83
  squirtle #007 │
84
84
  wartortle #008 │ Types
85
+
85
86
  │ Grass / Poison
86
87
 
87
88
  │ Characteristics
89
+
88
90
  │ - Height: 1m
89
91
  │ - Weight: 13kg
90
92
 
91
93
  │ Abilities
94
+
92
95
  │ - Chlorophyll
93
96
  │ - Overgrow
94
97
 
95
98
 
96
99
  │ Types
97
-
98
- │ Grass
99
- │ ─────────────────
100
100
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
101
101
 
102
102
  "
@@ -119,21 +119,21 @@ test('list with detail view display and navigation', async () => {
119
119
  charmeleon #005 │ Illustration
120
120
  squirtle #007 │
121
121
  wartortle #008 │ Types
122
+
122
123
  │ Fire
123
124
 
124
125
  │ Characteristics
126
+
125
127
  │ - Height: 0.6m
126
128
  │ - Weight: 8.5kg
127
129
 
128
130
  │ Abilities
131
+
129
132
  │ - Blaze
130
133
  │ - Solar Power
131
134
 
132
135
 
133
136
  │ Types
134
-
135
- │ Fire
136
- │ ─────────────────
137
137
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
138
138
 
139
139
  "
@@ -173,7 +173,7 @@ test('list with detail view display and navigation', async () => {
173
173
  │ ↵ select ↑↓ navigate │
174
174
  │ │
175
175
  ╰──────────────────────────────────────────────────────────────────────────╯
176
- ─────────────────
176
+ Types
177
177
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
178
178
 
179
179
  "
@@ -242,21 +242,21 @@ test('list with detail view display and navigation', async () => {
242
242
  charmeleon #005 │ Illustration
243
243
  squirtle #007 │
244
244
  wartortle #008 │ Types
245
+
245
246
  │ Fire
246
247
 
247
248
  │ Characteristics
249
+
248
250
  │ - Height: 0.6m
249
251
  │ - Weight: 8.5kg
250
252
 
251
253
  │ Abilities
254
+
252
255
  │ - Blaze
253
256
  │ - Solar Power
254
257
 
255
258
 
256
259
  │ Types
257
-
258
- │ Fire
259
- │ ─────────────────
260
260
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
261
261
 
262
262
  "
@@ -291,21 +291,21 @@ test('list detail view search functionality', async () => {
291
291
  │ Illustration
292
292
 
293
293
  │ Types
294
+
294
295
  │ Fire
295
296
 
296
297
  │ Characteristics
298
+
297
299
  │ - Height: 0.6m
298
300
  │ - Weight: 8.5kg
299
301
 
300
302
  │ Abilities
303
+
301
304
  │ - Blaze
302
305
  │ - Solar Power
303
306
 
304
307
 
305
308
  │ Types
306
-
307
- │ Fire
308
- │ ─────────────────
309
309
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
310
310
 
311
311
  "
@@ -337,21 +337,21 @@ test('list detail view search functionality', async () => {
337
337
  │ Illustration
338
338
 
339
339
  │ Types
340
+
340
341
  │ Water
341
342
 
342
343
  │ Characteristics
344
+
343
345
  │ - Height: 1m
344
346
  │ - Weight: 22.5kg
345
347
 
346
348
  │ Abilities
349
+
347
350
  │ - Torrent
348
351
  │ - Rain Dish
349
352
 
350
353
 
351
354
  │ Types
352
-
353
- │ Water
354
- │ ─────────────────
355
355
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
356
356
 
357
357
  "
@@ -374,21 +374,21 @@ test('list detail view search functionality', async () => {
374
374
  │ Illustration
375
375
 
376
376
  │ Types
377
+
377
378
  │ Water
378
379
 
379
380
  │ Characteristics
381
+
380
382
  │ - Height: 1m
381
383
  │ - Weight: 22.5kg
382
384
 
383
385
  │ Abilities
386
+
384
387
  │ - Torrent
385
388
  │ - Rain Dish
386
389
 
387
390
 
388
391
  │ Types
389
-
390
- │ Water
391
- │ ─────────────────
392
392
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
393
393
 
394
394
  "
@@ -419,25 +419,25 @@ test('list detail metadata rendering', async () => {
419
419
 
420
420
  ›bulbasaur #001
421
421
  ivysaur #002 │ bulbasaur ▲
422
- charmander #004 │
422
+ charmander #004 │
423
423
  charmeleon #005 │ Illustration
424
424
  squirtle #007 │
425
425
  wartortle #008 │ Types
426
+
426
427
  │ Grass / Poison
427
428
 
428
429
  │ Characteristics
430
+
429
431
  │ - Height: 0.7m
430
432
  │ - Weight: 6.9kg
431
433
 
432
434
  │ Abilities
435
+
433
436
  │ - Chlorophyll
434
437
  │ - Overgrow
435
438
 
436
439
 
437
440
  │ Types
438
-
439
- │ Grass
440
- │ ─────────────────
441
441
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
442
442
 
443
443
  "
@@ -467,21 +467,21 @@ test('list detail metadata rendering', async () => {
467
467
  charmeleon #005 │ Illustration
468
468
  ›squirtle #007 │
469
469
  wartortle #008 │ Types
470
+
470
471
  │ Water
471
472
 
472
473
  │ Characteristics
474
+
473
475
  │ - Height: 0.5m
474
476
  │ - Weight: 9kg
475
477
 
476
478
  │ Abilities
479
+
477
480
  │ - Torrent
478
481
  │ - Rain Dish
479
482
 
480
483
 
481
484
  │ Types
482
-
483
- │ Water
484
- │ ─────────────────
485
485
  ↵ toggle detail ↑↓ navigate ^k a │ ▼
486
486
 
487
487
  "
@@ -563,6 +563,7 @@ test('list with detail layout consistency - short vs long detail content', async
563
563
  Another Item │ content ▀
564
564
 
565
565
  │ Section 1
566
+
566
567
  │ This is a very long description
567
568
  │ that contains multiple paragraphs
568
569
  │ and sections to test how the
@@ -570,11 +571,10 @@ test('list with detail layout consistency - short vs long detail content', async
570
571
  │ panel content overflows.
571
572
 
572
573
  │ Section 2
574
+
573
575
  │ More content here to ensure we
574
576
  │ have enough text to cause
575
- │ vertical overflow in the detail
576
- │ panel scrollbox.
577
- ↑↓ navigate ^k actions │ ▼
577
+ ↑↓ navigate ^k actions │ vertical overflow in the detail
578
578
 
579
579
  "
580
580
  `)
@@ -313,6 +313,7 @@ test('list with sections search functionality', async () => {
313
313
  Freshly baked bread from our bakery.
314
314
 
315
315
  Product Details
316
+
316
317
  - Baked fresh daily
317
318
  - Made with organic flour
318
319
  - No preservatives
@@ -322,7 +323,6 @@ test('list with sections search functionality', async () => {
322
323
  esc go back
323
324
 
324
325
 
325
-
326
326
  "
327
327
  `)
328
328
  }, 10000)
@@ -0,0 +1,96 @@
1
+ // Example: Detail view with markdown content including a diagram code block.
2
+ // Tests that the <markdown> element properly renders headings, prose,
3
+ // code blocks, and diagram content.
4
+
5
+ import { Detail } from 'termcast'
6
+ import { renderWithProviders } from '../utils'
7
+
8
+ const markdown = `# Architecture Overview
9
+
10
+ This document describes the system architecture.
11
+
12
+ ## Components
13
+
14
+ The system has three main components:
15
+
16
+ - **Client** - handles user interaction
17
+ - **Server** - processes requests
18
+ - **Database** - stores data
19
+
20
+ ## Links
21
+
22
+ Check out the [GitHub repository](https://github.com/remorses/termcast) for the source code.
23
+
24
+ See the [API documentation](https://developers.raycast.com/api-reference) for more details.
25
+
26
+ A paragraph with [multiple](https://example.com/one) links [inline](https://example.com/two) here.
27
+
28
+ Nested formatting: **bold with [link inside](https://example.com/bold)** and *italic with [link](https://example.com/italic)*.
29
+
30
+ ## Configuration Table
31
+
32
+ | Setting | Default | Description |
33
+ |---------|---------|-------------|
34
+ | Host | localhost | Database host address |
35
+ | Port | 5432 | Database port number |
36
+ | SSL | false | Enable TLS encryption |
37
+ | Pool Size | 10 | Max connections |
38
+
39
+ ## Flow Diagram
40
+
41
+ \`\`\`diagram
42
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
43
+ │ Client │────▶│ Server │────▶│ Database │
44
+ └─────────────┘ └─────────────┘ └─────────────┘
45
+ \`\`\`
46
+
47
+ ## Vertical Flow
48
+
49
+ \`\`\`diagram
50
+ ┌─────────┐
51
+ │ Start │
52
+ └────┬────┘
53
+
54
+
55
+ ┌─────────┐
56
+ │ Process │
57
+ └────┬────┘
58
+
59
+
60
+ ┌─────────┐
61
+ │ End │
62
+ └─────────┘
63
+ \`\`\`
64
+
65
+ ## Code Example
66
+
67
+ \`\`\`typescript
68
+ interface Config {
69
+ host: string
70
+ port: number
71
+ ssl: boolean
72
+ }
73
+
74
+ async function connect(config: Config): Promise<Connection> {
75
+ const validated = validate(config)
76
+ return db.connect(validated)
77
+ }
78
+ \`\`\`
79
+
80
+ ## Task List
81
+
82
+ - [x] Design system architecture
83
+ - [x] Implement core components
84
+ - [ ] Add monitoring
85
+ - [ ] Deploy to production
86
+
87
+ > **Note:** All connections use TLS encryption in production.
88
+
89
+ The system handles ~10k requests/second. For more info visit [the docs](https://termcast.app).
90
+ `
91
+
92
+ function SimpleDetailMarkdown() {
93
+ return <Detail markdown={markdown} />
94
+ }
95
+
96
+ renderWithProviders(<SimpleDetailMarkdown />)