@tooee/view 0.1.22 → 0.2.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.
Files changed (83) hide show
  1. package/README.md +67 -12
  2. package/dist/View.d.ts.map +1 -1
  3. package/dist/View.js +5 -1
  4. package/dist/View.js.map +1 -1
  5. package/dist/components/ViewScreen.d.ts +28 -0
  6. package/dist/components/ViewScreen.d.ts.map +1 -0
  7. package/dist/components/ViewScreen.js +29 -0
  8. package/dist/components/ViewScreen.js.map +1 -0
  9. package/dist/components/subviews/CodeSubview.d.ts +1 -1
  10. package/dist/components/subviews/CodeSubview.d.ts.map +1 -1
  11. package/dist/components/subviews/CodeSubview.js +20 -89
  12. package/dist/components/subviews/CodeSubview.js.map +1 -1
  13. package/dist/components/subviews/CustomSubview.d.ts +1 -1
  14. package/dist/components/subviews/CustomSubview.d.ts.map +1 -1
  15. package/dist/components/subviews/CustomSubview.js +19 -94
  16. package/dist/components/subviews/CustomSubview.js.map +1 -1
  17. package/dist/components/subviews/MarkdownSubview.d.ts +1 -1
  18. package/dist/components/subviews/MarkdownSubview.d.ts.map +1 -1
  19. package/dist/components/subviews/MarkdownSubview.js +32 -106
  20. package/dist/components/subviews/MarkdownSubview.js.map +1 -1
  21. package/dist/components/subviews/TableSubview.d.ts +1 -1
  22. package/dist/components/subviews/TableSubview.d.ts.map +1 -1
  23. package/dist/components/subviews/TableSubview.js +22 -123
  24. package/dist/components/subviews/TableSubview.js.map +1 -1
  25. package/dist/components/subviews/index.d.ts +0 -1
  26. package/dist/components/subviews/index.d.ts.map +1 -1
  27. package/dist/components/subviews/index.js +0 -1
  28. package/dist/components/subviews/index.js.map +1 -1
  29. package/dist/components/subviews/types.d.ts +6 -0
  30. package/dist/components/subviews/types.d.ts.map +1 -1
  31. package/dist/default-provider.d.ts +6 -3
  32. package/dist/default-provider.d.ts.map +1 -1
  33. package/dist/default-provider.js +24 -34
  34. package/dist/default-provider.js.map +1 -1
  35. package/dist/hooks/index.d.ts +2 -3
  36. package/dist/hooks/index.d.ts.map +1 -1
  37. package/dist/hooks/index.js +2 -3
  38. package/dist/hooks/index.js.map +1 -1
  39. package/dist/hooks/useContentCommands.d.ts +15 -0
  40. package/dist/hooks/useContentCommands.d.ts.map +1 -0
  41. package/dist/hooks/useContentCommands.js +21 -0
  42. package/dist/hooks/useContentCommands.js.map +1 -0
  43. package/dist/hooks/useViewCommandContext.d.ts +10 -42
  44. package/dist/hooks/useViewCommandContext.d.ts.map +1 -1
  45. package/dist/hooks/useViewCommandContext.js +3 -30
  46. package/dist/hooks/useViewCommandContext.js.map +1 -1
  47. package/dist/index.d.ts +2 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/types.d.ts +15 -16
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js.map +1 -1
  54. package/package.json +14 -14
  55. package/src/View.tsx +6 -1
  56. package/src/components/ViewScreen.tsx +76 -0
  57. package/src/components/subviews/CodeSubview.tsx +30 -120
  58. package/src/components/subviews/CustomSubview.tsx +32 -123
  59. package/src/components/subviews/MarkdownSubview.tsx +49 -130
  60. package/src/components/subviews/TableSubview.tsx +36 -164
  61. package/src/components/subviews/index.ts +0 -1
  62. package/src/components/subviews/types.ts +6 -0
  63. package/src/default-provider.ts +37 -34
  64. package/src/hooks/index.ts +2 -7
  65. package/src/hooks/useContentCommands.ts +29 -0
  66. package/src/hooks/useViewCommandContext.ts +13 -81
  67. package/src/index.ts +1 -1
  68. package/src/types.ts +15 -16
  69. package/dist/components/SubviewLayout.d.ts +0 -16
  70. package/dist/components/SubviewLayout.d.ts.map +0 -1
  71. package/dist/components/SubviewLayout.js +0 -18
  72. package/dist/components/SubviewLayout.js.map +0 -1
  73. package/dist/hooks/useMarkState.d.ts +0 -14
  74. package/dist/hooks/useMarkState.d.ts.map +0 -1
  75. package/dist/hooks/useMarkState.js +0 -71
  76. package/dist/hooks/useMarkState.js.map +0 -1
  77. package/dist/hooks/useViewCommands.d.ts +0 -13
  78. package/dist/hooks/useViewCommands.d.ts.map +0 -1
  79. package/dist/hooks/useViewCommands.js +0 -20
  80. package/dist/hooks/useViewCommands.js.map +0 -1
  81. package/src/components/SubviewLayout.tsx +0 -48
  82. package/src/hooks/useMarkState.ts +0 -100
  83. package/src/hooks/useViewCommands.ts +0 -36
@@ -1,14 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useRef } from "react"
2
- import { CodeView, type RowDocumentRenderable } from "@tooee/renderers"
3
- import { useTheme } from "@tooee/themes"
4
- import { useHasModalOverlay } from "@tooee/overlays"
5
- import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
6
- import { useCopy, useNavigation } from "@tooee/shell"
7
- import { findMatchingLines, useSearch } from "@tooee/search"
1
+ import { useMemo } from "react"
2
+ import { CodeView, sourceLines, sourceLineAdapter, type SourceLineRow } from "@tooee/renderers"
3
+ import { useDocumentController } from "@tooee/shell"
8
4
  import { getTextContent, type CustomContent, type ContentRenderer } from "../../types.js"
9
- import { useMarkState } from "../../hooks/useMarkState.js"
10
- import { useViewCommands } from "../../hooks/useViewCommands.js"
11
- import { SubviewLayout } from "../SubviewLayout.js"
5
+ import { useContentCommands } from "../../hooks/useContentCommands.js"
6
+ import { ViewScreen } from "../ViewScreen.js"
12
7
  import type { SubviewProps } from "./types.js"
13
8
 
14
9
  interface CustomSubviewProps extends SubviewProps {
@@ -18,134 +13,48 @@ interface CustomSubviewProps extends SubviewProps {
18
13
 
19
14
  export function CustomSubview({
20
15
  content,
21
- providerMarks,
22
- userMarks,
23
- setMarkSet,
24
- clearMarkNamespace,
25
- clearAllUserMarks,
26
- reload,
27
- streaming,
16
+ decorations,
28
17
  actions,
29
18
  renderers,
19
+ ...screen
30
20
  }: CustomSubviewProps) {
31
- const { theme } = useTheme()
32
- const docRef = useRef<RowDocumentRenderable>(null)
33
- const hasModalOverlay = useHasModalOverlay()
34
21
  const textContent = useMemo(() => getTextContent(content), [content])
35
- const lines = useMemo(() => textContent.split("\n"), [textContent])
36
- const lineCount = lines.length
22
+ // Fallback rows are source lines; row index is the source line.
23
+ const lineRows = useMemo(() => sourceLines(textContent), [textContent])
37
24
 
38
- const nav = useNavigation({
39
- rowCount: lineCount,
40
- multiSelect: true,
41
- })
42
- const search = useSearch({
43
- match: (query) => findMatchingLines(textContent, query),
44
- onJump: nav.setCursor,
45
- })
46
- useCopy({
47
- getRowText: (index) => lines[index] ?? "",
48
- cursor: nav.cursor,
49
- selection: nav.selection,
50
- toggledIndices: nav.toggledIndices,
51
- })
52
- const layoutNav = { ...nav, ...search }
53
-
54
- useEffect(() => {
55
- if (nav.cursor !== null) {
56
- docRef.current?.scrollToRow(nav.cursor, "nearest")
57
- }
58
- }, [nav.cursor])
59
-
60
- const { themeName } = useViewCommands({ content, textContent, actions })
25
+ useContentCommands({ content, textContent })
61
26
 
62
- const markState = useMarkState({
63
- nav,
64
- search,
65
- theme,
66
- providerMarks,
67
- userMarks,
68
- })
69
-
70
- useViewCommandContext({
71
- content,
72
- nav,
73
- reload,
74
- providerMarks,
75
- userMarks,
76
- setMarkSet,
77
- clearMarkNamespace,
78
- clearAllUserMarks,
27
+ // Custom content has no action rows of its own, so no context menu is bound.
28
+ const document = useDocumentController<SourceLineRow>({
29
+ rows: lineRows,
30
+ adapter: sourceLineAdapter,
31
+ multiSelect: true,
32
+ decorations,
79
33
  })
80
34
 
81
- const extraStatusItems = useMemo(() => {
82
- const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
83
- const toggledCount = nav.toggledIndices.size
84
- const selectionItems =
85
- toggledCount > 0
86
- ? [{ label: "Selected:", value: String(toggledCount) }]
87
- : selectionCount > 0
88
- ? [{ label: "Selected:", value: String(selectionCount) }]
89
- : []
90
- return [
35
+ const statusItems = useMemo(
36
+ () => [
91
37
  { label: "Format:", value: content.format },
92
- { label: "Lines:", value: String(lineCount) },
93
- ...selectionItems,
94
- ]
95
- }, [content.format, lineCount, nav.selection, nav.toggledIndices])
96
-
97
- // Left-click selection for custom renderers. The framework does not own the
98
- // renderer's markup, so it cannot map clicks to rows itself; instead it hands
99
- // the renderer this callback (the mouse equivalent of the keyboard cursor
100
- // move) to call from its own handlers. Guarded like the built-in views so a
101
- // click never mutates the covered app while a modal overlay is up.
102
- const setCursor = nav.setCursor
103
- const handleSelectRow = useCallback(
104
- (index: number) => {
105
- if (hasModalOverlay) return
106
- setCursor(index)
107
- },
108
- [hasModalOverlay, setCursor],
38
+ { label: "Lines:", value: String(lineRows.length) },
39
+ ],
40
+ [content.format, lineRows.length],
109
41
  )
110
42
 
111
43
  const customRenderer = renderers?.[content.format]
112
- if (customRenderer) {
113
- const cursorLine = nav.cursor ?? undefined
114
- const selectionStart = nav.selection?.start ?? undefined
115
- const selectionEnd = nav.selection?.end ?? undefined
116
-
117
- return (
118
- <SubviewLayout
119
- content={content}
120
- nav={layoutNav}
121
- streaming={streaming}
122
- themeName={themeName}
123
- extraStatusItems={extraStatusItems}
124
- >
125
- {customRenderer({
126
- content,
127
- lineCount,
128
- cursor: cursorLine,
129
- selectionStart,
130
- selectionEnd,
131
- marks: markState,
132
- onSelectRow: handleSelectRow,
133
- })}
134
- </SubviewLayout>
135
- )
136
- }
137
44
 
138
- // No renderer for this custom format -- fall back to text
139
- const text = getTextContent(content)
140
45
  return (
141
- <SubviewLayout
46
+ <ViewScreen
142
47
  content={content}
143
- nav={layoutNav}
144
- streaming={streaming}
145
- themeName={themeName}
146
- extraStatusItems={extraStatusItems}
48
+ controller={document}
49
+ actions={actions}
50
+ statusItems={statusItems}
51
+ {...screen}
147
52
  >
148
- <CodeView content={text} showLineNumbers={false} marks={markState} docRef={docRef} />
149
- </SubviewLayout>
53
+ {customRenderer ? (
54
+ customRenderer({ content, document })
55
+ ) : (
56
+ <CodeView content={textContent} showLineNumbers={false} document={document} />
57
+ )}
58
+ </ViewScreen>
150
59
  )
151
60
  }
@@ -1,22 +1,21 @@
1
- import { useCallback, useEffect, useMemo, useRef } from "react"
2
- import { marked } from "marked"
1
+ import { useMemo, useRef } from "react"
3
2
  import type { TextBufferRenderable } from "@opentui/core"
4
3
  import {
5
4
  MarkdownView,
6
- flattenTokens,
5
+ flattenMarkdown,
6
+ getFlatBlockText,
7
7
  type CodeBlockRenderer,
8
- type RowDocumentRenderable,
8
+ type FlatBlock,
9
9
  } from "@tooee/renderers"
10
- import { useTheme } from "@tooee/themes"
11
- import { useCommand, useCommandContext } from "@tooee/commands"
12
- import { useHasModalOverlay } from "@tooee/overlays"
13
- import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
14
- import { actionsToContextMenuEntries, useContextMenu, useCopy, useNavigation } from "@tooee/shell"
15
- import { useSearch } from "@tooee/search"
10
+ import { useCommand } from "@tooee/commands"
11
+ import {
12
+ actionsToContextMenuEntries,
13
+ useDocumentController,
14
+ type DocumentRowAdapter,
15
+ } from "@tooee/shell"
16
16
  import type { MarkdownContent } from "../../types.js"
17
- import { useMarkState } from "../../hooks/useMarkState.js"
18
- import { useViewCommands } from "../../hooks/useViewCommands.js"
19
- import { SubviewLayout } from "../SubviewLayout.js"
17
+ import { useContentCommands } from "../../hooks/useContentCommands.js"
18
+ import { ViewScreen } from "../ViewScreen.js"
20
19
  import type { SubviewProps } from "./types.js"
21
20
 
22
21
  interface MarkdownSubviewProps extends SubviewProps {
@@ -27,68 +26,43 @@ interface MarkdownSubviewProps extends SubviewProps {
27
26
  /** Columns moved per h/l press when scrolling a wide block horizontally. */
28
27
  const BLOCK_HSCROLL_STEP = 4
29
28
 
29
+ /**
30
+ * Blocks are the row unit. `getFlatBlockText` keeps search/copy in step with the
31
+ * source mapping (notably for synthetic bullet rows), and `getSource` projects
32
+ * each block's Markdown provenance onto the controller's anchors.
33
+ */
34
+ const MARKDOWN_BLOCK_ADAPTER: DocumentRowAdapter<FlatBlock> = {
35
+ getText: (block) => getFlatBlockText(block),
36
+ getSource: (block) => block.source,
37
+ }
38
+
30
39
  export function MarkdownSubview({
31
40
  content,
32
41
  codeBlockRenderers,
33
- providerMarks,
34
- userMarks,
35
- setMarkSet,
36
- clearMarkNamespace,
37
- clearAllUserMarks,
38
- reload,
39
- streaming,
42
+ decorations,
40
43
  actions,
44
+ ...screen
41
45
  }: MarkdownSubviewProps) {
42
- const { theme } = useTheme()
43
- const docRef = useRef<RowDocumentRenderable>(null)
44
- const hasModalOverlay = useHasModalOverlay()
45
- const contextMenu = useContextMenu()
46
- const { invoke } = useCommandContext()
47
46
  const textContent = content.markdown
48
47
  const lineCount = useMemo(() => textContent.split("\n").length, [textContent])
49
- const blocks = useMemo(() => flattenTokens(marked.lexer(content.markdown)), [content.markdown])
48
+ const blocks = useMemo(() => flattenMarkdown(content.markdown), [content.markdown])
50
49
 
51
- const nav = useNavigation({
52
- rowCount: blocks.length,
50
+ const { showLineNumbers } = useContentCommands({ content, textContent })
51
+ const contextMenu = useMemo(() => actionsToContextMenuEntries(actions), [actions])
52
+
53
+ const document = useDocumentController<FlatBlock>({
54
+ rows: blocks,
55
+ adapter: MARKDOWN_BLOCK_ADAPTER,
53
56
  multiSelect: true,
57
+ decorations,
58
+ contextMenu,
54
59
  })
55
- const search = useSearch({
56
- match: (query) => {
57
- const lowerQuery = query.toLowerCase()
58
- return blocks.flatMap((block, index) => {
59
- const { token } = block
60
- const raw = "raw" in token && typeof token.raw === "string" ? token.raw : ""
61
- return raw.toLowerCase().includes(lowerQuery) ? [index] : []
62
- })
63
- },
64
- onJump: nav.setCursor,
65
- })
66
- useCopy({
67
- getRowText: (index) => {
68
- const block = blocks[index]
69
- if (!block) return ""
70
- const { token } = block
71
- return "raw" in token && typeof token.raw === "string" ? token.raw : ""
72
- },
73
- cursor: nav.cursor,
74
- selection: nav.selection,
75
- toggledIndices: nav.toggledIndices,
76
- })
77
- const layoutNav = { ...nav, ...search }
78
-
79
- useEffect(() => {
80
- if (nav.cursor !== null) {
81
- docRef.current?.scrollToRow(nav.cursor, "nearest")
82
- }
83
- }, [nav.cursor])
84
60
 
85
- // Horizontal panning for wide blocks (mermaid diagrams and code blocks).
86
- // Blocks that can overflow horizontally register their text-buffer
87
- // renderable by block index; h/l in cursor mode pans the block under the
88
- // nav cursor via `scrollX` (the setter clamps to the content width).
89
61
  const hScrollableBlocksRef = useRef<Map<number, TextBufferRenderable>>(new Map())
90
62
  const cursorScrollable = () =>
91
- nav.cursor !== null ? hScrollableBlocksRef.current.get(nav.cursor) : undefined
63
+ document.activeIndex !== null
64
+ ? hScrollableBlocksRef.current.get(document.activeIndex)
65
+ : undefined
92
66
  useCommand({
93
67
  id: "block-scroll-left",
94
68
  title: "Scroll block left",
@@ -112,85 +86,30 @@ export function MarkdownSubview({
112
86
  },
113
87
  })
114
88
 
115
- const menuEntries = useMemo(() => actionsToContextMenuEntries(actions), [actions])
116
-
117
- // Left-click selects the clicked block; right-click selects it and opens the
118
- // same app-provided action menu as table/code rows. Both stand down while a
119
- // modal overlay is up (theme picker, command palette, Ask/Choose), matching
120
- // keyboard stand-down semantics.
121
- const setCursor = nav.setCursor
122
- const openContextMenu = contextMenu.open
123
- const handleRowClick = useCallback(
124
- (index: number) => {
125
- if (hasModalOverlay) return
126
- setCursor(index)
127
- },
128
- [hasModalOverlay, setCursor],
129
- )
130
- const handleRowContextMenu = useCallback(
131
- (index: number, x: number, y: number) => {
132
- if (hasModalOverlay) return
133
- setCursor(index)
134
- openContextMenu(x, y, menuEntries, invoke)
135
- },
136
- [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
137
- )
138
-
139
- const { themeName, showLineNumbers } = useViewCommands({ content, textContent, actions })
140
-
141
- const markState = useMarkState({
142
- nav,
143
- search,
144
- theme,
145
- providerMarks,
146
- userMarks,
147
- })
148
-
149
- useViewCommandContext({
150
- content,
151
- nav,
152
- reload,
153
- providerMarks,
154
- userMarks,
155
- setMarkSet,
156
- clearMarkNamespace,
157
- clearAllUserMarks,
158
- })
159
-
160
- const extraStatusItems = useMemo(() => {
161
- const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
162
- const toggledCount = nav.toggledIndices.size
163
- const selectionItems =
164
- toggledCount > 0
165
- ? [{ label: "Selected:", value: String(toggledCount) }]
166
- : selectionCount > 0
167
- ? [{ label: "Selected:", value: String(selectionCount) }]
168
- : []
169
- return [
89
+ const statusItems = useMemo(
90
+ () => [
170
91
  { label: "Format:", value: content.format },
171
92
  { label: "Lines:", value: String(lineCount) },
172
- ...selectionItems,
173
- ]
174
- }, [content.format, lineCount, nav.selection, nav.toggledIndices])
93
+ ],
94
+ [content.format, lineCount],
95
+ )
175
96
 
176
97
  return (
177
- <SubviewLayout
98
+ <ViewScreen
178
99
  content={content}
179
- nav={layoutNav}
180
- streaming={streaming}
181
- themeName={themeName}
182
- extraStatusItems={extraStatusItems}
100
+ controller={document}
101
+ actions={actions}
102
+ statusItems={statusItems}
103
+ {...screen}
183
104
  >
184
105
  <MarkdownView
185
106
  content={content.markdown}
107
+ blocks={blocks}
186
108
  showLineNumbers={showLineNumbers}
187
- marks={markState}
188
- docRef={docRef}
109
+ document={document}
189
110
  hScrollableBlocksRef={hScrollableBlocksRef}
190
111
  codeBlockRenderers={codeBlockRenderers}
191
- onRowClick={handleRowClick}
192
- onRowContextMenu={handleRowContextMenu}
193
112
  />
194
- </SubviewLayout>
113
+ </ViewScreen>
195
114
  )
196
115
  }
@@ -1,184 +1,56 @@
1
- import { useCallback, useEffect, useMemo, useRef } from "react"
2
- import { Table, type RowDocumentRenderable } from "@tooee/renderers"
3
- import { useTheme } from "@tooee/themes"
4
- import { useCommandContext } from "@tooee/commands"
5
- import { useHasModalOverlay } from "@tooee/overlays"
6
- import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
7
- import { actionsToContextMenuEntries, useContextMenu, useCopy, useNavigation } from "@tooee/shell"
8
- import { useSearch } from "@tooee/search"
1
+ import { useMemo } from "react"
2
+ import { Table, type TableRow } from "@tooee/renderers"
3
+ import { actionsToContextMenuEntries, useDocumentController } from "@tooee/shell"
9
4
  import { getTextContent, type TableContent } from "../../types.js"
10
- import { useMarkState } from "../../hooks/useMarkState.js"
11
- import { useViewCommands } from "../../hooks/useViewCommands.js"
12
- import { SubviewLayout } from "../SubviewLayout.js"
5
+ import { useContentCommands } from "../../hooks/useContentCommands.js"
6
+ import { ViewScreen } from "../ViewScreen.js"
13
7
  import type { SubviewProps } from "./types.js"
14
8
 
15
9
  interface TableSubviewProps extends SubviewProps {
16
10
  content: TableContent
17
11
  }
18
12
 
19
- export function TableSubview({
20
- content,
21
- providerMarks,
22
- userMarks,
23
- setMarkSet,
24
- clearMarkNamespace,
25
- clearAllUserMarks,
26
- reload,
27
- streaming,
28
- actions,
29
- }: TableSubviewProps) {
30
- const { theme } = useTheme()
31
- const docRef = useRef<RowDocumentRenderable>(null)
13
+ export function TableSubview({ content, decorations, actions, ...screen }: TableSubviewProps) {
32
14
  const textContent = useMemo(() => getTextContent(content), [content])
33
- const contextMenu = useContextMenu()
34
- const { invoke } = useCommandContext()
35
- const hasModalOverlay = useHasModalOverlay()
15
+ const { showLineNumbers } = useContentCommands({ content, textContent })
36
16
 
37
- const nav = useNavigation({
38
- rowCount: content.rows.length,
39
- multiSelect: true,
40
- })
41
- const search = useSearch({
42
- match: (query) => {
43
- const lowerQuery = query.toLowerCase()
44
- return content.rows.flatMap((row, index) =>
45
- content.columns.some((column) =>
46
- stringifyRowCell(row[column.key]).toLowerCase().includes(lowerQuery),
47
- )
48
- ? [index]
49
- : [],
50
- )
51
- },
52
- onJump: nav.setCursor,
53
- })
54
- useCopy({
55
- getRowText: (index) =>
56
- content.columns
57
- .map((column) => stringifyRowCell(content.rows[index]?.[column.key]))
58
- .join("\t"),
59
- cursor: nav.cursor,
60
- selection: nav.selection,
61
- toggledIndices: nav.toggledIndices,
62
- })
63
- const layoutNav = { ...nav, ...search }
64
-
65
- useEffect(() => {
66
- if (nav.cursor !== null) {
67
- docRef.current?.scrollToRow(nav.cursor, "nearest")
68
- }
69
- }, [nav.cursor])
70
-
71
- const { themeName, showLineNumbers } = useViewCommands({ content, textContent, actions })
72
-
73
- const markState = useMarkState({
74
- nav,
75
- search,
76
- theme,
77
- providerMarks,
78
- userMarks,
79
- })
80
-
81
- function getActiveRow(): Record<string, unknown> | undefined {
82
- if (nav.cursor === null) return undefined
83
- return content.rows[nav.cursor]
84
- }
85
-
86
- function getSelectedRows(): Record<string, unknown>[] {
87
- if (nav.toggledIndices.size) {
88
- return Array.from(nav.toggledIndices)
89
- .map((i) => content.rows[i])
90
- .filter(Boolean)
91
- }
92
- if (nav.selection) {
93
- const start = nav.selection.start
94
- const end = nav.selection.end
95
- return content.rows.slice(start, end + 1)
96
- }
97
- return []
98
- }
17
+ const { columns, rows } = content
18
+ const adapter = useMemo(
19
+ () => ({
20
+ getText: (row: TableRow) =>
21
+ columns.map((column) => stringifyRowCell(row[column.key])).join("\t"),
22
+ }),
23
+ [columns],
24
+ )
25
+ const contextMenu = useMemo(() => actionsToContextMenuEntries(actions), [actions])
99
26
 
100
- useViewCommandContext({
101
- content,
102
- nav,
103
- reload,
104
- providerMarks,
105
- userMarks,
106
- setMarkSet,
107
- clearMarkNamespace,
108
- clearAllUserMarks,
109
- extras: {
110
- activeRow: getActiveRow(),
111
- selectedRows: getSelectedRows(),
112
- },
27
+ const document = useDocumentController<TableRow>({
28
+ rows,
29
+ adapter,
30
+ multiSelect: true,
31
+ decorations,
32
+ contextMenu,
113
33
  })
114
34
 
115
- // Row-scoped context-menu entries come from the app-provided actions.
116
- const menuEntries = useMemo(() => actionsToContextMenuEntries(actions), [actions])
117
-
118
- // Row mouse handlers stand down while a modal overlay (theme picker, command
119
- // palette, Ask/Choose overlays, the context menu itself) is up: centered
120
- // overlays leave clickable margins around them, and mouse events route
121
- // through the hit-grid, bypassing command-surface arbitration. Memoized so
122
- // Table's rowElements memo keeps doing its job.
123
- const setCursor = nav.setCursor
124
- const openContextMenu = contextMenu.open
125
- const handleRowClick = useCallback(
126
- (index: number) => {
127
- if (hasModalOverlay) return
128
- setCursor(index)
129
- },
130
- [hasModalOverlay, setCursor],
131
- )
132
- const handleRowContextMenu = useCallback(
133
- (index: number, x: number, y: number) => {
134
- if (hasModalOverlay) return
135
- setCursor(index)
136
- openContextMenu(x, y, menuEntries, invoke)
137
- },
138
- [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
139
- )
140
-
141
- const extraStatusItems = useMemo(() => {
142
- const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
143
- const toggledCount = nav.toggledIndices.size
144
- const selectionItems =
145
- toggledCount > 0
146
- ? [{ label: "Selected:", value: String(toggledCount) }]
147
- : selectionCount > 0
148
- ? [{ label: "Selected:", value: String(selectionCount) }]
149
- : []
150
- return [
35
+ const statusItems = useMemo(
36
+ () => [
151
37
  { label: "Format:", value: content.format },
152
- { label: "Rows:", value: String(content.rows.length) },
153
- { label: "Cols:", value: String(content.columns.length) },
154
- ...selectionItems,
155
- ]
156
- }, [
157
- content.format,
158
- content.rows.length,
159
- content.columns.length,
160
- nav.selection,
161
- nav.toggledIndices,
162
- ])
38
+ { label: "Rows:", value: String(rows.length) },
39
+ { label: "Cols:", value: String(columns.length) },
40
+ ],
41
+ [content.format, rows.length, columns.length],
42
+ )
163
43
 
164
44
  return (
165
- <SubviewLayout
45
+ <ViewScreen
166
46
  content={content}
167
- nav={layoutNav}
168
- streaming={streaming}
169
- themeName={themeName}
170
- extraStatusItems={extraStatusItems}
47
+ controller={document}
48
+ actions={actions}
49
+ statusItems={statusItems}
50
+ {...screen}
171
51
  >
172
- <Table
173
- columns={content.columns}
174
- rows={content.rows}
175
- showLineNumbers={showLineNumbers}
176
- marks={markState}
177
- docRef={docRef}
178
- onRowClick={handleRowClick}
179
- onRowContextMenu={handleRowContextMenu}
180
- />
181
- </SubviewLayout>
52
+ <Table columns={columns} rows={rows} showLineNumbers={showLineNumbers} document={document} />
53
+ </ViewScreen>
182
54
  )
183
55
  }
184
56
 
@@ -2,5 +2,4 @@ export { MarkdownSubview } from "./MarkdownSubview.js"
2
2
  export { CodeSubview } from "./CodeSubview.js"
3
3
  export { TableSubview } from "./TableSubview.js"
4
4
  export { CustomSubview } from "./CustomSubview.js"
5
- export { SubviewLayout } from "../SubviewLayout.js"
6
5
  export type { SubviewProps } from "./types.js"
@@ -1,9 +1,15 @@
1
1
  import type { MarkSet } from "@tooee/marks"
2
2
  import type { ActionDefinition } from "@tooee/commands"
3
+ import type { DecorationLayer } from "@tooee/renderers"
3
4
  import type { AnyContent } from "../../types.js"
4
5
 
5
6
  export interface SubviewProps {
6
7
  content: AnyContent
8
+ /**
9
+ * Provider and user mark sets as decoration layers. They compose with the
10
+ * interaction layers (search, selection, cursor) the controller generates.
11
+ */
12
+ decorations: DecorationLayer[]
7
13
  providerMarks: MarkSet[]
8
14
  userMarks: MarkSet[]
9
15
  setMarkSet: (set: MarkSet) => void