clean up unused files and update gitignore
This commit is contained in:
24
.gitignore
vendored
24
.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,73 +0,0 @@
|
||||
# Card Detail Modal Redesign — Design Document
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
The current card detail modal uses a 60/40 split layout with a massive markdown editor on the left and all metadata (cover, labels, due date, checklist, attachments) crammed into a narrow right sidebar. This is unbalanced — the description field dominates despite being lightly used, while actionable sections like checklist are squeezed.
|
||||
|
||||
## Design Decisions (from brainstorming)
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| Primary use when opening card | Scan everything at once |
|
||||
| Description importance | Light use (short notes) |
|
||||
| Modal size | Go wider |
|
||||
| Layout style | Dashboard grid |
|
||||
| Section prominence | Equal weight |
|
||||
| Long checklist behavior | Scroll within cell |
|
||||
| Title position | Full-width header with cover color |
|
||||
| Attachment display | Compact list |
|
||||
| Description visibility | Always visible cell in grid |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ██████████████████████████████████████████████████ [×] │ Cover color header
|
||||
│ Card Title (click to edit) │ Inline-editable
|
||||
├────────────────────────────┬─────────────────────────────┤
|
||||
│ LABELS │ DUE DATE │ Row 1: metadata
|
||||
│ [Tag] [Tag] [Tag] [+] │ Feb 20 · 5 days left │
|
||||
├────────────────────────────┼─────────────────────────────┤
|
||||
│ CHECKLIST 2/5 │ DESCRIPTION │ Row 2: content
|
||||
│ ✓ item 1 │ Short notes here... │
|
||||
│ ☐ item 2 (scroll) │ (click to edit) │
|
||||
│ + Add item │ │
|
||||
├────────────────────────────┼─────────────────────────────┤
|
||||
│ COVER │ ATTACHMENTS │ Row 3: secondary
|
||||
│ ○ ○ ○ ○ ○ ○ ○ ○ × │ file.pdf · doc.png │
|
||||
│ │ [+ Add file] │
|
||||
└────────────────────────────┴─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Header
|
||||
- Full-width bar with cover color as background (or neutral `pylon-surface` if no cover)
|
||||
- White text on colored bg, normal text on surface bg
|
||||
- Inline-editable title (click → input → Enter/Escape)
|
||||
- Close [×] button top-right
|
||||
|
||||
### Grid Body
|
||||
- CSS Grid: `grid-template-columns: 1fr 1fr`, `gap: 1rem`
|
||||
- Each cell: `rounded-lg bg-pylon-column/50 p-4`
|
||||
- Section headers: `font-mono text-xs uppercase tracking-widest text-pylon-text-secondary`
|
||||
- Row 1: Labels + Due Date (small metadata)
|
||||
- Row 2: Checklist + Description (main content, max-h ~200px with internal scroll)
|
||||
- Row 3: Cover Color + Attachments (secondary)
|
||||
|
||||
### Modal
|
||||
- Width: `max-w-4xl` (up from `max-w-3xl`)
|
||||
- Max height: `max-h-[85vh]` with body scrollable
|
||||
- Shared layout animation preserved (`layoutId`)
|
||||
|
||||
### Animation
|
||||
- Grid cells stagger in with `fadeSlideUp` + `staggerContainer(0.05)`
|
||||
- Backdrop blur + fade (existing)
|
||||
- Escape/click-outside to close (existing)
|
||||
|
||||
## Files to Modify
|
||||
- `src/components/card-detail/CardDetailModal.tsx` — Full rewrite of layout
|
||||
- `src/components/card-detail/MarkdownEditor.tsx` — Remove min-h-200, adapt for grid cell
|
||||
- `src/components/card-detail/ChecklistSection.tsx` — Add max-height + scroll
|
||||
- Minor: LabelPicker, DueDatePicker, AttachmentSection — No structural changes needed
|
||||
@@ -1,467 +0,0 @@
|
||||
# Card Detail Modal Redesign — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the 60/40 split card detail modal with a 2-column dashboard grid where every section gets equal weight.
|
||||
|
||||
**Architecture:** Full rewrite of `CardDetailModal.tsx` to use CSS Grid (2 cols, 3 rows) under a cover-color header. Sub-components (`MarkdownEditor`, `ChecklistSection`) get minor tweaks for cell sizing. `CoverColorPicker` moves from inline private component to its own grid cell. Framer Motion stagger preserved.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, Framer Motion 12, Tailwind 4, Zustand
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Rewrite CardDetailModal — header + grid shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/card-detail/CardDetailModal.tsx` (full rewrite, lines 1-245)
|
||||
|
||||
**Step 1: Replace the entire file with the new layout**
|
||||
|
||||
Replace the full contents of `CardDetailModal.tsx` with:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { X } from "lucide-react";
|
||||
import { useBoardStore } from "@/stores/board-store";
|
||||
import { MarkdownEditor } from "@/components/card-detail/MarkdownEditor";
|
||||
import { ChecklistSection } from "@/components/card-detail/ChecklistSection";
|
||||
import { LabelPicker } from "@/components/card-detail/LabelPicker";
|
||||
import { DueDatePicker } from "@/components/card-detail/DueDatePicker";
|
||||
import { AttachmentSection } from "@/components/card-detail/AttachmentSection";
|
||||
import { springs, staggerContainer, fadeSlideUp } from "@/lib/motion";
|
||||
|
||||
interface CardDetailModalProps {
|
||||
cardId: string | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) {
|
||||
const card = useBoardStore((s) =>
|
||||
cardId ? s.board?.cards[cardId] ?? null : null
|
||||
);
|
||||
const boardLabels = useBoardStore((s) => s.board?.labels ?? []);
|
||||
const updateCard = useBoardStore((s) => s.updateCard);
|
||||
|
||||
const open = cardId != null && card != null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && card && cardId && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
layoutId={`card-${cardId}`}
|
||||
className="relative w-full max-w-4xl overflow-hidden rounded-xl bg-pylon-surface shadow-2xl"
|
||||
transition={springs.gentle}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<EscapeHandler onClose={onClose} />
|
||||
<span className="sr-only">Card detail editor</span>
|
||||
|
||||
{/* Header: cover color background + title + close */}
|
||||
<div
|
||||
className="relative flex items-center gap-3 px-6 py-4"
|
||||
style={{
|
||||
backgroundColor: card.coverColor
|
||||
? `oklch(55% 0.12 ${card.coverColor})`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<InlineTitle
|
||||
cardId={cardId}
|
||||
title={card.title}
|
||||
updateCard={updateCard}
|
||||
hasColor={card.coverColor != null}
|
||||
/>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`absolute right-4 top-1/2 -translate-y-1/2 rounded-md p-1 transition-colors ${
|
||||
card.coverColor
|
||||
? "text-white/70 hover:bg-white/20 hover:text-white"
|
||||
: "text-pylon-text-secondary hover:bg-pylon-column hover:text-pylon-text"
|
||||
}`}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dashboard grid body */}
|
||||
<motion.div
|
||||
className="grid max-h-[calc(85vh-4rem)] grid-cols-2 gap-4 overflow-y-auto p-5"
|
||||
variants={staggerContainer(0.05)}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{/* Row 1: Labels + Due Date */}
|
||||
<motion.div
|
||||
className="rounded-lg bg-pylon-column/50 p-4"
|
||||
variants={fadeSlideUp}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
<LabelPicker
|
||||
cardId={cardId}
|
||||
cardLabelIds={card.labels}
|
||||
boardLabels={boardLabels}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="rounded-lg bg-pylon-column/50 p-4"
|
||||
variants={fadeSlideUp}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
<DueDatePicker cardId={cardId} dueDate={card.dueDate} />
|
||||
</motion.div>
|
||||
|
||||
{/* Row 2: Checklist + Description */}
|
||||
<motion.div
|
||||
className="rounded-lg bg-pylon-column/50 p-4"
|
||||
variants={fadeSlideUp}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
<ChecklistSection
|
||||
cardId={cardId}
|
||||
checklist={card.checklist}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="rounded-lg bg-pylon-column/50 p-4"
|
||||
variants={fadeSlideUp}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
<MarkdownEditor cardId={cardId} value={card.description} />
|
||||
</motion.div>
|
||||
|
||||
{/* Row 3: Cover + Attachments */}
|
||||
<motion.div
|
||||
className="rounded-lg bg-pylon-column/50 p-4"
|
||||
variants={fadeSlideUp}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
<CoverColorPicker
|
||||
cardId={cardId}
|
||||
coverColor={card.coverColor}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="rounded-lg bg-pylon-column/50 p-4"
|
||||
variants={fadeSlideUp}
|
||||
transition={springs.bouncy}
|
||||
>
|
||||
<AttachmentSection
|
||||
cardId={cardId}
|
||||
attachments={card.attachments}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Escape key handler ---------- */
|
||||
|
||||
function EscapeHandler({ onClose }: { onClose: () => void }) {
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- Inline editable title ---------- */
|
||||
|
||||
interface InlineTitleProps {
|
||||
cardId: string;
|
||||
title: string;
|
||||
updateCard: (cardId: string, updates: { title: string }) => void;
|
||||
hasColor: boolean;
|
||||
}
|
||||
|
||||
function InlineTitle({ cardId, title, updateCard, hasColor }: InlineTitleProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(title);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(title);
|
||||
}, [title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
function handleSave() {
|
||||
const trimmed = draft.trim();
|
||||
if (trimmed && trimmed !== title) {
|
||||
updateCard(cardId, { title: trimmed });
|
||||
} else {
|
||||
setDraft(title);
|
||||
}
|
||||
setEditing(false);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === "Escape") {
|
||||
setDraft(title);
|
||||
setEditing(false);
|
||||
}
|
||||
}
|
||||
|
||||
const textColor = hasColor ? "text-white" : "text-pylon-text";
|
||||
const hoverColor = hasColor ? "hover:text-white/80" : "hover:text-pylon-accent";
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`flex-1 font-heading text-xl bg-transparent outline-none border-b pb-0.5 ${
|
||||
hasColor ? "text-white border-white/50" : "text-pylon-text border-pylon-accent"
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<h2
|
||||
onClick={() => setEditing(true)}
|
||||
className={`flex-1 cursor-pointer font-heading text-xl transition-colors ${textColor} ${hoverColor}`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Cover color picker ---------- */
|
||||
|
||||
function CoverColorPicker({
|
||||
cardId,
|
||||
coverColor,
|
||||
}: {
|
||||
cardId: string;
|
||||
coverColor: string | null;
|
||||
}) {
|
||||
const updateCard = useBoardStore((s) => s.updateCard);
|
||||
const presets = [
|
||||
{ hue: "160", label: "Teal" },
|
||||
{ hue: "240", label: "Blue" },
|
||||
{ hue: "300", label: "Purple" },
|
||||
{ hue: "350", label: "Pink" },
|
||||
{ hue: "25", label: "Red" },
|
||||
{ hue: "55", label: "Orange" },
|
||||
{ hue: "85", label: "Yellow" },
|
||||
{ hue: "130", label: "Lime" },
|
||||
{ hue: "200", label: "Cyan" },
|
||||
{ hue: "0", label: "Slate" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
|
||||
Cover
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
onClick={() => updateCard(cardId, { coverColor: null })}
|
||||
className="flex size-6 items-center justify-center rounded-full border border-border text-xs text-pylon-text-secondary hover:bg-pylon-column"
|
||||
title="None"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{presets.map(({ hue, label }) => (
|
||||
<button
|
||||
key={hue}
|
||||
onClick={() => updateCard(cardId, { coverColor: hue })}
|
||||
className="size-6 rounded-full transition-transform hover:scale-110"
|
||||
style={{
|
||||
backgroundColor: `oklch(55% 0.12 ${hue})`,
|
||||
outline:
|
||||
coverColor === hue ? "2px solid currentColor" : "none",
|
||||
outlineOffset: "1px",
|
||||
}}
|
||||
title={label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify TypeScript compiles**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/card-detail/CardDetailModal.tsx
|
||||
git commit -m "feat: rewrite card detail modal as 2-column dashboard grid"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Adapt MarkdownEditor for grid cell
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/card-detail/MarkdownEditor.tsx` (lines 93-103)
|
||||
|
||||
**Step 1: Replace `min-h-[200px]` with cell-friendly sizing**
|
||||
|
||||
In `MarkdownEditor.tsx`, change the textarea className (line 99):
|
||||
|
||||
```
|
||||
Old: className="min-h-[200px] w-full resize-y rounded-md ...
|
||||
New: className="min-h-[100px] max-h-[160px] w-full resize-y rounded-md ...
|
||||
```
|
||||
|
||||
And change the preview container className (line 103):
|
||||
|
||||
```
|
||||
Old: className="min-h-[200px] cursor-pointer rounded-md ...
|
||||
New: className="min-h-[100px] max-h-[160px] overflow-y-auto cursor-pointer rounded-md ...
|
||||
```
|
||||
|
||||
**Step 2: Verify TypeScript compiles**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/card-detail/MarkdownEditor.tsx
|
||||
git commit -m "feat: adapt markdown editor sizing for dashboard grid cell"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add scroll containment to ChecklistSection
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/card-detail/ChecklistSection.tsx` (lines 52-64)
|
||||
|
||||
**Step 1: Add max-height + overflow to the checklist items container**
|
||||
|
||||
In `ChecklistSection.tsx`, change the items container (line 53):
|
||||
|
||||
```
|
||||
Old: <div className="flex flex-col gap-1">
|
||||
New: <div className="flex max-h-[160px] flex-col gap-1 overflow-y-auto">
|
||||
```
|
||||
|
||||
Also add a small progress bar under the header. Change lines 39-50 (the header section) to:
|
||||
|
||||
```tsx
|
||||
{/* Header + progress */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
|
||||
Checklist
|
||||
</h4>
|
||||
{checklist.length > 0 && (
|
||||
<span className="font-mono text-xs text-pylon-text-secondary">
|
||||
{checked}/{checklist.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{checklist.length > 0 && (
|
||||
<div className="h-1 w-full overflow-hidden rounded-full bg-pylon-column">
|
||||
<div
|
||||
className="h-full rounded-full bg-pylon-accent transition-all duration-300"
|
||||
style={{ width: `${(checked / checklist.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Step 2: Verify TypeScript compiles**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/card-detail/ChecklistSection.tsx
|
||||
git commit -m "feat: add scroll containment and progress bar to checklist"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Remove unused Separator import + visual verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/card-detail/CardDetailModal.tsx` (verify no stale imports)
|
||||
|
||||
**Step 1: Verify the file has no unused imports**
|
||||
|
||||
The rewrite in Task 1 already removed the `Separator` import. Confirm the import block does NOT include:
|
||||
- `import { Separator } from "@/components/ui/separator";`
|
||||
|
||||
If it's still there, delete it.
|
||||
|
||||
**Step 2: Run TypeScript check**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Run the dev server and visually verify**
|
||||
|
||||
Run: `npm run dev` (or `npx tauri dev` if checking in Tauri)
|
||||
|
||||
Verify:
|
||||
- Card detail modal opens on card click
|
||||
- Full-width header shows cover color (or neutral bg)
|
||||
- Title is editable (click to edit, Enter/Escape)
|
||||
- Close button [x] works
|
||||
- 2x3 grid: Labels | Due Date / Checklist | Description / Cover | Attachments
|
||||
- Each cell has rounded-lg background
|
||||
- Checklist scrolls when > ~6 items
|
||||
- Description shows compact preview
|
||||
- All animations stagger in
|
||||
- Escape closes modal
|
||||
- Click outside closes modal
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: card detail modal dashboard grid redesign complete"
|
||||
```
|
||||
@@ -1,87 +0,0 @@
|
||||
# Custom Date Picker — Design Document
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
The DueDatePicker uses a native `<input type="date">` which looks out of place in the app's custom dark theme with OKLCH colors. Need a fully custom calendar widget that matches the design language.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| Trigger | Click the entire due date grid cell |
|
||||
| Calendar position | Popover floating below the cell |
|
||||
| Navigation | Month + year dropdown selectors |
|
||||
| Today button | Yes, at bottom of calendar |
|
||||
| Past dates | Selectable but dimmed |
|
||||
| Clear action | Both: x on cell display AND Clear button in calendar footer |
|
||||
| Approach | Fully custom (date-fns + Radix Popover, no new deps) |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
┌─ Due Date Grid Cell ──────────────────────┐
|
||||
│ DUE DATE [×] │
|
||||
│ Feb 20, 2026 · in 5 days │
|
||||
└───────────────────────────────────────────┘
|
||||
│
|
||||
▼ popover (280px wide)
|
||||
┌───────────────────────────────────────┐
|
||||
│ ◀ [February ▾] [2026 ▾] ▶ │
|
||||
├───────────────────────────────────────┤
|
||||
│ Mo Tu We Th Fr Sa Su │
|
||||
│ ·· ·· ·· ·· ·· 1 2 │
|
||||
│ 3 4 5 6 7 8 9 │
|
||||
│ 10 11 12 13 14 ⬤15 16 │
|
||||
│ 17 18 19 ■20 21 22 23 │
|
||||
│ 24 25 26 27 28 ·· ·· │
|
||||
├───────────────────────────────────────┤
|
||||
│ [Today] [Clear] │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### DueDatePicker (modified)
|
||||
- Remove `<input type="date">` entirely
|
||||
- Cell display: formatted date + relative time, or placeholder
|
||||
- × clear button in section header (visible when date is set)
|
||||
- Clicking cell body opens CalendarPopover
|
||||
- Overdue dates in `text-pylon-danger`
|
||||
|
||||
### CalendarPopover (new)
|
||||
- Radix Popover anchored below the cell
|
||||
- 280px wide, `bg-pylon-surface rounded-xl shadow-2xl`
|
||||
|
||||
#### Header
|
||||
- Left/right arrow buttons for prev/next month
|
||||
- Clickable month name → month selector (3×4 grid of month names)
|
||||
- Clickable year → year selector (grid of years, current ±5)
|
||||
|
||||
#### Day Grid
|
||||
- 7 columns (Mo-Su), 6 rows max
|
||||
- Day cells: `size-9 text-sm rounded-lg`
|
||||
- Selected: `bg-pylon-accent text-white`
|
||||
- Today: `ring-1 ring-pylon-accent`
|
||||
- Past: `opacity-50`
|
||||
- Other month days: hidden (empty cells)
|
||||
- Hover: `bg-pylon-column`
|
||||
|
||||
#### Footer
|
||||
- "Today" button (left) — jumps to and selects today
|
||||
- "Clear" button (right) — removes due date
|
||||
|
||||
### Animation
|
||||
- Popover: `scaleIn` + `springs.snappy`
|
||||
- Month/year selector: `AnimatePresence mode="wait"` crossfade
|
||||
|
||||
## Files
|
||||
- Create: `src/components/card-detail/CalendarPopover.tsx`
|
||||
- Modify: `src/components/card-detail/DueDatePicker.tsx`
|
||||
|
||||
## Dependencies
|
||||
- date-fns v4 (already installed): `startOfMonth`, `endOfMonth`, `startOfWeek`, `endOfWeek`, `eachDayOfInterval`, `format`, `isSameDay`, `isSameMonth`, `isToday`, `isPast`, `addMonths`, `subMonths`, `setMonth`, `setYear`, `getYear`
|
||||
- Radix Popover (already installed via `src/components/ui/popover.tsx`)
|
||||
- Framer Motion (already installed)
|
||||
@@ -1,456 +0,0 @@
|
||||
# Custom Date Picker — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the native HTML date input with a fully custom calendar widget that matches the app's dark OKLCH theme.
|
||||
|
||||
**Architecture:** Create a new `CalendarPopover` component (calendar grid + month/year selectors + footer) using date-fns for date math and Radix Popover for positioning. Rewrite `DueDatePicker` to use it instead of `<input type="date">`. No new dependencies.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, date-fns v4, Framer Motion 12, Tailwind 4, Radix Popover
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create CalendarPopover component
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/card-detail/CalendarPopover.tsx`
|
||||
|
||||
**Step 1: Create the file with the complete component**
|
||||
|
||||
Create `src/components/card-detail/CalendarPopover.tsx` with:
|
||||
|
||||
```tsx
|
||||
import { useState, useMemo } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import {
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
eachDayOfInterval,
|
||||
format,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
isToday as isTodayFn,
|
||||
isPast,
|
||||
addMonths,
|
||||
subMonths,
|
||||
setMonth,
|
||||
setYear,
|
||||
getYear,
|
||||
getMonth,
|
||||
} from "date-fns";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { springs } from "@/lib/motion";
|
||||
|
||||
interface CalendarPopoverProps {
|
||||
selectedDate: Date | null;
|
||||
onSelect: (date: Date) => void;
|
||||
onClear: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type ViewMode = "days" | "months" | "years";
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
];
|
||||
|
||||
const WEEKDAYS = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||
|
||||
export function CalendarPopover({
|
||||
selectedDate,
|
||||
onSelect,
|
||||
onClear,
|
||||
children,
|
||||
}: CalendarPopoverProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [viewDate, setViewDate] = useState(() => selectedDate ?? new Date());
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("days");
|
||||
|
||||
// Reset view when opening
|
||||
function handleOpenChange(nextOpen: boolean) {
|
||||
if (nextOpen) {
|
||||
setViewDate(selectedDate ?? new Date());
|
||||
setViewMode("days");
|
||||
}
|
||||
setOpen(nextOpen);
|
||||
}
|
||||
|
||||
function handleSelectDate(date: Date) {
|
||||
onSelect(date);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function handleToday() {
|
||||
const today = new Date();
|
||||
onSelect(today);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
onClear();
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
// Build the 6×7 grid of days for the current viewDate month
|
||||
const calendarDays = useMemo(() => {
|
||||
const monthStart = startOfMonth(viewDate);
|
||||
const monthEnd = endOfMonth(viewDate);
|
||||
const gridStart = startOfWeek(monthStart, { weekStartsOn: 1 }); // Monday
|
||||
const gridEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
|
||||
return eachDayOfInterval({ start: gridStart, end: gridEnd });
|
||||
}, [viewDate]);
|
||||
|
||||
// Year range for year selector: current year ± 5
|
||||
const yearRange = useMemo(() => {
|
||||
const center = getYear(viewDate);
|
||||
const years: number[] = [];
|
||||
for (let y = center - 5; y <= center + 5; y++) {
|
||||
years.push(y);
|
||||
}
|
||||
return years;
|
||||
}, [viewDate]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-[280px] bg-pylon-surface p-0 rounded-xl shadow-2xl border-border"
|
||||
>
|
||||
{/* Navigation header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setViewDate((d) => subMonths(d, 1))}
|
||||
className="text-pylon-text-secondary hover:text-pylon-text"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setViewMode(viewMode === "months" ? "days" : "months")}
|
||||
className="rounded-md px-2 py-0.5 text-sm font-medium text-pylon-text transition-colors hover:bg-pylon-column"
|
||||
>
|
||||
{format(viewDate, "MMMM")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode(viewMode === "years" ? "days" : "years")}
|
||||
className="rounded-md px-2 py-0.5 text-sm font-medium text-pylon-text transition-colors hover:bg-pylon-column"
|
||||
>
|
||||
{format(viewDate, "yyyy")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setViewDate((d) => addMonths(d, 1))}
|
||||
className="text-pylon-text-secondary hover:text-pylon-text"
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Body: days / months / years */}
|
||||
<div className="p-3">
|
||||
<AnimatePresence mode="wait">
|
||||
{viewMode === "days" && (
|
||||
<motion.div
|
||||
key="days"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
{/* Weekday headers */}
|
||||
<div className="mb-1 grid grid-cols-7">
|
||||
{WEEKDAYS.map((wd) => (
|
||||
<div
|
||||
key={wd}
|
||||
className="flex h-8 items-center justify-center font-mono text-[10px] uppercase tracking-wider text-pylon-text-secondary"
|
||||
>
|
||||
{wd}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7">
|
||||
{calendarDays.map((day) => {
|
||||
const inMonth = isSameMonth(day, viewDate);
|
||||
const today = isTodayFn(day);
|
||||
const selected = selectedDate != null && isSameDay(day, selectedDate);
|
||||
const past = isPast(day) && !today;
|
||||
|
||||
if (!inMonth) {
|
||||
return <div key={day.toISOString()} className="h-9" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={day.toISOString()}
|
||||
onClick={() => handleSelectDate(day)}
|
||||
className={`flex h-9 items-center justify-center rounded-lg text-sm transition-colors
|
||||
${selected
|
||||
? "bg-pylon-accent font-medium text-white"
|
||||
: today
|
||||
? "font-medium text-pylon-accent ring-1 ring-pylon-accent"
|
||||
: past
|
||||
? "text-pylon-text opacity-50 hover:bg-pylon-column hover:opacity-75"
|
||||
: "text-pylon-text hover:bg-pylon-column"
|
||||
}`}
|
||||
>
|
||||
{format(day, "d")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{viewMode === "months" && (
|
||||
<motion.div
|
||||
key="months"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="grid grid-cols-3 gap-1"
|
||||
>
|
||||
{MONTH_NAMES.map((name, i) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => {
|
||||
setViewDate((d) => setMonth(d, i));
|
||||
setViewMode("days");
|
||||
}}
|
||||
className={`rounded-lg py-2 text-sm transition-colors ${
|
||||
getMonth(viewDate) === i
|
||||
? "bg-pylon-accent font-medium text-white"
|
||||
: "text-pylon-text hover:bg-pylon-column"
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{viewMode === "years" && (
|
||||
<motion.div
|
||||
key="years"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="grid grid-cols-3 gap-1"
|
||||
>
|
||||
{yearRange.map((year) => (
|
||||
<button
|
||||
key={year}
|
||||
onClick={() => {
|
||||
setViewDate((d) => setYear(d, year));
|
||||
setViewMode("days");
|
||||
}}
|
||||
className={`rounded-lg py-2 text-sm transition-colors ${
|
||||
getYear(viewDate) === year
|
||||
? "bg-pylon-accent font-medium text-white"
|
||||
: "text-pylon-text hover:bg-pylon-column"
|
||||
}`}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between border-t border-border px-3 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={handleToday}
|
||||
className="text-pylon-accent hover:text-pylon-accent"
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={handleClear}
|
||||
className="text-pylon-text-secondary hover:text-pylon-danger"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify TypeScript compiles**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/card-detail/CalendarPopover.tsx
|
||||
git commit -m "feat: create custom CalendarPopover component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rewrite DueDatePicker to use CalendarPopover
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/card-detail/DueDatePicker.tsx` (full rewrite)
|
||||
|
||||
**Step 1: Replace the entire file**
|
||||
|
||||
Replace `src/components/card-detail/DueDatePicker.tsx` with:
|
||||
|
||||
```tsx
|
||||
import { format, formatDistanceToNow, isPast, isToday } from "date-fns";
|
||||
import { X } from "lucide-react";
|
||||
import { useBoardStore } from "@/stores/board-store";
|
||||
import { CalendarPopover } from "@/components/card-detail/CalendarPopover";
|
||||
|
||||
interface DueDatePickerProps {
|
||||
cardId: string;
|
||||
dueDate: string | null;
|
||||
}
|
||||
|
||||
export function DueDatePicker({ cardId, dueDate }: DueDatePickerProps) {
|
||||
const updateCard = useBoardStore((s) => s.updateCard);
|
||||
|
||||
const dateObj = dueDate ? new Date(dueDate) : null;
|
||||
const overdue = dateObj != null && isPast(dateObj) && !isToday(dateObj);
|
||||
|
||||
function handleSelect(date: Date) {
|
||||
updateCard(cardId, { dueDate: format(date, "yyyy-MM-dd") });
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
updateCard(cardId, { dueDate: null });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Header with clear button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
|
||||
Due Date
|
||||
</h4>
|
||||
{dueDate && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="rounded p-0.5 text-pylon-text-secondary transition-colors hover:bg-pylon-danger/10 hover:text-pylon-danger"
|
||||
aria-label="Clear due date"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Clickable date display → opens calendar */}
|
||||
<CalendarPopover
|
||||
selectedDate={dateObj}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-1 py-1 text-left transition-colors hover:bg-pylon-column/60">
|
||||
{dateObj ? (
|
||||
<>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
overdue ? "text-pylon-danger" : "text-pylon-text"
|
||||
}`}
|
||||
>
|
||||
{format(dateObj, "MMM d, yyyy")}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs ${
|
||||
overdue ? "text-pylon-danger" : "text-pylon-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{overdue
|
||||
? `overdue by ${formatDistanceToNow(dateObj)}`
|
||||
: isToday(dateObj)
|
||||
? "today"
|
||||
: `in ${formatDistanceToNow(dateObj)}`}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm italic text-pylon-text-secondary/60">
|
||||
Click to set date...
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</CalendarPopover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify TypeScript compiles**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/card-detail/DueDatePicker.tsx
|
||||
git commit -m "feat: rewrite DueDatePicker to use custom CalendarPopover"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Visual verification and final commit
|
||||
|
||||
**Step 1: Run TypeScript check**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
**Step 2: Run the dev server**
|
||||
|
||||
Run: `npx tauri dev`
|
||||
|
||||
Verify:
|
||||
- Open a card → Due Date cell shows "Click to set date..." or the current date
|
||||
- Click the cell → calendar popover appears below
|
||||
- Calendar shows correct month with today highlighted (ring)
|
||||
- Click a date → it's selected (filled accent), popover closes, cell shows formatted date
|
||||
- Click month name → month selector grid appears, click a month → returns to days
|
||||
- Click year → year selector grid appears, click a year → returns to days
|
||||
- Left/right arrows navigate months
|
||||
- "Today" button selects today and closes
|
||||
- "Clear" button in popover footer removes the date and closes
|
||||
- × button in cell header clears the date without opening calendar
|
||||
- Past dates are dimmed but clickable
|
||||
- Overdue dates show in red
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: custom date picker with calendar popover complete"
|
||||
```
|
||||
@@ -1,188 +0,0 @@
|
||||
# Motion, Dark Mode & Custom Titlebar Design
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Goal:** Add playful bouncy animations everywhere, lighten dark mode for HDR monitors, and implement custom window decorations merged into the TopBar
|
||||
**Approach:** Centralized motion system + CSS variable tuning + Tauri decoration override
|
||||
|
||||
---
|
||||
|
||||
## 1. Motion System Foundation
|
||||
|
||||
Create `src/lib/motion.ts` — shared animation config imported by all components.
|
||||
|
||||
### Spring Presets (Bouncy Profile)
|
||||
- **bouncy** — stiffness: 400, damping: 15, mass: 0.8 (main preset, visible overshoot)
|
||||
- **snappy** — stiffness: 500, damping: 20 (micro-interactions — buttons, toggles)
|
||||
- **gentle** — stiffness: 200, damping: 20 (larger elements — modals, page transitions)
|
||||
- **wobbly** — stiffness: 300, damping: 10 (playful emphasis — toasts, notifications)
|
||||
|
||||
### Reusable Variants
|
||||
- `fadeSlideUp` — enters from below with opacity fade (cards, list items)
|
||||
- `fadeSlideDown` — enters from above (dropdowns, menus)
|
||||
- `scaleIn` — scales from 0.9 to 1 with bounce (modals, popovers)
|
||||
- `staggerContainer` — parent variant that staggers children
|
||||
|
||||
### Stagger Helper
|
||||
`staggerChildren(delay = 0.04)` — generates parent transition variants for cascading entrances.
|
||||
|
||||
---
|
||||
|
||||
## 2. Component-by-Component Motion Rollout
|
||||
|
||||
### Page Transitions (App.tsx)
|
||||
- Wrap view switch in `AnimatePresence mode="wait"`
|
||||
- Board-list exits with fade+slide-left, board enters with fade+slide-right
|
||||
- Uses `gentle` spring
|
||||
|
||||
### Board List (BoardList.tsx)
|
||||
- Board cards stagger in on mount using `staggerContainer`
|
||||
- Each BoardCard uses `fadeSlideUp` entrance
|
||||
- Empty state fades in
|
||||
|
||||
### Board View (BoardView.tsx)
|
||||
- Columns stagger in from left to right on mount (0.06s delay each)
|
||||
- Cards within each column stagger in (0.03s delay)
|
||||
|
||||
### Card Thumbnails (CardThumbnail.tsx)
|
||||
- Migrate existing spring to shared `bouncy` preset
|
||||
- `whileHover` scale 1.02 with shadow elevation
|
||||
- `whileTap` scale 0.98
|
||||
|
||||
### Card Detail Modal (CardDetailModal.tsx)
|
||||
- **Shared layout animation** — CardThumbnail gets `layoutId={card-${card.id}}`, modal wrapper gets same layoutId
|
||||
- Card morphs into the modal on open — hero transition
|
||||
- Backdrop blurs in with animated opacity + backdropFilter
|
||||
- Modal content sections stagger in after layout animation
|
||||
|
||||
### Column Header (ColumnHeader.tsx)
|
||||
- Dropdown menu items stagger in with `fadeSlideDown`
|
||||
|
||||
### TopBar
|
||||
- Buttons have `whileHover` and `whileTap` micro-animations
|
||||
- Saving status text fades in/out with AnimatePresence
|
||||
|
||||
### Toast Notifications (ToastContainer.tsx)
|
||||
- Migrate to `wobbly` spring for extra personality
|
||||
- Exit slides down + fades
|
||||
|
||||
### Settings Dialog
|
||||
- Tab content crossfades with AnimatePresence
|
||||
- Accent color swatches have `whileHover` scale pulse
|
||||
|
||||
### Command Palette
|
||||
- Results stagger in as you type
|
||||
|
||||
---
|
||||
|
||||
## 3. Gesture-Reactive Drag & Drop
|
||||
|
||||
Override dnd-kit's default drag overlay with Framer Motion-powered custom overlay.
|
||||
|
||||
- **On drag start:** Card lifts with `scale: 1.05`, box-shadow, slight rotate based on grab offset
|
||||
- **During drag:** Card tilts based on pointer velocity (useMotionValue + useTransform). Max tilt: ~5 degrees
|
||||
- **On drop:** Spring back to `scale: 1, rotate: 0`. Target column cards spring apart using `layout` prop
|
||||
- dnd-kit handles position/sorting logic; we layer gesture transforms on top
|
||||
|
||||
---
|
||||
|
||||
## 4. Dark Mode — Subtle Lift for HDR
|
||||
|
||||
### Pylon Dark Variables (in `.dark {}`)
|
||||
|
||||
| Variable | Current | New |
|
||||
|----------|---------|-----|
|
||||
| `--pylon-bg` | `oklch(18% 0.01 50)` | `oklch(25% 0.012 50)` |
|
||||
| `--pylon-surface` | `oklch(22% 0.01 50)` | `oklch(29% 0.012 50)` |
|
||||
| `--pylon-column` | `oklch(20% 0.012 50)` | `oklch(27% 0.014 50)` |
|
||||
| `--pylon-text` | `oklch(90% 0.01 50)` | `oklch(92% 0.01 50)` |
|
||||
| `--pylon-text-secondary` | `oklch(55% 0.01 50)` | `oklch(58% 0.01 50)` |
|
||||
| `--pylon-accent` | `oklch(60% 0.12 160)` | `oklch(62% 0.13 160)` |
|
||||
| `--pylon-danger` | `oklch(60% 0.18 25)` | `oklch(62% 0.18 25)` |
|
||||
|
||||
### Shadcn Dark Variables
|
||||
|
||||
| Variable | Current | New |
|
||||
|----------|---------|-----|
|
||||
| `--background` | `oklch(0.145 0 0)` | `oklch(0.22 0 0)` |
|
||||
| `--card`, `--popover` | `oklch(0.205 0 0)` | `oklch(0.27 0 0)` |
|
||||
| `--secondary`, `--muted`, `--accent` | `oklch(0.269 0 0)` | `oklch(0.32 0 0)` |
|
||||
| `--border` | `oklch(1 0 0 / 10%)` | `oklch(1 0 0 / 12%)` |
|
||||
| `--input` | `oklch(1 0 0 / 15%)` | `oklch(1 0 0 / 18%)` |
|
||||
| `--sidebar` | `oklch(0.205 0 0)` | `oklch(0.27 0 0)` |
|
||||
| `--sidebar-accent` | `oklch(0.269 0 0)` | `oklch(0.32 0 0)` |
|
||||
|
||||
Color scheme (hue 50 warmth) preserved. Slight chroma bump for HDR vibrancy.
|
||||
|
||||
---
|
||||
|
||||
## 5. Custom Window Titlebar
|
||||
|
||||
### Tauri Configuration
|
||||
Set `"decorations": false` in `tauri.conf.json` to remove native OS titlebar.
|
||||
|
||||
### TopBar Integration
|
||||
Window controls added to far right of existing TopBar, after a thin vertical separator:
|
||||
|
||||
```
|
||||
[Back] .......... [Board Title] .......... [Undo][Redo][Settings][Save][Search][Gear] | [—][□][×]
|
||||
```
|
||||
|
||||
### WindowControls Component
|
||||
Inline in TopBar or extracted to `src/components/layout/WindowControls.tsx`.
|
||||
|
||||
**Buttons:**
|
||||
- Minimize: `Minus` icon from Lucide → `getCurrentWindow().minimize()`
|
||||
- Maximize/Restore: `Square` / `Copy` icon → `getCurrentWindow().toggleMaximize()`
|
||||
- Close: `X` icon → `getCurrentWindow().close()`
|
||||
|
||||
**Styling:**
|
||||
- 32x32px hit area, 16x16px icons
|
||||
- Default: `text-pylon-text-secondary`
|
||||
- Hover: Background with accent at 10% opacity
|
||||
- Close hover: `pylon-danger` at 15% opacity (red highlight — convention)
|
||||
- All have Framer Motion `whileHover` and `whileTap` springs
|
||||
|
||||
**State tracking:**
|
||||
- Listen to `getCurrentWindow().onResized()` for maximize state
|
||||
- Query `isMaximized()` on mount for initial icon
|
||||
- `data-tauri-drag-region` stays on header; window buttons do NOT propagate drag
|
||||
|
||||
---
|
||||
|
||||
## Files Affected
|
||||
|
||||
### New Files
|
||||
- `src/lib/motion.ts` — shared spring presets, variants, helpers
|
||||
|
||||
### Modified Files
|
||||
- `src/App.tsx` — AnimatePresence page transitions
|
||||
- `src/index.css` — dark mode color value updates
|
||||
- `src/components/layout/TopBar.tsx` — window controls, motion on buttons
|
||||
- `src/components/layout/AppShell.tsx` — support page transition wrapper
|
||||
- `src/components/boards/BoardList.tsx` — stagger animation on board cards
|
||||
- `src/components/boards/BoardCard.tsx` — fadeSlideUp entrance, hover/tap
|
||||
- `src/components/board/BoardView.tsx` — column stagger, gesture-reactive drag overlay
|
||||
- `src/components/board/KanbanColumn.tsx` — card stagger, layout animation for reorder
|
||||
- `src/components/board/CardThumbnail.tsx` — shared layoutId, bouncy preset, hover/tap
|
||||
- `src/components/board/ColumnHeader.tsx` — dropdown animation
|
||||
- `src/components/card-detail/CardDetailModal.tsx` — shared layout animation (hero), content stagger
|
||||
- `src/components/toast/ToastContainer.tsx` — wobbly spring
|
||||
- `src/components/settings/SettingsDialog.tsx` — tab crossfade, swatch hover
|
||||
- `src/components/command-palette/CommandPalette.tsx` — result stagger
|
||||
- `src/components/shortcuts/ShortcutHelpModal.tsx` — entrance animation
|
||||
- `src-tauri/tauri.conf.json` — decorations: false
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Motion system foundation (`src/lib/motion.ts`)
|
||||
2. Dark mode CSS variable updates
|
||||
3. Custom titlebar (Tauri config + WindowControls)
|
||||
4. Page transitions (App.tsx + AnimatePresence)
|
||||
5. Board list animations (stagger + BoardCard)
|
||||
6. Board view column stagger + card stagger
|
||||
7. Card thumbnail hover/tap + shared layoutId
|
||||
8. Card detail modal shared layout animation
|
||||
9. Gesture-reactive drag overlay
|
||||
10. Micro-interactions (TopBar, ColumnHeader dropdowns)
|
||||
11. Toast, Settings, Command Palette, ShortcutHelp animations
|
||||
12. Polish pass — verify all springs feel cohesive, test reduced-motion
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
||||
# OpenPylon — Local-First Kanban Board Design Document
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
OpenPylon is a local-first Kanban board desktop app for personal projects and task management. No account required, no cloud sync — a fast, drag-and-drop board that saves to local JSON files. Replaces Trello ($5-10/mo), Asana ($11/mo), Monday.com ($9/mo).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime:** Tauri (Rust backend, system webview, ~5MB bundle)
|
||||
- **Frontend:** React + TypeScript
|
||||
- **State:** Zustand (monolithic store per board, debounced JSON persistence)
|
||||
- **Styling:** Tailwind CSS + shadcn/ui
|
||||
- **Drag & Drop:** dnd-kit
|
||||
- **Undo/Redo:** zundo (Zustand temporal middleware)
|
||||
|
||||
## Architecture: Monolithic State Store
|
||||
|
||||
Single Zustand store per board, loaded entirely into memory from JSON on open. All mutations go through the store and auto-save back to disk with debounced writes (500ms). Board data is small (even 500 cards is ~1MB of JSON), so full in-memory loading is fine.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
~/.openpylon/
|
||||
├── settings.json # Global app settings
|
||||
├── boards/
|
||||
│ ├── board-<ulid>.json # One file per board
|
||||
│ └── board-<ulid>.json
|
||||
└── attachments/
|
||||
└── board-<ulid>/ # Copied attachments (when setting enabled)
|
||||
└── <ulid>-filename.png
|
||||
```
|
||||
|
||||
### Schema
|
||||
|
||||
```typescript
|
||||
interface Board {
|
||||
id: string; // ULID
|
||||
title: string;
|
||||
color: string; // Accent color stripe for board list
|
||||
createdAt: string; // ISO 8601
|
||||
updatedAt: string;
|
||||
columns: Column[];
|
||||
cards: Record<string, Card>; // Flat map, referenced by columns
|
||||
labels: Label[]; // Board-level label definitions
|
||||
settings: BoardSettings; // Per-board settings (attachment mode, etc.)
|
||||
}
|
||||
|
||||
interface Column {
|
||||
id: string;
|
||||
title: string;
|
||||
cardIds: string[]; // Ordered references
|
||||
width: "narrow" | "standard" | "wide"; // Collapsible widths
|
||||
}
|
||||
|
||||
interface Card {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string; // Markdown
|
||||
labels: string[]; // Label IDs
|
||||
checklist: ChecklistItem[];
|
||||
dueDate: string | null;
|
||||
attachments: Attachment[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Label {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface ChecklistItem {
|
||||
id: string;
|
||||
text: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string; // Absolute (link mode) or relative (copy mode)
|
||||
mode: "link" | "copy";
|
||||
}
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ULIDs instead of UUIDs — sortable by creation time, no collisions
|
||||
- Cards stored flat (`cards: Record<string, Card>`) with columns referencing via `cardIds[]` — drag-and-drop reordering is a simple array splice
|
||||
- Labels defined at board level, referenced by ID on cards
|
||||
|
||||
---
|
||||
|
||||
## State Management & Persistence
|
||||
|
||||
### Stores
|
||||
|
||||
- `useBoardStore` — active board's full state + all mutation actions
|
||||
- `useAppStore` — global app state: theme, recent boards, settings, current view
|
||||
|
||||
### Persistence Flow
|
||||
|
||||
1. Board open: Tauri `fs.readTextFile()` → parse JSON → validate with Zod → hydrate Zustand store
|
||||
2. On mutation: store subscribes to itself, debounces writes at 500ms
|
||||
3. On board close / app quit: immediate flush via Tauri `window.onCloseRequested`
|
||||
|
||||
### Auto-Backup
|
||||
|
||||
On every successful save, rotate previous version to `board-<ulid>.backup.json` (one backup per board).
|
||||
|
||||
### Undo/Redo
|
||||
|
||||
zundo (Zustand temporal middleware) tracks state history. Ctrl+Z / Ctrl+Shift+Z. Capped at ~50 steps.
|
||||
|
||||
### Search
|
||||
|
||||
Global search reads all board JSON files from disk and searches card titles + descriptions. For personal Kanban (5-20 boards), this is instant. No index needed.
|
||||
|
||||
---
|
||||
|
||||
## UI Design
|
||||
|
||||
### Aesthetic Direction: Industrial Utility with Warmth
|
||||
|
||||
"Pylon" evokes infrastructure and strength. The app should feel like a well-made tool — a carpenter's organized workshop, not an IKEA showroom.
|
||||
|
||||
### Color Palette (OKLCH)
|
||||
|
||||
**Light mode:**
|
||||
- Background: `oklch(97% 0.005 80)` — warm off-white
|
||||
- Surface/cards: `oklch(99% 0.003 80)` — barely-there warmth
|
||||
- Column background: `oklch(95% 0.008 80)` — subtle sand
|
||||
- Primary accent: `oklch(55% 0.12 160)` — muted teal-green
|
||||
- Text primary: `oklch(25% 0.015 50)` — warm near-black
|
||||
- Text secondary: `oklch(55% 0.01 50)` — warm gray
|
||||
- Danger/overdue: `oklch(55% 0.18 25)` — terracotta red
|
||||
|
||||
**Dark mode:**
|
||||
- Background: `oklch(18% 0.01 50)` — warm dark
|
||||
- Surface: `oklch(22% 0.01 50)`
|
||||
- Cards: `oklch(25% 0.012 50)`
|
||||
|
||||
### Typography
|
||||
|
||||
- **Headings:** Instrument Serif — heritage serif with personality
|
||||
- **Body/cards:** Satoshi — clean geometric sans, readable at small sizes
|
||||
- **Metadata (labels, dates, counts):** Geist Mono — reinforces "tool" identity
|
||||
|
||||
**Scale:**
|
||||
- Board title: `clamp(1.25rem, 2vw, 1.5rem)`, bold
|
||||
- Column headers: `0.8rem` uppercase, `letter-spacing: 0.08em`, weight 600
|
||||
- Card titles: `0.875rem`, weight 500
|
||||
- Card metadata: `0.75rem` monospace
|
||||
|
||||
### App Shell Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ ← Boards Sprint Planning ⌘K ⚙ │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ TO DO IN PROGRESS DONE │
|
||||
│ ───── 4 ─────────── 2 ──── 3 │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ Card title │ │ Card title │ │ Card title │ │
|
||||
│ │ 🟢🔵 Feb28 ▮▮▯│ │ 🟢 ▮▮▮▮│ │ ▮▮▯ │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
│ │
|
||||
│ + Add card + Add card + Add card │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key layout decisions:**
|
||||
- No vertical column dividers — whitespace gaps (24-32px) instead
|
||||
- Column headers: uppercase, tracked-out, small — like section dividers
|
||||
- Card count as quiet number beside underline, not a badge
|
||||
- Command palette (`Ctrl+K`) replaces search icon
|
||||
- Theme toggle lives in settings, not top bar
|
||||
- Board title is click-to-edit inline, no `[edit]` button
|
||||
|
||||
### Card Design
|
||||
|
||||
- **Label dots:** 8px colored circles in a row, hover for tooltip with name
|
||||
- **Due date:** Monospace, right-aligned, no icon. Overdue turns terracotta with subtle tint.
|
||||
- **Checklist:** Tiny progress bar (filled/unfilled blocks), not "3/4" text
|
||||
- **No card borders.** Subtle shadow (`0 1px 3px oklch(0% 0 0 / 0.06)`) for separation.
|
||||
- **Hover:** `translateY(-1px)` lift with faint shadow deepening, spring physics, 150ms
|
||||
- **Drag ghost:** 5-degree rotation, `scale(1.03)`, `opacity(0.9)`, elevated shadow
|
||||
|
||||
### Column Widths
|
||||
|
||||
Columns support three widths: narrow (titles only), standard, wide (active focus). Double-click header to cycle. Adds spatial meaning.
|
||||
|
||||
### Card Detail Modal (Two-Panel)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Fix auth token refresh ✕ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌────────────────────┐ │
|
||||
│ │ │ │ LABELS │ │
|
||||
│ │ Markdown description │ │ 🟢 Bug 🔵 Backend │ │
|
||||
│ │ with live preview │ │ │ │
|
||||
│ │ │ │ DUE DATE │ │
|
||||
│ │ │ │ Feb 28, 2026 │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ CHECKLIST 3/4 │ │
|
||||
│ │ │ │ ✓ Research APIs │ │
|
||||
│ │ │ │ ✓ Write tests │ │
|
||||
│ │ │ │ ✓ Implement │ │
|
||||
│ │ │ │ ○ Code review │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ ATTACHMENTS │ │
|
||||
│ │ │ │ spec.pdf │ │
|
||||
│ └─────────────────────────┘ └────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Left panel (60%): Title (inline edit) + markdown description (edit/preview toggle)
|
||||
- Right sidebar (40%): Labels, due date, checklist, attachments. Each collapsible.
|
||||
- No save button — auto-persist with subtle "Saved" indicator
|
||||
- **Card-to-modal morph animation** via Framer Motion `layoutId` — modal grows from card position
|
||||
|
||||
### Command Palette (`Ctrl+K`)
|
||||
|
||||
Using shadcn's `cmdk` component:
|
||||
- Search all cards across all boards by title/description
|
||||
- Switch between boards
|
||||
- Create new cards/boards
|
||||
- Toggle theme
|
||||
- Open settings
|
||||
- Navigate to specific column
|
||||
- Filter current board by label/date
|
||||
|
||||
### Board List (Home Screen)
|
||||
|
||||
Grid of board cards with:
|
||||
- Color accent stripe at top (user-chosen per board)
|
||||
- Title, card count, column count
|
||||
- Relative time ("2 min ago", "Yesterday")
|
||||
- Right-click context menu: Duplicate, Export, Delete, Change color
|
||||
- Empty state: "Create your first board" + single button
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
### Global
|
||||
|
||||
| Action | Shortcut |
|
||||
|---|---|
|
||||
| Command palette | `Ctrl+K` |
|
||||
| New card in focused column | `N` |
|
||||
| New board | `Ctrl+N` |
|
||||
| Undo | `Ctrl+Z` |
|
||||
| Redo | `Ctrl+Shift+Z` |
|
||||
| Settings | `Ctrl+,` |
|
||||
| Close modal / cancel | `Escape` |
|
||||
| Save & close card detail | `Ctrl+Enter` |
|
||||
|
||||
### Board Navigation
|
||||
|
||||
- `Arrow Left/Right` — focus prev/next column
|
||||
- `Arrow Up/Down` — focus prev/next card in column
|
||||
- `Enter` — open focused card detail
|
||||
- `Space` — quick-toggle first unchecked checklist item
|
||||
- `D` — set/edit due date on focused card
|
||||
- `L` — open label picker on focused card
|
||||
|
||||
### Drag-and-Drop Keyboard
|
||||
|
||||
dnd-kit keyboard sensor: `Space` to pick up, arrows to move, `Space` to drop, `Escape` to cancel. Movements announced via `aria-live` region.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- All interactive elements reachable via Tab
|
||||
- Focus indicators: `2px solid` accent color, `2px offset`, visible in both themes
|
||||
- Modal focus trapping
|
||||
- Column/card counts via `aria-label`
|
||||
- `prefers-reduced-motion`: all animations collapse to instant
|
||||
- `prefers-contrast`: increased shadow intensity, subtle borders restored
|
||||
- Minimum touch target: 44x44px on all buttons
|
||||
|
||||
---
|
||||
|
||||
## Import/Export
|
||||
|
||||
### Export
|
||||
|
||||
- **JSON:** The board file itself is the export. Save As dialog.
|
||||
- **CSV:** Flattened — one row per card with all fields.
|
||||
- **ZIP:** For boards with copy-mode attachments — board JSON + attachments folder.
|
||||
|
||||
### Import
|
||||
|
||||
- **OpenPylon JSON:** Drop file onto board list or use File > Import. Schema validation + preview before importing.
|
||||
- **CSV:** Import wizard — map columns, preview rows, choose target board.
|
||||
- **Trello JSON:** Dedicated adapter mapping Trello schema to OpenPylon.
|
||||
- **Drag-and-drop import:** Dropping `.json` or `.csv` anywhere triggers import flow.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Corrupted board file:** Recovery dialog — inspect in file explorer or restore from `.backup.json`
|
||||
- **Data directory inaccessible:** Dialog to choose new directory on startup
|
||||
- **Disk full:** Inline toast, changes preserved in memory, retry every 30s
|
||||
- **File locked:** Warning dialog
|
||||
- **Schema migration:** On load, validate with Zod, add missing fields with defaults, preserve unknown fields
|
||||
- **Drag edge cases:** Empty column droppable, drop outside cancels with spring return
|
||||
|
||||
## Micro-Interactions
|
||||
|
||||
| Interaction | Animation | Duration |
|
||||
|---|---|---|
|
||||
| Card appears (new) | Fade in + slide down | 200ms, spring |
|
||||
| Card drag start | Lift + rotate + shadow | 150ms |
|
||||
| Card drop | Settle with slight bounce | 250ms, spring |
|
||||
| Column add | Slide in from right | 300ms |
|
||||
| Card detail open | Morph from card position | 250ms |
|
||||
| Card detail close | Reverse morph to card | 200ms |
|
||||
| Checklist check | Strikethrough sweep + fill | 200ms |
|
||||
| Board switch | Crossfade | 300ms |
|
||||
|
||||
All animations respect `prefers-reduced-motion`.
|
||||
|
||||
---
|
||||
|
||||
## Empty States
|
||||
|
||||
- **No boards:** "Create your first board" + button + minimal illustration
|
||||
- **Empty column:** Dashed border area + "Drag cards here or click + to add"
|
||||
- **No search results:** "No matches" + suggestion to broaden
|
||||
- **No labels:** "Create your first label" + color swatches
|
||||
@@ -1,235 +0,0 @@
|
||||
# OpenPylon Visual Glow-Up Design
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Goal:** Transform OpenPylon from functional-but-bare to visually polished and delightful
|
||||
**Approach:** Settings-first foundation — build the settings infrastructure, then layer visual features on top
|
||||
|
||||
---
|
||||
|
||||
## 1. Settings Model & Infrastructure
|
||||
|
||||
Expand `AppSettings` in `src/types/settings.ts`:
|
||||
|
||||
```typescript
|
||||
export interface AppSettings {
|
||||
theme: "light" | "dark" | "system";
|
||||
dataDirectory: string | null;
|
||||
recentBoardIds: string[];
|
||||
|
||||
// Appearance
|
||||
accentColor: string; // OKLCH hue (0-360), default "160" (teal)
|
||||
uiZoom: number; // 0.75-1.5, default 1.0
|
||||
density: "compact" | "comfortable" | "spacious";
|
||||
|
||||
// Board defaults
|
||||
defaultColumnWidth: ColumnWidth; // default "standard"
|
||||
}
|
||||
```
|
||||
|
||||
### Zoom
|
||||
Set `font-size` on `<html>` to `uiZoom * 16px`. Everything uses `rem` via Tailwind, so the entire UI scales proportionally.
|
||||
|
||||
### Accent Color
|
||||
Store an OKLCH hue value. On apply, regenerate `--pylon-accent` as `oklch(55% 0.12 {hue})` (light) / `oklch(60% 0.12 {hue})` (dark).
|
||||
|
||||
### Density
|
||||
Set CSS custom property `--density-factor` (compact=0.75, comfortable=1.0, spacious=1.25). Use it to scale padding on columns, cards, and gaps.
|
||||
|
||||
### App Store Changes
|
||||
Add `setAccentColor`, `setUiZoom`, `setDensity`, `setDefaultColumnWidth` actions to `app-store.ts`. Each saves immediately (no Save button). Add `applyAppearance()` function that applies zoom, accent, and density to the DOM — called on init and on any change.
|
||||
|
||||
---
|
||||
|
||||
## 2. Settings Panel UI
|
||||
|
||||
Transform `SettingsDialog.tsx` from a tiny modal into a tabbed panel.
|
||||
|
||||
- Widen to `sm:max-w-lg`
|
||||
- 4 tabs: **Appearance** | **Boards** | **Keyboard Shortcuts** | **About**
|
||||
- Simple button-based tab navigation (no library needed)
|
||||
|
||||
### Appearance Tab
|
||||
- **Theme** — existing 3-button toggle (unchanged)
|
||||
- **UI Zoom** — slider 75%-150% in 5% steps, live preview, reset button, shows current %
|
||||
- **Accent Color** — 10 preset OKLCH hue swatches: teal/160, blue/240, purple/300, pink/350, red/25, orange/55, yellow/85, lime/130, cyan/200, slate/achromatic. Click to apply immediately.
|
||||
- **Density** — 3-button toggle: Compact / Comfortable / Spacious
|
||||
|
||||
### Boards Tab
|
||||
- **Default column width** — 3-button toggle: Narrow / Standard / Wide
|
||||
|
||||
### Keyboard Shortcuts Tab
|
||||
- Read-only reference table, two-column: key combo (mono font) | description
|
||||
- All shortcuts: Ctrl+K, Ctrl+Z, Ctrl+Y, Ctrl+N, ?, etc.
|
||||
|
||||
### About Tab
|
||||
- App name, version, tagline
|
||||
- Link to repo (opens via Tauri shell)
|
||||
|
||||
---
|
||||
|
||||
## 3. Board Color Applied to UI
|
||||
|
||||
Currently `board.color` only shows on BoardCard in the home screen.
|
||||
|
||||
- **TopBar:** 2px bottom border in board color when viewing a board. Color dot next to board title.
|
||||
- **Column headers:** 3px top-border in board color at 30% opacity.
|
||||
- **No full background tinting** — structural accents only (borders, dots).
|
||||
|
||||
---
|
||||
|
||||
## 4. Column Colors
|
||||
|
||||
Extend `Column` interface:
|
||||
|
||||
```typescript
|
||||
export interface Column {
|
||||
id: string;
|
||||
title: string;
|
||||
cardIds: string[];
|
||||
width: ColumnWidth;
|
||||
color: string | null; // optional OKLCH hue, null = use board color
|
||||
}
|
||||
```
|
||||
|
||||
- Set via "Color" submenu in ColumnHeader dropdown (same 10 swatches + "None")
|
||||
- Column's 3px top-border uses column color when set, falls back to board color
|
||||
- Column background stays neutral
|
||||
|
||||
---
|
||||
|
||||
## 5. Card Cover Colors
|
||||
|
||||
Extend `Card` interface:
|
||||
|
||||
```typescript
|
||||
export interface Card {
|
||||
// ...existing
|
||||
coverColor: string | null; // OKLCH hue for color strip
|
||||
}
|
||||
```
|
||||
|
||||
- No image uploads for v1 — just a color bar
|
||||
- 4px colored bar at top of CardThumbnail
|
||||
- Set via swatch picker in CardDetailModal
|
||||
- Simple CSS, no layout disruption
|
||||
|
||||
---
|
||||
|
||||
## 6. Richer Card Thumbnails
|
||||
|
||||
Add to existing CardThumbnail footer row:
|
||||
|
||||
- **Attachment indicator** — paperclip icon + count (if `attachments.length > 0`)
|
||||
- **Description indicator** — text-lines icon (if `description` is non-empty)
|
||||
- **Cover color bar** — from Section 5
|
||||
|
||||
No priority badges or assignees — keeping thumbnails clean.
|
||||
|
||||
---
|
||||
|
||||
## 7. Toast Notification System
|
||||
|
||||
- `useToastStore` — Zustand store: `{ id, message, type }[]`
|
||||
- `<ToastContainer>` in App.tsx — fixed bottom-right, pills with auto-dismiss (3s + fade)
|
||||
- Types: `success` (green), `error` (red), `info` (neutral)
|
||||
- Fires on: board deleted, board exported, board imported, import failed, save error
|
||||
|
||||
---
|
||||
|
||||
## 8. Undo/Redo Buttons in TopBar
|
||||
|
||||
- Two icon buttons: RotateCcw (undo) and RotateCw (redo)
|
||||
- Placed in TopBar right section, before command palette button
|
||||
- Disabled when at start/end of history
|
||||
- Only visible in board view
|
||||
- Tooltips show keyboard shortcuts (Ctrl+Z / Ctrl+Y)
|
||||
|
||||
---
|
||||
|
||||
## 9. Keyboard Shortcut Help Modal
|
||||
|
||||
- Triggered by `?` key (when not in an input/textarea)
|
||||
- Two-column grid grouped by category: Navigation, Board, Cards
|
||||
- Same data as Settings keyboard shortcuts tab
|
||||
- Lightweight modal, dismissible with Escape or clicking outside
|
||||
|
||||
---
|
||||
|
||||
## 10. Board Backgrounds
|
||||
|
||||
Extend `BoardSettings`:
|
||||
|
||||
```typescript
|
||||
export interface BoardSettings {
|
||||
attachmentMode: "link" | "copy";
|
||||
background: "none" | "dots" | "grid" | "gradient";
|
||||
}
|
||||
```
|
||||
|
||||
- **none** — plain (current)
|
||||
- **dots** — subtle radial-gradient dot pattern, 5% opacity
|
||||
- **grid** — subtle grid lines via CSS
|
||||
- **gradient** — soft gradient using board color at 3-5% opacity
|
||||
- Set via board settings dropdown (gear icon in TopBar when viewing a board)
|
||||
|
||||
---
|
||||
|
||||
## 11. Onboarding / Empty States
|
||||
|
||||
- **First launch (zero boards):** Upgraded empty state — welcoming message, prominent "Create Board" button, secondary "Import Board" option
|
||||
- **Empty column:** Dashed-border area with "Drop or add a card" text
|
||||
- **Empty description:** "Click to add a description..." placeholder
|
||||
- **Empty checklist:** "Add your first item..." when empty
|
||||
|
||||
---
|
||||
|
||||
## 12. Polish Pass
|
||||
|
||||
- Consistent hover transitions (200ms ease) across all interactive elements
|
||||
- Verify focus rings work with all accent colors
|
||||
- Test Framer Motion springs at different zoom levels
|
||||
- Dark mode testing for all new features (column colors, card covers, backgrounds)
|
||||
- Thin, themed scrollbars on column scroll areas
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Settings model + app store actions + CSS variable application
|
||||
2. Settings panel UI (tabbed, all sections)
|
||||
3. UI zoom
|
||||
4. Accent color
|
||||
5. Density toggle
|
||||
6. Board color in UI (TopBar + column headers)
|
||||
7. Column colors
|
||||
8. Card cover colors
|
||||
9. Richer card thumbnails
|
||||
10. Toast notification system
|
||||
11. Undo/redo buttons
|
||||
12. Keyboard shortcut help modal
|
||||
13. Board backgrounds
|
||||
14. Onboarding / empty states
|
||||
15. Polish pass
|
||||
|
||||
## Files Affected
|
||||
|
||||
- `src/types/settings.ts` — expanded AppSettings
|
||||
- `src/types/board.ts` — Column.color, Card.coverColor, BoardSettings.background
|
||||
- `src/stores/app-store.ts` — new actions, applyAppearance()
|
||||
- `src/components/settings/SettingsDialog.tsx` — full rewrite (tabbed)
|
||||
- `src/index.css` — density variables, zoom hook, background patterns
|
||||
- `src/components/layout/TopBar.tsx` — board color, undo/redo buttons, board settings gear
|
||||
- `src/components/board/KanbanColumn.tsx` — column color border
|
||||
- `src/components/board/ColumnHeader.tsx` — color submenu
|
||||
- `src/components/board/CardThumbnail.tsx` — cover bar, attachment/description indicators
|
||||
- `src/components/card-detail/CardDetailModal.tsx` — cover color picker
|
||||
- `src/components/board/BoardView.tsx` — background patterns
|
||||
- `src/App.tsx` — ToastContainer, shortcut help modal, appearance init
|
||||
- `src/stores/toast-store.ts` — NEW
|
||||
- `src/components/toast/ToastContainer.tsx` — NEW
|
||||
- `src/components/shortcuts/ShortcutHelpModal.tsx` — NEW
|
||||
- `src/stores/board-store.ts` — new actions for column color, card cover
|
||||
- `src/lib/board-factory.ts` — defaults for new fields
|
||||
- `src/lib/schemas.ts` — migration for new fields
|
||||
- `src/components/boards/BoardList.tsx` — upgraded empty state
|
||||
- `src/hooks/useKeyboardShortcuts.ts` — ? key handler
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,296 +0,0 @@
|
||||
# OpenPylon: 15 Improvements Design
|
||||
|
||||
## Overview
|
||||
|
||||
15 improvements to OpenPylon organized into 5 phases (0-4), designed for incremental delivery. Each phase builds on the previous. You can ship after any phase and have a coherent improvement.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Phasing**: 4 phases (quick wins first, progressively bigger features)
|
||||
- **Data compatibility**: New fields use Zod `.default()` values. No migration code. Old boards load cleanly.
|
||||
- **Templates storage**: JSON files in `data/templates/`
|
||||
- **Backup storage**: Timestamped files in `data/backups/{boardId}/`, keep last 10
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Data Model Foundation
|
||||
|
||||
All type/schema changes that later features depend on. Done first so everything builds on a stable base.
|
||||
|
||||
### Card type additions
|
||||
|
||||
```typescript
|
||||
// Added to Card interface + cardSchema
|
||||
priority: "none" | "low" | "medium" | "high" | "urgent"; // default: "none"
|
||||
comments: Comment[]; // default: []
|
||||
|
||||
// New type + schema
|
||||
interface Comment {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Column type additions
|
||||
|
||||
```typescript
|
||||
// Added to Column interface + columnSchema
|
||||
collapsed: boolean; // default: false
|
||||
wipLimit: number | null; // default: null
|
||||
```
|
||||
|
||||
### Files touched
|
||||
|
||||
- `src/types/board.ts` — Add fields to Card, Column interfaces. Add Comment interface.
|
||||
- `src/lib/schemas.ts` — Add Zod fields with defaults to cardSchema, columnSchema. Add commentSchema.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Quick Wins
|
||||
|
||||
Minimal changes, high value. ~4 files each.
|
||||
|
||||
### #8 — Consume defaultColumnWidth Setting
|
||||
|
||||
In `board-store.ts`, `addColumn` reads `useAppStore.getState().settings.defaultColumnWidth` instead of hardcoding `"standard"`.
|
||||
|
||||
**Files**: `src/stores/board-store.ts` (1 line change)
|
||||
|
||||
### #4 — Due Date Visual Indicators
|
||||
|
||||
Replace binary overdue/not logic in `CardThumbnail` with 4-tier color system:
|
||||
|
||||
| Status | Condition | Color |
|
||||
|--------|-----------|-------|
|
||||
| Overdue | past + not today | `pylon-danger` (red) |
|
||||
| Approaching | due within 2 days | amber `oklch(65% 0.15 70)` |
|
||||
| Comfortable | due but >2 days | green `oklch(55% 0.12 145)` |
|
||||
| No date | null | `pylon-text-secondary` (gray) |
|
||||
|
||||
Helper function `getDueDateStatus(dueDate: string | null)` returns `{ color, label }`.
|
||||
|
||||
**Files**: `src/components/board/CardThumbnail.tsx`
|
||||
|
||||
### #9 — Card Aging Visualization
|
||||
|
||||
Compute days since `card.updatedAt`. Apply opacity:
|
||||
|
||||
| Days stale | Opacity |
|
||||
|------------|---------|
|
||||
| 0-7 | 1.0 |
|
||||
| 7-14 | 0.85 |
|
||||
| 14-30 | 0.7 |
|
||||
| 30+ | 0.55 |
|
||||
|
||||
Applied as inline `opacity` on the card `motion.button`.
|
||||
|
||||
**Files**: `src/components/board/CardThumbnail.tsx`
|
||||
|
||||
### #12 — Open Attachments
|
||||
|
||||
Add "Open" button to each attachment in `AttachmentSection`. Uses `open()` from `@tauri-apps/plugin-opener` (already registered).
|
||||
|
||||
**Files**: `src/components/card-detail/AttachmentSection.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Card Interactions & UI Enhancements
|
||||
|
||||
5 features that transform how cards feel to use.
|
||||
|
||||
### #2 — Card Priority Levels
|
||||
|
||||
**Thumbnail indicator**: Colored dot in footer row. Color map:
|
||||
- `none`: hidden
|
||||
- `low`: blue
|
||||
- `medium`: yellow
|
||||
- `high`: orange
|
||||
- `urgent`: red with pulse animation
|
||||
|
||||
**Detail modal**: Priority picker section in left column (like LabelPicker). Row of 5 clickable chips with colors.
|
||||
|
||||
**Files**: `CardThumbnail.tsx`, `CardDetailModal.tsx` (new PriorityPicker component inline or separate), `board-store.ts` (no new action needed — `updateCard` handles it)
|
||||
|
||||
### #5 — Card Context Menu
|
||||
|
||||
Wrap `CardThumbnail` in Radix `ContextMenu`.
|
||||
|
||||
**Menu items**:
|
||||
- Move to → (submenu listing columns except current)
|
||||
- Set priority → (submenu with 5 options)
|
||||
- Duplicate (new card with same fields, `(copy)` suffix, new ID, inserted below original)
|
||||
- Separator
|
||||
- Delete (confirmation dialog)
|
||||
|
||||
**New store action**: `duplicateCard(cardId): string` — clones card, inserts after original in same column.
|
||||
|
||||
**Files**: `CardThumbnail.tsx`, `board-store.ts`
|
||||
|
||||
### #10 — WIP Limits
|
||||
|
||||
**Column header display**: Shows `3/5` when wipLimit set. Background tint:
|
||||
- Under limit: normal
|
||||
- At limit: amber tint `oklch(75% 0.08 70 / 15%)`
|
||||
- Over limit: red tint `oklch(70% 0.08 25 / 15%)`
|
||||
|
||||
**Setting UI**: New "Set WIP Limit" item in ColumnHeader dropdown menu. Preset choices: None / 3 / 5 / 7 / 10 / Custom.
|
||||
|
||||
**New store action**: `setColumnWipLimit(columnId: string, limit: number | null)`
|
||||
|
||||
**Files**: `ColumnHeader.tsx`, `KanbanColumn.tsx`, `board-store.ts`
|
||||
|
||||
### #3 — Column Collapse/Expand
|
||||
|
||||
When `collapsed`, render a 40px-wide strip instead of full column:
|
||||
- Vertical text via `writing-mode: vertical-rl; rotate: 180deg`
|
||||
- Card count badge
|
||||
- Click to expand
|
||||
|
||||
Animate width from full to 40px using existing `animate={{ width }}` on outer `motion.div` with `springs.bouncy`.
|
||||
|
||||
**New store action**: `toggleColumnCollapse(columnId: string)`
|
||||
|
||||
**Collapse button**: Added to ColumnHeader dropdown menu + a small chevron icon on the collapsed strip.
|
||||
|
||||
**Files**: `KanbanColumn.tsx`, `ColumnHeader.tsx`, `board-store.ts`
|
||||
|
||||
### #11 — Checklist Item Reordering
|
||||
|
||||
Wrap checklist `<ul>` in `ChecklistSection` with `DndContext` + `SortableContext` (vertical strategy). Each `ChecklistRow` becomes sortable.
|
||||
|
||||
**Drag handle**: `GripVertical` icon on left of each item, visible on hover.
|
||||
|
||||
**Drop indicator**: Horizontal glow line (same as card drag — vertical list so horizontal line is correct).
|
||||
|
||||
**New store action**: `reorderChecklistItems(cardId: string, fromIndex: number, toIndex: number)`
|
||||
|
||||
**Files**: `ChecklistSection.tsx`, `board-store.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Navigation & Power User Features
|
||||
|
||||
Features that make power users fall in love.
|
||||
|
||||
### #1 — Card Filtering & Quick Search
|
||||
|
||||
**Filter bar**: Slides down below TopBar. Triggered by filter icon in TopBar or `/` keyboard shortcut.
|
||||
|
||||
**Filter controls** (horizontal row):
|
||||
- Text input (debounced 200ms, title search)
|
||||
- Label multi-select dropdown (ANY match)
|
||||
- Due date dropdown: All / Overdue / Due this week / Due today / No date
|
||||
- Priority dropdown: All / Urgent / High / Medium / Low
|
||||
- Clear all button
|
||||
|
||||
**State**: Local state in `BoardView` (not persisted — filters are ephemeral).
|
||||
|
||||
**Rendering**: `KanbanColumn` receives filtered card IDs. Non-matching cards fade out. Column counts show `3 of 7` when filtering.
|
||||
|
||||
**Files**: New `FilterBar.tsx` component, `BoardView.tsx`, `KanbanColumn.tsx`, `TopBar.tsx` (filter button)
|
||||
|
||||
### #7 — Keyboard Card Navigation
|
||||
|
||||
**State**: `focusedCardId` in `BoardView` local state.
|
||||
|
||||
**Key bindings** (when no input/textarea focused):
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `J` / `ArrowDown` | Focus next card in column |
|
||||
| `K` / `ArrowUp` | Focus previous card in column |
|
||||
| `H` / `ArrowLeft` | Focus same-index card in previous column |
|
||||
| `L` / `ArrowRight` | Focus same-index card in next column |
|
||||
| `Enter` | Open focused card detail |
|
||||
| `Escape` | Clear focus / close modal |
|
||||
|
||||
**Visual**: Focused card gets `ring-2 ring-pylon-accent ring-offset-2`. Column auto-scrolls via `scrollIntoView({ block: "nearest" })`.
|
||||
|
||||
**Implementation**: `useKeyboardNavigation` hook. Passes `isFocused` through `KanbanColumn` to `CardThumbnail`.
|
||||
|
||||
**Files**: New `useKeyboardNavigation.ts` hook, `BoardView.tsx`, `KanbanColumn.tsx`, `CardThumbnail.tsx`
|
||||
|
||||
### #6 — Desktop Notifications for Due Dates
|
||||
|
||||
**Plugin**: Add `tauri-plugin-notification` to `Cargo.toml` and capabilities.
|
||||
|
||||
**Trigger**: On `useAppStore.init()`, after loading boards, scan all cards:
|
||||
- Cards due today → "You have X cards due today"
|
||||
- Cards overdue → "You have X overdue cards"
|
||||
|
||||
Batched (one notification per category). Store `lastNotificationCheck` in settings to skip if checked within last hour.
|
||||
|
||||
**Files**: `src-tauri/Cargo.toml`, `src-tauri/capabilities/default.json`, `src/stores/app-store.ts`, `src/types/settings.ts` (add `lastNotificationCheck`), `src/lib/schemas.ts`
|
||||
|
||||
### #13 — Card Comments / Activity Log
|
||||
|
||||
**UI in CardDetailModal**: New section in right column below description.
|
||||
- Scrollable comment list (newest first)
|
||||
- Each: text, relative timestamp, delete button (hover)
|
||||
- Add input: textarea + "Add" button. Enter submits, Shift+Enter newline.
|
||||
|
||||
**Store actions**: `addComment(cardId, text)`, `deleteComment(cardId, commentId)`. Comments get ULID IDs and `createdAt`.
|
||||
|
||||
**Files**: New `CommentsSection.tsx`, `CardDetailModal.tsx`, `board-store.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: System Features & Infrastructure
|
||||
|
||||
Deeper features touching storage and templates.
|
||||
|
||||
### #14 — Board Templates & Saved Structures
|
||||
|
||||
**Template type**:
|
||||
```typescript
|
||||
interface BoardTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
columns: { title: string; width: ColumnWidth; color: string | null; wipLimit: number | null }[];
|
||||
labels: Label[];
|
||||
settings: BoardSettings;
|
||||
}
|
||||
```
|
||||
|
||||
**Saving**: Context menu item on `BoardCard` — "Save as Template". Prompts for name. Strips cards/timestamps. Writes to `data/templates/{id}.json`.
|
||||
|
||||
**Creating**: `NewBoardDialog` shows built-in templates (Blank, Kanban, Sprint) + user templates below a separator. Delete button (X) on user templates.
|
||||
|
||||
**Storage functions**: `listTemplates()`, `saveTemplate()`, `deleteTemplate()` in `storage.ts`. `board-factory.ts` gets `createBoardFromTemplate()`.
|
||||
|
||||
**Files**: `storage.ts`, `board-factory.ts`, `NewBoardDialog.tsx`, `BoardCard.tsx`, new `src/types/template.ts`
|
||||
|
||||
### #15 — Auto-Backup & Version History
|
||||
|
||||
**Storage**: `data/backups/{boardId}/` directory. Timestamped files: `{boardId}-{ISO timestamp}.json`.
|
||||
|
||||
**Save flow** in `board-store.ts`:
|
||||
1. Read current file as previous version
|
||||
2. Write new board to `{boardId}.json`
|
||||
3. Write previous version to `data/backups/{boardId}/{boardId}-{timestamp}.json`
|
||||
4. Prune backups beyond 10
|
||||
|
||||
**UI — Version History dialog**: Accessible from board settings dropdown menu ("Version History"). Shows:
|
||||
- List of backups sorted newest-first
|
||||
- Each entry: relative timestamp, card count, column count
|
||||
- "Restore" button with confirmation dialog
|
||||
- Current board auto-backed-up before restore (restore is reversible)
|
||||
|
||||
**Storage functions**: `listBackups(boardId)`, `restoreBackup(boardId, filename)`, `pruneBackups(boardId, keep)`.
|
||||
|
||||
**Files**: `storage.ts`, `board-store.ts`, new `VersionHistoryDialog.tsx`, `TopBar.tsx` (menu item)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 0 (data model)
|
||||
└── Phase 1 (quick wins) — no deps on Phase 0 except #8
|
||||
└── Phase 2 (card interactions) — needs priority + collapsed + wipLimit from Phase 0
|
||||
└── Phase 3 (power user) — needs priority for filtering, context menu patterns from Phase 2
|
||||
└── Phase 4 (infrastructure) — needs wipLimit in templates from Phase 0+2
|
||||
```
|
||||
|
||||
Phase 1 can actually run in parallel with Phase 0 since its features don't touch the new fields. Phases 2-4 are strictly sequential.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenPylon</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
Reference in New Issue
Block a user