@tooee/view 0.1.21 → 0.2.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.
Files changed (83) hide show
  1. package/README.md +83 -0
  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 -91
  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 -108
  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 -125
  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 +3 -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 +24 -25
  44. package/dist/hooks/useViewCommandContext.d.ts.map +1 -1
  45. package/dist/hooks/useViewCommandContext.js +26 -21
  46. package/dist/hooks/useViewCommandContext.js.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  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 -126
  58. package/src/components/subviews/CustomSubview.tsx +32 -123
  59. package/src/components/subviews/MarkdownSubview.tsx +49 -137
  60. package/src/components/subviews/TableSubview.tsx +36 -170
  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 +7 -3
  65. package/src/hooks/useContentCommands.ts +29 -0
  66. package/src/hooks/useViewCommandContext.ts +62 -49
  67. package/src/index.ts +10 -0
  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,23 +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 ContextMenuEntry,
9
- type RowDocumentRenderable,
8
+ type FlatBlock,
10
9
  } from "@tooee/renderers"
11
- import { useTheme } from "@tooee/themes"
12
- import { useCommand, useCommandContext } from "@tooee/commands"
13
- import { useHasModalOverlay } from "@tooee/overlays"
14
- import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
15
- import { useContextMenu, useCopy, useNavigation } from "@tooee/shell"
16
- import { useSearch } from "@tooee/search"
10
+ import { useCommand } from "@tooee/commands"
11
+ import {
12
+ actionsToContextMenuEntries,
13
+ useDocumentController,
14
+ type DocumentRowAdapter,
15
+ } from "@tooee/shell"
17
16
  import type { MarkdownContent } from "../../types.js"
18
- import { useMarkState } from "../../hooks/useMarkState.js"
19
- import { useViewCommands } from "../../hooks/useViewCommands.js"
20
- import { SubviewLayout } from "../SubviewLayout.js"
17
+ import { useContentCommands } from "../../hooks/useContentCommands.js"
18
+ import { ViewScreen } from "../ViewScreen.js"
21
19
  import type { SubviewProps } from "./types.js"
22
20
 
23
21
  interface MarkdownSubviewProps extends SubviewProps {
@@ -28,68 +26,43 @@ interface MarkdownSubviewProps extends SubviewProps {
28
26
  /** Columns moved per h/l press when scrolling a wide block horizontally. */
29
27
  const BLOCK_HSCROLL_STEP = 4
30
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
+
31
39
  export function MarkdownSubview({
32
40
  content,
33
41
  codeBlockRenderers,
34
- providerMarks,
35
- userMarks,
36
- setMarkSet,
37
- clearMarkNamespace,
38
- clearAllUserMarks,
39
- reload,
40
- streaming,
42
+ decorations,
41
43
  actions,
44
+ ...screen
42
45
  }: MarkdownSubviewProps) {
43
- const { theme } = useTheme()
44
- const docRef = useRef<RowDocumentRenderable>(null)
45
- const hasModalOverlay = useHasModalOverlay()
46
- const contextMenu = useContextMenu()
47
- const { invoke } = useCommandContext()
48
46
  const textContent = content.markdown
49
47
  const lineCount = useMemo(() => textContent.split("\n").length, [textContent])
50
- const blocks = useMemo(() => flattenTokens(marked.lexer(content.markdown)), [content.markdown])
48
+ const blocks = useMemo(() => flattenMarkdown(content.markdown), [content.markdown])
51
49
 
52
- const nav = useNavigation({
53
- 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,
54
56
  multiSelect: true,
57
+ decorations,
58
+ contextMenu,
55
59
  })
56
- const search = useSearch({
57
- match: (query) => {
58
- const lowerQuery = query.toLowerCase()
59
- return blocks.flatMap((block, index) => {
60
- const { token } = block
61
- const raw = "raw" in token && typeof token.raw === "string" ? token.raw : ""
62
- return raw.toLowerCase().includes(lowerQuery) ? [index] : []
63
- })
64
- },
65
- onJump: nav.setCursor,
66
- })
67
- useCopy({
68
- getRowText: (index) => {
69
- const block = blocks[index]
70
- if (!block) return ""
71
- const { token } = block
72
- return "raw" in token && typeof token.raw === "string" ? token.raw : ""
73
- },
74
- cursor: nav.cursor,
75
- selection: nav.selection,
76
- toggledIndices: nav.toggledIndices,
77
- })
78
- const layoutNav = { ...nav, ...search }
79
-
80
- useEffect(() => {
81
- if (nav.cursor !== null) {
82
- docRef.current?.scrollToRow(nav.cursor, "nearest")
83
- }
84
- }, [nav.cursor])
85
60
 
86
- // Horizontal panning for wide blocks (mermaid diagrams and code blocks).
87
- // Blocks that can overflow horizontally register their text-buffer
88
- // renderable by block index; h/l in cursor mode pans the block under the
89
- // nav cursor via `scrollX` (the setter clamps to the content width).
90
61
  const hScrollableBlocksRef = useRef<Map<number, TextBufferRenderable>>(new Map())
91
62
  const cursorScrollable = () =>
92
- nav.cursor !== null ? hScrollableBlocksRef.current.get(nav.cursor) : undefined
63
+ document.activeIndex !== null
64
+ ? hScrollableBlocksRef.current.get(document.activeIndex)
65
+ : undefined
93
66
  useCommand({
94
67
  id: "block-scroll-left",
95
68
  title: "Scroll block left",
@@ -113,91 +86,30 @@ export function MarkdownSubview({
113
86
  },
114
87
  })
115
88
 
116
- const menuEntries = useMemo<ContextMenuEntry[]>(
117
- () =>
118
- (actions ?? [])
119
- .filter((action) => !action.hidden)
120
- .map((action) => ({ id: action.id, title: action.title, hotkey: action.hotkey })),
121
- [actions],
122
- )
123
-
124
- // Left-click selects the clicked block; right-click selects it and opens the
125
- // same app-provided action menu as table/code rows. Both stand down while a
126
- // modal overlay is up (theme picker, command palette, Ask/Choose), matching
127
- // keyboard stand-down semantics.
128
- const setCursor = nav.setCursor
129
- const openContextMenu = contextMenu.open
130
- const handleRowClick = useCallback(
131
- (index: number) => {
132
- if (hasModalOverlay) return
133
- setCursor(index)
134
- },
135
- [hasModalOverlay, setCursor],
136
- )
137
- const handleRowContextMenu = useCallback(
138
- (index: number, x: number, y: number) => {
139
- if (hasModalOverlay) return
140
- setCursor(index)
141
- openContextMenu(x, y, menuEntries, invoke)
142
- },
143
- [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
144
- )
145
-
146
- const { themeName, showLineNumbers } = useViewCommands({ content, textContent, actions })
147
-
148
- const markState = useMarkState({
149
- nav,
150
- search,
151
- theme,
152
- providerMarks,
153
- userMarks,
154
- })
155
-
156
- useViewCommandContext({
157
- content,
158
- nav,
159
- reload,
160
- providerMarks,
161
- userMarks,
162
- setMarkSet,
163
- clearMarkNamespace,
164
- clearAllUserMarks,
165
- })
166
-
167
- const extraStatusItems = useMemo(() => {
168
- const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
169
- const toggledCount = nav.toggledIndices.size
170
- const selectionItems =
171
- toggledCount > 0
172
- ? [{ label: "Selected:", value: String(toggledCount) }]
173
- : selectionCount > 0
174
- ? [{ label: "Selected:", value: String(selectionCount) }]
175
- : []
176
- return [
89
+ const statusItems = useMemo(
90
+ () => [
177
91
  { label: "Format:", value: content.format },
178
92
  { label: "Lines:", value: String(lineCount) },
179
- ...selectionItems,
180
- ]
181
- }, [content.format, lineCount, nav.selection, nav.toggledIndices])
93
+ ],
94
+ [content.format, lineCount],
95
+ )
182
96
 
183
97
  return (
184
- <SubviewLayout
98
+ <ViewScreen
185
99
  content={content}
186
- nav={layoutNav}
187
- streaming={streaming}
188
- themeName={themeName}
189
- extraStatusItems={extraStatusItems}
100
+ controller={document}
101
+ actions={actions}
102
+ statusItems={statusItems}
103
+ {...screen}
190
104
  >
191
105
  <MarkdownView
192
106
  content={content.markdown}
107
+ blocks={blocks}
193
108
  showLineNumbers={showLineNumbers}
194
- marks={markState}
195
- docRef={docRef}
109
+ document={document}
196
110
  hScrollableBlocksRef={hScrollableBlocksRef}
197
111
  codeBlockRenderers={codeBlockRenderers}
198
- onRowClick={handleRowClick}
199
- onRowContextMenu={handleRowContextMenu}
200
112
  />
201
- </SubviewLayout>
113
+ </ViewScreen>
202
114
  )
203
115
  }
@@ -1,190 +1,56 @@
1
- import { useCallback, useEffect, useMemo, useRef } from "react"
2
- import { Table, type RowDocumentRenderable, type ContextMenuEntry } 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 { 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
- }
99
-
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
- },
113
- })
114
-
115
- // Row-scoped context-menu entries come from the app-provided actions.
116
- const menuEntries = useMemo<ContextMenuEntry[]>(
117
- () =>
118
- (actions ?? [])
119
- .filter((action) => !action.hidden)
120
- .map((action) => ({ id: action.id, title: action.title, hotkey: action.hotkey })),
121
- [actions],
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],
122
24
  )
25
+ const contextMenu = useMemo(() => actionsToContextMenuEntries(actions), [actions])
123
26
 
124
- // Row mouse handlers stand down while a modal overlay (theme picker, command
125
- // palette, Ask/Choose overlays, the context menu itself) is up: centered
126
- // overlays leave clickable margins around them, and mouse events route
127
- // through the hit-grid, bypassing command-surface arbitration. Memoized so
128
- // Table's rowElements memo keeps doing its job.
129
- const setCursor = nav.setCursor
130
- const openContextMenu = contextMenu.open
131
- const handleRowClick = useCallback(
132
- (index: number) => {
133
- if (hasModalOverlay) return
134
- setCursor(index)
135
- },
136
- [hasModalOverlay, setCursor],
137
- )
138
- const handleRowContextMenu = useCallback(
139
- (index: number, x: number, y: number) => {
140
- if (hasModalOverlay) return
141
- setCursor(index)
142
- openContextMenu(x, y, menuEntries, invoke)
143
- },
144
- [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
145
- )
27
+ const document = useDocumentController<TableRow>({
28
+ rows,
29
+ adapter,
30
+ multiSelect: true,
31
+ decorations,
32
+ contextMenu,
33
+ })
146
34
 
147
- const extraStatusItems = useMemo(() => {
148
- const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
149
- const toggledCount = nav.toggledIndices.size
150
- const selectionItems =
151
- toggledCount > 0
152
- ? [{ label: "Selected:", value: String(toggledCount) }]
153
- : selectionCount > 0
154
- ? [{ label: "Selected:", value: String(selectionCount) }]
155
- : []
156
- return [
35
+ const statusItems = useMemo(
36
+ () => [
157
37
  { label: "Format:", value: content.format },
158
- { label: "Rows:", value: String(content.rows.length) },
159
- { label: "Cols:", value: String(content.columns.length) },
160
- ...selectionItems,
161
- ]
162
- }, [
163
- content.format,
164
- content.rows.length,
165
- content.columns.length,
166
- nav.selection,
167
- nav.toggledIndices,
168
- ])
38
+ { label: "Rows:", value: String(rows.length) },
39
+ { label: "Cols:", value: String(columns.length) },
40
+ ],
41
+ [content.format, rows.length, columns.length],
42
+ )
169
43
 
170
44
  return (
171
- <SubviewLayout
45
+ <ViewScreen
172
46
  content={content}
173
- nav={layoutNav}
174
- streaming={streaming}
175
- themeName={themeName}
176
- extraStatusItems={extraStatusItems}
47
+ controller={document}
48
+ actions={actions}
49
+ statusItems={statusItems}
50
+ {...screen}
177
51
  >
178
- <Table
179
- columns={content.columns}
180
- rows={content.rows}
181
- showLineNumbers={showLineNumbers}
182
- marks={markState}
183
- docRef={docRef}
184
- onRowClick={handleRowClick}
185
- onRowContextMenu={handleRowContextMenu}
186
- />
187
- </SubviewLayout>
52
+ <Table columns={columns} rows={rows} showLineNumbers={showLineNumbers} document={document} />
53
+ </ViewScreen>
188
54
  )
189
55
  }
190
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