4 Commits

Author SHA1 Message Date
Your Name
8492ffcfb7 fix card click delay when reduced motion is active
Skip layoutId and use instant transitions on the card detail modal
when reduced motion is on. The shared layout animation kept the z-50
overlay in the DOM during its exit spring, blocking all card clicks
until it settled.
2026-02-16 18:51:35 +02:00
Your Name
25f71544f0 fix layout animation blocking card clicks when reduce motion is on
Skip Framer Motion layout/layoutId props on cards when reduced motion
is active. These props cause pointerEvents:none during layout
transitions, making cards unclickable for ~1s after any layout shift.
2026-02-16 18:38:04 +02:00
Your Name
30263d6ac7 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.
2026-02-16 17:51:23 +02:00
Your Name
d1636745f1 add custom app icon - teal squircle with lighthouse 2026-02-16 16:07:20 +02:00
28 changed files with 1350 additions and 15 deletions

1282
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

2
src-tauri/Cargo.lock generated
View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.3 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

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

View File

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

View File

@@ -134,14 +134,14 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick, isFocu
className={`w-full rounded-lg bg-pylon-surface shadow-sm text-left ${
isFocused ? "ring-2 ring-pylon-accent ring-offset-2 ring-offset-pylon-column" : ""
}`}
layoutId={`card-${card.id}`}
layoutId={prefersReducedMotion ? undefined : `card-${card.id}`}
variants={fadeSlideUp}
initial={prefersReducedMotion ? false : "hidden"}
animate="visible"
whileHover={{ scale: 1.02, y: -2, boxShadow: "0 4px 12px oklch(0% 0 0 / 10%)" }}
whileTap={{ scale: 0.98 }}
transition={springs.bouncy}
layout
layout={!prefersReducedMotion}
{...attributes}
{...listeners}
role="article"

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { X } from "lucide-react";
import { useBoardStore } from "@/stores/board-store";
@@ -25,6 +25,8 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) {
const updateCard = useBoardStore((s) => s.updateCard);
const open = cardId != null && card != null;
const prefersReducedMotion = useReducedMotion();
const instant = { duration: 0 };
return (
<AnimatePresence>
@@ -36,7 +38,7 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
transition={prefersReducedMotion ? instant : { duration: 0.2 }}
onClick={onClose}
/>
@@ -46,9 +48,12 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) {
onClick={onClose}
>
<motion.div
layoutId={`card-${cardId}`}
layoutId={prefersReducedMotion ? undefined : `card-${cardId}`}
className="relative w-full max-w-4xl overflow-hidden rounded-xl bg-pylon-surface shadow-2xl"
transition={springs.gentle}
initial={prefersReducedMotion ? { opacity: 0 } : undefined}
animate={prefersReducedMotion ? { opacity: 1 } : undefined}
exit={prefersReducedMotion ? { opacity: 0 } : undefined}
transition={prefersReducedMotion ? instant : springs.gentle}
onClick={(e) => e.stopPropagation()}
>
<EscapeHandler onClose={onClose} />

View File

@@ -92,6 +92,7 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
const setAccentColor = useAppStore((s) => s.setAccentColor);
const setUiZoom = useAppStore((s) => s.setUiZoom);
const setDensity = useAppStore((s) => s.setDensity);
const setReduceMotion = useAppStore((s) => s.setReduceMotion);
const setDefaultColumnWidth = useAppStore((s) => s.setDefaultColumnWidth);
const roRef = useRef<ResizeObserver | null>(null);
@@ -268,6 +269,30 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
))}
</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;
}
}
.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"),
boardManualOrder: z.array(z.string()).default([]),
lastNotificationCheck: z.string().nullable().default(null),
reduceMotion: z.boolean().default(false),
});

View File

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

View File

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