mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
feat(app): add reduce motion appearance setting (#57103)
This commit is contained in:
committed by
GitHub
Unverified
parent
b673e33e32
commit
22ad26f24c
@@ -178,7 +178,7 @@
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-slot="titlebar-update-loader"] {
|
||||
animation: none;
|
||||
}
|
||||
@@ -262,7 +262,7 @@
|
||||
--session-dropzone-card: var(--v2-background-bg-layer-01);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="session-dropzone"][data-visible="true"],
|
||||
[data-component="session-dropzone"][data-visible="true"] [data-slot="session-dropzone-content"],
|
||||
[data-component="session-dropzone"][data-visible="true"] [data-slot="session-dropzone-upload"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { createQuads, type Quads, type Rgba } from "./pin-quads"
|
||||
import { PinSound } from "./pin-sound"
|
||||
import { prefersReducedMotion } from "@opencode/ui/motion"
|
||||
|
||||
// The wordmark as a 78×14 pin grid: 1 = counter shadow, 2 = "open", 3 = "code".
|
||||
const GRID = [
|
||||
@@ -98,7 +99,7 @@ function mountPinBoard(back: HTMLCanvasElement, front: HTMLCanvasElement, logo:
|
||||
const stage = back.parentElement!
|
||||
// The pins behind the composer and the ones popped in front of it are each one layer of quads.
|
||||
const layers = { back: createQuads(back, () => wake()), front: createQuads(front, () => wake()) }
|
||||
const still = matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
const still = prefersReducedMotion()
|
||||
const pins: Pin[] = GRID.flatMap((row, r) =>
|
||||
[...row].flatMap((ch, c) => (ch === "." ? [] : [createPin(c, r, Number(ch))])),
|
||||
)
|
||||
|
||||
@@ -753,6 +753,11 @@ export const dict = {
|
||||
"settings.general.row.interfaceMode.inbox.description":
|
||||
"Every session across your servers, sorted by what needs you next.",
|
||||
"settings.general.row.interfaceMode.experimental": "Experimental",
|
||||
"settings.appearance.row.reduceMotion.title": "Reduce motion",
|
||||
"settings.appearance.row.reduceMotion.description": "Reduce animations or match your system",
|
||||
"settings.appearance.row.reduceMotion.system": "System",
|
||||
"settings.appearance.row.reduceMotion.on": "On",
|
||||
"settings.appearance.row.reduceMotion.off": "Off",
|
||||
"settings.notifications.description": "Choose when to receive notifications and hear sounds",
|
||||
"settings.shortcuts.description": "Customize shortcuts for common actions",
|
||||
"settings.servers.description": "Manage server connections",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
margin: 4px -4px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] & {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { createMediaQuery } from "@solid-primitives/media"
|
||||
import type { createTimelineProjection } from "./projection"
|
||||
import { observeElementOffsetReconnectAware } from "./observe-element-offset"
|
||||
import { filterVirtualIndexes } from "./virtual-items"
|
||||
import { prefersReducedMotion } from "@opencode/ui/motion"
|
||||
|
||||
const fallbackItemSize = 60
|
||||
|
||||
@@ -869,7 +870,7 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
"box-shadow": "var(--v2-elevation-raised), 0px 2px 8px var(--v2-background-bg-base)",
|
||||
}}
|
||||
onClick={() =>
|
||||
input.onResumeScroll(matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth")
|
||||
input.onResumeScroll(prefersReducedMotion() ? "auto" : "smooth")
|
||||
}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createEffect, For, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { prefersReducedMotion } from "@opencode/ui/motion"
|
||||
|
||||
const target = ["o", "p", "e", "n", "c", "o", "d", "e"] as const
|
||||
|
||||
@@ -16,7 +17,7 @@ export function AnimatedWordmark(props: { active: boolean }) {
|
||||
timers.forEach(clearTimeout)
|
||||
timers.clear()
|
||||
|
||||
if (!active || matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
if (!active || prefersReducedMotion()) {
|
||||
setState("letters", [...target])
|
||||
|
||||
return
|
||||
|
||||
@@ -10,6 +10,12 @@ import "@/settings/settings.css"
|
||||
|
||||
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
|
||||
|
||||
const reduceMotionOptions = [
|
||||
{ value: "system" as const, label: "settings.appearance.row.reduceMotion.system" as const },
|
||||
{ value: "on" as const, label: "settings.appearance.row.reduceMotion.on" as const },
|
||||
{ value: "off" as const, label: "settings.appearance.row.reduceMotion.off" as const },
|
||||
]
|
||||
|
||||
const fontSettings = {
|
||||
ui: {
|
||||
action: "settings-ui-font",
|
||||
@@ -128,6 +134,22 @@ export const SettingsAppearance: Component = () => {
|
||||
<FontSetting kind="ui" fonts={appearance.fonts} />
|
||||
<FontSetting kind="code" fonts={appearance.fonts} />
|
||||
<FontSetting kind="terminal" fonts={appearance.fonts} />
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.appearance.row.reduceMotion.title")}
|
||||
description={language.t("settings.appearance.row.reduceMotion.description")}
|
||||
>
|
||||
<Select
|
||||
data-action="settings-reduce-motion"
|
||||
options={reduceMotionOptions}
|
||||
current={reduceMotionOptions.find((option) => option.value === appearance.reduceMotion.current())}
|
||||
placement="bottom-end"
|
||||
gutter={6}
|
||||
value={(option) => option.value}
|
||||
label={(option) => language.t(option.label)}
|
||||
onSelect={(option) => option && appearance.reduceMotion.select(option.value)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
sansDefault,
|
||||
sansFontFamily,
|
||||
sansInput,
|
||||
type ReduceMotion,
|
||||
terminalDefault,
|
||||
terminalFontFamily,
|
||||
terminalInput,
|
||||
@@ -90,6 +91,10 @@ export function createAppearanceSettingsController() {
|
||||
current: createMemo(() => themes().find((option) => option.id === theme.themeId())),
|
||||
select: (option: { id: string } | null) => option && theme.setTheme(option.id),
|
||||
},
|
||||
reduceMotion: {
|
||||
current: settings.appearance.reduceMotion,
|
||||
select: (value: ReduceMotion) => settings.appearance.setReduceMotion(value),
|
||||
},
|
||||
fonts: {
|
||||
ui: createMemo(() => ({
|
||||
value: sansInput(settings.appearance.uiFont()),
|
||||
|
||||
@@ -104,6 +104,7 @@ describe("settings schema", () => {
|
||||
sans: "",
|
||||
terminal: "",
|
||||
tabLayout: "horizontal",
|
||||
reduceMotion: "system",
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: { autoApprove: false, autoApproveProjects: {} },
|
||||
@@ -150,6 +151,7 @@ describe("settings schema", () => {
|
||||
sans: "",
|
||||
terminal: "",
|
||||
tabLayout: "vertical",
|
||||
reduceMotion: "system",
|
||||
})
|
||||
expect(settings.permissions).toEqual({ autoApprove: true, autoApproveProjects: { on: true, off: false } })
|
||||
expect(settings.workspaces).toEqual({ defaultDestination: "new", lastUsed: { good: "workspace" } })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { produce, reconcile, unwrap } from "solid-js/store"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Effect, Option, Predicate, Schema, SchemaGetter } from "effect"
|
||||
import { createSimpleContext } from "@opencode/ui/context"
|
||||
import { timelinePresets, type TimelineCategory, type TimelineDetail } from "@opencode/session-ui/timeline/detail"
|
||||
@@ -17,6 +18,8 @@ export type FollowUpBehavior = Settings["general"]["followUpBehavior"]
|
||||
|
||||
export type TabLayout = Settings["appearance"]["tabLayout"]
|
||||
|
||||
export type ReduceMotion = Settings["appearance"]["reduceMotion"]
|
||||
|
||||
export type NotificationSettings = Settings["notifications"]
|
||||
|
||||
export type SoundSettings = Settings["sounds"]
|
||||
@@ -132,6 +135,7 @@ const appearanceSchema = Persistence.struct({
|
||||
// "inbox" is experimental: the vertical layout with a server-wide session inbox in place of tabs.
|
||||
tabLayout: Schema.Literals(["horizontal", "vertical", "inbox"]),
|
||||
sidebarOpen: Persistence.optional(Schema.Boolean),
|
||||
reduceMotion: Schema.Literals(["system", "on", "off"]),
|
||||
})
|
||||
|
||||
const permissionsSchema = Persistence.struct({
|
||||
@@ -287,7 +291,7 @@ export const defaultSettings: Settings = {
|
||||
mobileTitlebarPosition: "top",
|
||||
followUpBehavior: "steer",
|
||||
},
|
||||
appearance: { fontSize: 14, mono: "", sans: "", terminal: "", tabLayout: "horizontal" },
|
||||
appearance: { fontSize: 14, mono: "", sans: "", terminal: "", tabLayout: "horizontal", reduceMotion: "system" },
|
||||
keybinds: {},
|
||||
permissions: { autoApprove: false, autoApproveProjects: {} },
|
||||
workspaces: { defaultDestination: "last-used", lastUsed: {} },
|
||||
@@ -330,6 +334,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
void document.fonts?.load(`440 13px ${mono}`).catch(() => undefined)
|
||||
})
|
||||
|
||||
const reduceMotion = withFallback(() => store.appearance?.reduceMotion, defaultSettings.appearance.reduceMotion)
|
||||
const systemReducedMotion = createMediaQuery("(prefers-reduced-motion: reduce)")
|
||||
|
||||
// Motion styles key off this attribute instead of the media query so the setting can override the system.
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
const reduce = reduceMotion() === "system" ? systemReducedMotion() : reduceMotion() === "on"
|
||||
document.documentElement.dataset.motion = reduce ? "reduce" : "full"
|
||||
})
|
||||
|
||||
const autoApproveDefault = withFallback(
|
||||
() => store.permissions?.autoApprove,
|
||||
defaultSettings.permissions.autoApprove,
|
||||
@@ -406,6 +420,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setSidebarOpen(value: boolean) {
|
||||
setStore("appearance", "sidebarOpen", value)
|
||||
},
|
||||
reduceMotion,
|
||||
setReduceMotion(value: ReduceMotion) {
|
||||
setStore("appearance", "reduceMotion", value)
|
||||
},
|
||||
},
|
||||
keybinds: {
|
||||
get: (action: string) => store.keybinds?.[action],
|
||||
|
||||
@@ -106,6 +106,12 @@ export const clientSettings: Entry<SettingsRootTab>[] = [
|
||||
target: "settings-terminal-font",
|
||||
keywords: "terminal typeface",
|
||||
},
|
||||
{
|
||||
tab: "appearance",
|
||||
label: "settings.appearance.row.reduceMotion.title",
|
||||
target: "settings-reduce-motion",
|
||||
keywords: "animation accessibility",
|
||||
},
|
||||
{
|
||||
tab: "notifications",
|
||||
label: "settings.general.notifications.agent.title",
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-search-target]::before {
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
|
||||
@@ -943,7 +943,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
.settings-provider-console-chevron {
|
||||
transition: none;
|
||||
}
|
||||
@@ -1459,7 +1459,7 @@
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
.settings-models-group-chevron svg,
|
||||
.provider-model-group-chevron {
|
||||
transition: none;
|
||||
@@ -1709,7 +1709,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
.settings-workspaces-row-motion {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
.project-settings-icon-overlay,
|
||||
.project-settings-color,
|
||||
.project-settings-shared-chevron {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-slot="mobile-drawer-content"][data-transitioning],
|
||||
[data-slot="mobile-drawer-content"][data-closing] {
|
||||
transition: none;
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-titlebar-tab] [data-slot="tab-prompt-pulse"] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="session-tab-popover"],
|
||||
[data-component="session-tab-popover"][data-closed] {
|
||||
animation: none;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="browser-comment"],
|
||||
[data-component="browser-comment"] [data-slot="browser-comment-editor"] {
|
||||
animation: none;
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
.session-summary-popover,
|
||||
.session-service-menu {
|
||||
animation: none;
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] & {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="markdown"] :is([data-markdown-word], code)[data-markdown-enter] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
background-color 150ms ease-out,
|
||||
color 150ms ease-out;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] & {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -1411,7 +1411,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] & {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] & {
|
||||
[data-slot="question-option"],
|
||||
.question-custom-field[data-component="text-input-v2"] {
|
||||
transition: none;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="tool-count-label"] [data-slot="tool-count-label-suffix"] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="tool-count-summary"] [data-slot="tool-count-summary-empty"],
|
||||
[data-component="tool-count-summary"] [data-slot="tool-count-summary-item"],
|
||||
[data-component="tool-count-summary"] [data-slot="tool-count-summary-prefix"] {
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="tool-status-title"] [data-slot="tool-status-swap"],
|
||||
[data-component="tool-status-title"] [data-slot="tool-status-tail"] {
|
||||
transition-duration: 0ms;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="session-progress-indicator-v2"] [data-frame-content] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,20 @@ const Direction = (props: { value?: unknown }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
// The app derives this attribute from its reduce motion setting; stories follow the system preference.
|
||||
const Motion = () => {
|
||||
onMount(() => {
|
||||
const query = matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const apply = () => {
|
||||
document.documentElement.dataset.motion = query.matches ? "reduce" : "full"
|
||||
}
|
||||
apply()
|
||||
query.addEventListener("change", apply)
|
||||
onCleanup(() => query.removeEventListener("change", apply))
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const BodyTypography = () => {
|
||||
onMount(() => {
|
||||
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
|
||||
@@ -74,6 +88,7 @@ const frame = createJSXDecorator((Story, context) => {
|
||||
<UiI18nBridge>
|
||||
<Scheme value={scheme} />
|
||||
<Direction value={context.globals?.direction} />
|
||||
<Motion />
|
||||
<BodyTypography />
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="animated-number"] [data-slot="animated-number-value"][data-resizing] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="text-reveal"] [data-slot="text-reveal-track"] {
|
||||
transition-duration: 0ms !important;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-slot="text-strikethrough-line"] {
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="loader-v2"] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
.toast-v2,
|
||||
.toast-v2 > * {
|
||||
animation: none !important;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Portal } from "solid-js/web"
|
||||
import { useI18n } from "../../context/i18n"
|
||||
import "../../actions/button/button.css"
|
||||
import "./toast.css"
|
||||
import { prefersReducedMotion } from "../../motion"
|
||||
|
||||
export interface ToastRegionProps extends ToasterProps {}
|
||||
|
||||
@@ -307,7 +308,7 @@ function pulseToast(toastId: number) {
|
||||
requestAnimationFrame(() => {
|
||||
const element = document.querySelector<HTMLElement>(`[data-testid="toast-v2-${toastId}"]`)
|
||||
|
||||
if (!element || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
||||
if (!element || prefersReducedMotion()) return
|
||||
element.animate([{ scale: 1 }, { scale: 1.025 }, { scale: 1 }], { duration: 160, easing: "ease-out" })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const frameLoop: {
|
||||
}
|
||||
function createFrameLoop(scheduler?: { request(callback: (time: number) => void): number; cancel(id: number): void })
|
||||
|
||||
const prefersReducedMotion: () => boolean // live matchMedia
|
||||
const prefersReducedMotion: () => boolean // root data-motion, else live matchMedia
|
||||
function observeReducedMotion(listener: () => void): () => void
|
||||
|
||||
class VelocityTracker {
|
||||
|
||||
@@ -26,7 +26,20 @@ function media(): MediaQueryList | undefined {
|
||||
return result
|
||||
}
|
||||
|
||||
export const prefersReducedMotion = (): boolean => media()?.matches ?? false
|
||||
// The app resolves its reduce motion setting into this attribute; without it the system preference applies.
|
||||
function override(): boolean | undefined {
|
||||
const value = globalThis.document?.documentElement.getAttribute("data-motion")
|
||||
|
||||
if (value === "reduce") return true
|
||||
|
||||
if (value === "full") return false
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
let attribute: MutationObserver | undefined
|
||||
|
||||
export const prefersReducedMotion = (): boolean => override() ?? media()?.matches ?? false
|
||||
|
||||
/** Also settle active WAAPI jobs when the preference changes mid-animation. SSR-safe. */
|
||||
export function observeReducedMotion(listener: () => void): () => void {
|
||||
@@ -34,11 +47,20 @@ export function observeReducedMotion(listener: () => void): () => void {
|
||||
|
||||
if (!preference) return () => {}
|
||||
|
||||
if (listeners.size === 0) preference.addEventListener("change", notify)
|
||||
if (listeners.size === 0) {
|
||||
preference.addEventListener("change", notify)
|
||||
|
||||
if (globalThis.document && globalThis.MutationObserver) {
|
||||
attribute = new MutationObserver(notify)
|
||||
attribute.observe(document.documentElement, { attributes: true, attributeFilter: ["data-motion"] })
|
||||
}
|
||||
}
|
||||
listeners.add(listener)
|
||||
|
||||
return () => {
|
||||
if (!listeners.delete(listener) || listeners.size > 0) return
|
||||
result?.removeEventListener("change", notify)
|
||||
attribute?.disconnect()
|
||||
attribute = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import "./segmented-control.css"
|
||||
import { prefersReducedMotion } from "../../motion"
|
||||
|
||||
type OnChange = (value: string | null) => void
|
||||
|
||||
@@ -108,8 +109,6 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
||||
|
||||
const selected = createMemo(() => (isControlled() ? (local.value ?? null) : internal()))
|
||||
|
||||
const reducedMotion = () => globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true
|
||||
|
||||
const x = useMotionValue(0)
|
||||
const width = useMotionValue(0)
|
||||
|
||||
@@ -135,7 +134,7 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
||||
|
||||
target = next
|
||||
|
||||
if (!ready() || !shouldAnimate || reducedMotion()) {
|
||||
if (!ready() || !shouldAnimate || prefersReducedMotion()) {
|
||||
x.jump(next.x)
|
||||
width.jump(next.width)
|
||||
setReady(true)
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="tooltip-v2"],
|
||||
[data-component="tooltip-v2"][data-closed] {
|
||||
animation: none;
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
@source not "../../../src/**/*.test.{ts,tsx}";
|
||||
@import "./utilities.css";
|
||||
|
||||
/* The app resolves its reduce motion setting, including the system preference, into this attribute. */
|
||||
@custom-variant motion-reduce (&:where(:root[data-motion="reduce"] *));
|
||||
@custom-variant motion-safe (&:where(:root:not([data-motion="reduce"]) *));
|
||||
|
||||
@import "../index.css";
|
||||
|
||||
@theme {
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-motion="reduce"] {
|
||||
[data-component="text-shimmer"] [data-slot="text-shimmer-char-base"],
|
||||
[data-component="text-shimmer"] [data-slot="text-shimmer-char-shimmer"] {
|
||||
transition-duration: 0ms;
|
||||
|
||||
Reference in New Issue
Block a user