M12 — the Android client, end to end #2
@@ -1,6 +1,13 @@
|
||||
# Tauri desktop (Linux) build — SEPARATE from ci.yml on purpose: this is a heavy
|
||||
# Rust + AppImage build (~20-40 min) that should NOT run on backend/frontend-only
|
||||
# pushes. Scoped to desktop/** (+ this file). Produces the .deb and .AppImage.
|
||||
# Tauri desktop (Linux) build — SEPARATE from ci.yml on purpose: a Rust + AppImage
|
||||
# build that shouldn't run on server-only pushes. Produces the .deb and .AppImage.
|
||||
#
|
||||
# It DOES run on frontend changes. tauri's generate_context! embeds the built
|
||||
# frontend in the binary, so a frontend commit that never triggers this ships to
|
||||
# the web and silently never reaches the desktop app — and desktop, web and Android
|
||||
# are peer surfaces held to one quality bar, not a primary and its fallbacks. The
|
||||
# filter was once narrowed to the adapter/bridge directories against a "~20-40 min"
|
||||
# build; measured runs are 4-5 minutes, so the cost that justified the narrowing
|
||||
# isn't there.
|
||||
#
|
||||
# Toolchain comes from the ci-tauri image (Rust + Node + WebKitGTK 4.1 + tauri-cli);
|
||||
# runs-on is just a registered scheduling label (Label Model B), not a per-purpose
|
||||
@@ -13,10 +20,11 @@ on:
|
||||
tags: ["v*"]
|
||||
paths:
|
||||
- "desktop/**"
|
||||
# The desktop app embeds the frontend, and the data seam / Tauri bridge are
|
||||
# what the offline core rides on — rebuild the app when those change too.
|
||||
- "frontend/src/adapters/**"
|
||||
- "frontend/src/desktop/**"
|
||||
# The whole frontend, not just the adapter/bridge seam: it is compiled INTO
|
||||
# the desktop binary, so any part of it changing means the shipped app is out
|
||||
# of date. Config and lockfile included — a dependency bump changes the bundle
|
||||
# as surely as a component does.
|
||||
- "frontend/**"
|
||||
- ".forgejo/workflows/desktop.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { onBeforeUnmount, readonly, ref, type Ref } from "vue";
|
||||
|
||||
const QUERY = "(prefers-reduced-motion: reduce)";
|
||||
|
||||
/**
|
||||
* Whether the user has asked their system to reduce motion.
|
||||
*
|
||||
* The CSS half of this lives in style.css and neutralises every declarative
|
||||
* transition on its own. This is for the half CSS can't reach: motion driven from
|
||||
* JavaScript — the FLIP reflow and the card↔editor morph — where the honest
|
||||
* response isn't a faster animation but no animation at all, jumping straight to
|
||||
* the final state.
|
||||
*
|
||||
* Reactive rather than a one-shot read: the setting can change while the app is
|
||||
* open (a system toggle, or a "reduce motion during battery saver" rule), and a
|
||||
* board that keeps sliding until relaunch would be ignoring an answer already given.
|
||||
*/
|
||||
export function useReducedMotion(): Readonly<Ref<boolean>> {
|
||||
const reduced = ref(prefersReducedMotion());
|
||||
|
||||
// Guard the whole listener: SSR and test environments have no matchMedia, and
|
||||
// this must degrade to "motion is fine" rather than throwing during setup.
|
||||
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
||||
const media = window.matchMedia(QUERY);
|
||||
const update = (e: MediaQueryListEvent) => {
|
||||
reduced.value = e.matches;
|
||||
};
|
||||
media.addEventListener("change", update);
|
||||
onBeforeUnmount(() => media.removeEventListener("change", update));
|
||||
}
|
||||
|
||||
return readonly(reduced);
|
||||
}
|
||||
|
||||
/**
|
||||
* The same answer without a component instance — for module-level helpers and
|
||||
* one-off checks inside an event handler, where there is no lifecycle to hang a
|
||||
* listener on.
|
||||
*/
|
||||
export function prefersReducedMotion(): boolean {
|
||||
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
||||
return window.matchMedia(QUERY).matches;
|
||||
}
|
||||
@@ -18,6 +18,27 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Motion is a feature, not a given. Anyone whose OS says "reduce motion" has told
|
||||
* us something about vestibular comfort or attention, and the answer is to arrive
|
||||
* instantly rather than to animate faster.
|
||||
*
|
||||
* Global and blunt on purpose: it catches every Tailwind `transition` utility
|
||||
* already scattered through the components, and it will catch the M7 motion work
|
||||
* without each new component having to remember. Near-zero rather than `none` so
|
||||
* `transitionend`/`animationend` listeners still fire and no state machine that
|
||||
* waits on them can hang. JS-driven motion can't be reached from CSS — that reads
|
||||
* the same preference through `useReducedMotion`. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.icon-btn {
|
||||
@apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none
|
||||
|
||||
Reference in New Issue
Block a user