@tooee/view 0.1.9 → 0.1.12

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 (84) hide show
  1. package/dist/DirectoryView.js.map +1 -1
  2. package/dist/View.d.ts.map +1 -1
  3. package/dist/View.js +41 -353
  4. package/dist/View.js.map +1 -1
  5. package/dist/components/SubviewLayout.d.ts +15 -0
  6. package/dist/components/SubviewLayout.d.ts.map +1 -0
  7. package/dist/components/SubviewLayout.js +26 -0
  8. package/dist/components/SubviewLayout.js.map +1 -0
  9. package/dist/components/subviews/CodeSubview.d.ts +8 -0
  10. package/dist/components/subviews/CodeSubview.d.ts.map +1 -0
  11. package/dist/components/subviews/CodeSubview.js +71 -0
  12. package/dist/components/subviews/CodeSubview.js.map +1 -0
  13. package/dist/components/subviews/CustomSubview.d.ts +9 -0
  14. package/dist/components/subviews/CustomSubview.d.ts.map +1 -0
  15. package/dist/components/subviews/CustomSubview.js +87 -0
  16. package/dist/components/subviews/CustomSubview.js.map +1 -0
  17. package/dist/components/subviews/ImageSubview.d.ts +8 -0
  18. package/dist/components/subviews/ImageSubview.d.ts.map +1 -0
  19. package/dist/components/subviews/ImageSubview.js +32 -0
  20. package/dist/components/subviews/ImageSubview.js.map +1 -0
  21. package/dist/components/subviews/MarkdownSubview.d.ts +8 -0
  22. package/dist/components/subviews/MarkdownSubview.d.ts.map +1 -0
  23. package/dist/components/subviews/MarkdownSubview.js +80 -0
  24. package/dist/components/subviews/MarkdownSubview.js.map +1 -0
  25. package/dist/components/subviews/TableSubview.d.ts +8 -0
  26. package/dist/components/subviews/TableSubview.d.ts.map +1 -0
  27. package/dist/components/subviews/TableSubview.js +113 -0
  28. package/dist/components/subviews/TableSubview.js.map +1 -0
  29. package/dist/components/subviews/index.d.ts +8 -0
  30. package/dist/components/subviews/index.d.ts.map +1 -0
  31. package/dist/components/subviews/index.js +7 -0
  32. package/dist/components/subviews/index.js.map +1 -0
  33. package/dist/components/subviews/types.d.ts +15 -0
  34. package/dist/components/subviews/types.d.ts.map +1 -0
  35. package/dist/components/subviews/types.js +2 -0
  36. package/dist/components/subviews/types.js.map +1 -0
  37. package/dist/hooks/index.d.ts +5 -0
  38. package/dist/hooks/index.d.ts.map +1 -0
  39. package/dist/hooks/index.js +5 -0
  40. package/dist/hooks/index.js.map +1 -0
  41. package/dist/hooks/useContentLoader.d.ts +10 -0
  42. package/dist/hooks/useContentLoader.d.ts.map +1 -0
  43. package/dist/hooks/useContentLoader.js +125 -0
  44. package/dist/hooks/useContentLoader.js.map +1 -0
  45. package/dist/hooks/useMarkState.d.ts +13 -0
  46. package/dist/hooks/useMarkState.d.ts.map +1 -0
  47. package/dist/hooks/useMarkState.js +71 -0
  48. package/dist/hooks/useMarkState.js.map +1 -0
  49. package/dist/hooks/useViewCommandContext.d.ts +18 -0
  50. package/dist/hooks/useViewCommandContext.d.ts.map +1 -0
  51. package/dist/hooks/useViewCommandContext.js +23 -0
  52. package/dist/hooks/useViewCommandContext.js.map +1 -0
  53. package/dist/hooks/useViewCommands.d.ts +13 -0
  54. package/dist/hooks/useViewCommands.d.ts.map +1 -0
  55. package/dist/hooks/useViewCommands.js +20 -0
  56. package/dist/hooks/useViewCommands.js.map +1 -0
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/launch.d.ts.map +1 -1
  62. package/dist/launch.js.map +1 -1
  63. package/dist/types.d.ts +8 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js.map +1 -1
  66. package/package.json +24 -23
  67. package/src/DirectoryView.tsx +1 -1
  68. package/src/View.tsx +49 -462
  69. package/src/components/SubviewLayout.tsx +55 -0
  70. package/src/components/subviews/CodeSubview.tsx +112 -0
  71. package/src/components/subviews/CustomSubview.tsx +139 -0
  72. package/src/components/subviews/ImageSubview.tsx +64 -0
  73. package/src/components/subviews/MarkdownSubview.tsx +122 -0
  74. package/src/components/subviews/TableSubview.tsx +153 -0
  75. package/src/components/subviews/index.ts +7 -0
  76. package/src/components/subviews/types.ts +15 -0
  77. package/src/hooks/index.ts +4 -0
  78. package/src/hooks/useContentLoader.ts +147 -0
  79. package/src/hooks/useMarkState.ts +99 -0
  80. package/src/hooks/useViewCommandContext.ts +49 -0
  81. package/src/hooks/useViewCommands.ts +36 -0
  82. package/src/index.ts +6 -1
  83. package/src/launch.tsx +1 -6
  84. package/src/types.ts +9 -0
package/src/View.tsx CHANGED
@@ -1,31 +1,16 @@
1
- import { useState, useEffect, useRef, useMemo, useCallback } from "react"
2
- import { MarkdownView, CodeView, ImageView, Table, type RowDocumentRenderable } from "@tooee/renderers"
3
- import { AppLayout } from "@tooee/layout"
1
+ import { useState, useCallback } from "react"
4
2
  import { useTheme } from "@tooee/themes"
5
- import { useActions, useProvideCommandContext } from "@tooee/commands"
6
3
  import type { ActionDefinition } from "@tooee/commands"
4
+ import type { MarkSet } from "@tooee/marks"
5
+ import { isCustomContent, type ContentProvider, type ContentRenderer } from "./types.js"
6
+ import { useContentLoader } from "./hooks/useContentLoader.js"
7
7
  import {
8
- useThemeCommands,
9
- useQuitCommand,
10
- useCopyCommand,
11
- useToggleLineNumbersCommand,
12
- useModalNavigationCommands,
13
- useCommandPalette,
14
- } from "@tooee/shell"
15
- import { useConfig } from "@tooee/config"
16
- import { marked } from "marked"
17
- import {
18
- getTextContent,
19
- isBuiltinContent,
20
- isCustomContent,
21
- type AnyContent,
22
- type Content,
23
- type ContentFormat,
24
- type ContentProvider,
25
- type ContentChunk,
26
- type ContentRenderer,
27
- type CustomContent,
28
- } from "./types.js"
8
+ MarkdownSubview,
9
+ CodeSubview,
10
+ TableSubview,
11
+ ImageSubview,
12
+ CustomSubview,
13
+ } from "./components/subviews/index.js"
29
14
 
30
15
  interface ViewProps {
31
16
  contentProvider: ContentProvider
@@ -33,309 +18,32 @@ interface ViewProps {
33
18
  renderers?: Record<string, ContentRenderer>
34
19
  }
35
20
 
36
- function isAsyncIterable(value: unknown): value is AsyncIterable<ContentChunk> {
37
- return value != null && typeof value === "object" && Symbol.asyncIterator in value
38
- }
39
-
40
- function createEmptyContent(format: string, title?: string): AnyContent {
41
- switch (format) {
42
- case "markdown":
43
- return { format, markdown: "", title }
44
- case "code":
45
- return { format, code: "", title }
46
- case "text":
47
- return { format, text: "", title }
48
- case "image":
49
- return { format, src: "", title }
50
- case "table":
51
- return { format, columns: [], rows: [], title }
52
- default:
53
- return { format, data: undefined, title } as CustomContent
54
- }
55
- }
56
-
57
- function ensureContentFormat<F extends ContentFormat>(
58
- current: AnyContent | null,
59
- format: F,
60
- title?: string,
61
- ): Extract<Content, { format: F }> {
62
- if (!current || current.format !== format) {
63
- return createEmptyContent(format, title) as Extract<Content, { format: F }>
64
- }
65
- return current as Extract<Content, { format: F }>
66
- }
67
-
68
- function applyContentChunk(
69
- current: AnyContent | null,
70
- chunk: ContentChunk,
71
- title?: string,
72
- ): AnyContent {
73
- switch (chunk.type) {
74
- case "replace":
75
- return chunk.content
76
- case "append":
77
- if (chunk.format === "markdown") {
78
- const target = ensureContentFormat(current, "markdown", title)
79
- return { ...target, markdown: target.markdown + chunk.data }
80
- }
81
- if (chunk.format === "code") {
82
- const target = ensureContentFormat(current, "code", title)
83
- return {
84
- ...target,
85
- code: target.code + chunk.data,
86
- language: chunk.language ?? target.language,
87
- }
88
- }
89
- {
90
- const target = ensureContentFormat(current, "text", title)
91
- return { ...target, text: target.text + chunk.data }
92
- }
93
- case "patch":
94
- return chunk.apply(current)
95
- default:
96
- return current ?? createEmptyContent("markdown", title)
97
- }
98
- }
99
-
100
21
  export function View({ contentProvider, actions, renderers }: ViewProps) {
101
22
  const { theme } = useTheme()
102
- const config = useConfig()
103
- const [content, setContent] = useState<AnyContent | null>(null)
104
- const [streaming, setStreaming] = useState(false)
105
- const [error, setError] = useState<string | null>(null)
106
- const docRef = useRef<RowDocumentRenderable>(null)
107
23
  const [reloadTrigger, setReloadTrigger] = useState(0)
108
24
  const reload = useCallback(() => setReloadTrigger((n) => n + 1), [])
109
25
 
110
- useEffect(() => {
111
- setError(null)
112
- setStreaming(false)
113
- const loaded = contentProvider.load()
114
-
115
- if (isAsyncIterable(loaded)) {
116
- const fallbackFormat = contentProvider.format ?? "markdown"
117
- const title = contentProvider.title
118
- let cancelled = false
119
- let current: AnyContent | null = createEmptyContent(fallbackFormat, title)
120
- setContent(current)
121
- setStreaming(true)
122
-
123
- ;(async () => {
124
- try {
125
- for await (const chunk of loaded) {
126
- if (cancelled) break
127
- current = applyContentChunk(current, chunk, title)
128
- setContent(current)
129
- }
130
- } catch (error) {
131
- if (!cancelled && error instanceof Error) {
132
- setError(error.message)
133
- }
134
- } finally {
135
- if (!cancelled) {
136
- setStreaming(false)
137
- }
138
- }
139
- })()
140
-
141
- return () => {
142
- cancelled = true
143
- }
144
- }
145
-
146
- if (loaded instanceof Promise) {
147
- let active = true
148
- loaded
149
- .then((value) => {
150
- if (active) setContent(value)
151
- })
152
- .catch((error: Error) => {
153
- if (active) setError(error.message)
154
- })
155
- return () => {
156
- active = false
157
- }
158
- }
159
-
160
- setContent(loaded)
161
- // eslint-disable-next-line react-hooks/exhaustive-deps
162
- }, [contentProvider, reloadTrigger])
163
-
164
- const TABLE_SEARCH_HEADER_OFFSET = 1
165
-
166
- const textContent = useMemo(() => (content ? getTextContent(content) : ""), [content])
167
-
168
- const lineCount = useMemo(() => {
169
- if (!content) return 0
170
- if (isBuiltinContent(content) && content.format === "table") {
171
- return content.rows.length
172
- }
173
- return textContent.split("\n").length
174
- }, [content, textContent])
175
-
176
- const { blockCount, blockLineMap } = useMemo(() => {
177
- if (!content) return { blockCount: undefined, blockLineMap: undefined }
178
- if (!isBuiltinContent(content)) return { blockCount: undefined, blockLineMap: undefined }
179
-
180
- if (content.format === "table") {
181
- const map = content.rows.map((_: unknown, i: number) => i)
182
- return { blockCount: content.rows.length, blockLineMap: map }
183
- }
184
-
185
- if (content.format !== "markdown") {
186
- return { blockCount: undefined, blockLineMap: undefined }
187
- }
188
-
189
- const tokens = marked.lexer(content.markdown)
190
- const blocks = tokens.filter((t) => t.type !== "space")
191
- const lineMap: number[] = []
192
- let lineOffset = 0
193
- for (const token of tokens) {
194
- if (token.type === "space") {
195
- if ("raw" in token && typeof token.raw === "string") {
196
- lineOffset += token.raw.split("\n").length - 1
197
- }
198
- continue
199
- }
200
- lineMap.push(lineOffset)
201
- if ("raw" in token && typeof token.raw === "string") {
202
- lineOffset += token.raw.split("\n").length - 1
203
- }
204
- }
205
- return { blockCount: blocks.length, blockLineMap: lineMap }
206
- }, [content])
207
-
208
- const nav = useModalNavigationCommands({
209
- totalLines: lineCount,
210
- getText: () => (content ? textContent : undefined),
211
- blockCount,
212
- blockLineMap,
213
- searchLineOffset: content?.format === "table" ? TABLE_SEARCH_HEADER_OFFSET : 0,
214
- multiSelect: true,
215
- })
216
-
217
- useEffect(() => {
218
- if (nav.cursor) {
219
- docRef.current?.scrollToRow(nav.cursor.line, "nearest")
220
- }
221
- }, [nav.cursor])
222
-
223
- const [showLineNumbers, setShowLineNumbers] = useState(config.view?.gutter ?? true)
224
-
225
- const { name: themeName } = useThemeCommands()
226
- useQuitCommand()
227
- useCopyCommand({
228
- getText: () => (content ? textContent : undefined),
229
- })
230
- useToggleLineNumbersCommand({
231
- showLineNumbers,
232
- onToggle: () => setShowLineNumbers((v) => !v),
233
- })
234
-
235
- const _palette = useCommandPalette()
236
-
237
- useActions(actions)
238
-
239
- function getActiveRow(): Record<string, unknown> | undefined {
240
- if (!content || !isBuiltinContent(content) || content.format !== "table") return undefined
241
- if (!nav.cursor) return undefined
242
- return content.rows[nav.cursor.line]
243
- }
244
-
245
- function getSelectedRows(): Record<string, unknown>[] {
246
- if (!content || !isBuiltinContent(content) || content.format !== "table") return []
247
- if (nav.toggledIndices.size) {
248
- return Array.from(nav.toggledIndices)
249
- .map((i) => content.rows[i])
250
- .filter(Boolean)
251
- }
252
- if (nav.selection) {
253
- const start = nav.selection.start.line
254
- const end = nav.selection.end.line
255
- return content.rows.slice(start, end + 1)
256
- }
257
- return []
258
- }
259
-
260
- useProvideCommandContext(() => ({
261
- view: {
262
- content,
263
- format: content?.format,
264
- cursor: nav.cursor,
265
- selection: nav.selection,
266
- mode: nav.mode,
267
- activeRow: getActiveRow(),
268
- selectedRows: getSelectedRows(),
269
- toggledIndices: nav.toggledIndices,
270
- reload,
271
- },
272
- }))
273
-
274
- const matchingLinesSet = useMemo(
275
- () => (nav.matchingLines.length > 0 ? new Set<number>(nav.matchingLines) : undefined),
276
- [nav.matchingLines],
26
+ const { content, streaming, error, providerMarks } = useContentLoader(
27
+ contentProvider,
28
+ reloadTrigger,
277
29
  )
278
- const currentMatchLine =
279
- nav.matchingLines.length > 0 ? nav.matchingLines[nav.currentMatchIndex] : undefined
280
30
 
281
- // For markdown: convert matching lines to matching blocks
282
- const matchingBlocks = useMemo(() => {
283
- if (!matchingLinesSet || !blockLineMap || blockLineMap.length === 0) return undefined
284
- if (content?.format === "table") return undefined // table uses matchingRows instead
285
- const blocks = new Set<number>()
286
- for (const line of matchingLinesSet) {
287
- let blockIdx = 0
288
- for (let i = blockLineMap.length - 1; i >= 0; i--) {
289
- if (blockLineMap[i] <= line) {
290
- blockIdx = i
291
- break
292
- }
293
- }
294
- blocks.add(blockIdx)
295
- }
296
- return blocks.size > 0 ? blocks : undefined
297
- }, [matchingLinesSet, blockLineMap, content?.format])
31
+ const [userMarks, setUserMarks] = useState<MarkSet[]>([])
298
32
 
299
- const currentMatchBlock = useMemo(() => {
300
- if (currentMatchLine == null || !blockLineMap || blockLineMap.length === 0) return undefined
301
- if (content?.format === "table") return undefined
302
- for (let i = blockLineMap.length - 1; i >= 0; i--) {
303
- if (blockLineMap[i] <= currentMatchLine) return i
304
- }
305
- return 0
306
- }, [currentMatchLine, blockLineMap, content?.format])
33
+ const setMarkSet = useCallback((set: MarkSet) => {
34
+ setUserMarks((prev) => {
35
+ const filtered = prev.filter((s) => s.namespace !== set.namespace)
36
+ return [...filtered, set]
37
+ })
38
+ }, [])
307
39
 
308
- // For table: convert matching lines to matching row indices
309
- const matchingRowsSet = useMemo(() => {
310
- if (!matchingLinesSet || content?.format !== "table") return undefined
311
- const rows = new Set<number>()
312
- for (const line of matchingLinesSet) {
313
- const row = line - TABLE_SEARCH_HEADER_OFFSET
314
- if (row >= 0) rows.add(row)
315
- }
316
- return rows.size > 0 ? rows : undefined
317
- }, [matchingLinesSet, content?.format])
40
+ const clearMarkNamespace = useCallback((namespace: string) => {
41
+ setUserMarks((prev) => prev.filter((s) => s.namespace !== namespace))
42
+ }, [])
318
43
 
319
- const currentMatchRow = useMemo(() => {
320
- if (content?.format !== "table" || currentMatchLine == null) return undefined
321
- const row = currentMatchLine - TABLE_SEARCH_HEADER_OFFSET
322
- return row >= 0 ? row : undefined
323
- }, [content?.format, currentMatchLine])
324
-
325
- const toggledBlocks = useMemo(() => {
326
- if (content?.format !== "markdown" || nav.toggledIndices.size === 0) return undefined
327
- return new Set<number>(nav.toggledIndices)
328
- }, [content?.format, nav.toggledIndices])
329
-
330
- const toggledRows = useMemo(() => {
331
- if (content?.format !== "table" || nav.toggledIndices.size === 0) return undefined
332
- return new Set<number>(nav.toggledIndices)
333
- }, [content?.format, nav.toggledIndices])
334
-
335
- const toggledLines = useMemo(() => {
336
- if (!content || (content.format !== "code" && content.format !== "text")) return undefined
337
- return nav.toggledIndices.size > 0 ? new Set<number>(nav.toggledIndices) : undefined
338
- }, [content, nav.toggledIndices])
44
+ const clearAllUserMarks = useCallback(() => {
45
+ setUserMarks([])
46
+ }, [])
339
47
 
340
48
  if (error) {
341
49
  return (
@@ -353,151 +61,30 @@ export function View({ contentProvider, actions, renderers }: ViewProps) {
353
61
  )
354
62
  }
355
63
 
356
- const cursorLine = nav.cursor?.line ?? undefined
357
- const selectionStart = nav.selection?.start.line ?? undefined
358
- const selectionEnd = nav.selection?.end.line ?? undefined
359
- const renderContent = () => {
360
- // Custom content: use registered renderer or fall back to text
361
- if (isCustomContent(content)) {
362
- const customRenderer = renderers?.[content.format]
363
- if (customRenderer) {
364
- return customRenderer({
365
- content,
366
- lineCount,
367
- cursor: cursorLine,
368
- selectionStart,
369
- selectionEnd,
370
- })
371
- }
372
- // No renderer for this custom format — fall back to text
373
- const text = getTextContent(content)
374
- return (
375
- <CodeView
376
- content={text}
377
- showLineNumbers={false}
378
- cursor={cursorLine}
379
- selectionStart={selectionStart}
380
- selectionEnd={selectionEnd}
381
- docRef={docRef}
382
- />
383
- )
384
- }
385
-
386
- // Built-in formats
387
- switch (content.format) {
388
- case "markdown":
389
- return (
390
- <MarkdownView
391
- content={content.markdown}
392
- showLineNumbers={showLineNumbers}
393
- activeBlock={cursorLine}
394
- selectedBlocks={
395
- selectionStart != null && selectionEnd != null
396
- ? { start: selectionStart, end: selectionEnd }
397
- : undefined
398
- }
399
- matchingBlocks={matchingBlocks}
400
- currentMatchBlock={currentMatchBlock}
401
- toggledBlocks={toggledBlocks}
402
- docRef={docRef}
403
- />
404
- )
405
- case "code":
406
- return (
407
- <CodeView
408
- content={content.code}
409
- language={content.language}
410
- showLineNumbers={showLineNumbers}
411
- cursor={cursorLine}
412
- selectionStart={selectionStart}
413
- selectionEnd={selectionEnd}
414
- matchingLines={matchingLinesSet}
415
- currentMatchLine={currentMatchLine}
416
- toggledLines={toggledLines}
417
- docRef={docRef}
418
- />
419
- )
420
- case "text":
421
- return (
422
- <CodeView
423
- content={content.text}
424
- showLineNumbers={showLineNumbers}
425
- cursor={cursorLine}
426
- selectionStart={selectionStart}
427
- selectionEnd={selectionEnd}
428
- matchingLines={matchingLinesSet}
429
- currentMatchLine={currentMatchLine}
430
- toggledLines={toggledLines}
431
- docRef={docRef}
432
- />
433
- )
434
- case "image":
435
- return <ImageView src={content.src} />
436
- case "table":
437
- return (
438
- <Table
439
- columns={content.columns}
440
- rows={content.rows}
441
- showLineNumbers={showLineNumbers}
442
- cursor={cursorLine}
443
- selectionStart={selectionStart}
444
- selectionEnd={selectionEnd}
445
- matchingRows={matchingRowsSet}
446
- currentMatchRow={currentMatchRow}
447
- toggledRows={toggledRows}
448
- docRef={docRef}
449
- />
450
- )
451
- }
64
+ const shared = {
65
+ providerMarks,
66
+ userMarks,
67
+ setMarkSet,
68
+ clearMarkNamespace,
69
+ clearAllUserMarks,
70
+ reload,
71
+ streaming,
72
+ actions,
452
73
  }
453
74
 
454
- const selectionCount =
455
- nav.selection != null ? nav.selection.end.line - nav.selection.start.line + 1 : 0
456
- const toggledCount = nav.toggledIndices.size
457
- const selectionItems =
458
- toggledCount > 0
459
- ? [{ label: "Selected:", value: String(toggledCount) }]
460
- : selectionCount > 0
461
- ? [{ label: "Selected:", value: String(selectionCount) }]
462
- : []
463
-
464
- const statusItems = [
465
- { label: "Theme:", value: themeName },
466
- { label: "Format:", value: content.format },
467
- ...(isBuiltinContent(content) && content.format === "table"
468
- ? [
469
- { label: "Rows:", value: String(content.rows.length) },
470
- { label: "Cols:", value: String(content.columns.length) },
471
- ]
472
- : [{ label: "Lines:", value: String(lineCount) }]),
473
- { label: "Mode:", value: nav.mode },
474
- { label: "Cursor:", value: nav.cursor ? String(nav.cursor.line) : "-" },
475
- ...selectionItems,
476
- ...(streaming ? [{ label: "Status:", value: "streaming" }] : []),
477
- ...(nav.searchActive ? [{ label: "Search:", value: nav.searchQuery }] : []),
478
- ]
75
+ if (isCustomContent(content)) {
76
+ return <CustomSubview content={content} renderers={renderers} {...shared} />
77
+ }
479
78
 
480
- return (
481
- <AppLayout
482
- titleBar={
483
- content.title
484
- ? { title: content.title, subtitle: content.format }
485
- : { title: content.format }
486
- }
487
- statusBar={{ items: statusItems }}
488
- searchBar={{
489
- active: nav.searchActive,
490
- query: nav.searchQuery,
491
- onQueryChange: nav.setSearchQuery,
492
- onSubmit: nav.submitSearch,
493
- onCancel: () => {
494
- nav.setSearchQuery("")
495
- },
496
- matchCount: nav.matchingLines.length,
497
- currentMatch: nav.currentMatchIndex,
498
- }}
499
- >
500
- {renderContent()}
501
- </AppLayout>
502
- )
79
+ switch (content.format) {
80
+ case "markdown":
81
+ return <MarkdownSubview content={content} {...shared} />
82
+ case "code":
83
+ case "text":
84
+ return <CodeSubview content={content} {...shared} />
85
+ case "table":
86
+ return <TableSubview content={content} {...shared} />
87
+ case "image":
88
+ return <ImageSubview content={content} {...shared} />
89
+ }
503
90
  }
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from "react"
2
+ import { AppLayout, type StatusBarItem } from "@tooee/layout"
3
+ import type { NavigationState, SearchState } from "@tooee/shell"
4
+ import type { AnyContent } from "../types.js"
5
+
6
+ interface SubviewLayoutProps {
7
+ content: AnyContent
8
+ nav: NavigationState & SearchState
9
+ streaming: boolean
10
+ themeName: string
11
+ extraStatusItems?: StatusBarItem[]
12
+ children: ReactNode
13
+ }
14
+
15
+ export function SubviewLayout({
16
+ content,
17
+ nav,
18
+ streaming,
19
+ themeName,
20
+ extraStatusItems,
21
+ children,
22
+ }: SubviewLayoutProps) {
23
+ const statusItems = [
24
+ { label: "Theme:", value: themeName },
25
+ ...(extraStatusItems ?? []),
26
+ { label: "Mode:", value: nav.mode },
27
+ { label: "Cursor:", value: nav.cursor ? String(nav.cursor.line) : "-" },
28
+ ...(streaming ? [{ label: "Status:", value: "streaming" }] : []),
29
+ ...(nav.searchActive ? [{ label: "Search:", value: nav.searchQuery }] : []),
30
+ ]
31
+
32
+ return (
33
+ <AppLayout
34
+ titleBar={
35
+ content.title
36
+ ? { title: content.title, subtitle: content.format }
37
+ : { title: content.format }
38
+ }
39
+ statusBar={{ items: statusItems }}
40
+ searchBar={{
41
+ active: nav.searchActive,
42
+ query: nav.searchQuery,
43
+ onQueryChange: nav.setSearchQuery,
44
+ onSubmit: nav.submitSearch,
45
+ onCancel: () => {
46
+ nav.setSearchQuery("")
47
+ },
48
+ matchCount: nav.matchingLines.length,
49
+ currentMatch: nav.currentMatchIndex,
50
+ }}
51
+ >
52
+ {children}
53
+ </AppLayout>
54
+ )
55
+ }