summaryrefslogtreecommitdiffhomepage
path: root/packages/web/src/components
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-07-03 19:15:38 -0400
committerJay V <[email protected]>2025-07-03 19:15:38 -0400
commitb8de69dceda9486d4cc4bb75738238012f58f197 (patch)
treea8ef8bf1bc8a2b931803efc0d203ccea5c342f45 /packages/web/src/components
parente7fcb692a42079c518cc76569352e99a932b4599 (diff)
downloadopencode-b8de69dceda9486d4cc4bb75738238012f58f197.tar.gz
opencode-b8de69dceda9486d4cc4bb75738238012f58f197.zip
docs: fix share page scroll performance
Diffstat (limited to 'packages/web/src/components')
-rw-r--r--packages/web/src/components/Share.tsx154
1 files changed, 119 insertions, 35 deletions
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index f1a1f1aa4..fb9ddc99e 100644
--- a/packages/web/src/components/Share.tsx
+++ b/packages/web/src/components/Share.tsx
@@ -597,6 +597,8 @@ export default function Share(props: {
}) {
let lastScrollY = 0
let scrollTimeout: number | undefined
+ let scrollSentinel: HTMLElement | undefined
+ let scrollObserver: IntersectionObserver | undefined
const id = props.id
const params = new URLSearchParams(window.location.search)
@@ -604,6 +606,7 @@ export default function Share(props: {
const [showScrollButton, setShowScrollButton] = createSignal(false)
const [isButtonHovered, setIsButtonHovered] = createSignal(false)
+ const [isNearBottom, setIsNearBottom] = createSignal(false)
const [store, setStore] = createStore<{
info?: Session.Info
@@ -713,10 +716,9 @@ export default function Share(props: {
const currentScrollY = window.scrollY
const isScrollingDown = currentScrollY > lastScrollY
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
- const shouldShow = isScrollingDown && scrolled && !isNearBottom
+ const shouldShow = isScrollingDown && scrolled && !isNearBottom()
// Update last scroll position
lastScrollY = currentScrollY
@@ -732,7 +734,7 @@ export default function Share(props: {
if (!isButtonHovered()) {
setShowScrollButton(false)
}
- }, 3000)
+ }, 1500)
} else if (!isButtonHovered()) {
// Only hide if not hovered (to prevent disappearing while user is about to click)
setShowScrollButton(false)
@@ -744,6 +746,26 @@ export default function Share(props: {
onMount(() => {
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()
window.addEventListener("scroll", checkScrollNeed)
window.addEventListener("resize", checkScrollNeed)
@@ -752,6 +774,15 @@ export default function Share(props: {
onCleanup(() => {
window.removeEventListener("scroll", checkScrollNeed)
window.removeEventListener("resize", checkScrollNeed)
+
+ // Clean up observer and sentinel
+ if (scrollObserver) {
+ scrollObserver.disconnect()
+ }
+ if (scrollSentinel) {
+ document.body.removeChild(scrollSentinel)
+ }
+
if (scrollTimeout) {
clearTimeout(scrollTimeout)
}
@@ -855,7 +886,6 @@ export default function Share(props: {
</span>
)}
</div>
-
</div>
</div>
@@ -880,8 +910,11 @@ export default function Share(props: {
)
return null
- const anchor = createMemo(() => `${msg.id}-${partIndex()}`)
- const [showResults, setShowResults] = createSignal(false)
+ const anchor = createMemo(
+ () => `${msg.id}-${partIndex()}`,
+ )
+ const [showResults, setShowResults] =
+ createSignal(false)
const isLastPart = createMemo(
() =>
data().messages.length === msgIndex() + 1 &&
@@ -903,7 +936,9 @@ export default function Share(props: {
const duration = DateTime.fromMillis(
metadata?.time.end || 0,
)
- .diff(DateTime.fromMillis(metadata?.time.start || 0))
+ .diff(
+ DateTime.fromMillis(metadata?.time.start || 0),
+ )
.toMillis()
return { metadata, args, result, duration }
@@ -921,7 +956,9 @@ export default function Share(props: {
{/* User text */}
<Match
when={
- msg.role === "user" && part.type === "text" && part
+ msg.role === "user" &&
+ part.type === "text" &&
+ part
}
>
{(part) => (
@@ -972,7 +1009,9 @@ export default function Share(props: {
expand={isLastPart()}
text={stripEnclosingTag(part().text)}
/>
- <Show when={isLastPart() && data().completed}>
+ <Show
+ when={isLastPart() && data().completed}
+ >
<span
data-part-footer
title={DateTime.fromMillis(
@@ -1041,7 +1080,8 @@ export default function Share(props: {
}
>
{(_part) => {
- const matches = () => toolData()?.metadata?.matches
+ const matches = () =>
+ toolData()?.metadata?.matches
const splitArgs = () => {
const { pattern, ...rest } = toolData()?.args
return { pattern, rest }
@@ -1066,11 +1106,14 @@ export default function Share(props: {
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Grep</span>
- <b>&ldquo;{splitArgs().pattern}&rdquo;</b>
+ <b>
+ &ldquo;{splitArgs().pattern}&rdquo;
+ </b>
</div>
<Show
when={
- Object.keys(splitArgs().rest).length > 0
+ Object.keys(splitArgs().rest)
+ .length > 0
}
>
<div data-part-tool-args>
@@ -1299,8 +1342,10 @@ export default function Share(props: {
data().rootDir,
),
)
- const hasError = () => toolData()?.metadata?.error
- const preview = () => toolData()?.metadata?.preview
+ const hasError = () =>
+ toolData()?.metadata?.error
+ const preview = () =>
+ toolData()?.metadata?.preview
return (
<div
@@ -1333,7 +1378,9 @@ export default function Share(props: {
</div>
</Match>
{/* 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>
<ResultsButton
showCopy="Show preview"
@@ -1346,7 +1393,9 @@ export default function Share(props: {
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
- lang={getShikiLang(filePath())}
+ lang={getShikiLang(
+ filePath(),
+ )}
code={preview()}
/>
</div>
@@ -1354,7 +1403,12 @@ export default function Share(props: {
</div>
</Match>
{/* 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>
<ResultsButton
results={showResults()}
@@ -1398,7 +1452,8 @@ export default function Share(props: {
data().rootDir,
),
)
- const hasError = () => toolData()?.metadata?.error
+ const hasError = () =>
+ toolData()?.metadata?.error
const content = () => toolData()?.args?.content
const diagnostics = createMemo(() =>
getDiagnostics(
@@ -1415,7 +1470,10 @@ export default function Share(props: {
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
- <IconDocumentPlus width={18} height={18} />
+ <IconDocumentPlus
+ width={18}
+ height={18}
+ />
</AnchorIcon>
<div></div>
</div>
@@ -1435,7 +1493,7 @@ export default function Share(props: {
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
- toolData()?.result
+ toolData()?.result,
)}
</ErrorPart>
</div>
@@ -1453,8 +1511,12 @@ export default function Share(props: {
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
- lang={getShikiLang(filePath())}
- code={toolData()?.args?.content}
+ lang={getShikiLang(
+ filePath(),
+ )}
+ code={
+ toolData()?.args?.content
+ }
/>
</div>
</Show>
@@ -1481,8 +1543,10 @@ export default function Share(props: {
>
{(_part) => {
const diff = () => toolData()?.metadata?.diff
- const message = () => toolData()?.metadata?.message
- const hasError = () => toolData()?.metadata?.error
+ const message = () =>
+ toolData()?.metadata?.message
+ const hasError = () =>
+ toolData()?.metadata?.error
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args.filePath,
@@ -1504,7 +1568,10 @@ export default function Share(props: {
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
- <IconPencilSquare width={18} height={18} />
+ <IconPencilSquare
+ width={18}
+ height={18}
+ />
</AnchorIcon>
<div></div>
</div>
@@ -1527,7 +1594,9 @@ export default function Share(props: {
<Match when={diff()}>
<div data-part-tool-edit>
<DiffView
- class={styles["diff-code-block"]}
+ class={
+ styles["diff-code-block"]
+ }
diff={diff()}
lang={getShikiLang(filePath())}
/>
@@ -1556,9 +1625,12 @@ export default function Share(props: {
}
>
{(_part) => {
- const command = () => toolData()?.metadata?.title
- const desc = () => toolData()?.metadata?.description
- const result = () => toolData()?.metadata?.stdout
+ const command = () =>
+ toolData()?.metadata?.title
+ const desc = () =>
+ toolData()?.metadata?.description
+ const result = () =>
+ toolData()?.metadata?.stdout
const error = () => toolData()?.metadata?.stderr
return (
@@ -1569,7 +1641,10 @@ export default function Share(props: {
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
- <IconCommandLine width={18} height={18} />
+ <IconCommandLine
+ width={18}
+ height={18}
+ />
</AnchorIcon>
<div></div>
</div>
@@ -1604,7 +1679,9 @@ export default function Share(props: {
>
{(_part) => {
const todos = createMemo(() =>
- sortTodosByStatus(toolData()?.args?.todos ?? []),
+ sortTodosByStatus(
+ toolData()?.args?.todos ?? [],
+ ),
)
const starting = () =>
todos().every((t) => t.status === "pending")
@@ -1670,7 +1747,8 @@ export default function Share(props: {
{(_part) => {
const url = () => toolData()?.args.url
const format = () => toolData()?.args.format
- const hasError = () => toolData()?.metadata?.error
+ const hasError = () =>
+ toolData()?.metadata?.error
return (
<div
@@ -1793,7 +1871,8 @@ export default function Share(props: {
</Match>
<Match
when={
- part().toolInvocation.state === "call"
+ part().toolInvocation.state ===
+ "call"
}
>
<TextPart
@@ -1839,7 +1918,10 @@ export default function Share(props: {
</Match>
<Match when={msg.role === "user"}>
- <IconUserCircle width={18} height={18} />
+ <IconUserCircle
+ width={18}
+ height={18}
+ />
</Match>
</Switch>
</AnchorIcon>
@@ -1848,7 +1930,9 @@ export default function Share(props: {
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
- <span data-element-label>{part.type}</span>
+ <span data-element-label>
+ {part.type}
+ </span>
</div>
<TextPart
text={JSON.stringify(part, null, 2)}