summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/pages/session/session-timeline-header.tsx4
-rw-r--r--packages/ui/package.json3
-rw-r--r--packages/ui/src/components/context-tool-results.tsx4
-rw-r--r--packages/ui/src/components/grow-box.tsx34
-rw-r--r--packages/ui/src/components/message-part.tsx19
-rw-r--r--packages/ui/src/components/motion-spring.tsx4
-rw-r--r--packages/ui/src/components/rolling-results.tsx7
-rw-r--r--packages/ui/src/components/shell-rolling-results.tsx4
-rw-r--r--packages/ui/src/components/text-reveal.tsx5
-rw-r--r--packages/ui/src/components/tool-status-title.tsx5
-rw-r--r--packages/ui/src/components/tool-utils.ts101
-rw-r--r--packages/ui/src/hooks/index.ts2
-rw-r--r--packages/ui/src/hooks/use-element-height.ts25
-rw-r--r--packages/ui/src/hooks/use-page-visible.ts11
-rw-r--r--packages/ui/src/hooks/use-reduced-motion.ts17
15 files changed, 116 insertions, 129 deletions
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<string>()
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",
+ }}
>
<div ref={body} style={{ "padding-top": gap() > 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<Record<string, boolean>>({})
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<SpringOptions, "visualDuration" | "bounce" | "stiffness" | "damping" | "mass" | "velocity">
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<T> = {
items: T[]
@@ -27,8 +27,7 @@ export function RollingResults<T>(props: RollingResultsProps<T>) {
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<T>(props: RollingResultsProps<T>) {
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<string>()
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<string>
}) {
- 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> | (() => HTMLElement | undefined),
- initial = 0,
-): Accessor<number> {
- 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()
+})