From f386137fbaf2e2f56fb32f8656e802f592a41341 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 8 Mar 2026 06:34:02 -0500 Subject: [PATCH] chore: refactoring ui hooks --- bun.lock | 7 ++ .../pages/session/session-timeline-header.tsx | 4 +- packages/ui/package.json | 3 + .../src/components/context-tool-results.tsx | 4 +- packages/ui/src/components/grow-box.tsx | 34 +++--- packages/ui/src/components/message-part.tsx | 19 ++-- packages/ui/src/components/motion-spring.tsx | 4 +- .../ui/src/components/rolling-results.tsx | 7 +- .../src/components/shell-rolling-results.tsx | 4 +- packages/ui/src/components/text-reveal.tsx | 5 +- .../ui/src/components/tool-status-title.tsx | 5 +- packages/ui/src/components/tool-utils.ts | 101 ++++++++++-------- packages/ui/src/hooks/index.ts | 2 - packages/ui/src/hooks/use-element-height.ts | 25 ----- packages/ui/src/hooks/use-page-visible.ts | 11 -- packages/ui/src/hooks/use-reduced-motion.ts | 17 +-- 16 files changed, 123 insertions(+), 129 deletions(-) delete mode 100644 packages/ui/src/hooks/use-element-height.ts delete mode 100644 packages/ui/src/hooks/use-page-visible.ts diff --git a/bun.lock b/bun.lock index f19cacbe3..360ef8fac 100644 --- a/bun.lock +++ b/bun.lock @@ -483,8 +483,11 @@ "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/lifecycle": "0.1.2", "@solid-primitives/media": "2.3.3", + "@solid-primitives/page-visibility": "2.1.1", "@solid-primitives/resize-observer": "2.1.3", + "@solid-primitives/rootless": "1.5.2", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "dompurify": "3.3.1", @@ -1834,10 +1837,14 @@ "@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA=="], + "@solid-primitives/lifecycle": ["@solid-primitives/lifecycle@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-+K0T10kZXqorocFj0coIqt8NYm2UqoZfpF3nm2RwrDMZMV+C+SC0Oi3N6Dnq2i7W/n1cHAnfpoV4CBLsW21lJw=="], + "@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="], "@solid-primitives/media": ["@solid-primitives/media@2.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="], + "@solid-primitives/page-visibility": ["@solid-primitives/page-visibility@2.1.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.1", "@solid-primitives/rootless": "^1.5.1", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-CV9BqMqhunf4OOyBkhJCH9f5ivg0ADavdcaBsrqoFvwIk1FoD/blPSHYM4CK8IjS/AEXNcsjlNVc34lMu+2Wdg=="], + "@solid-primitives/props": ["@solid-primitives/props@3.2.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-lZOTwFJajBrshSyg14nBMEP0h8MXzPowGO0s3OeiR3z6nXHTfj0FhzDtJMv+VYoRJKQHG2QRnJTgCzK6erARAw=="], "@solid-primitives/refs": ["@solid-primitives/refs@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg=="], diff --git a/packages/app/src/pages/session/session-timeline-header.tsx b/packages/app/src/pages/session/session-timeline-header.tsx index d10fe1a27..32412f0a7 100644 --- a/packages/app/src/pages/session/session-timeline-header.tsx +++ b/packages/app/src/pages/session/session-timeline-header.tsx @@ -2,10 +2,10 @@ import { createEffect, createMemo, on, onCleanup, Show } from "solid-js" import { createStore, produce } from "solid-js/store" import { useNavigate, useParams } from "@solidjs/router" import { Button } from "@opencode-ai/ui/button" +import { useReducedMotion } from "@opencode-ai/ui/hooks" import { IconButton } from "@opencode-ai/ui/icon-button" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" -import { prefersReducedMotion } from "@opencode-ai/ui/hooks" import { InlineInput } from "@opencode-ai/ui/inline-input" import { animate, type AnimationPlaybackControls, clearFadeStyles, FAST_SPRING } from "@opencode-ai/ui/motion" import { showToast } from "@opencode-ai/ui/toast" @@ -32,7 +32,7 @@ export function SessionTimelineHeader(props: { const sync = useSync() const dialog = useDialog() const language = useLanguage() - const reduce = prefersReducedMotion + const reduce = useReducedMotion() const [title, setTitle] = createStore({ draft: "", diff --git a/packages/ui/package.json b/packages/ui/package.json index 664fd9752..6384df19b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -48,8 +48,11 @@ "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/lifecycle": "0.1.2", "@solid-primitives/media": "2.3.3", + "@solid-primitives/page-visibility": "2.1.1", "@solid-primitives/resize-observer": "2.1.3", + "@solid-primitives/rootless": "1.5.2", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "dompurify": "3.3.1", diff --git a/packages/ui/src/components/context-tool-results.tsx b/packages/ui/src/components/context-tool-results.tsx index 25d120e05..abed1d84b 100644 --- a/packages/ui/src/components/context-tool-results.tsx +++ b/packages/ui/src/components/context-tool-results.tsx @@ -1,8 +1,8 @@ import { createMemo, createSignal, For, onMount } from "solid-js" import type { ToolPart } from "@opencode-ai/sdk/v2" import { getFilename } from "@opencode-ai/util/path" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { useI18n } from "../context/i18n" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" import { ToolCall } from "./basic-tool" import { ToolStatusTitle } from "./tool-status-title" import { AnimatedCountList } from "./tool-count-summary" @@ -149,10 +149,10 @@ export function ContextToolExpandedList(props: { parts: ToolPart[]; expanded: bo } export function ContextToolRollingResults(props: { parts: ToolPart[]; pending: boolean }) { + const reduce = useReducedMotion() const wiped = new Set() const [mounted, setMounted] = createSignal(false) onMount(() => setMounted(true)) - const reduce = prefersReducedMotion const show = () => mounted() && props.pending const opacity = useSpring(() => (show() ? 1 : 0), GROW_SPRING) const blur = useSpring(() => (show() ? 0 : 2), GROW_SPRING) diff --git a/packages/ui/src/components/grow-box.tsx b/packages/ui/src/components/grow-box.tsx index ec4921ab3..c8ea6f3b3 100644 --- a/packages/ui/src/components/grow-box.tsx +++ b/packages/ui/src/components/grow-box.tsx @@ -1,6 +1,6 @@ import { createEffect, on, type JSX, onMount, onCleanup } from "solid-js" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { animate, tunableSpringValue, type AnimationPlaybackControls, GROW_SPRING, type SpringConfig } from "./motion" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" export interface GrowBoxProps { children: JSX.Element @@ -49,7 +49,7 @@ export interface GrowBoxProps { * Used for timeline turns, assistant part groups, and user messages. */ export function GrowBox(props: GrowBoxProps) { - const reduce = prefersReducedMotion + const reduce = useReducedMotion() const spring = () => props.spring ?? GROW_SPRING const toggleSpring = () => props.toggleSpring ?? spring() let mode: "mount" | "toggle" = "mount" @@ -293,6 +293,18 @@ export function GrowBox(props: GrowBoxProps) { offChange() }) + if (watch()) { + observer = new ResizeObserver(() => { + if (!open()) return + if (resizeFrame !== undefined) return + resizeFrame = requestAnimationFrame(() => { + resizeFrame = undefined + setHeight("mount") + }) + }) + observer.observe(body) + } + if (!animated()) { setInstant(open()) return @@ -318,17 +330,6 @@ export function GrowBox(props: GrowBoxProps) { if (grow()) setHeight("mount") }) } - if (watch()) { - observer = new ResizeObserver(() => { - if (!open()) return - if (resizeFrame !== undefined) return - resizeFrame = requestAnimationFrame(() => { - resizeFrame = undefined - setHeight("mount") - }) - }) - observer.observe(body) - } }) createEffect( @@ -402,7 +403,12 @@ export function GrowBox(props: GrowBoxProps) { ref={root} data-slot={props.slot} class={props.class} - style={{ transform: "translateZ(0)", position: "relative" }} + style={{ + transform: "translateZ(0)", + position: "relative", + height: open() ? undefined : "0px", + overflow: open() ? undefined : "clip", + }} >
0 ? `${gap()}px` : undefined }}> {props.children} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index be99f36fd..1885c19f5 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1,3 +1,4 @@ +import { usePageVisibility } from "@solid-primitives/page-visibility" import { Component, createEffect, createMemo, createSignal, For, Match, on, Show, Switch, type JSX } from "solid-js" import stripAnsi from "strip-ansi" import { createStore } from "solid-js/store" @@ -254,8 +255,6 @@ function urls(text: string | undefined) { const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"]) const HIDDEN_TOOLS = new Set(["todowrite", "todoread"]) -import { pageVisible } from "../hooks/use-page-visible" - function createGroupOpenState() { const [state, setState] = createStore>({}) const read = (key?: string, collapse?: boolean) => { @@ -277,6 +276,7 @@ function createGroupOpenState() { function shouldCollapseGroup( statuses: (string | undefined)[], opts: { afterTool?: boolean; groupTail?: boolean; working?: boolean }, + pageVisible: () => boolean, ) { if (opts.afterTool) return true if (opts.groupTail === false) return true @@ -363,6 +363,7 @@ export function AssistantParts(props: { }) { const data = useData() const emptyParts: PartType[] = [] + const pageVisible = usePageVisibility() const groupState = createGroupOpenState() const grouped = createMemo(() => { const keys: string[] = [] @@ -485,11 +486,15 @@ export function AssistantParts(props: { groupTail?: boolean, group?: { part: ToolPart; message: AssistantMessage }[], ) => - shouldCollapseGroup(group?.map((item) => item.part.state.status) ?? [], { - afterTool, - groupTail, - working: props.working, - }) + shouldCollapseGroup( + group?.map((item) => item.part.state.status) ?? [], + { + afterTool, + groupTail, + working: props.working, + }, + pageVisible, + ) const value = ctx() if (value) return groupState.read(value.groupKey, collapse(value.afterTool, value.tail, value.parts)) const entry = part() diff --git a/packages/ui/src/components/motion-spring.tsx b/packages/ui/src/components/motion-spring.tsx index 5deefcfa6..c7ff1fbcd 100644 --- a/packages/ui/src/components/motion-spring.tsx +++ b/packages/ui/src/components/motion-spring.tsx @@ -1,7 +1,7 @@ import { attachSpring, motionValue } from "motion" import type { SpringOptions } from "motion" import { createEffect, createSignal, onCleanup } from "solid-js" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" +import { useReducedMotion } from "../hooks/use-reduced-motion" type Opt = Pick const eq = (a: Opt | undefined, b: Opt | undefined) => @@ -14,7 +14,7 @@ const eq = (a: Opt | undefined, b: Opt | undefined) => export function useSpring(target: () => number, options?: Opt | (() => Opt)) { const read = () => (typeof options === "function" ? options() : options) - const reduce = prefersReducedMotion + const reduce = useReducedMotion() const [value, setValue] = createSignal(target()) const source = motionValue(value()) const spring = motionValue(value()) diff --git a/packages/ui/src/components/rolling-results.tsx b/packages/ui/src/components/rolling-results.tsx index d2f30105e..77ffdb1b3 100644 --- a/packages/ui/src/components/rolling-results.tsx +++ b/packages/ui/src/components/rolling-results.tsx @@ -1,6 +1,6 @@ import { For, Show, batch, createEffect, createMemo, createSignal, on, onCleanup, onMount, type JSX } from "solid-js" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { animate, clearMaskStyles, GROW_SPRING, type AnimationPlaybackControls, type SpringConfig } from "./motion" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" export type RollingResultsProps = { items: T[] @@ -27,8 +27,7 @@ export function RollingResults(props: RollingResultsProps) { let shift: AnimationPlaybackControls | undefined let resize: AnimationPlaybackControls | undefined let edgeFade: AnimationPlaybackControls | undefined - - const reducedMotion = prefersReducedMotion + const reduce = useReducedMotion() const rows = createMemo(() => Math.max(1, Math.round(props.rows ?? 3))) const rowHeight = createMemo(() => Math.max(16, Math.round(props.rowHeight ?? 22))) @@ -54,7 +53,7 @@ export function RollingResults(props: RollingResultsProps) { return count() - rendered().length }) const open = createMemo(() => props.open !== false) - const active = createMemo(() => (props.animate !== false || props.spring !== undefined) && !reducedMotion()) + const active = createMemo(() => (props.animate !== false || props.spring !== undefined) && !reduce()) const noFade = () => props.noFadeOnCollapse === true const overflowing = createMemo(() => count() > rows()) const shown = createMemo(() => Math.min(rows(), count())) diff --git a/packages/ui/src/components/shell-rolling-results.tsx b/packages/ui/src/components/shell-rolling-results.tsx index 6a3b7b02c..4deef089e 100644 --- a/packages/ui/src/components/shell-rolling-results.tsx +++ b/packages/ui/src/components/shell-rolling-results.tsx @@ -1,7 +1,7 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" import stripAnsi from "strip-ansi" import type { ToolPart } from "@opencode-ai/sdk/v2" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { useI18n } from "../context/i18n" import { RollingResults } from "./rolling-results" import { Icon } from "./icon" @@ -178,6 +178,7 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) { export function ShellRollingResults(props: { part: ToolPart; animate?: boolean }) { const i18n = useI18n() + const reduce = useReducedMotion() const wiped = new Set() const [mounted, setMounted] = createSignal(false) const [userToggled, setUserToggled] = createSignal(false) @@ -208,7 +209,6 @@ export function ShellRollingResults(props: { part: ToolPart; animate?: boolean } if (typeof value === "string") return value return "" }) - const reduce = prefersReducedMotion const skip = () => reduce() || props.animate === false const opacity = useSpring(() => (mounted() ? 1 : 0), GROW_SPRING) const blur = useSpring(() => (mounted() ? 0 : 2), GROW_SPRING) diff --git a/packages/ui/src/components/text-reveal.tsx b/packages/ui/src/components/text-reveal.tsx index 7ddf4a50b..edf5dbf83 100644 --- a/packages/ui/src/components/text-reveal.tsx +++ b/packages/ui/src/components/text-reveal.tsx @@ -1,4 +1,5 @@ import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { animate, type AnimationPlaybackControls, @@ -7,7 +8,6 @@ import { GROW_SPRING, WIPE_MASK, } from "./motion" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" const px = (value: number | string | undefined, fallback: number) => { if (typeof value === "number") return `${value}px` @@ -143,12 +143,13 @@ export function TextWipe(props: { text?: string; class?: string; delay?: number; let ref: HTMLSpanElement | undefined let frame: number | undefined let anim: AnimationPlaybackControls | undefined + const reduce = useReducedMotion() const run = () => { if (props.animate === false) return const el = ref if (!el || !props.text || typeof window === "undefined") return - if (prefersReducedMotion()) return + if (reduce()) return const mask = typeof CSS !== "undefined" && diff --git a/packages/ui/src/components/tool-status-title.tsx b/packages/ui/src/components/tool-status-title.tsx index 0669f8cf2..444955af9 100644 --- a/packages/ui/src/components/tool-status-title.tsx +++ b/packages/ui/src/components/tool-status-title.tsx @@ -1,8 +1,8 @@ import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { animate, type AnimationPlaybackControls, GROW_SPRING } from "./motion" import { TextShimmer } from "./text-shimmer" import { commonPrefix } from "./text-utils" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" function contentWidth(el: HTMLSpanElement | undefined) { if (!el) return 0 @@ -18,6 +18,7 @@ export function ToolStatusTitle(props: { class?: string split?: boolean }) { + const reduce = useReducedMotion() const split = createMemo(() => commonPrefix(props.activeText, props.doneText)) const suffix = createMemo( () => @@ -38,8 +39,6 @@ export function ToolStatusTitle(props: { const node = () => (suffix() ? tailRef : swapRef) - const reduce = prefersReducedMotion - const setNodeWidth = (width: string) => { if (swapRef) swapRef.style.width = width if (tailRef) tailRef.style.width = width diff --git a/packages/ui/src/components/tool-utils.ts b/packages/ui/src/components/tool-utils.ts index 171649e3d..4d57c626e 100644 --- a/packages/ui/src/components/tool-utils.ts +++ b/packages/ui/src/components/tool-utils.ts @@ -1,4 +1,6 @@ +import type { ToolPart } from "@opencode-ai/sdk/v2" import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js" +import { useReducedMotion } from "../hooks/use-reduced-motion" import { animate, type AnimationPlaybackControls, @@ -8,8 +10,6 @@ import { GROW_SPRING, WIPE_MASK, } from "./motion" -import { prefersReducedMotion } from "../hooks/use-reduced-motion" -import type { ToolPart } from "@opencode-ai/sdk/v2" export const TEXT_RENDER_THROTTLE_MS = 100 @@ -106,57 +106,67 @@ export function useCollapsible(options: { measure?: () => number onOpen?: () => void }) { + const reduce = useReducedMotion() let heightAnim: AnimationPlaybackControls | undefined let fadeAnim: AnimationPlaybackControls | undefined let gen = 0 createEffect( - on( - options.open, - (isOpen) => { - const content = options.content() - const body = options.body() - if (!content || !body) return - heightAnim?.stop() - fadeAnim?.stop() - const id = ++gen + on(options.open, (isOpen) => { + const content = options.content() + const body = options.body() + if (!content || !body) return + heightAnim?.stop() + fadeAnim?.stop() + if (reduce()) { + body.style.opacity = "" + body.style.filter = "" if (isOpen) { content.style.display = "" - content.style.height = "0px" - body.style.opacity = "0" - body.style.filter = "blur(2px)" - fadeAnim = animate(body, { opacity: [0, 1], filter: ["blur(2px)", "blur(0px)"] }, COLLAPSIBLE_SPRING) - queueMicrotask(() => { - if (gen !== id) return - const c = options.content() - if (!c) return - const h = options.measure?.() ?? Math.ceil(body.getBoundingClientRect().height) - heightAnim = animate(c, { height: ["0px", `${h}px`] }, COLLAPSIBLE_SPRING) - heightAnim.finished.then( - () => { - if (gen !== id) return - c.style.height = "auto" - options.onOpen?.() - }, - () => {}, - ) - }) + content.style.height = "auto" + options.onOpen?.() return } + content.style.height = "0px" + content.style.display = "none" + return + } + const id = ++gen + if (isOpen) { + content.style.display = "" + content.style.height = "0px" + body.style.opacity = "0" + body.style.filter = "blur(2px)" + fadeAnim = animate(body, { opacity: [0, 1], filter: ["blur(2px)", "blur(0px)"] }, COLLAPSIBLE_SPRING) + queueMicrotask(() => { + if (gen !== id) return + const c = options.content() + if (!c) return + const h = options.measure?.() ?? Math.ceil(body.getBoundingClientRect().height) + heightAnim = animate(c, { height: ["0px", `${h}px`] }, COLLAPSIBLE_SPRING) + heightAnim.finished.then( + () => { + if (gen !== id) return + c.style.height = "auto" + options.onOpen?.() + }, + () => {}, + ) + }) + return + } - const h = content.getBoundingClientRect().height - heightAnim = animate(content, { height: [`${h}px`, "0px"] }, COLLAPSIBLE_SPRING) - fadeAnim = animate(body, { opacity: [1, 0], filter: ["blur(0px)", "blur(2px)"] }, COLLAPSIBLE_SPRING) - heightAnim.finished.then( - () => { - if (gen !== id) return - content.style.display = "none" - }, - () => {}, - ) - }, - { defer: true }, - ), + const h = content.getBoundingClientRect().height + heightAnim = animate(content, { height: [`${h}px`, "0px"] }, COLLAPSIBLE_SPRING) + fadeAnim = animate(body, { opacity: [1, 0], filter: ["blur(0px)", "blur(2px)"] }, COLLAPSIBLE_SPRING) + heightAnim.finished.then( + () => { + if (gen !== id) return + content.style.display = "none" + }, + () => {}, + ) + }), ) onCleanup(() => { @@ -181,7 +191,7 @@ export function useRowWipe(opts: { ref: () => HTMLElement | undefined seen: Set }) { - const reduce = prefersReducedMotion + const reduce = useReducedMotion() createEffect(() => { const id = opts.id() @@ -265,13 +275,14 @@ export function useToolFade( const delay = options?.delay ?? 0 const wipe = options?.wipe ?? false const active = options?.animate !== false + const reduce = useReducedMotion() onMount(() => { if (!active) return const el = ref() if (!el || typeof window === "undefined") return - if (prefersReducedMotion()) return + if (reduce()) return const mask = wipe && diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts index 4a218024d..0fcf6f086 100644 --- a/packages/ui/src/hooks/index.ts +++ b/packages/ui/src/hooks/index.ts @@ -1,5 +1,3 @@ export * from "./use-filtered-list" export * from "./create-auto-scroll" -export * from "./use-element-height" export * from "./use-reduced-motion" -export * from "./use-page-visible" diff --git a/packages/ui/src/hooks/use-element-height.ts b/packages/ui/src/hooks/use-element-height.ts deleted file mode 100644 index a9f06ec8b..000000000 --- a/packages/ui/src/hooks/use-element-height.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js" - -/** - * Tracks an element's height via ResizeObserver. - * Returns a reactive signal that updates whenever the element resizes. - */ -export function useElementHeight( - ref: Accessor | (() => HTMLElement | undefined), - initial = 0, -): Accessor { - const [height, setHeight] = createSignal(initial) - - createEffect(() => { - const el = ref() - if (!el) return - setHeight(el.getBoundingClientRect().height) - const observer = new ResizeObserver(() => { - setHeight(el.getBoundingClientRect().height) - }) - observer.observe(el) - onCleanup(() => observer.disconnect()) - }) - - return height -} diff --git a/packages/ui/src/hooks/use-page-visible.ts b/packages/ui/src/hooks/use-page-visible.ts deleted file mode 100644 index 88788ef4a..000000000 --- a/packages/ui/src/hooks/use-page-visible.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSignal } from "solid-js" - -export const pageVisible = /* @__PURE__ */ (() => { - const [visible, setVisible] = createSignal(true) - if (typeof document !== "undefined") { - const sync = () => setVisible(document.visibilityState !== "hidden") - sync() - document.addEventListener("visibilitychange", sync) - } - return visible -})() diff --git a/packages/ui/src/hooks/use-reduced-motion.ts b/packages/ui/src/hooks/use-reduced-motion.ts index 7fa815bbd..0038760ec 100644 --- a/packages/ui/src/hooks/use-reduced-motion.ts +++ b/packages/ui/src/hooks/use-reduced-motion.ts @@ -1,9 +1,10 @@ -import { createSignal } from "solid-js" +import { isHydrated } from "@solid-primitives/lifecycle" +import { createMediaQuery } from "@solid-primitives/media" +import { createHydratableSingletonRoot } from "@solid-primitives/rootless" -export const prefersReducedMotion = /* @__PURE__ */ (() => { - if (typeof window === "undefined") return () => false - const mql = window.matchMedia("(prefers-reduced-motion: reduce)") - const [reduced, setReduced] = createSignal(mql.matches) - mql.addEventListener("change", () => setReduced(mql.matches)) - return reduced -})() +const query = "(prefers-reduced-motion: reduce)" + +export const useReducedMotion = createHydratableSingletonRoot(() => { + const value = createMediaQuery(query) + return () => !isHydrated() || value() +})