@workerdeck/ui 0.23.0 → 1.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/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-I5QSLLb0.mjs} +2266 -3459
- package/build/SessionPanel-I5QSLLb0.mjs.map +1 -0
- package/build/SessionPanel-yNO60Tzw.d.mts +151 -0
- package/build/format-C_lpFq0l.d.mts +16 -0
- package/build/format.d.mts +8 -76
- package/build/format.mjs +2 -2
- package/build/index.d.mts +291 -1695
- package/build/index.mjs +6 -301
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +32 -23
- package/build/{status-BE-zg88x.mjs → status-Dm9INVTt.mjs} +17 -88
- package/build/status-Dm9INVTt.mjs.map +1 -0
- package/build/workspace.d.mts +8 -187
- package/build/workspace.mjs +13 -144
- package/build/workspace.mjs.map +1 -1
- package/package.json +24 -24
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -4,47 +4,20 @@ import { cn } from '../../lib/utils.ts'
|
|
|
4
4
|
import { CopyAction, WithActions } from './affordances.tsx'
|
|
5
5
|
import { Band } from './row.tsx'
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Markdown on the character grid.
|
|
9
|
-
*
|
|
10
|
-
* The `lines` variant did this by letting the renderer draw its prose defaults
|
|
11
|
-
* and then overriding roughly sixty declarations back off with `!important` —
|
|
12
|
-
* every margin, every list gap, the fenced-code card's four nested boxes, the
|
|
13
|
-
* table's frame and its floating button pill. That is a losing position by
|
|
14
|
-
* construction: each renderer upgrade is a new set of boxes to find and unpaint,
|
|
15
|
-
* and the CSS says what the output must *not* look like rather than what it is.
|
|
16
|
-
*
|
|
17
|
-
* So this maps the elements instead. Streamdown keeps what only it can do —
|
|
18
|
-
* streaming-safe parsing of half-written markdown — and every block it emits is
|
|
19
|
-
* built from the same {@link Row}/{@link Band} primitives the rest of the theme
|
|
20
|
-
* uses. There is no `!important` here and there is no CSS fighting anything.
|
|
21
|
-
*
|
|
22
|
-
* The rendering rules are a terminal's, not a document's:
|
|
23
|
-
*
|
|
24
|
-
* - **One type size.** Headings are weight and colour; a bigger glyph would
|
|
25
|
-
* break the only line height the grid has.
|
|
26
|
-
* - **Markers are cells.** A bullet is `- ` (two columns), an ordered marker
|
|
27
|
-
* `1. ` (three), and the text starts on the next column exactly as it does in
|
|
28
|
-
* the markdown source — so a wrapped line hangs under the text, not the
|
|
29
|
-
* bullet, and nesting costs one marker width per level.
|
|
30
|
-
* - **Code is a band, not a card.** No frame, no language strip, no floating
|
|
31
|
-
* buttons: a fenced block is a wash of dim text running to the screen edge,
|
|
32
|
-
* which is what a terminal shows.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/** Pull the text out of a fenced block's React children (`<code>…</code>`). */
|
|
36
7
|
function codeText(node: ReactNode): string {
|
|
37
|
-
if (node === null || node === undefined || typeof node === 'boolean')
|
|
38
|
-
|
|
39
|
-
|
|
8
|
+
if (node === null || node === undefined || typeof node === 'boolean') {
|
|
9
|
+
return ''
|
|
10
|
+
}
|
|
11
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
12
|
+
return String(node)
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(node)) {
|
|
15
|
+
return node.map(codeText).join('')
|
|
16
|
+
}
|
|
40
17
|
const element = node as { props?: { children?: ReactNode } }
|
|
41
18
|
return element.props ? codeText(element.props.children) : ''
|
|
42
19
|
}
|
|
43
20
|
|
|
44
|
-
/** The language a fence declared, from the `language-*` class react-markdown
|
|
45
|
-
* puts on the inner `<code>`. Kept as a data attribute rather than used to
|
|
46
|
-
* highlight: the CLI does not colour a fenced block inside a message, and a
|
|
47
|
-
* second highlighter here would be a second theme to keep in sync. */
|
|
48
21
|
function fenceLanguage(node: ReactNode): string | undefined {
|
|
49
22
|
const child = Array.isArray(node) ? node.find(Boolean) : node
|
|
50
23
|
const className = (child as { props?: { className?: string } } | undefined)?.props?.className
|
|
@@ -52,39 +25,28 @@ function fenceLanguage(node: ReactNode): string | undefined {
|
|
|
52
25
|
return match?.[1]
|
|
53
26
|
}
|
|
54
27
|
|
|
55
|
-
/**
|
|
56
|
-
* A fenced block: a band of dim text, and the one place a copy affordance earns
|
|
57
|
-
* its keep most — a command in a message is there to be run.
|
|
58
|
-
*
|
|
59
|
-
* The renderer's own copy/download buttons are turned off (`controls={false}`)
|
|
60
|
-
* in favour of this: they are web buttons floating over the content in another
|
|
61
|
-
* application's idiom, and they are not switchable by the surface the way
|
|
62
|
-
* everything else here is.
|
|
63
|
-
*/
|
|
64
28
|
function CodeBand({ code, language }: { code: string; language?: string }) {
|
|
65
29
|
return (
|
|
66
|
-
<WithActions className=
|
|
67
|
-
<Band className=
|
|
68
|
-
<pre className=
|
|
30
|
+
<WithActions className="term-block" actions={<CopyAction text={code} label="Copy code" />}>
|
|
31
|
+
<Band className="term-code" data-language={language}>
|
|
32
|
+
<pre className="term-pre">{code}</pre>
|
|
69
33
|
</Band>
|
|
70
34
|
</WithActions>
|
|
71
35
|
)
|
|
72
36
|
}
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* renderer's own component signature rather than a narrower hand-written one. */
|
|
77
|
-
const heading = (tone: 'bright' | 'fg'): Components['h1'] =>
|
|
78
|
-
function Heading({ children }) {
|
|
38
|
+
function heading(tone: 'bright' | 'fg'): Components['h1'] {
|
|
39
|
+
return function Heading({ children }) {
|
|
79
40
|
return (
|
|
80
|
-
<div className=
|
|
41
|
+
<div className="term-block" data-tone={tone} data-weight="bold">
|
|
81
42
|
{children}
|
|
82
43
|
</div>
|
|
83
44
|
)
|
|
84
45
|
}
|
|
46
|
+
}
|
|
85
47
|
|
|
86
48
|
const TERMINAL_COMPONENTS: Components = {
|
|
87
|
-
p: ({ children }) => <div className=
|
|
49
|
+
p: ({ children }) => <div className="term-block">{children}</div>,
|
|
88
50
|
|
|
89
51
|
h1: heading('bright'),
|
|
90
52
|
h2: heading('bright'),
|
|
@@ -93,68 +55,53 @@ const TERMINAL_COMPONENTS: Components = {
|
|
|
93
55
|
h5: heading('fg'),
|
|
94
56
|
h6: heading('fg'),
|
|
95
57
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// straight up against its neighbour (a list running into the paragraph after
|
|
99
|
-
// it, which is exactly how this was found).
|
|
100
|
-
ul: ({ children }) => <ul className='term-block term-list'>{children}</ul>,
|
|
101
|
-
ol: ({ children }) => <ol className='term-block term-list term-list-ordered'>{children}</ol>,
|
|
102
|
-
// The marker is the gutter's `::before` (a CSS counter for the ordered case),
|
|
103
|
-
// so a list item is literally a Row: same two columns, same hanging indent,
|
|
104
|
-
// and a nested list inside the body indents by exactly one marker width.
|
|
58
|
+
ul: ({ children }) => <ul className="term-block term-list">{children}</ul>,
|
|
59
|
+
ol: ({ children }) => <ol className="term-block term-list term-list-ordered">{children}</ol>,
|
|
105
60
|
li: ({ children }) => (
|
|
106
|
-
<li className=
|
|
107
|
-
<span className=
|
|
108
|
-
<div className=
|
|
61
|
+
<li className="term-row term-li">
|
|
62
|
+
<span className="term-gutter" aria-hidden />
|
|
63
|
+
<div className="term-body">{children}</div>
|
|
109
64
|
</li>
|
|
110
65
|
),
|
|
111
66
|
|
|
112
67
|
blockquote: ({ children }) => (
|
|
113
|
-
<blockquote className=
|
|
68
|
+
<blockquote className="term-block term-quote" data-tone="dim">
|
|
114
69
|
{children}
|
|
115
70
|
</blockquote>
|
|
116
71
|
),
|
|
117
72
|
|
|
118
|
-
hr: () => <div className=
|
|
73
|
+
hr: () => <div className="term-block term-rule" aria-hidden />,
|
|
119
74
|
|
|
120
|
-
// Fenced code. `pre` owns the whole block — the inner `<code>` is only where
|
|
121
|
-
// the text and the language live — so the band is built here and `code` never
|
|
122
|
-
// sees a fence.
|
|
123
75
|
pre: ({ children }) => <CodeBand code={codeText(children)} language={fenceLanguage(children)} />,
|
|
124
76
|
code: ({ children }) => (
|
|
125
|
-
<code className=
|
|
77
|
+
<code className="term-inline-code" data-tone="blue">
|
|
126
78
|
{children}
|
|
127
79
|
</code>
|
|
128
80
|
),
|
|
129
81
|
|
|
130
82
|
strong: ({ children }) => (
|
|
131
|
-
<strong data-tone=
|
|
83
|
+
<strong data-tone="bright" data-weight="bold">
|
|
132
84
|
{children}
|
|
133
85
|
</strong>
|
|
134
86
|
),
|
|
135
|
-
em: ({ children }) => <em className=
|
|
87
|
+
em: ({ children }) => <em className="term-em">{children}</em>,
|
|
136
88
|
a: ({ children, href }) => (
|
|
137
|
-
<a className=
|
|
89
|
+
<a className="term-link" data-tone="blue" href={href} target="_blank" rel="noreferrer">
|
|
138
90
|
{children}
|
|
139
91
|
</a>
|
|
140
92
|
),
|
|
141
93
|
|
|
142
|
-
// Tables keep the grid by being a grid: monospace cells, one line per row, and
|
|
143
|
-
// dim box-drawing rules instead of borders that would land between cells.
|
|
144
94
|
table: ({ children }) => (
|
|
145
|
-
<div className=
|
|
146
|
-
<table className=
|
|
95
|
+
<div className="term-block term-table-wrap">
|
|
96
|
+
<table className="term-table">{children}</table>
|
|
147
97
|
</div>
|
|
148
98
|
),
|
|
149
|
-
// Every table element
|
|
150
|
-
|
|
151
|
-
// single one of those puts its rows off the line grid (`td`'s `py-2` was
|
|
152
|
-
// making table rows 23px in an 18px theme).
|
|
153
|
-
thead: ({ children }) => <thead className='term-thead'>{children}</thead>,
|
|
99
|
+
// Every table element must be mapped: an unmapped `td` keeps its `py-2` and puts rows off the line grid (23px in an 18px theme).
|
|
100
|
+
thead: ({ children }) => <thead className="term-thead">{children}</thead>,
|
|
154
101
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
|
155
102
|
tr: ({ children }) => <tr>{children}</tr>,
|
|
156
103
|
th: ({ children }) => (
|
|
157
|
-
<th data-tone=
|
|
104
|
+
<th data-tone="bright" data-weight="bold">
|
|
158
105
|
{children}
|
|
159
106
|
</th>
|
|
160
107
|
),
|
|
@@ -163,7 +110,6 @@ const TERMINAL_COMPONENTS: Components = {
|
|
|
163
110
|
|
|
164
111
|
export interface TerminalMarkdownProps {
|
|
165
112
|
children: string
|
|
166
|
-
/** Streaming text: tolerate half-written markdown (unclosed fences, half links). */
|
|
167
113
|
streaming?: boolean
|
|
168
114
|
className?: string
|
|
169
115
|
}
|
|
@@ -174,18 +120,13 @@ export const TerminalMarkdown = memo(
|
|
|
174
120
|
<Streamdown
|
|
175
121
|
mode={streaming ? 'streaming' : 'static'}
|
|
176
122
|
parseIncompleteMarkdown={streaming}
|
|
177
|
-
// The renderer's copy/download affordances are web buttons floating over
|
|
178
|
-
// the content. A terminal has none, and the transcript's own selection
|
|
179
|
-
// is how you copy from one.
|
|
180
123
|
controls={false}
|
|
181
124
|
components={TERMINAL_COMPONENTS}
|
|
182
|
-
className={cn('term-md', className)}
|
|
125
|
+
className={cn('term-md', className)}
|
|
126
|
+
>
|
|
183
127
|
{children}
|
|
184
128
|
</Streamdown>
|
|
185
129
|
)
|
|
186
130
|
},
|
|
187
|
-
(prev, next) =>
|
|
188
|
-
prev.children === next.children &&
|
|
189
|
-
prev.streaming === next.streaming &&
|
|
190
|
-
prev.className === next.className,
|
|
131
|
+
(prev, next) => prev.children === next.children && prev.streaming === next.streaming && prev.className === next.className,
|
|
191
132
|
)
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, type ReactNode } from 'react'
|
|
2
2
|
import { cn } from '../../lib/utils.ts'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* A row you can open, that you can also select text out of.
|
|
6
|
-
*
|
|
7
|
-
* A `<button>` cannot be both: text inside one is selectable in principle, but
|
|
8
|
-
* the drag that selects it ends in a `click`, so releasing the mouse collapses
|
|
9
|
-
* the very block you were highlighting — and the selection is discarded with it.
|
|
10
|
-
* A transcript is *read* far more often than it is opened, so copying a command
|
|
11
|
-
* out of a row has to win over the affordance that expands it.
|
|
12
|
-
*
|
|
13
|
-
* So: a `div` with the button role and the keyboard behaviour restored by hand,
|
|
14
|
-
* and a press that is refused when the pointer travelled (a drag, not a click)
|
|
15
|
-
* or when a selection is standing. Both checks are cheap and neither is a
|
|
16
|
-
* heuristic about intent — a pointer that moved four pixels was dragging, and a
|
|
17
|
-
* non-collapsed selection *is* the user having selected something.
|
|
18
|
-
*/
|
|
19
4
|
const DRAG_SLOP = 4
|
|
20
5
|
|
|
21
6
|
export function Pressable({
|
|
@@ -25,7 +10,6 @@ export function Pressable({
|
|
|
25
10
|
children,
|
|
26
11
|
}: {
|
|
27
12
|
onPress: () => void
|
|
28
|
-
/** Mirrored to `aria-expanded` when this press opens something. */
|
|
29
13
|
expanded?: boolean
|
|
30
14
|
className?: string
|
|
31
15
|
children: ReactNode
|
|
@@ -33,7 +17,7 @@ export function Pressable({
|
|
|
33
17
|
const origin = useRef<{ x: number; y: number } | null>(null)
|
|
34
18
|
return (
|
|
35
19
|
<div
|
|
36
|
-
role=
|
|
20
|
+
role="button"
|
|
37
21
|
tabIndex={0}
|
|
38
22
|
aria-expanded={expanded}
|
|
39
23
|
className={cn('term-press', className)}
|
|
@@ -43,62 +27,51 @@ export function Pressable({
|
|
|
43
27
|
onClick={(event) => {
|
|
44
28
|
const from = origin.current
|
|
45
29
|
origin.current = null
|
|
46
|
-
if (from && Math.abs(event.clientX - from.x) + Math.abs(event.clientY - from.y) > DRAG_SLOP)
|
|
30
|
+
if (from && Math.abs(event.clientX - from.x) + Math.abs(event.clientY - from.y) > DRAG_SLOP) {
|
|
47
31
|
return
|
|
48
|
-
|
|
49
|
-
// reads as a click; one that ends a selection inside this row is the
|
|
50
|
-
// tail of a drag the slop check may have missed (a slow, short drag).
|
|
32
|
+
}
|
|
51
33
|
const selection = window.getSelection?.()
|
|
52
|
-
if (selection && !selection.isCollapsed && selection.containsNode(event.currentTarget, true))
|
|
34
|
+
if (selection && !selection.isCollapsed && selection.containsNode(event.currentTarget, true)) {
|
|
53
35
|
return
|
|
36
|
+
}
|
|
54
37
|
onPress()
|
|
55
38
|
}}
|
|
56
39
|
onKeyDown={(event) => {
|
|
57
|
-
if (event.key !== 'Enter' && event.key !== ' ')
|
|
40
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
58
43
|
event.preventDefault()
|
|
59
44
|
onPress()
|
|
60
|
-
}}
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
61
47
|
{children}
|
|
62
48
|
</div>
|
|
63
49
|
)
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
/**
|
|
67
|
-
* Keep an expanding block's *first* line reachable.
|
|
68
|
-
*
|
|
69
|
-
* A row that grows from one line to eighty pushes its own top off the screen:
|
|
70
|
-
* the reader presses a summary and lands somewhere in the middle of what they
|
|
71
|
-
* opened, with no clue that the beginning is above them. The fix is not a
|
|
72
|
-
* scroll-into-view on every expand — that would yank a block already fully in
|
|
73
|
-
* view — but the narrow one: if the block now starts above the fold, bring its
|
|
74
|
-
* first line back to the top edge.
|
|
75
|
-
*
|
|
76
|
-
* Deliberately one-directional and only on the open transition. Collapsing needs
|
|
77
|
-
* nothing (the block shrinks toward its own top, which is already on screen),
|
|
78
|
-
* and a block whose top is already visible must not move at all.
|
|
79
|
-
*/
|
|
80
52
|
export function useRevealOnOpen(open: boolean) {
|
|
81
53
|
const ref = useRef<HTMLDivElement>(null)
|
|
82
54
|
const previous = useRef(open)
|
|
83
55
|
useEffect(() => {
|
|
84
56
|
const opened = open && !previous.current
|
|
85
57
|
previous.current = open
|
|
86
|
-
if (!opened)
|
|
58
|
+
if (!opened) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
87
61
|
const element = ref.current
|
|
88
|
-
if (!element)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// offset read
|
|
62
|
+
if (!element) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
// After paint: the new rows must be laid out and the virtualizer's size-change correction run before an offset read means anything.
|
|
92
66
|
const frame = requestAnimationFrame(() => {
|
|
93
67
|
const scroller = scrollParent(element)
|
|
94
|
-
if (!scroller)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// scroller's edge — the same blank line every block gets.
|
|
68
|
+
if (!scroller) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const top = element.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop
|
|
72
|
+
if (top >= scroller.scrollTop) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
102
75
|
const line = Number.parseFloat(getComputedStyle(element).lineHeight) || 0
|
|
103
76
|
scroller.scrollTop = Math.max(0, top - line)
|
|
104
77
|
})
|
|
@@ -107,13 +80,13 @@ export function useRevealOnOpen(open: boolean) {
|
|
|
107
80
|
return ref
|
|
108
81
|
}
|
|
109
82
|
|
|
110
|
-
/** The nearest ancestor that actually scrolls. */
|
|
111
83
|
function scrollParent(from: HTMLElement): HTMLElement | null {
|
|
112
84
|
let node = from.parentElement
|
|
113
85
|
while (node) {
|
|
114
86
|
const overflow = getComputedStyle(node).overflowY
|
|
115
|
-
if ((overflow === 'auto' || overflow === 'scroll') && node.scrollHeight > node.clientHeight)
|
|
87
|
+
if ((overflow === 'auto' || overflow === 'scroll') && node.scrollHeight > node.clientHeight) {
|
|
116
88
|
return node
|
|
89
|
+
}
|
|
117
90
|
node = node.parentElement
|
|
118
91
|
}
|
|
119
92
|
return null
|