add reduce motion toggle and bump to v1.0.1

Add in-app reduce motion setting under Settings > Appearance so users
can disable animations without changing their OS preference. Applies a
.reduce-motion CSS class to kill all CSS transitions/animations and
wraps the app in MotionConfig to globally disable Framer Motion springs,
layout animations, and enter/exit transitions. Setting persists to disk.

Also removes leftover default Square*.png icons and bumps version to
1.0.1.
This commit is contained in:
Your Name
2026-02-16 17:51:23 +02:00
parent d1636745f1
commit 30263d6ac7
21 changed files with 1339 additions and 9 deletions

1282
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "openpylon", "name": "openpylon",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -45,7 +45,10 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",
"png-to-ico": "^3.0.1",
"puppeteer-core": "^24.37.3",
"shadcn": "^3.8.4", "shadcn": "^3.8.4",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",

2
src-tauri/Cargo.lock generated
View File

@@ -2360,7 +2360,7 @@ dependencies = [
[[package]] [[package]]
name = "openpylon" name = "openpylon"
version = "0.1.0" version = "1.0.1"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "openpylon" name = "openpylon"
version = "1.0.0" version = "1.0.1"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "openpylon", "productName": "openpylon",
"version": "1.0.0", "version": "1.0.1",
"identifier": "com.openpylon.app", "identifier": "com.openpylon.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { getCurrentWindow, LogicalSize, LogicalPosition } from "@tauri-apps/api/window"; import { getCurrentWindow, LogicalSize, LogicalPosition } from "@tauri-apps/api/window";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { springs, fadeSlideLeft, fadeSlideRight } from "@/lib/motion"; import { springs, fadeSlideLeft, fadeSlideRight } from "@/lib/motion";
import { useAppStore } from "@/stores/app-store"; import { useAppStore } from "@/stores/app-store";
import { useBoardStore } from "@/stores/board-store"; import { useBoardStore } from "@/stores/board-store";
@@ -18,6 +18,7 @@ export default function App() {
const initialized = useAppStore((s) => s.initialized); const initialized = useAppStore((s) => s.initialized);
const init = useAppStore((s) => s.init); const init = useAppStore((s) => s.init);
const view = useAppStore((s) => s.view); const view = useAppStore((s) => s.view);
const reduceMotion = useAppStore((s) => s.settings.reduceMotion);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false); const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
@@ -127,7 +128,7 @@ export default function App() {
} }
return ( return (
<> <MotionConfig reducedMotion={reduceMotion ? "always" : "user"}>
<AppShell> <AppShell>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{view.type === "board-list" ? ( {view.type === "board-list" ? (
@@ -161,6 +162,6 @@ export default function App() {
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} /> <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
<ToastContainer /> <ToastContainer />
<ShortcutHelpModal open={shortcutHelpOpen} onOpenChange={setShortcutHelpOpen} /> <ShortcutHelpModal open={shortcutHelpOpen} onOpenChange={setShortcutHelpOpen} />
</> </MotionConfig>
); );
} }

View File

@@ -92,6 +92,7 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
const setAccentColor = useAppStore((s) => s.setAccentColor); const setAccentColor = useAppStore((s) => s.setAccentColor);
const setUiZoom = useAppStore((s) => s.setUiZoom); const setUiZoom = useAppStore((s) => s.setUiZoom);
const setDensity = useAppStore((s) => s.setDensity); const setDensity = useAppStore((s) => s.setDensity);
const setReduceMotion = useAppStore((s) => s.setReduceMotion);
const setDefaultColumnWidth = useAppStore((s) => s.setDefaultColumnWidth); const setDefaultColumnWidth = useAppStore((s) => s.setDefaultColumnWidth);
const roRef = useRef<ResizeObserver | null>(null); const roRef = useRef<ResizeObserver | null>(null);
@@ -268,6 +269,30 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
))} ))}
</div> </div>
</div> </div>
<Separator />
{/* Reduce Motion */}
<div>
<SectionLabel>Reduce Motion</SectionLabel>
<p className="mb-2 text-xs text-pylon-text-secondary">
Reduces animations and transitions for accessibility.
</p>
<div className="flex gap-2">
{([false, true] as const).map((value) => (
<Button
key={String(value)}
type="button"
variant={settings.reduceMotion === value ? "default" : "outline"}
size="sm"
onClick={() => setReduceMotion(value)}
className="flex-1"
>
{value ? "Reduced" : "Normal"}
</Button>
))}
</div>
</div>
</> </>
)} )}

View File

@@ -200,3 +200,12 @@
scroll-behavior: auto !important; scroll-behavior: auto !important;
} }
} }
.reduce-motion *,
.reduce-motion *::before,
.reduce-motion *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}

View File

@@ -87,4 +87,5 @@ export const appSettingsSchema = z.object({
boardSortOrder: z.enum(["manual", "title", "created", "updated"]).default("updated"), boardSortOrder: z.enum(["manual", "title", "created", "updated"]).default("updated"),
boardManualOrder: z.array(z.string()).default([]), boardManualOrder: z.array(z.string()).default([]),
lastNotificationCheck: z.string().nullable().default(null), lastNotificationCheck: z.string().nullable().default(null),
reduceMotion: z.boolean().default(false),
}); });

View File

@@ -21,6 +21,7 @@ interface AppState {
setView: (view: View) => void; setView: (view: View) => void;
refreshBoards: () => Promise<void>; refreshBoards: () => Promise<void>;
addRecentBoard: (boardId: string) => void; addRecentBoard: (boardId: string) => void;
setReduceMotion: (reduceMotion: boolean) => void;
setBoardSortOrder: (order: BoardSortOrder) => void; setBoardSortOrder: (order: BoardSortOrder) => void;
setBoardManualOrder: (ids: string[]) => void; setBoardManualOrder: (ids: string[]) => void;
getSortedBoards: () => BoardMeta[]; getSortedBoards: () => BoardMeta[];
@@ -36,6 +37,10 @@ function applyTheme(theme: AppSettings["theme"]): void {
} }
} }
function applyReduceMotion(on: boolean): void {
document.documentElement.classList.toggle("reduce-motion", on);
}
function applyAppearance(settings: AppSettings): void { function applyAppearance(settings: AppSettings): void {
const root = document.documentElement; const root = document.documentElement;
root.style.fontSize = `${settings.uiZoom * 16}px`; root.style.fontSize = `${settings.uiZoom * 16}px`;
@@ -70,6 +75,7 @@ export const useAppStore = create<AppState>((set, get) => ({
boardSortOrder: "updated", boardSortOrder: "updated",
boardManualOrder: [], boardManualOrder: [],
lastNotificationCheck: null, lastNotificationCheck: null,
reduceMotion: false,
}, },
boards: [], boards: [],
view: { type: "board-list" }, view: { type: "board-list" },
@@ -82,6 +88,7 @@ export const useAppStore = create<AppState>((set, get) => ({
set({ settings, boards, initialized: true }); set({ settings, boards, initialized: true });
applyTheme(settings.theme); applyTheme(settings.theme);
applyAppearance(settings); applyAppearance(settings);
applyReduceMotion(settings.reduceMotion);
// Due date notifications (once per hour) // Due date notifications (once per hour)
const lastCheck = settings.lastNotificationCheck; const lastCheck = settings.lastNotificationCheck;
@@ -148,6 +155,11 @@ export const useAppStore = create<AppState>((set, get) => ({
updateAndSave(get, set, { defaultColumnWidth }); updateAndSave(get, set, { defaultColumnWidth });
}, },
setReduceMotion: (reduceMotion) => {
updateAndSave(get, set, { reduceMotion });
applyReduceMotion(reduceMotion);
},
setView: (view) => set({ view }), setView: (view) => set({ view }),
refreshBoards: async () => { refreshBoards: async () => {

View File

@@ -22,4 +22,5 @@ export interface AppSettings {
boardSortOrder: BoardSortOrder; boardSortOrder: BoardSortOrder;
boardManualOrder: string[]; boardManualOrder: string[];
lastNotificationCheck: string | null; lastNotificationCheck: string | null;
reduceMotion: boolean;
} }