@tooee/view 0.1.0

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