@workerdeck/ui 0.22.0 → 1.0.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 (145) hide show
  1. package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +80 -24
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -124
  14. package/build/workspace.mjs +13 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +4 -6
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +345 -923
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +108 -215
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +136 -19
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-13l25ubU.d.mts +0 -609
  143. package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. 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') return ''
38
- if (typeof node === 'string' || typeof node === 'number') return String(node)
39
- if (Array.isArray(node)) return node.map(codeText).join('')
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='term-block' actions={<CopyAction text={code} label='Copy code' />}>
67
- <Band className='term-code' data-language={language}>
68
- <pre className='term-pre'>{code}</pre>
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
- /** Headings differ only in tone a terminal has one type size, so `h1` and `h4`
75
- * cannot differ in anything else. Return-typed so the parameter picks up the
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='term-block' data-tone={tone} data-weight='bold'>
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='term-block'>{children}</div>,
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
- // `term-block` on every block-level output, without exception: it is what the
97
- // one-blank-line-between-blocks rule keys on, and a block that forgets it butts
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='term-row term-li'>
107
- <span className='term-gutter' aria-hidden />
108
- <div className='term-body'>{children}</div>
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='term-block term-quote' data-tone='dim'>
68
+ <blockquote className="term-block term-quote" data-tone="dim">
114
69
  {children}
115
70
  </blockquote>
116
71
  ),
117
72
 
118
- hr: () => <div className='term-block term-rule' aria-hidden />,
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='term-inline-code' data-tone='blue'>
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='bright' data-weight='bold'>
83
+ <strong data-tone="bright" data-weight="bold">
132
84
  {children}
133
85
  </strong>
134
86
  ),
135
- em: ({ children }) => <em className='term-em'>{children}</em>,
87
+ em: ({ children }) => <em className="term-em">{children}</em>,
136
88
  a: ({ children, href }) => (
137
- <a className='term-link' data-tone='blue' href={href} target='_blank' rel='noreferrer'>
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='term-block term-table-wrap'>
146
- <table className='term-table'>{children}</table>
95
+ <div className="term-block term-table-wrap">
96
+ <table className="term-table">{children}</table>
147
97
  </div>
148
98
  ),
149
- // Every table element, and not just the ones that looked wrong: any element
150
- // left unmapped keeps the renderer's own padded, bordered default, and a
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='bright' data-weight='bold'>
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='button'
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
- // A click that merely *ends* a selection elsewhere on the page still
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 !== ' ') return
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) return
58
+ if (!opened) {
59
+ return
60
+ }
87
61
  const element = ref.current
88
- if (!element) return
89
- // After paint: the rows this block just grew by have to be laid out, and
90
- // the virtualizer's own size-change correction has to have run, before an
91
- // offset read here means anything.
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) return
95
- const top =
96
- element.getBoundingClientRect().top -
97
- scroller.getBoundingClientRect().top +
98
- scroller.scrollTop
99
- if (top >= scroller.scrollTop) return
100
- // One line of air above it, so the first row isn't flush against the
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