-

- {/* Glow effect behind image */}
-
+ {/* Play button overlay */}
+
+
+
+ {/* Video modal */}
+
+ {isVideoOpen && hasVideo && (
+ {
+ if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
+ setIsVideoOpen(false);
+ }
+ }}
+ onClick={() => setIsVideoOpen(false)}
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
+ >
+ e.stopPropagation()}
+ >
+
+
+
+
+
+
+ )}
+
+ >
+ );
+}
+
+export default function Hero() {
+ const config = getProductConfig();
+
+ return (
+
+
+
+ {/* Text content — centered in viewport */}
+
+ {/* Version badge */}
+
+
+ v0.6 Early access
+
+
+
+ {/* Headline */}
+
+ Your Markdown.
+
+ Your Machine.
+
+ Your Rules.
+
+
+ {/* Subtext */}
+
+ A note app that doesn't phone home, doesn't lock you in, and doesn't need
+ an internet connection. Just you, your files, and Markdown.
+
+
+ {/* CTAs */}
+
+
+
+ Download for Mac
+
+
+
+ View on GitHub
+
+
+
+ {/* Version info */}
+
+ v0.6.2 | macOS 13+ | Windows & Linux coming soon
+
+
+ {/* Trust line */}
+
+ Offline-first
+ |
+ Open source
+ |
+ Free forever · Pro {config.trialDays}-day trial
+
+
+
+ {/* Video / Editor preview */}
+
+ {/*
+ TODO: Replace with real video once recorded.
+ 1. Upload video to YouTube, get embed URL
+ 2. Take a screenshot for thumbnailSrc
+ 3. Swap videoSrc and thumbnailSrc below
+ */}
+
);
diff --git a/apps/web/components/landing/SocialProof.tsx b/apps/web/components/landing/SocialProof.tsx
index 32be610e..f1cf259b 100644
--- a/apps/web/components/landing/SocialProof.tsx
+++ b/apps/web/components/landing/SocialProof.tsx
@@ -1,29 +1,29 @@
-import { WifiOff, FileText, Monitor, Star } from 'lucide-react';
+import { WifiOff, HardDrive, Github, Puzzle, Monitor } from 'lucide-react';
+
+import { Marquee } from '@/components/magicui/marquee';
+
+const badges = [
+ { icon: WifiOff, label: 'Offline First' },
+ { icon: HardDrive, label: 'Local Storage' },
+ { icon: Github, label: 'Open Source' },
+ { icon: Puzzle, label: 'Plugin System' },
+ { icon: Monitor, label: 'Cross-platform' },
+];
export default function SocialProof() {
return (
-
-
-
- Works 100% offline
-
-
-
- Standard Markdown
-
-
-
- macOS, Windows & Linux
-
-
-
- Star on GitHub
-
-
+
+
+
);
}
diff --git a/apps/web/components/landing/Testimonials.tsx b/apps/web/components/landing/Testimonials.tsx
new file mode 100644
index 00000000..aa010341
--- /dev/null
+++ b/apps/web/components/landing/Testimonials.tsx
@@ -0,0 +1,134 @@
+'use client';
+
+import { Star } from 'lucide-react';
+import { Marquee } from '@/components/magicui/marquee';
+import { cn } from '@/lib/utils';
+
+/* ─── Review data (replace with real reviews when available) ─── */
+
+const reviews = [
+ {
+ name: 'Alex C.',
+ role: 'Senior Developer',
+ stars: 5,
+ text: 'Finally a note app that respects my workflow. Plain Markdown, no sync drama, just works. Exactly what I needed.',
+ date: 'Mar 2026',
+ },
+ {
+ name: 'Maria G.',
+ role: 'Technical Writer',
+ stars: 5,
+ text: "I switched from Notion and haven't looked back. Readied is fast, offline, and my files are just .md on disk. Love it.",
+ date: 'Mar 2026',
+ },
+ {
+ name: 'James W.',
+ role: 'Indie Hacker',
+ stars: 5,
+ text: 'The plugin system is surprisingly powerful for such an early product. Built a custom snippet plugin in an afternoon.',
+ date: 'Feb 2026',
+ },
+ {
+ name: 'Yuki T.',
+ role: 'Software Engineer',
+ stars: 5,
+ text: 'Open source, local-first, Markdown — checks all my boxes. The CodeMirror 6 editor is buttery smooth.',
+ date: 'Feb 2026',
+ },
+ {
+ name: 'Sarah M.',
+ role: 'Privacy Researcher',
+ stars: 5,
+ text: 'Zero telemetry, zero cloud. I audited the source. This is the real deal for people who care about data ownership.',
+ date: 'Jan 2026',
+ },
+ {
+ name: 'Daniel P.',
+ role: 'Full-Stack Developer',
+ stars: 5,
+ text: 'Cmd+P to jump between notes is addictive. I organize everything in Markdown now — meeting notes, journals, code docs.',
+ date: 'Jan 2026',
+ },
+];
+
+const firstRow = reviews.slice(0, reviews.length / 2);
+const secondRow = reviews.slice(reviews.length / 2);
+
+function Stars({ count }: { count: number }) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+function ReviewCard({
+ name,
+ role,
+ stars,
+ text,
+ date,
+}: {
+ name: string;
+ role: string;
+ stars: number;
+ text: string;
+ date: string;
+}) {
+ return (
+
+
+ {/* Avatar placeholder */}
+
+ {name
+ .split(' ')
+ .map(n => n[0])
+ .join('')}
+
+
+
+
+
+
+ {text}
+
+ {date}
+
+ );
+}
+
+export default function Testimonials() {
+ return (
+
+
+
Reviews
+
What developers are saying
+
+ Early feedback from our beta community.
+
+
+
+ {/* Two-row marquee like Inkdrop's masonry */}
+
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/VideoGuides.tsx b/apps/web/components/landing/VideoGuides.tsx
new file mode 100644
index 00000000..3ecba286
--- /dev/null
+++ b/apps/web/components/landing/VideoGuides.tsx
@@ -0,0 +1,180 @@
+'use client';
+
+import { useState } from 'react';
+import { Play, XIcon } from 'lucide-react';
+import { AnimatePresence, motion } from 'framer-motion';
+
+/* ─── Video data (replace with real URLs when recorded) ─── */
+
+const videos = [
+ {
+ id: 'getting-started',
+ title: 'Getting started with Readied',
+ description: 'Set up your workspace and write your first note in under 2 minutes.',
+ videoSrc: '', // TODO: YouTube embed URL
+ duration: '2:30',
+ },
+ {
+ id: 'markdown-workflow',
+ title: 'Markdown workflow tips',
+ description: 'Keyboard shortcuts, split preview, and Cmd+P navigation.',
+ videoSrc: '', // TODO: YouTube embed URL
+ duration: '4:15',
+ },
+ {
+ id: 'plugins',
+ title: 'Extending with plugins',
+ description: 'How to install, configure, and build plugins for Readied.',
+ videoSrc: '', // TODO: YouTube embed URL
+ duration: '5:00',
+ },
+];
+
+/* ─── Video card with play overlay ─── */
+
+function VideoCard({
+ title,
+ description,
+ duration,
+ videoSrc,
+}: {
+ title: string;
+ description: string;
+ duration: string;
+ videoSrc: string;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+ const hasVideo = videoSrc.length > 0;
+
+ return (
+ <>
+
hasVideo && setIsOpen(true)}
+ onKeyDown={(e: React.KeyboardEvent) => {
+ if (hasVideo && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault();
+ setIsOpen(true);
+ }
+ }}
+ className={`group relative w-full overflow-hidden rounded-xl border border-border bg-surface text-left transition-all hover:border-accent/30 ${
+ hasVideo ? 'cursor-pointer' : 'cursor-default'
+ }`}
+ >
+ {/* Thumbnail placeholder */}
+
+ {/* Decorative code lines */}
+
+
+ {/* Play button */}
+
+
+ {/* Duration badge */}
+
+ {duration}
+
+
+ {/* "Coming soon" overlay if no video */}
+ {!hasVideo && (
+
+
+ Coming soon
+
+
+ )}
+
+
+ {/* Text */}
+
+
+ {title}
+
+
{description}
+
+
+
+ {/* Video modal */}
+
+ {isOpen && hasVideo && (
+ {
+ if (e.key === 'Escape') setIsOpen(false);
+ }}
+ onClick={() => setIsOpen(false)}
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md"
+ >
+ e.stopPropagation()}
+ >
+
+
+
+
+
+
+ )}
+
+ >
+ );
+}
+
+/* ─── Section ─── */
+
+export default function VideoGuides() {
+ return (
+
+
+
+
Learn
+
See Readied in action
+
+ Short guides to help you get the most out of your writing workflow.
+
+
+
+
+ {videos.map(video => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/WhyLocal.tsx b/apps/web/components/landing/WhyLocal.tsx
index 66b3688c..bdac47d3 100644
--- a/apps/web/components/landing/WhyLocal.tsx
+++ b/apps/web/components/landing/WhyLocal.tsx
@@ -1,100 +1,331 @@
-import { FolderOpen, FileText } from 'lucide-react';
-import ComparisonTable from './ComparisonTable';
+'use client';
+
+import React, { forwardRef, useRef } from 'react';
+import {
+ FolderOpen,
+ FileText,
+ Check,
+ X,
+ WifiOff,
+ Lock,
+ CloudOff,
+ Terminal,
+ HardDrive,
+ Pen,
+ FolderClosed,
+} from 'lucide-react';
+import { AnimatedBeam } from '@/components/magicui/animated-beam';
+import { BorderBeam } from '@/components/magicui/border-beam';
+import { cn } from '@/lib/utils';
+
+/* ─── Comparison table ─── */
+
+type Row = { label: string; readied: boolean | string; cloud: boolean | string };
+
+const rows: Row[] = [
+ { label: 'Data Location', readied: 'Your machine', cloud: 'Their servers' },
+ { label: 'Works Offline', readied: true, cloud: false },
+ { label: 'Export Anytime', readied: true, cloud: 'Maybe' },
+ { label: 'Open Source', readied: true, cloud: false },
+ { label: 'Vendor Lock-in', readied: false, cloud: true },
+];
+
+function CellValue({ value, positive }: { value: boolean | string; positive: boolean }) {
+ if (typeof value === 'string') {
+ return (
+
{value}
+ );
+ }
+ const label = value ? 'Yes' : 'No';
+ if (value) {
+ return positive ? (
+ <>
+
+
{label}
+ >
+ ) : (
+ <>
+
+
{label}
+ >
+ );
+ }
+ return positive ? (
+ <>
+
+
{label}
+ >
+ ) : (
+ <>
+
+
{label}
+ >
+ );
+}
+
+/* ─── Bento card ─── */
+
+function BentoCard({ className, children }: { className?: string; children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+/* ─── Circle node (matches official Magic UI pattern) ─── */
+
+const Circle = forwardRef
(
+ ({ className, children }, ref) => (
+
+ {children}
+
+ )
+);
+Circle.displayName = 'Circle';
+
+/* ─── Data flow: You → Readied → local .md files ─── */
+
+function DataFlowDiagram() {
+ const containerRef = useRef(null);
+ const userRef = useRef(null);
+ const centerRef = useRef(null);
+ const file1Ref = useRef(null);
+ const file2Ref = useRef(null);
+ const file3Ref = useRef(null);
+ const folderRef = useRef(null);
+
+ return (
+
+
+ {/* Left: You */}
+
+
+ {/* Center: Readied */}
+
+
+ R
+
+
+
+ {/* Right: Local files */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* You → Readied */}
+
+
+ {/* Readied → files */}
+
+
+
+
+
+ );
+}
+
+/* ─── Main section ─── */
export default function WhyLocal() {
return (
-
-
-
- Why local-first?
-
- Most note apps treat your data like
-
- their business asset.
+
+
+ {/* Header */}
+
+
Why Local
+
+ Your notes should live on your machine
+ {' — '}not someone else's server.
-
- We don't. Your files stay on your disk, in standard Markdown, forever.
-
-
-
-
-
- {/* Navigation demo */}
-
-

-
- Quick-open: jump between notes with Cmd+P
+
+ Cloud note apps hold your data hostage. Readied takes a different approach: everything
+ is local, everything is Markdown, everything is yours.
- {/* Visual: file format proof */}
-
-
- {/* Left: what you write */}
-
-
-
- What you write
-
-
-
-
- #
- Meeting Notes
-
-
-
- - Decided on local-first architecture
-
-
- - Launch timeline: Q2 2026
-
-
- - Next step: prototype by Friday
-
+ {/* Bento grid */}
+
+ {/* Row 1: Data flow diagram (span 2) + Offline card */}
+
+
+
+ Your data never leaves
+
+
+ You write → Readied saves → plain .md files on your disk. No cloud.
+
+
+
+
+
+
+
+
+
+
No internet required
+
+ Everything runs locally. Write on a plane, in a cabin, anywhere.
+
+
+
+
+ {/* Row 2: Three feature cards */}
+
+
+
+ Your data stays yours
+
+ Notes live in plain .md files on your filesystem. No cloud sync, no telemetry.
+
+
- {/* Right: what's on disk */}
-
-
-
- What's on your disk
-
+
+
+
+
+ No vendor lock-in
+
+ Standard Markdown. Open your files with any editor, any time.
+
+
+
+
+
+
+
+ Open source
+
+ Every line of code is on GitHub. Audit it, fork it, contribute.
+
+
+
+ {/* Row 3: Comparison table (span 2) + File tree */}
+
+
+
+
+ | Feature |
+ Readied |
+ Cloud Apps |
+
+
+
+ {rows.map(row => (
+
+ | {row.label} |
+
+
+ |
+
+
+ |
+
+ ))}
+
+
+
+
+
+
+
+ On your disk
+
+
+
+
+ ~/notes/
-
-
-
- ~/notes/
-
-
-
- meeting-notes.md
- 1.2 KB
-
-
-
- project-plan.md
- 3.4 KB
-
-
-
- ideas.md
- 0.8 KB
-
+
+
+ meeting-notes.md
+ 1.2 KB
+
+
+
+ project-plan.md
+ 3.4 KB
+
+
+
+ ideas.md
+ 0.8 KB
-
- Plain .md files. Open with any editor.
-
-
+
+ Plain .md files. Open with any editor.
+
+
diff --git a/apps/web/components/magicui/animated-beam.tsx b/apps/web/components/magicui/animated-beam.tsx
new file mode 100644
index 00000000..b824e4e9
--- /dev/null
+++ b/apps/web/components/magicui/animated-beam.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import { type RefObject, useEffect, useId, useState } from 'react';
+import { motion } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+export interface AnimatedBeamProps {
+ className?: string;
+ containerRef: RefObject
;
+ fromRef: RefObject;
+ toRef: RefObject;
+ curvature?: number;
+ reverse?: boolean;
+ duration?: number;
+ delay?: number;
+ pathColor?: string;
+ pathWidth?: number;
+ pathOpacity?: number;
+ gradientStartColor?: string;
+ gradientStopColor?: string;
+ startXOffset?: number;
+ startYOffset?: number;
+ endXOffset?: number;
+ endYOffset?: number;
+}
+
+export const AnimatedBeam: React.FC = ({
+ className,
+ containerRef,
+ fromRef,
+ toRef,
+ curvature = 0,
+ reverse = false,
+ duration = Math.random() * 3 + 4,
+ delay = 0,
+ pathColor = 'gray',
+ pathWidth = 2,
+ pathOpacity = 0.2,
+ gradientStartColor = '#ffaa40',
+ gradientStopColor = '#9c40ff',
+ startXOffset = 0,
+ startYOffset = 0,
+ endXOffset = 0,
+ endYOffset = 0,
+}) => {
+ const id = useId();
+ const [pathD, setPathD] = useState('');
+ const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 });
+
+ const gradientCoordinates = reverse
+ ? {
+ x1: ['90%', '-10%'],
+ x2: ['100%', '0%'],
+ y1: ['0%', '0%'],
+ y2: ['0%', '0%'],
+ }
+ : {
+ x1: ['10%', '110%'],
+ x2: ['0%', '100%'],
+ y1: ['0%', '0%'],
+ y2: ['0%', '0%'],
+ };
+
+ useEffect(() => {
+ const updatePath = () => {
+ if (containerRef.current && fromRef.current && toRef.current) {
+ const containerRect = containerRef.current.getBoundingClientRect();
+ const rectA = fromRef.current.getBoundingClientRect();
+ const rectB = toRef.current.getBoundingClientRect();
+
+ const svgWidth = containerRect.width;
+ const svgHeight = containerRect.height;
+ setSvgDimensions({ width: svgWidth, height: svgHeight });
+
+ const startX = rectA.left - containerRect.left + rectA.width / 2 + startXOffset;
+ const startY = rectA.top - containerRect.top + rectA.height / 2 + startYOffset;
+ const endX = rectB.left - containerRect.left + rectB.width / 2 + endXOffset;
+ const endY = rectB.top - containerRect.top + rectB.height / 2 + endYOffset;
+
+ const controlY = startY - curvature;
+ const d = `M ${startX},${startY} Q ${(startX + endX) / 2},${controlY} ${endX},${endY}`;
+ setPathD(d);
+ }
+ };
+
+ const resizeObserver = new ResizeObserver(() => {
+ updatePath();
+ });
+
+ if (containerRef.current) {
+ resizeObserver.observe(containerRef.current);
+ }
+
+ updatePath();
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, [containerRef, fromRef, toRef, curvature, startXOffset, startYOffset, endXOffset, endYOffset]);
+
+ return (
+
+ );
+};
diff --git a/apps/web/components/magicui/animated-grid-pattern.tsx b/apps/web/components/magicui/animated-grid-pattern.tsx
new file mode 100644
index 00000000..777dac64
--- /dev/null
+++ b/apps/web/components/magicui/animated-grid-pattern.tsx
@@ -0,0 +1,141 @@
+'use client';
+
+import { ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from 'react';
+import { motion } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+export interface AnimatedGridPatternProps extends ComponentPropsWithoutRef<'svg'> {
+ width?: number;
+ height?: number;
+ x?: number;
+ y?: number;
+ strokeDasharray?: number;
+ numSquares?: number;
+ maxOpacity?: number;
+ duration?: number;
+ repeatDelay?: number;
+}
+
+export function AnimatedGridPattern({
+ width = 40,
+ height = 40,
+ x = -1,
+ y = -1,
+ strokeDasharray = 0,
+ numSquares = 50,
+ className,
+ maxOpacity = 0.5,
+ duration = 4,
+ repeatDelay = 1,
+ ...props
+}: AnimatedGridPatternProps) {
+ const id = useId();
+ const containerRef = useRef(null);
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
+
+ const getPos = useCallback(() => {
+ return [
+ Math.floor((Math.random() * dimensions.width) / width),
+ Math.floor((Math.random() * dimensions.height) / height),
+ ];
+ }, [dimensions.width, dimensions.height, width, height]);
+
+ const generateSquares = useCallback(
+ (count: number) => {
+ return Array.from({ length: count }, (_, i) => ({
+ id: i,
+ pos: getPos(),
+ }));
+ },
+ [getPos]
+ );
+
+ const [squares, setSquares] = useState(() => generateSquares(numSquares));
+
+ // Function to update a single square's position
+ const updateSquarePosition = (id: number) => {
+ setSquares(currentSquares =>
+ currentSquares.map(sq =>
+ sq.id === id
+ ? {
+ ...sq,
+ pos: getPos(),
+ }
+ : sq
+ )
+ );
+ };
+
+ // Update squares to animate in
+ useEffect(() => {
+ if (dimensions.width && dimensions.height) {
+ setSquares(generateSquares(numSquares));
+ }
+ }, [dimensions, numSquares, generateSquares]);
+
+ // Resize observer to update container dimensions
+ useEffect(() => {
+ const currentRef = containerRef.current;
+ const resizeObserver = new ResizeObserver(entries => {
+ for (const entry of entries) {
+ setDimensions({
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ });
+ }
+ });
+
+ if (currentRef) {
+ resizeObserver.observe(currentRef);
+ }
+
+ return () => {
+ if (currentRef) {
+ resizeObserver.unobserve(currentRef);
+ }
+ };
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/apps/web/components/magicui/animated-shiny-text.tsx b/apps/web/components/magicui/animated-shiny-text.tsx
new file mode 100644
index 00000000..ad64881a
--- /dev/null
+++ b/apps/web/components/magicui/animated-shiny-text.tsx
@@ -0,0 +1,37 @@
+import { ComponentPropsWithoutRef, CSSProperties, FC } from 'react';
+import { cn } from '@/lib/utils';
+
+export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<'span'> {
+ shimmerWidth?: number;
+}
+
+export const AnimatedShinyText: FC = ({
+ children,
+ className,
+ shimmerWidth = 100,
+ ...props
+}) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/apps/web/components/magicui/border-beam.tsx b/apps/web/components/magicui/border-beam.tsx
new file mode 100644
index 00000000..d2e5a88b
--- /dev/null
+++ b/apps/web/components/magicui/border-beam.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import { motion, type MotionStyle, type Transition, useReducedMotion } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+interface BorderBeamProps {
+ /**
+ * The size of the border beam.
+ */
+ size?: number;
+ /**
+ * The duration of the border beam.
+ */
+ duration?: number;
+ /**
+ * The delay of the border beam.
+ */
+ delay?: number;
+ /**
+ * The color of the border beam from.
+ */
+ colorFrom?: string;
+ /**
+ * The color of the border beam to.
+ */
+ colorTo?: string;
+ /**
+ * The motion transition of the border beam.
+ */
+ transition?: Transition;
+ /**
+ * The class name of the border beam.
+ */
+ className?: string;
+ /**
+ * The style of the border beam.
+ */
+ style?: React.CSSProperties;
+ /**
+ * Whether to reverse the animation direction.
+ */
+ reverse?: boolean;
+ /**
+ * The initial offset position (0-100).
+ */
+ initialOffset?: number;
+ /**
+ * The border width of the beam.
+ */
+ borderWidth?: number;
+}
+
+export const BorderBeam = ({
+ className,
+ size = 50,
+ delay = 0,
+ duration = 6,
+ colorFrom = '#ffaa40',
+ colorTo = '#9c40ff',
+ transition,
+ style,
+ reverse = false,
+ initialOffset = 0,
+ borderWidth = 1,
+}: BorderBeamProps) => {
+ const shouldReduceMotion = useReducedMotion();
+
+ if (shouldReduceMotion) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/web/components/magicui/dot-pattern.tsx b/apps/web/components/magicui/dot-pattern.tsx
new file mode 100644
index 00000000..bfc57468
--- /dev/null
+++ b/apps/web/components/magicui/dot-pattern.tsx
@@ -0,0 +1,119 @@
+'use client';
+
+import React, { useEffect, useId, useRef, useState } from 'react';
+import { motion } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+interface DotPatternProps extends React.SVGProps {
+ width?: number;
+ height?: number;
+ x?: number;
+ y?: number;
+ cx?: number;
+ cy?: number;
+ cr?: number;
+ className?: string;
+ glow?: boolean;
+ [key: string]: unknown;
+}
+
+export function DotPattern({
+ width = 16,
+ height = 16,
+ x = 0,
+ y = 0,
+ cx = 1,
+ cy = 1,
+ cr = 1,
+ className,
+ glow = false,
+ ...props
+}: DotPatternProps) {
+ const id = useId();
+ const containerRef = useRef(null);
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
+
+ useEffect(() => {
+ const updateDimensions = () => {
+ if (containerRef.current) {
+ const { width, height } = containerRef.current.getBoundingClientRect();
+ setDimensions({ width, height });
+ }
+ };
+
+ updateDimensions();
+ window.addEventListener('resize', updateDimensions);
+ return () => window.removeEventListener('resize', updateDimensions);
+ }, []);
+
+ const safeWidth = Math.max(1, width);
+ const safeHeight = Math.max(1, height);
+
+ const dots = Array.from(
+ {
+ length:
+ dimensions.width > 0 && dimensions.height > 0
+ ? Math.ceil(dimensions.width / safeWidth) * Math.ceil(dimensions.height / safeHeight)
+ : 0,
+ },
+ (_, i) => {
+ const col = i % Math.ceil(dimensions.width / safeWidth);
+ const row = Math.floor(i / Math.ceil(dimensions.width / safeWidth));
+ return {
+ x: col * safeWidth + cx + x,
+ y: row * safeHeight + cy + y,
+ delay: Math.random() * 5,
+ duration: Math.random() * 3 + 2,
+ };
+ }
+ );
+
+ return (
+
+ );
+}
diff --git a/apps/web/components/magicui/hero-video-dialog.tsx b/apps/web/components/magicui/hero-video-dialog.tsx
new file mode 100644
index 00000000..d73dc0bf
--- /dev/null
+++ b/apps/web/components/magicui/hero-video-dialog.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import { useState } from 'react';
+import { Play, XIcon } from 'lucide-react';
+import { AnimatePresence, motion } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+type AnimationStyle =
+ | 'from-bottom'
+ | 'from-center'
+ | 'from-top'
+ | 'from-left'
+ | 'from-right'
+ | 'fade'
+ | 'top-in-bottom-out'
+ | 'left-in-right-out';
+
+interface HeroVideoProps {
+ animationStyle?: AnimationStyle;
+ videoSrc: string;
+ thumbnailSrc: string;
+ thumbnailAlt?: string;
+ className?: string;
+}
+
+const animationVariants = {
+ 'from-bottom': {
+ initial: { y: '100%', opacity: 0 },
+ animate: { y: 0, opacity: 1 },
+ exit: { y: '100%', opacity: 0 },
+ },
+ 'from-center': {
+ initial: { scale: 0.5, opacity: 0 },
+ animate: { scale: 1, opacity: 1 },
+ exit: { scale: 0.5, opacity: 0 },
+ },
+ 'from-top': {
+ initial: { y: '-100%', opacity: 0 },
+ animate: { y: 0, opacity: 1 },
+ exit: { y: '-100%', opacity: 0 },
+ },
+ 'from-left': {
+ initial: { x: '-100%', opacity: 0 },
+ animate: { x: 0, opacity: 1 },
+ exit: { x: '-100%', opacity: 0 },
+ },
+ 'from-right': {
+ initial: { x: '100%', opacity: 0 },
+ animate: { x: 0, opacity: 1 },
+ exit: { x: '100%', opacity: 0 },
+ },
+ fade: {
+ initial: { opacity: 0 },
+ animate: { opacity: 1 },
+ exit: { opacity: 0 },
+ },
+ 'top-in-bottom-out': {
+ initial: { y: '-100%', opacity: 0 },
+ animate: { y: 0, opacity: 1 },
+ exit: { y: '100%', opacity: 0 },
+ },
+ 'left-in-right-out': {
+ initial: { x: '-100%', opacity: 0 },
+ animate: { x: 0, opacity: 1 },
+ exit: { x: '100%', opacity: 0 },
+ },
+};
+
+export function HeroVideoDialog({
+ animationStyle = 'from-center',
+ videoSrc,
+ thumbnailSrc,
+ thumbnailAlt = 'Video thumbnail',
+ className,
+}: HeroVideoProps) {
+ const [isVideoOpen, setIsVideoOpen] = useState(false);
+ const selectedAnimation = animationVariants[animationStyle];
+
+ return (
+
+
+
+ {isVideoOpen && (
+ {
+ if (e.key === 'Escape') {
+ setIsVideoOpen(false);
+ }
+ }}
+ onClick={() => setIsVideoOpen(false)}
+ exit={{ opacity: 0 }}
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
+ >
+
+ setIsVideoOpen(false)}
+ className="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 ring-white/10 backdrop-blur-md"
+ >
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/components/magicui/marquee.tsx b/apps/web/components/magicui/marquee.tsx
new file mode 100644
index 00000000..6797af1c
--- /dev/null
+++ b/apps/web/components/magicui/marquee.tsx
@@ -0,0 +1,76 @@
+import { type ComponentPropsWithoutRef } from 'react';
+
+import { cn } from '@/lib/utils';
+
+interface MarqueeProps extends ComponentPropsWithoutRef<'div'> {
+ /**
+ * Optional CSS class name to apply custom styles
+ */
+ className?: string;
+ /**
+ * Whether to reverse the animation direction
+ * @default false
+ */
+ reverse?: boolean;
+ /**
+ * Whether to pause the animation on hover
+ * @default false
+ */
+ pauseOnHover?: boolean;
+ /**
+ * Content to be displayed in the marquee
+ */
+ children: React.ReactNode;
+ /**
+ * Whether to animate vertically instead of horizontally
+ * @default false
+ */
+ vertical?: boolean;
+ /**
+ * Number of times to repeat the content
+ * @default 4
+ */
+ repeat?: number;
+}
+
+export function Marquee({
+ className,
+ reverse = false,
+ pauseOnHover = false,
+ children,
+ vertical = false,
+ repeat = 4,
+ ...props
+}: MarqueeProps) {
+ const safeRepeat = Math.max(1, Math.floor(Number.isFinite(repeat) ? repeat : 4));
+ return (
+
+ {Array(safeRepeat)
+ .fill(0)
+ .map((_, i) => (
+
0 ? { 'aria-hidden': true, tabIndex: -1 } : {})}
+ className={cn('flex shrink-0 justify-around gap-[var(--gap)]', {
+ 'animate-marquee flex-row': !vertical,
+ 'animate-marquee-vertical flex-col': vertical,
+ 'group-hover:[animation-play-state:paused]': pauseOnHover,
+ '[animation-direction:reverse]': reverse,
+ })}
+ >
+ {children}
+
+ ))}
+
+ );
+}
diff --git a/apps/web/components/magicui/number-ticker.tsx b/apps/web/components/magicui/number-ticker.tsx
new file mode 100644
index 00000000..1fde0436
--- /dev/null
+++ b/apps/web/components/magicui/number-ticker.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { useEffect, useRef, type ComponentPropsWithoutRef } from 'react';
+import { useInView, useMotionValue, useSpring } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+interface NumberTickerProps extends ComponentPropsWithoutRef<'span'> {
+ value: number;
+ startValue?: number;
+ direction?: 'up' | 'down';
+ delay?: number;
+ decimalPlaces?: number;
+ locale?: string;
+}
+
+export function NumberTicker({
+ value,
+ startValue = 0,
+ direction = 'up',
+ delay = 0,
+ className,
+ decimalPlaces = 0,
+ locale,
+ ...props
+}: NumberTickerProps) {
+ const ref = useRef(null);
+ const motionValue = useMotionValue(direction === 'down' ? value : startValue);
+ const springValue = useSpring(motionValue, {
+ damping: 60,
+ stiffness: 100,
+ });
+ const isInView = useInView(ref, { once: true, margin: '0px' });
+
+ useEffect(() => {
+ let timer: ReturnType | null = null;
+
+ if (isInView) {
+ timer = setTimeout(() => {
+ motionValue.set(direction === 'down' ? startValue : value);
+ }, delay * 1000);
+ }
+
+ return () => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+ };
+ }, [motionValue, isInView, delay, value, direction, startValue]);
+
+ useEffect(
+ () =>
+ springValue.on('change', (latest: number) => {
+ if (ref.current) {
+ ref.current.textContent = Intl.NumberFormat(locale, {
+ minimumFractionDigits: decimalPlaces,
+ maximumFractionDigits: decimalPlaces,
+ }).format(Number(latest.toFixed(decimalPlaces)));
+ }
+ }),
+ [springValue, decimalPlaces, locale]
+ );
+
+ return (
+
+ {Intl.NumberFormat(locale, {
+ minimumFractionDigits: decimalPlaces,
+ maximumFractionDigits: decimalPlaces,
+ }).format(direction === 'down' ? value : startValue)}
+
+ );
+}
diff --git a/apps/web/components/magicui/shimmer-button.tsx b/apps/web/components/magicui/shimmer-button.tsx
new file mode 100644
index 00000000..f7dc6a79
--- /dev/null
+++ b/apps/web/components/magicui/shimmer-button.tsx
@@ -0,0 +1,76 @@
+import React, { type ComponentPropsWithoutRef, type CSSProperties } from 'react';
+
+import { cn } from '@/lib/utils';
+
+export interface ShimmerButtonProps extends ComponentPropsWithoutRef<'button'> {
+ shimmerColor?: string;
+ shimmerSize?: string;
+ borderRadius?: string;
+ shimmerDuration?: string;
+ background?: string;
+}
+
+export const ShimmerButton = React.forwardRef(
+ (
+ {
+ shimmerColor = '#ffffff',
+ shimmerSize = '0.05em',
+ shimmerDuration = '3s',
+ borderRadius = '100px',
+ background = 'rgba(0, 0, 0, 1)',
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+ );
+ }
+);
+
+ShimmerButton.displayName = 'ShimmerButton';
diff --git a/apps/web/components/magicui/text-reveal.tsx b/apps/web/components/magicui/text-reveal.tsx
new file mode 100644
index 00000000..f292d422
--- /dev/null
+++ b/apps/web/components/magicui/text-reveal.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import { useRef, type ComponentPropsWithoutRef, type FC, type ReactNode } from 'react';
+import { motion, type MotionValue, useReducedMotion, useScroll, useTransform } from 'framer-motion';
+
+import { cn } from '@/lib/utils';
+
+export interface TextRevealProps extends ComponentPropsWithoutRef<'div'> {
+ children: string;
+}
+
+export const TextReveal: FC = ({ children, className, ...rest }) => {
+ const sectionRef = useRef(null);
+ const shouldReduceMotion = useReducedMotion();
+ const { scrollYProgress } = useScroll({
+ target: sectionRef,
+ });
+
+ if (typeof children !== 'string') {
+ throw new Error('TextReveal: children must be a string');
+ }
+
+ const words = children.split(' ');
+
+ // Reduced motion: show all text at full opacity without scroll animation
+ if (shouldReduceMotion) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {words.map((word, i) => {
+ const start = i / words.length;
+ const end = start + 1 / words.length;
+ return (
+
+ {word}
+
+ );
+ })}
+
+
+
+ );
+};
+
+interface WordProps {
+ children: ReactNode;
+ progress: MotionValue;
+ range: [number, number];
+}
+
+const Word: FC = ({ children, progress, range }) => {
+ const opacity = useTransform(progress, range, [0, 1]);
+ return (
+
+
+ {children}
+
+
+ {children}
+
+
+ );
+};
diff --git a/apps/web/components/ui/accordion.tsx b/apps/web/components/ui/accordion.tsx
new file mode 100644
index 00000000..dff53e03
--- /dev/null
+++ b/apps/web/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import * as React from 'react';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import { ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+const Accordion = AccordionPrimitive.Root;
+
+const AccordionItem = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AccordionItem.displayName = 'AccordionItem';
+
+const AccordionTrigger = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180',
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+const AccordionContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/apps/web/components/ui/badge.tsx b/apps/web/components/ui/badge.tsx
new file mode 100644
index 00000000..00ec366d
--- /dev/null
+++ b/apps/web/components/ui/badge.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default: 'bg-accent text-white',
+ secondary: 'bg-white/5 text-text-secondary',
+ outline: 'border border-border text-text-secondary',
+ },
+ },
+ defaultVariants: { variant: 'default' },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes, VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return ;
+}
+
+export { Badge, badgeVariants };
diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx
new file mode 100644
index 00000000..863b32a0
--- /dev/null
+++ b/apps/web/components/ui/button.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-accent text-white hover:bg-accent-hover',
+ ghost: 'border border-border text-text-secondary hover:bg-white/5 hover:text-white',
+ link: 'text-accent underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-5 py-2',
+ sm: 'h-8 px-3 text-xs',
+ lg: 'h-12 px-7 py-3 text-base',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: { variant: 'default', size: 'default' },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes, VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, type = 'button', ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ );
+ }
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx
new file mode 100644
index 00000000..65e5f2fe
--- /dev/null
+++ b/apps/web/components/ui/card.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/apps/web/components/ui/separator.tsx b/apps/web/components/ui/separator.tsx
new file mode 100644
index 00000000..5fe735b6
--- /dev/null
+++ b/apps/web/components/ui/separator.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import * as React from 'react';
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
+import { cn } from '@/lib/utils';
+
+const Separator = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
+
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/apps/web/components/ui/sheet.tsx b/apps/web/components/ui/sheet.tsx
new file mode 100644
index 00000000..f6d9f426
--- /dev/null
+++ b/apps/web/components/ui/sheet.tsx
@@ -0,0 +1,91 @@
+'use client';
+
+import * as React from 'react';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+const Sheet = DialogPrimitive.Root;
+const SheetTrigger = DialogPrimitive.Trigger;
+const SheetClose = DialogPrimitive.Close;
+const SheetPortal = DialogPrimitive.Portal;
+
+const SheetOverlay = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const SheetContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+SheetContent.displayName = DialogPrimitive.Content.displayName;
+
+const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = 'SheetHeader';
+
+const SheetTitle = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = DialogPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetDescription,
+};
diff --git a/apps/web/content/docs/architecture/overview.mdx b/apps/web/content/docs/architecture/overview.mdx
index f5c0ead2..e446133f 100644
--- a/apps/web/content/docs/architecture/overview.mdx
+++ b/apps/web/content/docs/architecture/overview.mdx
@@ -9,60 +9,64 @@ Readied follows a clean architecture with strict separation of concerns.
## Monorepo Structure
-```
-readied/
-├── apps/
-│ ├── desktop/ # Electron app
-│ │ ├── src/main/ # Main process (SQLite, IPC, sync)
-│ │ ├── src/preload/ # Secure bridge
-│ │ └── src/renderer/ # React UI
-│ ├── docs-site/ # Documentation (VitePress)
-│ └── web/ # Marketing + docs (Next.js + Fumadocs)
-├── packages/
-│ ├── ai-assistant/ # AI assistant integration
-│ ├── api/ # Supabase API backend
-│ ├── command-registry/ # Command palette system
-│ ├── commands/ # Built-in commands
-│ ├── core/ # Domain logic + markdown parsing
-│ ├── design-system/ # Design tokens (CSS custom properties)
-│ ├── embeds/ # URL embed resolution
-│ ├── licensing/ # License validation
-│ ├── plugin-api/ # Plugin system API
-│ ├── plugin-cli/ # Plugin CLI scaffolding tool
-│ ├── product-config/ # Product configuration
-│ ├── storage-core/ # Storage interfaces + utilities
-│ ├── storage-sqlite/ # SQLite implementation
-│ ├── sync-core/ # Sync engine (Supabase)
-│ ├── tasks/ # Task management / extraction
-│ └── wikilinks/ # [[wikilink]] parsing + resolution
-└── docs/ # Static documentation assets
-```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
16 packages organized by domain responsibility.
-## Package Dependencies
+## Key Boundaries
-```mermaid
-graph TD
- A[desktop] --> B[core]
- A --> C[storage-sqlite]
- A --> E[command-registry]
- A --> F[commands]
- A --> G[plugin-api]
- A --> H[sync-core]
- A --> I[ai-assistant]
- A --> J[design-system]
- A --> K[licensing]
- A --> L[product-config]
- C --> D[storage-core]
- B -.-> D
- F --> E
- H --> M[api]
- A --> N[wikilinks]
- A --> O[embeds]
- A --> P[tasks]
- G -.-> J
-```
+
+
+ Pure domain logic. No Electron/React. Testable in Node.js.
+
+
+ Database adapter interfaces, migrations, backup/export/import.
+
+
+ CodeMirror 6 configuration and extension system.
+
+
+ Typed IPC channels between main and renderer processes.
+
+
## Data Flow
@@ -90,66 +94,6 @@ graph TD
└─────────────────────────────────────────────────────────────────┘
```
-## Key Boundaries
-
-### 1. Core Package (`@readied/core`)
-
-- Pure domain logic
-- No Electron or React dependencies
-- Testable in Node.js
-- Defines Note entity, operations, and contracts
-
-### 2. Storage Core (`@readied/storage-core`)
-
-- Database adapter interfaces
-- Migration runner
-- Backup/Export/Import utilities
-- No native dependencies
-
-### 3. Storage SQLite (`@readied/storage-sqlite`)
-
-- SQLite implementation (better-sqlite3)
-- Repository implementations
-- Migration definitions
-- **Only package with native deps**
-
-### 4. Plugin API (`@readied/plugin-api`)
-
-- Plugin lifecycle and registration
-- Extension points (editor, themes, commands)
-- Sandboxed plugin context
-- Theme registry for CSS variable overrides
-
-### 5. Sync Core (`@readied/sync-core`)
-
-- Supabase-based sync engine
-- Push/pull operations
-- Conflict resolution
-- Sync state management
-
-### 6. Command System (`@readied/command-registry` + `@readied/commands`)
-
-- Command palette infrastructure
-- Built-in command definitions
-- Keybinding management
-
-### 7. Companion Packages
-
-- **`@readied/wikilinks`** - `[[wikilink]]` parsing and resolution
-- **`@readied/tasks`** - Task extraction from markdown
-- **`@readied/embeds`** - URL embed resolution
-- **`@readied/ai-assistant`** - AI assistant integration
-- **`@readied/design-system`** - Design tokens as CSS custom properties
-- **`@readied/licensing`** - License validation
-- **`@readied/product-config`** - Pricing, plans, and product metadata
-
-### 8. Desktop App (`@readied/desktop`)
-
-- Electron + electron-vite
-- Main process: SQLite, IPC handlers, sync, plugins
-- Renderer: React + CodeMirror 6
-- Preload: Typed API bridge
-
## Security Model
| Rule | Description |
diff --git a/apps/web/content/docs/guide/principles.mdx b/apps/web/content/docs/guide/principles.mdx
index c042b3ad..b9cd742d 100644
--- a/apps/web/content/docs/guide/principles.mdx
+++ b/apps/web/content/docs/guide/principles.mdx
@@ -9,6 +9,10 @@ These principles guide every technical and product decision in Readied.
## Core Principles
+
+"Your notes survive the app."
+
+
### 1. Markdown is Sacred
> "If the user typed it, we keep it."
@@ -41,28 +45,28 @@ These principles guide every technical and product decision in Readied.
## Technical Principles
-### Strict Separation
-
-```
-Core ≠ UI ≠ Infra ≠ Packaging
-```
-
-- React never decides domain logic
-- SQLite queries never in renderer
-- IPC channels are typed contracts
-
-### Minimal Dependencies
-
-- Every library needs written justification
-- No "just in case" abstractions
-- Prefer standard APIs over libraries
-
-### Forward-Only Migrations
-
-- Schema changes are inevitable
-- Plan for them from day 1
-- Never rollback schema
-- Automatic backup before migration
+
+
+ ```
+ Core ≠ UI ≠ Infra ≠ Packaging
+ ```
+
+ - React never decides domain logic
+ - SQLite queries never in renderer
+ - IPC channels are typed contracts
+
+
+ - Every library needs written justification
+ - No "just in case" abstractions
+ - Prefer standard APIs over libraries
+
+
+ - Schema changes are inevitable
+ - Plan for them from day 1
+ - Never rollback schema
+ - Automatic backup before migration
+
+
## What Readied is NOT
@@ -72,7 +76,3 @@ Core ≠ UI ≠ Infra ≠ Packaging
| Collaboration tool | Single-user product |
| Mobile app | Desktop first |
| AI-powered | Not core value prop |
-
-## One-Liner
-
-> "Your notes survive the app."
diff --git a/apps/web/content/docs/index.mdx b/apps/web/content/docs/index.mdx
index 2065fb2c..c929f9ae 100644
--- a/apps/web/content/docs/index.mdx
+++ b/apps/web/content/docs/index.mdx
@@ -5,22 +5,47 @@ description: Technical documentation for Readied — Markdown-first, offline-for
# Readied Documentation
-Readied is a **Markdown-first desktop note app** for developers who value their data.
+Readied is a **Markdown-first desktop note app** for developers who value their data. Open source, offline-first, and built to last.
-## Features
+## Getting Started
-- **Markdown Sacred** — Your markdown is never auto-modified. What you type is exactly what gets saved. No hidden transformations.
-- **Plugin System** — 8 built-in plugins with an extensible architecture. Load community plugins from `~/.config/readied/plugins`.
-- **Offline First** — Works 100% offline by default. Optional cloud sync via Supabase keeps your notes in sync across devices when you want it.
-- **Portable Data** — Export anytime as markdown files. Import from Obsidian. Your notes survive the app — always.
-- **Fast & Native** — Built with Electron for cross-platform support. CodeMirror 6 editor for blazing fast editing.
-- **Theme System** — Customizable color palettes with dark/light awareness. Ships with Solarized built-in. Make Readied yours.
+
+
+ Download and install Readied on macOS, Windows, or Linux.
+
+
+ Understand the philosophy behind Readied's design decisions.
+
+
+ Deep dive into how Readied works under the hood.
+
+
+ Build your own plugins to extend Readied's capabilities.
+
+
-## Quick Links
+## Why Readied?
-- [Get Started](/docs/guide/getting-started) — Installation and first launch
-- [Principles](/docs/guide/principles) — Understand the philosophy
-- [Architecture](/docs/architecture/overview) — How it all works
-- [Plugin Development](/docs/plugins/getting-started) — Build your own plugins
-- [ADRs](/docs/decisions) — Key technical decisions
+- **Markdown Sacred** — Your markdown is never auto-modified. What you type is exactly what gets saved.
+- **Plugin System** — 8 built-in plugins with an extensible architecture for community plugins.
+- **Offline First** — Works 100% offline. Optional cloud sync via Supabase when you want it.
+- **Portable Data** — Export anytime as markdown files. Import from Obsidian. Your notes survive the app.
+- **Fast & Native** — Electron + CodeMirror 6 for a buttery-smooth editing experience.
+- **Theme System** — Customizable color palettes with dark/light awareness.
+## Explore
+
+
+
+ Overview of the 8 plugins that ship with Readied.
+
+
+ Set up optional Supabase sync across your devices.
+
+
+ How the CodeMirror 6 editor is configured and extended.
+
+
+ Architecture Decision Records (ADRs) behind key choices.
+
+
diff --git a/apps/web/content/docs/plugins/getting-started.mdx b/apps/web/content/docs/plugins/getting-started.mdx
index 73e74ee2..cc283272 100644
--- a/apps/web/content/docs/plugins/getting-started.mdx
+++ b/apps/web/content/docs/plugins/getting-started.mdx
@@ -9,7 +9,9 @@ Readied supports a plugin system that lets you extend the editor with custom fun
## Quick Start
-### 1. Scaffold a new plugin
+
+
+### Scaffold a new plugin
```bash
npx readied-plugin init "My Plugin"
@@ -19,9 +21,11 @@ npm install
This creates a plugin project with TypeScript, the plugin API types, and a starter template.
-> **Tip:** Readied ships with 8 built-in plugins you can use as reference. Check the `apps/desktop/src/renderer/plugins/` directory for real-world examples covering status bars, focus mode, code block rendering, and more.
+
+Readied ships with 8 built-in plugins you can use as reference. Check the `apps/desktop/src/renderer/plugins/` directory for real-world examples.
+
-### 2. Write your plugin
+### Write your plugin
Edit `src/index.ts`:
@@ -58,7 +62,7 @@ export const plugin: PluginManifest = {
};
```
-### 3. Build and install
+### Build and install
```bash
npm run build
@@ -66,17 +70,35 @@ npm run build
Copy the output to your Readied plugins directory:
-- **macOS:** `~/Library/Application Support/Readied/plugins/my-plugin/`
-- **Windows:** `%APPDATA%/Readied/plugins/my-plugin/`
-- **Linux:** `~/.config/Readied/plugins/my-plugin/`
-
-### 4. Reload Readied
+
+
+ ```bash
+ cp -r dist/ ~/Library/Application\ Support/Readied/plugins/my-plugin/
+ ```
+
+
+ ```bash
+ xcopy /E dist\ %APPDATA%\Readied\plugins\my-plugin\
+ ```
+
+
+ ```bash
+ cp -r dist/ ~/.config/Readied/plugins/my-plugin/
+ ```
+
+
+
+### Reload Readied
The plugin will be discovered and loaded automatically on next launch. Toggle it in **Settings > Plugins**.
+
+
## Plugin Contract
-Readied plugins must follow five rules to maintain the offline-first, markdown-portable principle:
+
+Readied plugins must follow these rules to maintain the offline-first, markdown-portable principle.
+
1. **Removable without breaking `.md` files** — Your plugin can be uninstalled and all notes remain valid markdown.
2. **No new syntax** — Don't invent markdown extensions that other editors can't read.
@@ -95,10 +117,20 @@ load → activate(context) → [user interacts] → dispose()
3. **Running** — Your plugin responds to commands, events, and editor changes
4. **Dispose** — When disabled or on app exit, your `dispose()` function cleans up
-> **Tip:** Always clean up. Return a `{ dispose() }` object from `activate()`. Unregister all commands, remove all UI components, and clear any subscriptions.
+
+Always clean up. Return a `{ dispose() }` object from `activate()`. Unregister all commands, remove all UI components, and clear any subscriptions.
+
## Next Steps
-- [API Reference](/docs/plugins/api-reference) — Full documentation of every method and type
-- [Layout Zones](/docs/plugins/layout-zones) — Where to place UI components
-- [Examples](/docs/plugins/examples) — Real-world plugin patterns
+
+
+ Full documentation of every method and type.
+
+
+ Where to place UI components in the editor.
+
+
+ Real-world plugin patterns and recipes.
+
+
diff --git a/apps/web/lib/layout.shared.tsx b/apps/web/lib/layout.shared.tsx
new file mode 100644
index 00000000..024b4fc0
--- /dev/null
+++ b/apps/web/lib/layout.shared.tsx
@@ -0,0 +1,26 @@
+import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
+
+export function baseOptions(): BaseLayoutProps {
+ return {
+ nav: {
+ title: (
+
+ readied.
+ docs
+
+ ),
+ transparentMode: 'top',
+ },
+ githubUrl: 'https://github.com/tomymaritano/readide',
+ links: [
+ {
+ text: 'Home',
+ url: '/',
+ },
+ {
+ text: 'Download',
+ url: '/download',
+ },
+ ],
+ };
+}
diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts
new file mode 100644
index 00000000..9ad0df42
--- /dev/null
+++ b/apps/web/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/web/mdx-components.tsx b/apps/web/mdx-components.tsx
index 352bfe89..daed3fd9 100644
--- a/apps/web/mdx-components.tsx
+++ b/apps/web/mdx-components.tsx
@@ -1,6 +1,29 @@
import type { MDXComponents } from 'mdx/types';
import defaultComponents from 'fumadocs-ui/mdx';
+import { Card, Cards } from 'fumadocs-ui/components/card';
+import { Callout } from 'fumadocs-ui/components/callout';
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
+import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
+import { File, Folder, Files } from 'fumadocs-ui/components/files';
+import { TypeTable } from 'fumadocs-ui/components/type-table';
export function useMDXComponents(components: MDXComponents): MDXComponents {
- return { ...defaultComponents, ...components };
+ return {
+ ...defaultComponents,
+ Card,
+ Cards,
+ Callout,
+ Step,
+ Steps,
+ Tab,
+ Tabs,
+ Accordion,
+ Accordions,
+ File,
+ Folder,
+ Files,
+ TypeTable,
+ ...components,
+ };
}
diff --git a/apps/web/package.json b/apps/web/package.json
index 41c5933b..7a0a3a38 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -3,28 +3,39 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
- "next": "^15.0.0",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@readied/product-config": "workspace:*",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.35.2",
"fumadocs-core": "^15.8.5",
"fumadocs-mdx": "^13.0.8",
"fumadocs-ui": "^15.8.5",
- "next-themes": "^0.4.6",
"lucide-react": "^0.475.0",
- "@headlessui/react": "^2.2.9",
- "@readied/product-config": "workspace:*"
+ "next": "^15.0.0",
+ "next-themes": "^0.4.6",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
+ "@fontsource-variable/jetbrains-mono": "^5.2.8",
+ "@fontsource/inter": "^5.2.8",
+ "@tailwindcss/postcss": "^4.0.0",
+ "@types/node": "25.4.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
- "typescript": "^5.7.0",
- "@tailwindcss/postcss": "^4.0.0",
+ "postcss": "^8.0.0",
"tailwindcss": "^4.0.0",
- "postcss": "^8.0.0"
+ "typescript": "^5.7.0"
}
}
diff --git a/docs/plans/2026-03-12-roadmap-auth-sync-ai.md b/docs/plans/2026-03-12-roadmap-auth-sync-ai.md
new file mode 100644
index 00000000..eb83eaa9
--- /dev/null
+++ b/docs/plans/2026-03-12-roadmap-auth-sync-ai.md
@@ -0,0 +1,132 @@
+# Roadmap: Auth Rethink + Sync Testing + AI UX
+
+> Created: 2026-03-12
+> Updated: 2026-03-13
+> Status: Phase 3 complete — Phase 4 & 5 pending
+> Branch: develop
+
+---
+
+## Backend Status (Verified 2026-03-13)
+
+| Endpoint | Status | Notes |
+| ------------------------ | ----------- | ------------------------------------------ |
+| `GET /health` | 200 OK | API live on Cloudflare Workers |
+| `POST /auth/magic-link` | 200 OK | Resend email delivery working |
+| `POST /auth/verify` | 400 (valid) | Zod validates UUID token format |
+| `GET /auth/me` + bad JWT | **401** | **Fixed**: all jose error types return 401 |
+
+- **API URL**: `https://api.readied.app`
+- **Email**: Resend (`noreply@readied.app`)
+- **Database**: Turso (libsql)
+- **Hosting**: Cloudflare Workers
+- **API Docs**: `docs/plans/api-reference.md`
+
+---
+
+## Phase 1: Foundation ✅ COMPLETE
+
+### 1.1 Website Redesign Fixes
+
+- [x] Apply 30 CodeRabbit review fixes (commit `507f8ce`)
+- [x] Push to `feature/website-redesign`
+- [x] Merge PR #148 to develop
+- [x] Fix NumberTicker `locale` ReferenceError (SSG prerender crash)
+- [x] Fix Turbopack incompatibility (remove --turbopack, use webpack extensionAlias)
+- [x] Fix licensing package exports (point to source, not dist/)
+
+### 1.2 Auth UX Rethink
+
+- [x] "Enable Sync" modal redesign
+- [x] Deep link race condition fix
+- [x] Auth middleware: all jose error types → 401 with JSON responses
+
+### 1.3 Backend Fixes
+
+- [x] Fix `/auth/me` returning 500 on invalid JWT → now returns 401
+- [x] Document API endpoints → `docs/plans/api-reference.md` (24 endpoints)
+
+---
+
+## Phase 2: Sync Stability ✅ COMPLETE
+
+### Known Issues — All Fixed
+
+- [x] `syncService.ts` — auto-sync error propagation to renderer via IPC events
+- [x] `syncService.ts` — exponential backoff on repeated failures (cap 5min)
+- [x] `syncService.ts` — 401 auto-stops sync + emits `auth-expired` event
+- [x] `syncService.ts` — abort in-flight sync on logout via AbortController
+- [x] `apiClient.ts` — token refresh returns typed errors (expired/network/device_limit)
+- [x] `apiClient.ts` — distinguish refresh failure modes for UI
+
+### Deliverables
+
+- [ ] End-to-end sync testing (all 6 scenarios) — needs manual testing
+- [x] Fix sync error handling (no silent failures)
+- [x] Sync onboarding flow (prompt after 5 notes, session-dismissable)
+- [ ] Conflict resolution UX improvements — existing UI functional
+- [x] Offline queue visibility in status bar (pending count + offline indicator)
+
+---
+
+## Phase 3: AI Commands (Cmd+K v1) ✅ COMPLETE
+
+### What Was Built
+
+- [x] Remap Cmd+K → AI panel; insert-link moved to Cmd+Shift+K
+- [x] AI command definitions: `ai:toggle-panel`, `ai:summarize`, `ai:rewrite`, `ai:tweet`
+- [x] New `'ai'` command category in registry
+- [x] AiPanel integrated into App layout (right-side panel)
+- [x] `useRegisterAiCommands` hook following existing patterns
+- [x] AI Settings section: API key, model selector, max context notes
+- [x] Settings schema v2 with migration from v1
+- [x] AiPanel reads settings store for API key and model
+- [x] Escape cascade updated: palette → AI panel → graph → search
+- [x] Existing `packages/ai-assistant/` (Claude client, RAG, prompts) wired into panel
+
+### Decisions Made
+
+1. **AI Provider**: API key stored in Settings > AI Assistant (encrypted via Zustand persist)
+2. **Keybinding**: Cmd+K = AI panel (global), Cmd+Shift+K = insert link (editor)
+3. **Streaming**: Wait for complete response (v1 simplicity)
+4. **Context**: Current note + RAG across all notes via `buildRagPrompt`
+
+---
+
+## Phase 4: AI Knowledge (Cmd+K v2) — PENDING
+
+- [ ] RAG integration — query across notes
+- [ ] "Ask your notes" command
+- [ ] Related notes context in AI prompts
+- [ ] Custom prompt with knowledge context
+
+---
+
+## Phase 5: Extensibility — PENDING
+
+- [ ] Plugin API for custom AI commands
+- [ ] Community command marketplace concept
+- [ ] Import/export AI command definitions
+
+---
+
+## Infrastructure Fixes (2026-03-13)
+
+- [x] Align develop with main (merge main → develop)
+- [x] Add `._*` to .gitignore (macOS resource forks on exFAT volumes)
+
+---
+
+## Verification Checklist
+
+| Feature | How to verify | Status |
+| ------------------- | --------------------------------------------------- | -------- |
+| Website | `pnpm typecheck` + CI green | ✅ Pass |
+| Auth middleware | Invalid JWT → 401 JSON (not 500) | ✅ Pass |
+| Sync error handling | Auto-sync failure → renderer sees error + backoff | ✅ Built |
+| Sync onboarding | Create 5+ notes → "Enable Sync" prompt in sidebar | ✅ Built |
+| Offline indicator | Go offline → "N changes pending" in sidebar footer | ✅ Built |
+| Cmd+K | Press Cmd+K → AI panel opens | ✅ Built |
+| AI Settings | Settings > AI Assistant → configure API key + model | ✅ Built |
+| Login | Enter email → receive link → verify in app | Manual |
+| Sync e2e | Create note → sync → verify on second device | Manual |
diff --git a/docs/plans/2026-03-12-website-redesign-design.md b/docs/plans/2026-03-12-website-redesign-design.md
new file mode 100644
index 00000000..3d504f12
--- /dev/null
+++ b/docs/plans/2026-03-12-website-redesign-design.md
@@ -0,0 +1,151 @@
+# Website Redesign — shadcn/ui + Magic UI
+
+## Goal
+
+Full rebuild of the marketing site with shadcn/ui as component system and Magic UI for animated effects. Establish a cohesive, modern, minimal brand identity from scratch.
+
+## Design Decisions
+
+- **Aesthetic:** Minimal & Clean (Linear/Vercel/Raycast style)
+- **Color:** Neutral Cool + Violet accent
+- **Theme:** Dark mode only
+- **Motion:** Moderate — fade-ins, shimmer, border beams, subtle backgrounds
+- **Logo:** Text-based "readied." in JetBrains Mono, violet dot
+- **Approach:** Full rebuild of all marketing components
+
+## Color Palette
+
+| Token | Value | Use |
+| -------------------- | ----------------------- | --------------------- |
+| `--background` | `#09090b` | Page background |
+| `--surface` | `#111113` | Cards, sections |
+| `--surface-elevated` | `#1a1a1f` | Hover, elevated cards |
+| `--border` | `#27272a` | Subtle borders |
+| `--border-accent` | `rgba(139,92,246,0.3)` | Highlighted borders |
+| `--text-primary` | `#fafafa` | Headings |
+| `--text-secondary` | `#a1a1aa` | Body text |
+| `--text-muted` | `#52525b` | Captions, hints |
+| `--accent` | `#8b5cf6` | Primary (violet-500) |
+| `--accent-hover` | `#7c3aed` | Hover (violet-600) |
+| `--accent-glow` | `rgba(139,92,246,0.15)` | Glow effects |
+
+## Typography
+
+- **Headings:** Inter (tight tracking, semibold/bold)
+- **Body:** Inter (regular, relaxed line height)
+- **Mono:** JetBrains Mono (code, badges, logo)
+
+## Component Stack
+
+### shadcn/ui (base system)
+
+- Button, Card, Badge, Separator, Accordion, Sheet
+
+### Magic UI (free effects)
+
+- AnimatedShinyText — Hero badge
+- BorderBeam — Card glow on hover, screenshot frame
+- DotPattern — Hero background
+- AnimatedGridPattern — Alternative background
+- Marquee — Social proof horizontal scroll
+- NumberTicker — Animated pricing number
+- ShimmerButton — Primary CTA
+- TextReveal — "Why Local" heading animation
+
+## Page Structure
+
+### Navbar
+
+- Fixed, backdrop-blur glassmorphism
+- Logo left, links center, CTA right
+- Mobile: shadcn Sheet as side drawer
+
+### Hero
+
+- AnimatedShinyText version badge
+- Large heading: "Your markdown, your machine, your rules."
+- Two CTAs: ShimmerButton "Download" + ghost "View on GitHub"
+- DotPattern background
+- Editor screenshot with BorderBeam frame
+
+### Social Proof
+
+- Marquee with trust badges (Offline, Local, Open Source, Plugins, Cross-platform)
+
+### Features
+
+- 3 large cards with icon + title + description
+- BorderBeam on hover
+- Core features: Markdown Sacred, Plugin Ecosystem, Offline First
+
+### Why Local
+
+- Split: text left, comparison table right
+- TextReveal animated heading
+- shadcn Card for comparison table
+
+### Pricing
+
+- Two Cards (Free / Pro)
+- Pro card with permanent BorderBeam + "Popular" Badge
+- NumberTicker for price
+- Accordion FAQs below
+
+### Download
+
+- Auto-detect OS, primary card for current OS
+- Secondary cards for other platforms
+- AnimatedShinyText version badge
+
+### Footer
+
+- Minimal: logo, link columns, social icons, newsletter
+- Separator top, DotPattern background
+
+## Technical Architecture
+
+### New Dependencies
+
+- shadcn/ui (CLI init, "new-york" style)
+- framer-motion (Magic UI requirement)
+- @fontsource/inter + @fontsource/jetbrains-mono
+- tailwindcss-animate
+
+### File Structure
+
+```
+apps/web/
+├── components/
+│ ├── ui/ ← shadcn/ui (Button, Card, Badge, etc.)
+│ ├── magicui/ ← Magic UI (copied source, not npm)
+│ ├── layout/ ← Navbar, Footer, MobileNav
+│ └── landing/ ← Hero, Features, WhyLocal, Pricing, etc.
+├── lib/
+│ └── utils.ts ← cn() helper
+├── app/
+│ ├── globals.css ← Design tokens + shadcn CSS vars
+│ ├── layout.tsx ← Root layout with fonts
+│ ├── (marketing)/ ← Same routes
+│ └── docs/ ← Fumadocs (untouched)
+```
+
+### Unchanged
+
+- Fumadocs docs/ — inherits dark theme
+- Marketing routes — same paths
+- product-config — source of truth for pricing
+- Static export to Cloudflare Pages
+
+### Removed
+
+- @headlessui/react — replaced by shadcn Sheet/Accordion
+- Current landing/ components — rewritten
+- Current globals.css — rewritten with shadcn vars
+- Custom glassmorphism classes — replaced
+
+## Out of Scope
+
+- Docs styling changes (Fumadocs handles this)
+- New marketing pages (only redesign existing)
+- Backend/API changes
+- Desktop app changes
diff --git a/docs/plans/2026-03-12-website-redesign-implementation.md b/docs/plans/2026-03-12-website-redesign-implementation.md
new file mode 100644
index 00000000..66b1bc3b
--- /dev/null
+++ b/docs/plans/2026-03-12-website-redesign-implementation.md
@@ -0,0 +1,879 @@
+# Website Redesign Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Full rebuild of the marketing site with shadcn/ui + Magic UI, establishing a modern dark-mode brand identity with violet accent.
+
+**Architecture:** Replace all current marketing components and globals.css with shadcn/ui-based components + Magic UI effects. Keep Fumadocs docs/, product-config, routes, and static export unchanged. Remove @headlessui/react.
+
+**Tech Stack:** Next.js 15, React 19, shadcn/ui (new-york style), Magic UI (copied source), Tailwind CSS v4, framer-motion, Inter + JetBrains Mono fonts
+
+---
+
+## Task 1: Install Dependencies and Create Utilities
+
+**Files:**
+
+- Modify: `apps/web/package.json`
+- Create: `apps/web/lib/utils.ts`
+- Create: `apps/web/components/ui/.gitkeep` (placeholder, shadcn CLI populates this)
+
+**Step 1: Install new dependencies**
+
+Run from repo root:
+
+```bash
+cd apps/web
+pnpm add framer-motion class-variance-authority clsx tailwind-merge tailwindcss-animate
+pnpm add -D @fontsource/inter @fontsource-variable/jetbrains-mono
+```
+
+Note: `class-variance-authority`, `clsx`, `tailwind-merge` are shadcn/ui peer requirements.
+
+**Step 2: Create `cn()` utility**
+
+Create `apps/web/lib/utils.ts`:
+
+```ts
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+```
+
+**Step 3: Verify build**
+
+```bash
+cd apps/web && pnpm exec next build
+```
+
+Expected: Build succeeds with no new errors.
+
+**Step 4: Commit**
+
+```bash
+git add apps/web/package.json apps/web/lib/utils.ts pnpm-lock.yaml
+git commit -m "feat(web): add shadcn/ui dependencies and cn() utility"
+```
+
+---
+
+## Task 2: Copy Magic UI Components
+
+Magic UI components are copied as source files (not npm). Copy only the free components needed per the design doc.
+
+**Files:**
+
+- Create: `apps/web/components/magicui/animated-shiny-text.tsx`
+- Create: `apps/web/components/magicui/border-beam.tsx`
+- Create: `apps/web/components/magicui/dot-pattern.tsx`
+- Create: `apps/web/components/magicui/marquee.tsx`
+- Create: `apps/web/components/magicui/number-ticker.tsx`
+- Create: `apps/web/components/magicui/shimmer-button.tsx`
+- Create: `apps/web/components/magicui/text-reveal.tsx`
+- Create: `apps/web/components/magicui/animated-grid-pattern.tsx`
+
+**Step 1: Create each Magic UI component**
+
+Copy source from the Magic UI docs (https://magicui.design/docs/components/). Each component is a single file that uses `framer-motion` and the `cn()` utility. Adapt imports to use `@/lib/utils`.
+
+Key patterns:
+
+- All components import `cn` from `@/lib/utils`
+- All animation components use `framer-motion`
+- Components are `'use client'` where they use hooks or framer-motion
+
+**Step 2: Verify build**
+
+```bash
+cd apps/web && pnpm exec next build
+```
+
+Expected: Build succeeds. Magic UI components compile without errors.
+
+**Step 3: Commit**
+
+```bash
+git add apps/web/components/magicui/
+git commit -m "feat(web): add Magic UI components (source copy)"
+```
+
+---
+
+## Task 3: Create shadcn/ui Base Components
+
+Manually create the shadcn/ui components needed by the design (Button, Card, Badge, Separator, Accordion, Sheet). Since we use Tailwind v4 CSS-first config (no tailwind.config.js), we create these manually following shadcn/ui "new-york" style patterns.
+
+**Files:**
+
+- Create: `apps/web/components/ui/button.tsx`
+- Create: `apps/web/components/ui/card.tsx`
+- Create: `apps/web/components/ui/badge.tsx`
+- Create: `apps/web/components/ui/separator.tsx`
+- Create: `apps/web/components/ui/accordion.tsx`
+- Create: `apps/web/components/ui/sheet.tsx`
+
+**Step 1: Create Button component**
+
+```tsx
+// apps/web/components/ui/button.tsx
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]',
+ ghost:
+ 'border border-[var(--border)] text-[var(--text-secondary)] hover:bg-white/5 hover:text-white',
+ link: 'text-[var(--accent)] underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-5 py-2',
+ sm: 'h-8 px-3 text-xs',
+ lg: 'h-12 px-7 py-3 text-base',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: { variant: 'default', size: 'default' },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes, VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ );
+ }
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
+```
+
+Install Radix primitives needed:
+
+```bash
+cd apps/web && pnpm add @radix-ui/react-slot @radix-ui/react-accordion @radix-ui/react-dialog @radix-ui/react-separator
+```
+
+**Step 2: Create remaining UI components**
+
+Create Card, Badge, Separator, Accordion, Sheet following shadcn/ui new-york patterns. Each uses `cn()` and CSS custom properties from our design tokens.
+
+- **Card**: Surface background, border, rounded-xl
+- **Badge**: Small label with accent/neutral variants
+- **Separator**: Thin border line, horizontal/vertical
+- **Accordion**: Radix-based, animated with chevron rotation
+- **Sheet**: Radix Dialog-based side drawer for mobile nav
+
+**Step 3: Verify build**
+
+```bash
+cd apps/web && pnpm exec next build
+```
+
+**Step 4: Commit**
+
+```bash
+git add apps/web/components/ui/ apps/web/package.json pnpm-lock.yaml
+git commit -m "feat(web): add shadcn/ui base components (Button, Card, Badge, Separator, Accordion, Sheet)"
+```
+
+---
+
+## Task 4: Rewrite Design System (globals.css + Root Layout)
+
+Replace current teal-accent globals.css with the violet-accent design system. Add Inter + JetBrains Mono fonts to root layout.
+
+**Files:**
+
+- Rewrite: `apps/web/app/globals.css`
+- Modify: `apps/web/app/layout.tsx`
+
+**Step 1: Rewrite globals.css**
+
+Replace entire contents of `apps/web/app/globals.css`:
+
+```css
+@import 'tailwindcss';
+@import 'fumadocs-ui/css/neutral.css';
+@import 'fumadocs-ui/css/preset.css';
+
+@theme {
+ /* Brand colors — violet accent on dark zinc */
+ --color-background: #09090b;
+ --color-surface: #111113;
+ --color-surface-elevated: #1a1a1f;
+ --color-inset: #0c0c0e;
+ --color-border: #27272a;
+ --color-border-accent: rgba(139, 92, 246, 0.3);
+
+ --color-text-primary: #fafafa;
+ --color-text-secondary: #a1a1aa;
+ --color-text-muted: #52525b;
+
+ --color-accent: #8b5cf6;
+ --color-accent-hover: #7c3aed;
+ --color-accent-glow: rgba(139, 92, 246, 0.15);
+
+ /* Font families */
+ --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono Variable', ui-monospace, monospace;
+}
+
+/* Marketing page utilities */
+@layer components {
+ .glass {
+ @apply bg-zinc-950/80 backdrop-blur-xl border border-white/[0.06];
+ }
+
+ .glass-card {
+ @apply bg-surface border border-border rounded-xl;
+ }
+
+ .glass-card-glow {
+ @apply bg-surface border border-accent/30 rounded-xl;
+ box-shadow: 0 0 40px var(--color-accent-glow);
+ }
+
+ .gradient-text {
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-violet-400 to-indigo-400;
+ }
+
+ .section-label {
+ @apply inline-block font-mono text-xs font-semibold uppercase tracking-widest text-accent mb-4;
+ }
+
+ .section-heading {
+ @apply text-3xl font-bold leading-tight tracking-tight lg:text-4xl mb-4;
+ }
+}
+
+@keyframes fade-in-up {
+ from {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@utility animate-fade-in-up {
+ animation: fade-in-up 0.6s ease-out forwards;
+}
+```
+
+**Step 2: Update root layout with fonts**
+
+Modify `apps/web/app/layout.tsx` to import fonts:
+
+```tsx
+import './globals.css';
+import '@fontsource/inter/400.css';
+import '@fontsource/inter/500.css';
+import '@fontsource/inter/600.css';
+import '@fontsource/inter/700.css';
+import '@fontsource-variable/jetbrains-mono';
+import { RootProvider } from 'fumadocs-ui/provider';
+import type { ReactNode } from 'react';
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: {
+ default: 'Readied — Offline-first Markdown editor for developers',
+ template: '%s | Readied',
+ },
+ description:
+ 'A beautiful Markdown editor that works offline, stores files locally, and never locks you in.',
+};
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+Note: `className="dark"` enforces dark-only per design. `antialiased` for crisp text.
+
+**Step 3: Verify build**
+
+```bash
+cd apps/web && pnpm exec next build
+```
+
+Expected: Build succeeds. Page renders with new font stack and violet tokens. Fumadocs docs inherit dark theme automatically.
+
+**Step 4: Commit**
+
+```bash
+git add apps/web/app/globals.css apps/web/app/layout.tsx
+git commit -m "feat(web): rewrite design system — violet accent, Inter + JetBrains Mono fonts"
+```
+
+---
+
+## Task 5: Rebuild Navbar
+
+Replace current Navbar + NavDropdown + MobileNav (using @headlessui) with shadcn/ui-based Navbar using Sheet for mobile.
+
+**Files:**
+
+- Rewrite: `apps/web/components/Navbar.tsx`
+- Delete: `apps/web/components/NavDropdown.tsx`
+- Delete: `apps/web/components/MobileNav.tsx`
+
+**Step 1: Rewrite Navbar**
+
+Create new `apps/web/components/Navbar.tsx`:
+
+- Fixed header with `backdrop-blur-xl` glassmorphism
+- Logo left: `font-mono text-lg font-bold` "readied" + violet dot
+- Center: nav links (Features, Pricing, Docs dropdown, Changelog)
+- Right: ghost "Download" + accent "Try Pro Free" buttons using `