Compare commits
5 Commits
v0.12.2-de
...
v0.12.2-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77fb1562e | ||
|
|
b33421a375 | ||
|
|
c64a9a03f9 | ||
|
|
0d215342e3 | ||
|
|
beb14ea0a2 |
19
package-lock.json
generated
19
package-lock.json
generated
@@ -3305,6 +3305,22 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.4.tgz",
|
||||
"integrity": "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-notification": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||
@@ -12066,7 +12082,8 @@
|
||||
"version": "0.12.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
"@tauri-apps/cli": "^2.9.4",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "^2.9.4"
|
||||
}
|
||||
},
|
||||
"packages/ui": {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"build": "tauri build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
"@tauri-apps/cli": "^2.9.4",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "^2.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,6 +900,11 @@ fn resolve_dist_entry(_app: &AppHandle) -> Option<String> {
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push(Some(dir.join("resources/server/dist/bin.js")));
|
||||
candidates.push(Some(dir.join("resources/server/dist/index.js")));
|
||||
candidates.push(Some(dir.join("resources/server/dist/server/bin.js")));
|
||||
candidates.push(Some(dir.join("resources/server/dist/server/index.js")));
|
||||
|
||||
let resources = dir.join("../Resources");
|
||||
candidates.push(Some(resources.join("server/dist/bin.js")));
|
||||
candidates.push(Some(resources.join("server/dist/index.js")));
|
||||
@@ -995,9 +1000,18 @@ fn first_existing(paths: Vec<Option<PathBuf>>) -> Option<String> {
|
||||
}
|
||||
|
||||
fn normalize_path(path: PathBuf) -> String {
|
||||
if let Ok(clean) = path.canonicalize() {
|
||||
clean.to_string_lossy().to_string()
|
||||
let resolved = if let Ok(clean) = path.canonicalize() {
|
||||
clean
|
||||
} else {
|
||||
path.to_string_lossy().to_string()
|
||||
path
|
||||
};
|
||||
|
||||
let rendered = resolved.to_string_lossy().to_string();
|
||||
if let Some(stripped) = rendered.strip_prefix("\\\\?\\UNC\\") {
|
||||
format!("\\\\{}", stripped)
|
||||
} else if let Some(stripped) = rendered.strip_prefix("\\\\?\\") {
|
||||
stripped.to_string()
|
||||
} else {
|
||||
rendered
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,7 +578,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
||||
|
||||
const isDeleteMessageHovered = () => {
|
||||
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
||||
|
||||
@@ -1290,12 +1289,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||
|
||||
let headerEl: HTMLDivElement | undefined
|
||||
let actionsEl: HTMLDivElement | undefined
|
||||
let primaryEl: HTMLSpanElement | undefined
|
||||
let metaMeasureEl: HTMLSpanElement | undefined
|
||||
const [showMetaInline, setShowMetaInline] = createSignal(true)
|
||||
|
||||
createEffect(() => {
|
||||
setExpanded(Boolean(props.defaultExpanded))
|
||||
})
|
||||
@@ -1323,33 +1316,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
|
||||
const hasMeta = () => Boolean(props.showAgentMeta && (agentIdentifier() || modelIdentifier()))
|
||||
|
||||
const updateMetaLayout = () => {
|
||||
if (!hasMeta()) return
|
||||
if (!headerEl || !actionsEl || !primaryEl || !metaMeasureEl) return
|
||||
|
||||
const headerWidth = headerEl.getBoundingClientRect().width
|
||||
const actionsWidth = actionsEl.getBoundingClientRect().width
|
||||
const primaryWidth = primaryEl.getBoundingClientRect().width
|
||||
const metaWidth = metaMeasureEl.getBoundingClientRect().width
|
||||
|
||||
const availableLeft = Math.max(0, headerWidth - actionsWidth - 12)
|
||||
setShowMetaInline(primaryWidth + metaWidth + 8 <= availableLeft)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!hasMeta() || typeof ResizeObserver === "undefined") {
|
||||
setShowMetaInline(true)
|
||||
return
|
||||
}
|
||||
|
||||
updateMetaLayout()
|
||||
const observer = new ResizeObserver(() => updateMetaLayout())
|
||||
if (headerEl) observer.observe(headerEl)
|
||||
if (actionsEl) observer.observe(actionsEl)
|
||||
if (primaryEl) observer.observe(primaryEl)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
const reasoningText = () => {
|
||||
const part = props.part as any
|
||||
if (!part) return ""
|
||||
@@ -1428,7 +1394,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
|
||||
return (
|
||||
<div class="delete-hover-scope message-reasoning-card">
|
||||
<div class="message-reasoning-header" ref={(el) => (headerEl = el)}>
|
||||
<div class="message-reasoning-header">
|
||||
<button
|
||||
type="button"
|
||||
class="message-reasoning-toggle"
|
||||
@@ -1437,7 +1403,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
aria-label={expanded() ? t("messageBlock.reasoning.collapseAriaLabel") : t("messageBlock.reasoning.expandAriaLabel")}
|
||||
>
|
||||
<span class="message-reasoning-label">
|
||||
<span class="message-reasoning-label-primary" ref={(el) => (primaryEl = el)}>
|
||||
<span class="message-reasoning-label-primary">
|
||||
<Show when={props.showDeleteMessage}>
|
||||
<input
|
||||
class="message-select-checkbox"
|
||||
@@ -1458,43 +1424,10 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
|
||||
<span>{t("messageBlock.reasoning.thinkingLabel")}</span>
|
||||
</span>
|
||||
|
||||
<Show when={hasMeta() && showMetaInline()}>
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={modelIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
<Show when={hasMeta()}>
|
||||
<span
|
||||
ref={(el) => (metaMeasureEl = el)}
|
||||
class="message-step-meta-inline message-step-meta-inline--measure"
|
||||
>
|
||||
<Show when={agentIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.agentLabel", { agent: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={modelIdentifier()}>
|
||||
{(value) => (
|
||||
<span class="font-medium text-[var(--message-assistant-border)]">{t("messageBlock.step.modelLabel", { model: value() })}</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="message-reasoning-actions" ref={(el) => (actionsEl = el)}>
|
||||
<div class="message-reasoning-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="message-action-button"
|
||||
@@ -1543,7 +1476,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={hasMeta() && !showMetaInline()}>
|
||||
<Show when={hasMeta()}>
|
||||
<div class="message-reasoning-meta-row">
|
||||
<span class="message-step-meta-inline">
|
||||
<Show when={agentIdentifier()}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Index, Show, createEffect, createMemo, createSignal, onCleanup, type Accessor, type JSX } from "solid-js"
|
||||
import VirtualItem from "./virtual-item"
|
||||
import VirtualItem, { type VirtualItemHeightChangeMeta } from "./virtual-item"
|
||||
|
||||
const DEFAULT_SCROLL_SENTINEL_MARGIN_PX = 48
|
||||
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
||||
@@ -374,7 +374,14 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
}
|
||||
|
||||
function handleContentRendered() {
|
||||
scheduleAnchorScroll()
|
||||
if (autoScroll() && !anchorLock()) {
|
||||
scheduleAutoPinToBottom()
|
||||
return
|
||||
}
|
||||
if (anchorLock() && !autoScroll()) {
|
||||
scheduleAnchorCorrection()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
@@ -470,9 +477,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
const bottomAfter = rect.bottom
|
||||
const bottomBefore = bottomAfter - delta
|
||||
const wasAboveViewport = bottomBefore < containerRect.top
|
||||
if (!wasAboveViewport) {
|
||||
return
|
||||
}
|
||||
if (!wasAboveViewport) return
|
||||
|
||||
const next = (pendingScrollCompensations.get(key) ?? 0) + delta
|
||||
pendingScrollCompensations.set(key, next)
|
||||
@@ -516,25 +521,51 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
}
|
||||
|
||||
let pendingAutoPin = false
|
||||
let pendingAutoPinFrame: number | null = null
|
||||
|
||||
function clearPendingAutoPinFrame() {
|
||||
if (pendingAutoPinFrame !== null) {
|
||||
cancelAnimationFrame(pendingAutoPinFrame)
|
||||
pendingAutoPinFrame = null
|
||||
}
|
||||
}
|
||||
|
||||
function applyAutoPinToBottom() {
|
||||
if (!containerRef) return false
|
||||
if (!autoScroll()) return false
|
||||
if (anchorLock()) return false
|
||||
|
||||
const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
|
||||
if (containerRef.scrollTop !== maxScrollTop) {
|
||||
containerRef.scrollTop = maxScrollTop
|
||||
lastKnownScrollTop = maxScrollTop
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function scheduleAutoPinToBottom() {
|
||||
if (!containerRef) return
|
||||
if (pendingAutoPin) return
|
||||
pendingAutoPin = true
|
||||
clearPendingAutoPinFrame()
|
||||
const gen = scrollCompensationGen
|
||||
|
||||
// Flush in a microtask so adjustments land before the next paint.
|
||||
// Flush in a microtask so adjustments land before the next paint,
|
||||
// then re-apply on the next two frames to catch deferred layout.
|
||||
queueMicrotask(() => {
|
||||
if (gen !== scrollCompensationGen) return
|
||||
pendingAutoPin = false
|
||||
if (!containerRef) return
|
||||
if (!autoScroll()) return
|
||||
if (anchorLock()) return
|
||||
|
||||
const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0)
|
||||
if (containerRef.scrollTop !== maxScrollTop) {
|
||||
containerRef.scrollTop = maxScrollTop
|
||||
lastKnownScrollTop = maxScrollTop
|
||||
}
|
||||
if (!applyAutoPinToBottom()) return
|
||||
pendingAutoPinFrame = requestAnimationFrame(() => {
|
||||
pendingAutoPinFrame = null
|
||||
if (gen !== scrollCompensationGen) return
|
||||
if (!applyAutoPinToBottom()) return
|
||||
pendingAutoPinFrame = requestAnimationFrame(() => {
|
||||
pendingAutoPinFrame = null
|
||||
if (gen !== scrollCompensationGen) return
|
||||
applyAutoPinToBottom()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -623,6 +654,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
pendingScrollCompensationScheduled = false
|
||||
pendingScrollCompensations = new Map()
|
||||
pendingAutoPin = false
|
||||
clearPendingAutoPinFrame()
|
||||
|
||||
suppressAutoScrollOnce = false
|
||||
pendingActiveScroll = false
|
||||
@@ -713,7 +745,13 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
suppressAutoScrollOnce = false
|
||||
return
|
||||
}
|
||||
if (autoScroll()) scheduleAnchorScroll(true)
|
||||
if (autoScroll()) {
|
||||
scheduleAutoPinToBottom()
|
||||
return
|
||||
}
|
||||
if (anchorLock() && !autoScroll()) {
|
||||
scheduleAnchorCorrection()
|
||||
}
|
||||
})
|
||||
|
||||
// Drop anchor lock if the anchored key is removed.
|
||||
@@ -820,6 +858,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
scrollCompensationGen += 1
|
||||
pendingScrollCompensationScheduled = false
|
||||
pendingScrollCompensations = new Map()
|
||||
clearPendingAutoPinFrame()
|
||||
clearScrollToBottomFrames()
|
||||
if (detachScrollIntentListeners) {
|
||||
detachScrollIntentListeners()
|
||||
@@ -883,6 +922,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
const anchorId = () => getAnchorId(key())
|
||||
const overscanPx = props.overscanPx ?? 800
|
||||
const suspendMeasurements = () => measurementsSuspended() || !isActive()
|
||||
const itemVirtualizationEnabled = () => virtualizationEnabled() && !autoScroll()
|
||||
return (
|
||||
<VirtualItem
|
||||
id={anchorId()}
|
||||
@@ -890,9 +930,9 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
scrollContainer={scrollElement}
|
||||
threshold={overscanPx}
|
||||
placeholderClass="message-stream-placeholder"
|
||||
virtualizationEnabled={virtualizationEnabled}
|
||||
virtualizationEnabled={itemVirtualizationEnabled}
|
||||
suspendMeasurements={suspendMeasurements}
|
||||
onHeightChange={(nextHeight, previousHeight) => {
|
||||
onHeightChange={(nextHeight, previousHeight, meta: VirtualItemHeightChangeMeta) => {
|
||||
const delta = nextHeight - previousHeight
|
||||
|
||||
// Follow mode: keep the viewport pinned to the bottom as
|
||||
@@ -913,12 +953,11 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
||||
// while scrolling upward, compensate scrollTop so visible
|
||||
// content stays stable.
|
||||
if (delta) {
|
||||
if (meta.isStaleCacheCorrection) return
|
||||
scheduleScrollCompensation(key(), delta)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{() => props.renderItem(item(), index)}
|
||||
</VirtualItem>
|
||||
>{() => props.renderItem(item(), index)}</VirtualItem>
|
||||
)
|
||||
}}
|
||||
</Index>
|
||||
|
||||
@@ -167,10 +167,17 @@ interface VirtualItemProps {
|
||||
forceVisible?: Accessor<boolean>
|
||||
suspendMeasurements?: Accessor<boolean>
|
||||
onMeasured?: () => void
|
||||
onHeightChange?: (nextHeight: number, previousHeight: number) => void
|
||||
onHeightChange?: (nextHeight: number, previousHeight: number, meta: VirtualItemHeightChangeMeta) => void
|
||||
id?: string
|
||||
}
|
||||
|
||||
export interface VirtualItemHeightChangeMeta {
|
||||
source: "initial-visible-measure" | "resize"
|
||||
previousCachedHeight: number | null
|
||||
isStaleCacheCorrection: boolean
|
||||
wasHidden: boolean
|
||||
}
|
||||
|
||||
export default function VirtualItem(props: VirtualItemProps) {
|
||||
const resolveContent = () => (typeof props.children === "function" ? (props.children as () => JSX.Element)() : props.children)
|
||||
const cachedHeight = sizeCache.get(props.cacheKey)
|
||||
@@ -183,10 +190,11 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
// When content first mounts, onHeightChange deltas should reflect the DOM's
|
||||
// placeholder height (not 0), otherwise scroll compensation can overshoot.
|
||||
const [measuredHeight, setMeasuredHeight] = createSignal(cachedHeight ?? fallbackPlaceholderHeight())
|
||||
const [hasMeasured, setHasMeasured] = createSignal(cachedHeight !== undefined)
|
||||
let hasReportedMeasurement = Boolean(cachedHeight && cachedHeight > 0)
|
||||
let pendingVisibility: boolean | null = null
|
||||
let visibilityFrame: number | null = null
|
||||
let awaitingVisibleMeasurement = true
|
||||
let lastMeasurementWhileHidden = true
|
||||
const flushVisibility = () => {
|
||||
if (visibilityFrame !== null) {
|
||||
cancelAnimationFrame(visibilityFrame)
|
||||
@@ -210,14 +218,14 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
}
|
||||
const virtualizationEnabled = () => (props.virtualizationEnabled ? props.virtualizationEnabled() : true)
|
||||
const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())
|
||||
const forceVisible = () => Boolean(props.forceVisible?.())
|
||||
const shouldHideContent = createMemo(() => {
|
||||
if (props.forceVisible?.()) return false
|
||||
if (forceVisible()) return false
|
||||
if (!virtualizationEnabled()) return false
|
||||
return !isIntersecting()
|
||||
})
|
||||
|
||||
let wrapperRef: HTMLDivElement | undefined
|
||||
|
||||
|
||||
let wrapperRef: HTMLDivElement | undefined
|
||||
let contentRef: HTMLDivElement | undefined
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
@@ -230,6 +238,17 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleVisibleMeasurements() {
|
||||
if (shouldHideContent() || measurementsSuspended()) return
|
||||
if (!contentRef) return
|
||||
queueMicrotask(() => {
|
||||
if (shouldHideContent() || measurementsSuspended()) return
|
||||
if (!contentRef) return
|
||||
updateMeasuredHeight()
|
||||
setupResizeObserver()
|
||||
})
|
||||
}
|
||||
|
||||
function cleanupIntersectionObserver() {
|
||||
if (intersectionCleanup) {
|
||||
intersectionCleanup()
|
||||
@@ -237,13 +256,24 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function persistMeasurement(nextHeight: number) {
|
||||
function persistMeasurement(nextHeight: number, meta?: { source: "initial-visible-measure" | "resize"; wasHidden: boolean }) {
|
||||
if (!Number.isFinite(nextHeight) || nextHeight < 0) {
|
||||
return
|
||||
}
|
||||
const before = measuredHeight()
|
||||
const normalized = nextHeight
|
||||
const previous = sizeCache.get(props.cacheKey) ?? measuredHeight()
|
||||
const previousCachedHeight = sizeCache.get(props.cacheKey) ?? null
|
||||
const previous = previousCachedHeight ?? measuredHeight()
|
||||
const measurementMeta: VirtualItemHeightChangeMeta = {
|
||||
source: meta?.source ?? "resize",
|
||||
previousCachedHeight,
|
||||
isStaleCacheCorrection:
|
||||
(meta?.source ?? "resize") === "initial-visible-measure" &&
|
||||
previousCachedHeight !== null &&
|
||||
normalized > 0 &&
|
||||
Math.abs(normalized - previousCachedHeight) > 1,
|
||||
wasHidden: meta?.wasHidden ?? shouldHideContent(),
|
||||
}
|
||||
// Only keep the previous measurement when the element reports 0 height.
|
||||
// Allow shrinkage so placeholder height matches real content height;
|
||||
// keeping the max height can cause mount/unmount jitter near the
|
||||
@@ -254,34 +284,40 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
hasReportedMeasurement = true
|
||||
props.onMeasured?.()
|
||||
}
|
||||
setHasMeasured(true)
|
||||
sizeCache.set(props.cacheKey, previous)
|
||||
setMeasuredHeight(previous)
|
||||
if (previous !== before) props.onHeightChange?.(previous, before)
|
||||
if (previous !== before) props.onHeightChange?.(previous, before, measurementMeta)
|
||||
return
|
||||
}
|
||||
if (normalized > 0) {
|
||||
sizeCache.set(props.cacheKey, normalized)
|
||||
setHasMeasured(true)
|
||||
if (!hasReportedMeasurement) {
|
||||
hasReportedMeasurement = true
|
||||
props.onMeasured?.()
|
||||
}
|
||||
}
|
||||
setMeasuredHeight(normalized)
|
||||
if (normalized !== before) props.onHeightChange?.(normalized, before)
|
||||
if (normalized !== before) props.onHeightChange?.(normalized, before, measurementMeta)
|
||||
}
|
||||
|
||||
function updateMeasuredHeight() {
|
||||
if (!contentRef || measurementsSuspended()) return
|
||||
if (!contentRef) return
|
||||
if (measurementsSuspended()) return
|
||||
// Prefer subpixel-accurate height for scroll compensation.
|
||||
// offsetHeight rounds to integers which can accumulate error.
|
||||
const rect = contentRef.getBoundingClientRect()
|
||||
const next = Math.max(0, Math.round(rect.height * 2) / 2)
|
||||
if (next === measuredHeight()) return
|
||||
persistMeasurement(next)
|
||||
const currentMeasured = measuredHeight()
|
||||
const measurementSource: "initial-visible-measure" | "resize" = awaitingVisibleMeasurement ? "initial-visible-measure" : "resize"
|
||||
const wasHidden = lastMeasurementWhileHidden
|
||||
if (measurementSource === "initial-visible-measure") {
|
||||
awaitingVisibleMeasurement = false
|
||||
lastMeasurementWhileHidden = false
|
||||
}
|
||||
if (next === currentMeasured) return
|
||||
persistMeasurement(next, { source: measurementSource, wasHidden })
|
||||
}
|
||||
|
||||
|
||||
function setupResizeObserver() {
|
||||
if (!contentRef || measurementsSuspended()) return
|
||||
cleanupResizeObserver()
|
||||
@@ -377,30 +413,29 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
cleanupResizeObserver()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
if (shouldHideContent() || measurementsSuspended()) {
|
||||
const hidden = shouldHideContent()
|
||||
if (hidden) {
|
||||
awaitingVisibleMeasurement = true
|
||||
lastMeasurementWhileHidden = true
|
||||
}
|
||||
if (hidden || measurementsSuspended()) {
|
||||
cleanupResizeObserver()
|
||||
} else if (contentRef) {
|
||||
queueMicrotask(() => {
|
||||
updateMeasuredHeight()
|
||||
setupResizeObserver()
|
||||
})
|
||||
}
|
||||
if (!hidden && !measurementsSuspended() && contentRef) {
|
||||
scheduleVisibleMeasurements()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
const key = props.cacheKey
|
||||
|
||||
const cached = sizeCache.get(key)
|
||||
if (cached !== undefined) {
|
||||
setMeasuredHeight(cached)
|
||||
setHasMeasured(true)
|
||||
} else {
|
||||
setMeasuredHeight(fallbackPlaceholderHeight())
|
||||
setHasMeasured(false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -418,7 +453,7 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
}
|
||||
return props.minPlaceholderHeight ?? MIN_PLACEHOLDER_HEIGHT
|
||||
})
|
||||
|
||||
|
||||
onCleanup(() => {
|
||||
cleanupResizeObserver()
|
||||
cleanupIntersectionObserver()
|
||||
|
||||
Reference in New Issue
Block a user