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
|
# Tauri desktop (Linux) build — SEPARATE from ci.yml on purpose: a Rust + AppImage
|
||||||
# Rust + AppImage build (~20-40 min) that should NOT run on backend/frontend-only
|
# build that shouldn't run on server-only pushes. Produces the .deb and .AppImage.
|
||||||
# pushes. Scoped to desktop/** (+ this file). 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);
|
# 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
|
# runs-on is just a registered scheduling label (Label Model B), not a per-purpose
|
||||||
@@ -13,10 +20,11 @@ on:
|
|||||||
tags: ["v*"]
|
tags: ["v*"]
|
||||||
paths:
|
paths:
|
||||||
- "desktop/**"
|
- "desktop/**"
|
||||||
# The desktop app embeds the frontend, and the data seam / Tauri bridge are
|
# The whole frontend, not just the adapter/bridge seam: it is compiled INTO
|
||||||
# what the offline core rides on — rebuild the app when those change too.
|
# the desktop binary, so any part of it changing means the shipped app is out
|
||||||
- "frontend/src/adapters/**"
|
# of date. Config and lockfile included — a dependency bump changes the bundle
|
||||||
- "frontend/src/desktop/**"
|
# as surely as a component does.
|
||||||
|
- "frontend/**"
|
||||||
- ".forgejo/workflows/desktop.yml"
|
- ".forgejo/workflows/desktop.yml"
|
||||||
workflow_dispatch:
|
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 {
|
@layer components {
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
@apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none
|
@apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none
|
||||||
|
|||||||
Reference in New Issue
Block a user