docs: fix share page scroll performance

This commit is contained in:
Jay V 2025-07-03 19:15:38 -04:00
parent e7fcb692a4
commit b8de69dced

View file

@ -597,6 +597,8 @@ export default function Share(props: {
}) { }) {
let lastScrollY = 0 let lastScrollY = 0
let scrollTimeout: number | undefined let scrollTimeout: number | undefined
let scrollSentinel: HTMLElement | undefined
let scrollObserver: IntersectionObserver | undefined
const id = props.id const id = props.id
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
@ -604,6 +606,7 @@ export default function Share(props: {
const [showScrollButton, setShowScrollButton] = createSignal(false) const [showScrollButton, setShowScrollButton] = createSignal(false)
const [isButtonHovered, setIsButtonHovered] = createSignal(false) const [isButtonHovered, setIsButtonHovered] = createSignal(false)
const [isNearBottom, setIsNearBottom] = createSignal(false)
const [store, setStore] = createStore<{ const [store, setStore] = createStore<{
info?: Session.Info info?: Session.Info
@ -713,10 +716,9 @@ export default function Share(props: {
const currentScrollY = window.scrollY const currentScrollY = window.scrollY
const isScrollingDown = currentScrollY > lastScrollY const isScrollingDown = currentScrollY > lastScrollY
const scrolled = currentScrollY > 200 // Show after scrolling 200px const scrolled = currentScrollY > 200 // Show after scrolling 200px
const isNearBottom = window.innerHeight + currentScrollY >= document.body.scrollHeight - 100
// Only show when scrolling down, scrolled enough, and not near bottom // Only show when scrolling down, scrolled enough, and not near bottom
const shouldShow = isScrollingDown && scrolled && !isNearBottom const shouldShow = isScrollingDown && scrolled && !isNearBottom()
// Update last scroll position // Update last scroll position
lastScrollY = currentScrollY lastScrollY = currentScrollY
@ -732,7 +734,7 @@ export default function Share(props: {
if (!isButtonHovered()) { if (!isButtonHovered()) {
setShowScrollButton(false) setShowScrollButton(false)
} }
}, 3000) }, 1500)
} else if (!isButtonHovered()) { } else if (!isButtonHovered()) {
// Only hide if not hovered (to prevent disappearing while user is about to click) // Only hide if not hovered (to prevent disappearing while user is about to click)
setShowScrollButton(false) setShowScrollButton(false)
@ -744,6 +746,26 @@ export default function Share(props: {
onMount(() => { onMount(() => {
lastScrollY = window.scrollY // Initialize scroll position lastScrollY = window.scrollY // Initialize scroll position
// Create sentinel element
const sentinel = document.createElement("div")
sentinel.style.height = "1px"
sentinel.style.position = "absolute"
sentinel.style.bottom = "100px"
sentinel.style.width = "100%"
sentinel.style.pointerEvents = "none"
document.body.appendChild(sentinel)
// Create intersection observer
const observer = new IntersectionObserver((entries) => {
setIsNearBottom(entries[0].isIntersecting)
})
observer.observe(sentinel)
// Store references for cleanup
scrollSentinel = sentinel
scrollObserver = observer
checkScrollNeed() checkScrollNeed()
window.addEventListener("scroll", checkScrollNeed) window.addEventListener("scroll", checkScrollNeed)
window.addEventListener("resize", checkScrollNeed) window.addEventListener("resize", checkScrollNeed)
@ -752,6 +774,15 @@ export default function Share(props: {
onCleanup(() => { onCleanup(() => {
window.removeEventListener("scroll", checkScrollNeed) window.removeEventListener("scroll", checkScrollNeed)
window.removeEventListener("resize", checkScrollNeed) window.removeEventListener("resize", checkScrollNeed)
// Clean up observer and sentinel
if (scrollObserver) {
scrollObserver.disconnect()
}
if (scrollSentinel) {
document.body.removeChild(scrollSentinel)
}
if (scrollTimeout) { if (scrollTimeout) {
clearTimeout(scrollTimeout) clearTimeout(scrollTimeout)
} }
@ -855,7 +886,6 @@ export default function Share(props: {
</span> </span>
)} )}
</div> </div>
</div> </div>
</div> </div>
@ -880,8 +910,11 @@ export default function Share(props: {
) )
return null return null
const anchor = createMemo(() => `${msg.id}-${partIndex()}`) const anchor = createMemo(
const [showResults, setShowResults] = createSignal(false) () => `${msg.id}-${partIndex()}`,
)
const [showResults, setShowResults] =
createSignal(false)
const isLastPart = createMemo( const isLastPart = createMemo(
() => () =>
data().messages.length === msgIndex() + 1 && data().messages.length === msgIndex() + 1 &&
@ -903,7 +936,9 @@ export default function Share(props: {
const duration = DateTime.fromMillis( const duration = DateTime.fromMillis(
metadata?.time.end || 0, metadata?.time.end || 0,
) )
.diff(DateTime.fromMillis(metadata?.time.start || 0)) .diff(
DateTime.fromMillis(metadata?.time.start || 0),
)
.toMillis() .toMillis()
return { metadata, args, result, duration } return { metadata, args, result, duration }
@ -921,7 +956,9 @@ export default function Share(props: {
{/* User text */} {/* User text */}
<Match <Match
when={ when={
msg.role === "user" && part.type === "text" && part msg.role === "user" &&
part.type === "text" &&
part
} }
> >
{(part) => ( {(part) => (
@ -972,7 +1009,9 @@ export default function Share(props: {
expand={isLastPart()} expand={isLastPart()}
text={stripEnclosingTag(part().text)} text={stripEnclosingTag(part().text)}
/> />
<Show when={isLastPart() && data().completed}> <Show
when={isLastPart() && data().completed}
>
<span <span
data-part-footer data-part-footer
title={DateTime.fromMillis( title={DateTime.fromMillis(
@ -1041,7 +1080,8 @@ export default function Share(props: {
} }
> >
{(_part) => { {(_part) => {
const matches = () => toolData()?.metadata?.matches const matches = () =>
toolData()?.metadata?.matches
const splitArgs = () => { const splitArgs = () => {
const { pattern, ...rest } = toolData()?.args const { pattern, ...rest } = toolData()?.args
return { pattern, rest } return { pattern, rest }
@ -1066,11 +1106,14 @@ export default function Share(props: {
<div data-part-tool-body> <div data-part-tool-body>
<div data-part-title> <div data-part-title>
<span data-element-label>Grep</span> <span data-element-label>Grep</span>
<b>&ldquo;{splitArgs().pattern}&rdquo;</b> <b>
&ldquo;{splitArgs().pattern}&rdquo;
</b>
</div> </div>
<Show <Show
when={ when={
Object.keys(splitArgs().rest).length > 0 Object.keys(splitArgs().rest)
.length > 0
} }
> >
<div data-part-tool-args> <div data-part-tool-args>
@ -1299,8 +1342,10 @@ export default function Share(props: {
data().rootDir, data().rootDir,
), ),
) )
const hasError = () => toolData()?.metadata?.error const hasError = () =>
const preview = () => toolData()?.metadata?.preview toolData()?.metadata?.error
const preview = () =>
toolData()?.metadata?.preview
return ( return (
<div <div
@ -1333,7 +1378,9 @@ export default function Share(props: {
</div> </div>
</Match> </Match>
{/* Always try to show CodeBlock if preview is available (even if empty string) */} {/* Always try to show CodeBlock if preview is available (even if empty string) */}
<Match when={typeof preview() === 'string'}> <Match
when={typeof preview() === "string"}
>
<div data-part-tool-result> <div data-part-tool-result>
<ResultsButton <ResultsButton
showCopy="Show preview" showCopy="Show preview"
@ -1346,7 +1393,9 @@ export default function Share(props: {
<Show when={showResults()}> <Show when={showResults()}>
<div data-part-tool-code> <div data-part-tool-code>
<CodeBlock <CodeBlock
lang={getShikiLang(filePath())} lang={getShikiLang(
filePath(),
)}
code={preview()} code={preview()}
/> />
</div> </div>
@ -1354,7 +1403,12 @@ export default function Share(props: {
</div> </div>
</Match> </Match>
{/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */} {/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */}
<Match when={typeof preview() !== 'string' && toolData()?.result}> <Match
when={
typeof preview() !== "string" &&
toolData()?.result
}
>
<div data-part-tool-result> <div data-part-tool-result>
<ResultsButton <ResultsButton
results={showResults()} results={showResults()}
@ -1398,7 +1452,8 @@ export default function Share(props: {
data().rootDir, data().rootDir,
), ),
) )
const hasError = () => toolData()?.metadata?.error const hasError = () =>
toolData()?.metadata?.error
const content = () => toolData()?.args?.content const content = () => toolData()?.args?.content
const diagnostics = createMemo(() => const diagnostics = createMemo(() =>
getDiagnostics( getDiagnostics(
@ -1415,7 +1470,10 @@ export default function Share(props: {
> >
<div data-section="decoration"> <div data-section="decoration">
<AnchorIcon id={anchor()}> <AnchorIcon id={anchor()}>
<IconDocumentPlus width={18} height={18} /> <IconDocumentPlus
width={18}
height={18}
/>
</AnchorIcon> </AnchorIcon>
<div></div> <div></div>
</div> </div>
@ -1435,7 +1493,7 @@ export default function Share(props: {
<div data-part-tool-result> <div data-part-tool-result>
<ErrorPart> <ErrorPart>
{formatErrorString( {formatErrorString(
toolData()?.result toolData()?.result,
)} )}
</ErrorPart> </ErrorPart>
</div> </div>
@ -1453,8 +1511,12 @@ export default function Share(props: {
<Show when={showResults()}> <Show when={showResults()}>
<div data-part-tool-code> <div data-part-tool-code>
<CodeBlock <CodeBlock
lang={getShikiLang(filePath())} lang={getShikiLang(
code={toolData()?.args?.content} filePath(),
)}
code={
toolData()?.args?.content
}
/> />
</div> </div>
</Show> </Show>
@ -1481,8 +1543,10 @@ export default function Share(props: {
> >
{(_part) => { {(_part) => {
const diff = () => toolData()?.metadata?.diff const diff = () => toolData()?.metadata?.diff
const message = () => toolData()?.metadata?.message const message = () =>
const hasError = () => toolData()?.metadata?.error toolData()?.metadata?.message
const hasError = () =>
toolData()?.metadata?.error
const filePath = createMemo(() => const filePath = createMemo(() =>
stripWorkingDirectory( stripWorkingDirectory(
toolData()?.args.filePath, toolData()?.args.filePath,
@ -1504,7 +1568,10 @@ export default function Share(props: {
> >
<div data-section="decoration"> <div data-section="decoration">
<AnchorIcon id={anchor()}> <AnchorIcon id={anchor()}>
<IconPencilSquare width={18} height={18} /> <IconPencilSquare
width={18}
height={18}
/>
</AnchorIcon> </AnchorIcon>
<div></div> <div></div>
</div> </div>
@ -1527,7 +1594,9 @@ export default function Share(props: {
<Match when={diff()}> <Match when={diff()}>
<div data-part-tool-edit> <div data-part-tool-edit>
<DiffView <DiffView
class={styles["diff-code-block"]} class={
styles["diff-code-block"]
}
diff={diff()} diff={diff()}
lang={getShikiLang(filePath())} lang={getShikiLang(filePath())}
/> />
@ -1556,9 +1625,12 @@ export default function Share(props: {
} }
> >
{(_part) => { {(_part) => {
const command = () => toolData()?.metadata?.title const command = () =>
const desc = () => toolData()?.metadata?.description toolData()?.metadata?.title
const result = () => toolData()?.metadata?.stdout const desc = () =>
toolData()?.metadata?.description
const result = () =>
toolData()?.metadata?.stdout
const error = () => toolData()?.metadata?.stderr const error = () => toolData()?.metadata?.stderr
return ( return (
@ -1569,7 +1641,10 @@ export default function Share(props: {
> >
<div data-section="decoration"> <div data-section="decoration">
<AnchorIcon id={anchor()}> <AnchorIcon id={anchor()}>
<IconCommandLine width={18} height={18} /> <IconCommandLine
width={18}
height={18}
/>
</AnchorIcon> </AnchorIcon>
<div></div> <div></div>
</div> </div>
@ -1604,7 +1679,9 @@ export default function Share(props: {
> >
{(_part) => { {(_part) => {
const todos = createMemo(() => const todos = createMemo(() =>
sortTodosByStatus(toolData()?.args?.todos ?? []), sortTodosByStatus(
toolData()?.args?.todos ?? [],
),
) )
const starting = () => const starting = () =>
todos().every((t) => t.status === "pending") todos().every((t) => t.status === "pending")
@ -1670,7 +1747,8 @@ export default function Share(props: {
{(_part) => { {(_part) => {
const url = () => toolData()?.args.url const url = () => toolData()?.args.url
const format = () => toolData()?.args.format const format = () => toolData()?.args.format
const hasError = () => toolData()?.metadata?.error const hasError = () =>
toolData()?.metadata?.error
return ( return (
<div <div
@ -1793,7 +1871,8 @@ export default function Share(props: {
</Match> </Match>
<Match <Match
when={ when={
part().toolInvocation.state === "call" part().toolInvocation.state ===
"call"
} }
> >
<TextPart <TextPart
@ -1839,7 +1918,10 @@ export default function Share(props: {
</Match> </Match>
<Match when={msg.role === "user"}> <Match when={msg.role === "user"}>
<IconUserCircle width={18} height={18} /> <IconUserCircle
width={18}
height={18}
/>
</Match> </Match>
</Switch> </Switch>
</AnchorIcon> </AnchorIcon>
@ -1848,7 +1930,9 @@ export default function Share(props: {
<div data-section="content"> <div data-section="content">
<div data-part-tool-body> <div data-part-tool-body>
<div data-part-title> <div data-part-title>
<span data-element-label>{part.type}</span> <span data-element-label>
{part.type}
</span>
</div> </div>
<TextPart <TextPart
text={JSON.stringify(part, null, 2)} text={JSON.stringify(part, null, 2)}