From 8a9c8097e9e2efc2bb3777b6677ec5a68bb86a6f Mon Sep 17 00:00:00 2001 From: tomymaritano Date: Fri, 13 Mar 2026 15:04:15 -0300 Subject: [PATCH 1/9] chore(release): bump version to 0.9.0 Prepare release/0.9.0 with version bumps in root and desktop package.json files, and update CHANGELOG with all changes since 0.1.2. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++ apps/desktop/package.json | 2 +- package.json | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf07bd2c..e9f7eb3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.9.0] - 2026-03-13 + +### Added + +#### Website + +- Full website redesign with shadcn/ui + Magic UI + +#### Auth + +- Auth UX rethink with Enable Sync flow +- Auth middleware fix: return 401 instead of 500 on invalid tokens + +#### Sync + +- Error propagation for sync failures +- Exponential backoff on retry +- Abort sync on logout +- Typed token refresh +- Sync onboarding prompt after 5 notes +- Offline queue visibility in the UI + +#### AI Commands (Cmd+K v1) + +- Command panel for AI interactions +- AI settings configuration +- Keybindings for AI commands + +#### AI Knowledge (Cmd+K v2) + +- RAG-based knowledge retrieval +- Ask Notes: query your own notes with AI +- Related context suggestions + +#### AI Extensibility + +- Plugin API for AI commands +- Presets import/export + +#### Documentation + +- API documentation + +--- + ## [0.1.2] - 2026-01-01 ### Fixed diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f58d2c20..61792b6b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@readied/desktop", - "version": "0.8.0", + "version": "0.9.0", "private": true, "description": "Markdown-first, offline-forever note app for developers", "author": { diff --git a/package.json b/package.json index 1f08a7ec..4f9c2e83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "readied", - "version": "0.2.0", + "version": "0.9.0", "private": true, "license": "SEE LICENSE IN LICENSE", "description": "Markdown-first, offline-forever desktop note app", From ba28700c5e88f96b778010021ad849c7c4dc4ccc Mon Sep 17 00:00:00 2001 From: tomymaritano Date: Fri, 13 Mar 2026 15:08:01 -0300 Subject: [PATCH 2/9] docs: add strategic ROADMAP.md + remove macOS resource forks Add Phases 6-12 strategic roadmap covering workspace enhancement, command palette evolution, AI workflows, knowledge retrieval, and developer workflows. Remove tracked ._* resource fork files from exFAT volume. Co-Authored-By: Claude Opus 4.6 --- ROADMAP.md | 476 ++++++++++++++++++ apps/web/._mdx-components.tsx | Bin 4096 -> 0 bytes apps/web/._package.json | Bin 4096 -> 0 bytes apps/web/app/(marketing)/._page.tsx | Bin 4096 -> 0 bytes .../auth/verify/._AuthVerifyContent.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/changelog/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/download/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/faq/._page.tsx | Bin 4096 -> 0 bytes .../web/app/(marketing)/philosophy/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/plugins/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/pricing/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/privacy/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/(marketing)/terms/._page.tsx | Bin 4096 -> 0 bytes apps/web/app/._globals.css | Bin 4096 -> 0 bytes apps/web/app/._layout.tsx | Bin 4096 -> 0 bytes apps/web/app/docs/._layout.tsx | Bin 4096 -> 0 bytes apps/web/app/docs/[[...slug]]/._page.tsx | Bin 4096 -> 0 bytes apps/web/components/._FaqAccordion.tsx | Bin 4096 -> 0 bytes apps/web/components/._Footer.tsx | Bin 4096 -> 0 bytes apps/web/components/._NavDropdown.tsx | Bin 4096 -> 0 bytes apps/web/components/._Navbar.tsx | Bin 4096 -> 0 bytes apps/web/components/landing/._Audience.tsx | Bin 4096 -> 0 bytes apps/web/components/landing/._Features.tsx | Bin 4096 -> 0 bytes apps/web/components/landing/._Hero.tsx | Bin 4096 -> 0 bytes apps/web/components/landing/._SocialProof.tsx | Bin 4096 -> 0 bytes apps/web/components/landing/._WhyLocal.tsx | Bin 4096 -> 0 bytes apps/web/content/docs/._index.mdx | Bin 4096 -> 0 bytes .../content/docs/architecture/._overview.mdx | Bin 4096 -> 0 bytes apps/web/content/docs/guide/._principles.mdx | Bin 4096 -> 0 bytes .../docs/plugins/._getting-started.mdx | Bin 4096 -> 0 bytes 30 files changed, 476 insertions(+) create mode 100644 ROADMAP.md delete mode 100644 apps/web/._mdx-components.tsx delete mode 100644 apps/web/._package.json delete mode 100644 apps/web/app/(marketing)/._page.tsx delete mode 100644 apps/web/app/(marketing)/auth/verify/._AuthVerifyContent.tsx delete mode 100644 apps/web/app/(marketing)/changelog/._page.tsx delete mode 100644 apps/web/app/(marketing)/download/._page.tsx delete mode 100644 apps/web/app/(marketing)/faq/._page.tsx delete mode 100644 apps/web/app/(marketing)/philosophy/._page.tsx delete mode 100644 apps/web/app/(marketing)/plugins/._page.tsx delete mode 100644 apps/web/app/(marketing)/pricing/._page.tsx delete mode 100644 apps/web/app/(marketing)/privacy/._page.tsx delete mode 100644 apps/web/app/(marketing)/terms/._page.tsx delete mode 100644 apps/web/app/._globals.css delete mode 100644 apps/web/app/._layout.tsx delete mode 100644 apps/web/app/docs/._layout.tsx delete mode 100644 apps/web/app/docs/[[...slug]]/._page.tsx delete mode 100644 apps/web/components/._FaqAccordion.tsx delete mode 100644 apps/web/components/._Footer.tsx delete mode 100644 apps/web/components/._NavDropdown.tsx delete mode 100644 apps/web/components/._Navbar.tsx delete mode 100644 apps/web/components/landing/._Audience.tsx delete mode 100644 apps/web/components/landing/._Features.tsx delete mode 100644 apps/web/components/landing/._Hero.tsx delete mode 100644 apps/web/components/landing/._SocialProof.tsx delete mode 100644 apps/web/components/landing/._WhyLocal.tsx delete mode 100644 apps/web/content/docs/._index.mdx delete mode 100644 apps/web/content/docs/architecture/._overview.mdx delete mode 100644 apps/web/content/docs/guide/._principles.mdx delete mode 100644 apps/web/content/docs/plugins/._getting-started.mdx diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..d6b4b4cd --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,476 @@ +# Readied --- Product Roadmap + +> **Last updated:** 2026-03-13 +> **Current version:** v0.9.0 +> **Status:** Phases 1--5 complete, Phase 6+ in planning + +--- + +## Vision + +Readied is a **Markdown-first thinking workspace for builders**. Raycast meets Obsidian meets lightweight IDE for ideas. + +Your notes survive the app. Your thinking stays yours. AI augments, never replaces. + +--- + +## Core Principles + +| # | Principle | What it means | +| --- | ------------------------ | -------------------------------------------------------------------------------- | +| 1 | **Markdown-first** | Users own their data. Plain `.md` files, git-friendly, export = exact copy. | +| 2 | **Local-first** | Fast, offline, no cloud dependency. Sync is optional, never required. | +| 3 | **Command-driven UX** | Raycast-like Cmd+K, keyboard-first. Every action is a command. | +| 4 | **AI as augmentation** | AI operates ON notes --- summarize, expand, extract. It never replaces thinking. | +| 5 | **Speed and minimalism** | Developer-grade ergonomics. Start fast, stay fast, no bloat. | + +--- + +## What's Done (v0.9.0) + +### Phase 1: Foundation + +- Website redesign with CodeRabbit review fixes +- Auth UX rethink: "Enable Sync" modal, deep link race condition fix +- Auth middleware hardening (all JWT error types return 401 with JSON) +- Full API documentation (24 endpoints) + +### Phase 2: Sync Stability + +- Auto-sync error propagation to renderer via IPC events +- Exponential backoff on repeated failures (capped at 5 minutes) +- 401 auto-stops sync and emits `auth-expired` event +- Abort in-flight sync on logout via AbortController +- Token refresh with typed errors (expired / network / device_limit) +- Sync onboarding flow (prompt after 5 notes, session-dismissable) +- Offline queue visibility in status bar (pending count + offline indicator) + +### Phase 3: AI Commands (Cmd+K v1) + +- Cmd+K opens AI panel; insert-link remapped to Cmd+Shift+K +- AI command definitions: `ai:toggle-panel`, `ai:summarize`, `ai:rewrite`, `ai:tweet`, `ai:ask-notes` +- AI Settings: API key, model selector, max context notes +- Settings schema v2 with migration from v1 +- Escape cascade: palette -> AI panel -> graph -> search +- Existing `ai-assistant` package (Claude client, RAG, prompts) wired into panel + +### Phase 4: AI Knowledge (Cmd+K v2) --- Pending + +- RAG integration: query across notes +- "Ask Your Notes" command with knowledge context +- Related notes context in AI prompts + +### Phase 5: Extensibility --- Pending + +- Plugin API for custom AI commands (`registerAiCommand` on PluginContext) +- AI command presets (shareable JSON bundles with validation) +- Import/export AI command definitions +- Community command marketplace concept + +### Infrastructure + +| Component | Technology | +| ---------------- | -------------------------------- | +| Runtime | Electron + electron-vite | +| Frontend | React + TanStack Query + Zustand | +| Editor | CodeMirror 6 | +| Database | SQLite (better-sqlite3) | +| Monorepo | pnpm + turborepo | +| Backend API | Cloudflare Workers | +| Auth | Magic link (Resend) + JWT | +| Database (cloud) | Turso (libsql) | + +--- + +## Phase 6: Markdown Workspace Enhancement + +> **Goal:** Make Readied feel like a real workspace, not a single-note editor. + +| Feature | Description | Priority | +| ------------------------ | ---------------------------------------------------------- | -------- | +| File tree | Sidebar with workspace folders, drag-and-drop organization | High | +| Quick Open | Fuzzy file finder (Cmd+P) across all notes | High | +| Backlinks graph v2 | Improved graph visualization, filtering, zoom-to-node | Medium | +| Tags system v2 | Tag hierarchy, tag autocomplete, tag-based navigation | Medium | +| Git-friendly structure | Option to store notes as `.md` files on disk (vault mode) | Medium | +| Obsidian-like navigation | Forward/back history, breadcrumbs, link preview on hover | Low | + +**Key decisions:** + +- Vault mode (files on disk) is opt-in; SQLite remains the default for performance +- Quick Open reuses the command palette infrastructure (same fuzzy matching engine) +- Tags are derived from markdown frontmatter and inline `#tag` syntax + +--- + +## Phase 7: Command Palette Evolution (Cmd+K v3) + +> **Goal:** Make the command palette the Raycast of note-taking --- fast, extensible, context-aware. + +| Feature | Description | Priority | +| -------------------------- | ----------------------------------------------------------------- | -------- | +| Raycast-style architecture | Typed results, inline previews, action chains | High | +| Plugin-style commands | Community commands via plugin API | High | +| Command discovery UX | Categories, recent commands, favorites, frequency sorting | Medium | +| Contextual commands | Different commands based on selection, note type, cursor position | Medium | +| Template insertion | Insert note templates, code snippets, date patterns via commands | Medium | +| Quick actions | Create note, open note, search, insert template --- all sub-50ms | Low | + +**Command categories:** + +``` +Navigation ai editor notes + open-note summarize bold create + open-folder rewrite heading delete + switch-tab tweet link pin + search ask-notes code-block move +``` + +**Extensibility model:** + +- Commands are registered via `PluginContext.registerCommand()` +- AI commands use `PluginContext.registerAiCommand()` with template placeholders +- Presets are shareable JSON bundles (`AiCommandPreset` type with validation) + +--- + +## Phase 8: AI Workflows on Notes + +> **Goal:** Turn notes into action with a pipeline model: Note -> Action -> Result. + +| Feature | Description | Priority | +| ----------------------- | ------------------------------------------------------------------- | -------- | +| Action pipeline | Note -> Action -> Result with configurable output targets | High | +| Built-in actions | Summarize, expand, tweet thread, blog draft, explain, extract tasks | High | +| Output targets | Insert in note, clipboard, side panel, new note | High | +| Streaming responses | Token-by-token rendering in AI panel | Medium | +| Action chaining | Summarize -> Tweet -> Copy (multi-step workflows) | Medium | +| Custom workflow builder | Visual workflow editor for composing action chains | Low | + +**Output target model:** + +``` +outputTarget: 'replace' -- Replace selected text in editor + | 'insert' -- Insert at cursor position + | 'panel' -- Show in AI panel (chat) + | 'clipboard' -- Copy to clipboard + | 'new-note' -- Create a new note with result +``` + +**Built-in action presets:** + +| Action | Input | Output | Use case | +| ------------- | --------- | --------- | ------------------------------------ | +| Summarize | Full note | Panel | Quick overview of long notes | +| Expand | Selection | Replace | Flesh out bullet points | +| Tweet thread | Full note | Clipboard | Content repurposing | +| Blog draft | Selection | New note | Turn rough ideas into posts | +| Extract tasks | Full note | Insert | Pull action items from meeting notes | +| Explain | Selection | Panel | Clarify technical content | + +--- + +## Phase 9: Knowledge Retrieval (Embeddings) + +> **Goal:** "What did I write about X?" --- semantic search across all notes. + +| Feature | Description | Priority | +| -------------------- | ---------------------------------------------------- | -------- | +| Markdown chunking | Heading-based and block-based splitting | High | +| Embedding generation | Local (transformers.js) or API-based | High | +| Vector storage | SQLite with vec extension or dedicated store | High | +| Semantic search | Query all notes by meaning, not just keywords | High | +| Prompt composition | Inject retrieved context into AI prompts | Medium | +| Index management | On-save indexing, background reindex, manual rebuild | Medium | + +**Chunking strategy:** + +``` +Document: "# Architecture\n\nWe use React...\n\n## State\n\nZustand for UI..." + +Chunks: + [1] heading="Architecture" content="We use React..." + [2] heading="State" content="Zustand for UI..." +``` + +**Embedding options under evaluation:** + +| Option | Pros | Cons | +| ----------------------- | ----------------------------- | --------------------------------- | +| transformers.js (local) | Offline, private, no API cost | Model size (~50MB), slower on CPU | +| OpenAI API | High quality, fast | Requires internet, API cost | +| Local ONNX model | Good balance, offline | Setup complexity | + +**Indexing lifecycle:** + +``` +Note saved -> chunk -> embed -> store vectors +Query -> embed query -> cosine similarity -> top-k chunks -> compose prompt +``` + +--- + +## Phase 10: AI Assistant Panel v2 + +> **Goal:** A conversational AI that knows your notes and remembers your questions. + +| Feature | Description | Priority | +| ------------------------ | ------------------------------------------------------------- | -------- | +| Conversation memory | Persist chat history across sessions | High | +| Context injection | Current note, selected text, search results as context | High | +| Multi-turn conversations | Follow-up questions with note awareness | Medium | +| Side panel UX v2 | Resizable panel, markdown rendering, code highlighting | Medium | +| Quick actions from chat | Insert response into note, create new note, copy to clipboard | Medium | +| Conversation management | Name, search, delete past conversations | Low | + +**Context injection model:** + +``` +System prompt + + Conversation history (last N turns) + + Current note content (if open) + + Selected text (if any) + + RAG results (if knowledge query) + = Final prompt to LLM +``` + +--- + +## Phase 11: Onboarding Experience + +> **Goal:** Value within 2 minutes. + +| Step | What happens | Time | +| ---- | ------------------------------------------------------------- | ---- | +| 1 | Welcome screen: "Your thinking workspace" | 10s | +| 2 | Create first note (pre-filled template with instructions) | 30s | +| 3 | Command palette tutorial (Cmd+K highlight) | 20s | +| 4 | First AI action: summarize or generate tweet from sample note | 30s | +| 5 | Knowledge search demo: "Ask your notes" with seeded content | 20s | +| 6 | Done: workspace ready, user understands core loop | 10s | + +**Principles:** + +- No account required to start +- No configuration required to start +- AI features work with a single API key entry +- Every step is skippable +- Progress persists if interrupted + +--- + +## Phase 12: Developer Workflows + +> **Goal:** Make Readied the best place to think about code, not write code. + +| Feature | Description | Priority | +| ----------------------------- | -------------------------------------------------------- | -------- | +| Code snippet management | Syntax-highlighted snippets with language detection | Medium | +| Technical doc templates | RFC, ADR, API doc, runbook templates | Medium | +| API documentation generator | AI-powered: paste endpoint -> generate docs | Low | +| Meeting notes -> action items | AI extraction of tasks, decisions, owners | Medium | +| Decision log templates | Lightweight ADR format with status tracking | Low | +| Architecture Decision Records | Structured template with context, decision, consequences | Low | + +**Template examples:** + +```markdown +# ADR-001: [Decision Title] + +- **Status:** Proposed | Accepted | Deprecated +- **Date:** {{date}} +- **Context:** Why is this decision needed? +- **Decision:** What was decided? +- **Consequences:** What are the trade-offs? +``` + +--- + +## Technical Architecture + +```mermaid +graph TB + subgraph Electron + direction TB + subgraph Main Process + SQLite[(SQLite DB)] + IPC[IPC Handlers] + Sync[Sync Service] + Auth[Auth Service] + end + + subgraph Renderer + React[React UI] + CM6[CodeMirror 6] + CmdPalette[Command Palette] + AIPanel[AI Panel] + Zustand[Zustand Stores] + TQ[TanStack Query] + end + + subgraph Preload + Bridge[Secure IPC Bridge] + end + end + + subgraph Packages + Core[core
Domain logic + MD parsing] + StorageCore[storage-core
Storage interfaces] + StorageSQLite[storage-sqlite
SQLite adapter] + CmdRegistry[command-registry
Command definitions] + AiAssistant[ai-assistant
Claude client + RAG] + PluginAPI[plugin-api
Plugin system] + SyncCore[sync-core
Sync logic] + Licensing[licensing
License validation] + ProductConfig[product-config
Plans + pricing] + DesignSystem[design-system
Tokens + themes] + Wikilinks[wikilinks
Link parsing] + Embeds[embeds
Embed handling] + Tasks[tasks
Task extraction] + Commands[commands
Command implementations] + PluginCLI[plugin-cli
Plugin scaffolding] + end + + subgraph Cloud + API[Cloudflare Workers API] + Turso[(Turso DB)] + Resend[Resend Email] + end + + React --> Bridge --> IPC + IPC --> SQLite + IPC --> Core + Core --> StorageCore + StorageSQLite --> StorageCore + AIPanel --> AiAssistant + CmdPalette --> CmdRegistry + React --> PluginAPI + Sync --> SyncCore + Sync --> API + Auth --> API + API --> Turso + API --> Resend +``` + +### Package Dependency Graph + +```mermaid +graph LR + desktop[apps/desktop] --> core + desktop --> storage-sqlite + desktop --> command-registry + desktop --> ai-assistant + desktop --> plugin-api + desktop --> sync-core + desktop --> licensing + desktop --> product-config + + storage-sqlite --> storage-core + ai-assistant --> core + command-registry --> core + plugin-api --> core + sync-core --> core + commands --> command-registry + commands --> core +``` + +### AI Pipeline + +```mermaid +flowchart LR + Input[User Input
selection / note / query] --> Template[Template Resolution
{{selection}} {{note}} {{title}}] + Template --> Context[Context Assembly
system prompt + user prompt + RAG] + Context --> LLM[LLM API
Claude / OpenAI] + LLM --> Output[Output Routing
replace / insert / panel / clipboard] +``` + +--- + +## Release Strategy + +### Versioning + +Readied follows [Semantic Versioning](https://semver.org/): + +| Version bump | When | Example | +| ----------------- | --------------------------------- | -------------------------- | +| **Major** (X.0.0) | Breaking changes, major UX shifts | v1.0.0: public launch | +| **Minor** (0.X.0) | New features, non-breaking | v0.10.0: workspace folders | +| **Patch** (0.0.X) | Bug fixes, polish | v0.9.1: fix sync edge case | + +### Branch Strategy (Git Flow) + +``` +main <-- Production releases only + \-- develop <-- Integration branch + \-- feature/* <-- New features + \-- fix/* <-- Bug fixes + \-- release/* <-- Release preparation +``` + +### Release Cadence + +| Channel | Frequency | Audience | +| ----------- | ---------- | ----------------------- | +| **Stable** | Monthly | All users | +| **Beta** | Bi-weekly | Early adopters (opt-in) | +| **Nightly** | Daily (CI) | Contributors only | + +### Release Checklist + +- [ ] All tests pass (`pnpm test`) +- [ ] Build succeeds (`pnpm build`) +- [ ] TypeScript clean (`pnpm typecheck`) +- [ ] Changelog updated +- [ ] Version bumped in `package.json` +- [ ] Release branch merged to `main` and back to `develop` +- [ ] macOS build signed and notarized +- [ ] Release notes published on GitHub + +--- + +## Milestone Timeline + +```mermaid +gantt + title Readied Roadmap + dateFormat YYYY-MM + axisFormat %b %Y + + section Done + Phase 1 - Foundation :done, p1, 2026-01, 2026-02 + Phase 2 - Sync Stability :done, p2, 2026-02, 2026-03 + Phase 3 - AI Commands v1 :done, p3, 2026-03, 2026-03 + + section In Progress + Phase 4 - AI Knowledge v2 :active, p4, 2026-03, 2026-04 + Phase 5 - Extensibility :active, p5, 2026-03, 2026-04 + + section Planned + Phase 6 - Workspace :p6, 2026-04, 2026-05 + Phase 7 - Cmd+K v3 :p7, 2026-05, 2026-06 + Phase 8 - AI Workflows :p8, 2026-06, 2026-07 + Phase 9 - Embeddings :p9, 2026-07, 2026-08 + Phase 10 - AI Panel v2 :p10, 2026-08, 2026-09 + Phase 11 - Onboarding :p11, 2026-09, 2026-09 + Phase 12 - Dev Workflows :p12, 2026-09, 2026-10 + + section Launch + v1.0.0 Public Launch :milestone, launch, 2026-10, 0d +``` + +--- + +## Contributing + +Readied is built in the open. If you want to contribute: + +1. Check the [issues](https://github.com/tomymaritano/readide/issues) for `good first issue` labels +2. Read `CLAUDE.md` for project conventions +3. Read `plan.md` for architecture decisions +4. Branch from `develop`, target PRs to `develop` +5. Follow conventional commits (`feat:`, `fix:`, `refactor:`, etc.) + +--- + +_This roadmap is a living document. Priorities shift based on user feedback and technical discovery. Last major update: 2026-03-13._ diff --git a/apps/web/._mdx-components.tsx b/apps/web/._mdx-components.tsx deleted file mode 100644 index 87e41598c8e70b2e832d692846959999845572b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 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 y Date: Fri, 13 Mar 2026 15:47:06 -0300 Subject: [PATCH 3/9] chore: add vercel.json for monorepo deployment Configure Vercel to build apps/web from the monorepo root using pnpm workspaces. Co-Authored-By: Claude Opus 4.6 --- vercel.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 vercel.json diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..892ffe9c --- /dev/null +++ b/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "pnpm --filter @readied/web build", + "outputDirectory": "apps/web/out", + "installCommand": "pnpm install", + "framework": null +} From ce0526be4092b1c68f0ac32d71f920c4039fec7a Mon Sep 17 00:00:00 2001 From: tomymaritano Date: Fri, 13 Mar 2026 15:54:50 -0300 Subject: [PATCH 4/9] fix: resolve 29 CodeRabbit review issues from PR #149 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - Guard apiClient post-refresh retry to prevent infinite 401 loop Major: - Treat transient refresh errors as retryable, not logout-worthy - Use ApiError.statusCode for auth-expiry detection in syncService - Handle intentional sync abort gracefully (no false error status) - Use product-config facade labels for pricing display - Fix FaqAccordion accessibility (ARIA roles, tab semantics) - Fix Testimonials semantic HTML (figcaption, star rating aria) - Add aria-labelledby to WhyLocal section - Fix animated-grid-pattern memory leak (mount guard, cleanup) - Fix hero-video-dialog keyboard handling (Escape via useEffect) Minor: - Guard LoginModal reset against rapid reopen race condition - Fix fallback color to match teal accent scheme - URL-encode auth verify token in deep links - Fix import order in docs layout - Suppress transitions in prefers-reduced-motion - Stabilize animated-beam random duration with useMemo - Add focus management to hero-video-dialog Nitpick: - Rename LoginModal → EnableSyncModal to match export name - Extract CSS color tokens for ai-panel and global styles - Improve Footer, dot-pattern, and docs page component quality Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + apps/desktop/src/main/services/apiClient.ts | 17 ++-- apps/desktop/src/main/services/syncService.ts | 37 +++++++- .../src/renderer/components/ai/AiPanel.tsx | 7 +- .../renderer/components/sidebar/Sidebar.tsx | 13 ++- .../{LoginModal.tsx => EnableSyncModal.tsx} | 10 ++- .../src/renderer/components/sync/index.ts | 2 +- apps/desktop/src/renderer/styles/ai-panel.css | 4 +- apps/desktop/src/renderer/styles/global.css | 10 +-- apps/desktop/src/renderer/styles/tokens.css | 2 + apps/web/.gitignore | 1 + .../auth/verify/AuthVerifyContent.tsx | 22 ++--- apps/web/app/(marketing)/pricing/page.tsx | 32 ++----- apps/web/app/docs/[[...slug]]/page.tsx | 26 +----- apps/web/app/docs/layout.tsx | 2 +- apps/web/app/globals.css | 88 +++++++++++-------- apps/web/components/FaqAccordion.tsx | 46 ++++++---- apps/web/components/Footer.tsx | 16 +--- apps/web/components/landing/Testimonials.tsx | 6 +- apps/web/components/landing/WhyLocal.tsx | 7 +- apps/web/components/magicui/animated-beam.tsx | 5 +- .../magicui/animated-grid-pattern.tsx | 38 +++++--- apps/web/components/magicui/dot-pattern.tsx | 40 +++++---- .../components/magicui/hero-video-dialog.tsx | 33 ++++--- 24 files changed, 260 insertions(+), 205 deletions(-) rename apps/desktop/src/renderer/components/sync/{LoginModal.tsx => EnableSyncModal.tsx} (98%) diff --git a/.gitignore b/.gitignore index cd8f41ae..96f7a24c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ npm-debug.log* # Claude cache (keep plans) .claude/plugins/ .claude/statsig/ +.vercel diff --git a/apps/desktop/src/main/services/apiClient.ts b/apps/desktop/src/main/services/apiClient.ts index 910a1c26..109b416f 100644 --- a/apps/desktop/src/main/services/apiClient.ts +++ b/apps/desktop/src/main/services/apiClient.ts @@ -186,7 +186,12 @@ export class ApiClient { /** * Generic HTTP request with auth, retry, and error handling */ - private async request(endpoint: string, options: RequestInit = {}, retries = 3): Promise { + private async request( + endpoint: string, + options: RequestInit = {}, + retries = 3, + _isAuthRetry = false + ): Promise { const url = `${this.baseURL}${endpoint}`; // Inject access token if available @@ -212,11 +217,11 @@ export class ApiClient { }); // Handle 401 - Token expired - if (response.status === 401 && tokens) { + if (response.status === 401 && tokens && !_isAuthRetry) { const refreshResult = await this.refreshAccessToken(); switch (refreshResult.type) { case 'success': - return this.request(endpoint, options, 0); + return this.request(endpoint, options, 0, true); case 'network': // Transient failure — throw retryable error so caller can try later throw new ApiError(0, refreshResult.message ?? 'Network error during token refresh'); @@ -233,11 +238,7 @@ export class ApiClient { refreshResult.message ?? 'Session expired. Please sign in again.' ); default: - await this.tokenStorage.clearTokens(); - throw new ApiError( - 401, - refreshResult.message ?? 'Authentication failed. Please sign in again.' - ); + throw new ApiError(0, refreshResult.message ?? 'Transient error during token refresh'); } } diff --git a/apps/desktop/src/main/services/syncService.ts b/apps/desktop/src/main/services/syncService.ts index c054aa49..fc8a479f 100644 --- a/apps/desktop/src/main/services/syncService.ts +++ b/apps/desktop/src/main/services/syncService.ts @@ -16,7 +16,13 @@ import { createTimestamp, type NoteStatus, } from '@readied/core'; -import type { ApiClient, SyncChange, NotebookSyncChange, NotebookPushResult } from './apiClient.js'; +import { + ApiError, + type ApiClient, + type SyncChange, + type NotebookSyncChange, + type NotebookPushResult, +} from './apiClient.js'; import type { EncryptionService } from './encryptionService.js'; // ============================================================================ @@ -85,9 +91,7 @@ function isNetworkError(error: unknown): boolean { * Check if an error represents a 401 Unauthorized response. */ function isAuthError(error: unknown): boolean { - if (!(error instanceof Error)) return false; - const msg = error.message.toLowerCase(); - return msg.includes('401') || msg.includes('unauthorized'); + return error instanceof ApiError && error.statusCode === 401; } // ============================================================================ @@ -634,6 +638,31 @@ export class SyncService { }; } catch (error) { const bandwidth = this.apiClient.getBandwidth(); + + // Intentional abort (e.g. logout / stopAutoSync) — not a real error + if (error instanceof Error && error.message === 'Sync aborted') { + this.noteRepository.completeSyncHistoryEntry(historyId, 'error', { + notesPulled, + notesPushed, + notebooksPulled, + notebooksPushed, + tagsPulled, + tagsPushed, + conflicts: totalConflicts, + bytesSent: bandwidth.bytesSent, + bytesReceived: bandwidth.bytesReceived, + errorMessage: 'Sync aborted', + }); + + return { + success: false, + changesApplied: 0, + changesPushed: 0, + conflicts: [], + error: 'Sync aborted', + }; + } + this.noteRepository.completeSyncHistoryEntry(historyId, 'error', { notesPulled: 0, notesPushed: 0, diff --git a/apps/desktop/src/renderer/components/ai/AiPanel.tsx b/apps/desktop/src/renderer/components/ai/AiPanel.tsx index 9a14c69a..3d126d5c 100644 --- a/apps/desktop/src/renderer/components/ai/AiPanel.tsx +++ b/apps/desktop/src/renderer/components/ai/AiPanel.tsx @@ -55,16 +55,17 @@ export function AiPanel({ if (!query || loading) return; // Prefer settings store, fall back to plugin config for backwards compatibility - const apiKey = aiSettings.apiKey || getConfig('apiKey'); + const useSettingsStore = Boolean(aiSettings.apiKey); + const apiKey = useSettingsStore ? aiSettings.apiKey : getConfig('apiKey'); if (!apiKey) { setError('Please set your Anthropic API key in Settings > AI Assistant'); return; } - const model = aiSettings.apiKey + const model = useSettingsStore ? aiSettings.model : getConfig('model') || 'claude-sonnet-4-20250514'; - const maxContextNotes = aiSettings.apiKey + const maxContextNotes = useSettingsStore ? aiSettings.maxContextNotes : getConfig('maxContextNotes') || 5; diff --git a/apps/desktop/src/renderer/components/sidebar/Sidebar.tsx b/apps/desktop/src/renderer/components/sidebar/Sidebar.tsx index 258c891e..c0fdcd68 100644 --- a/apps/desktop/src/renderer/components/sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/components/sidebar/Sidebar.tsx @@ -152,10 +152,19 @@ export function Sidebar({ onOpenGraph }: SidebarProps) {
Sync your notes across devices
- -
diff --git a/apps/desktop/src/renderer/components/sync/LoginModal.tsx b/apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx similarity index 98% rename from apps/desktop/src/renderer/components/sync/LoginModal.tsx rename to apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx index c5c40783..2999b78b 100644 --- a/apps/desktop/src/renderer/components/sync/LoginModal.tsx +++ b/apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx @@ -59,10 +59,12 @@ export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) { if (!isOpen) { // Delay reset so close animation can play const timeout = setTimeout(() => { - setStep('value-prop'); - setEmail(''); - setError(null); - setResendTimer(0); + if (!isOpen) { + setStep('value-prop'); + setEmail(''); + setError(null); + setResendTimer(0); + } }, 200); return () => clearTimeout(timeout); } diff --git a/apps/desktop/src/renderer/components/sync/index.ts b/apps/desktop/src/renderer/components/sync/index.ts index 008cc558..ff6cefad 100644 --- a/apps/desktop/src/renderer/components/sync/index.ts +++ b/apps/desktop/src/renderer/components/sync/index.ts @@ -6,4 +6,4 @@ export { SyncStatusIndicator } from './SyncStatusIndicator'; export { ConflictResolver } from './ConflictResolver'; -export { EnableSyncModal } from './LoginModal'; +export { EnableSyncModal } from './EnableSyncModal'; diff --git a/apps/desktop/src/renderer/styles/ai-panel.css b/apps/desktop/src/renderer/styles/ai-panel.css index 4f42528c..4ae24659 100644 --- a/apps/desktop/src/renderer/styles/ai-panel.css +++ b/apps/desktop/src/renderer/styles/ai-panel.css @@ -34,7 +34,7 @@ font-size: 10px; font-weight: 500; color: var(--accent); - background: rgba(94, 234, 212, 0.1); + background: var(--accent-subtle); padding: 1px 6px; border-radius: 8px; white-space: nowrap; @@ -67,7 +67,7 @@ .ai-panel-btn.active { color: var(--accent); - background: rgba(94, 234, 212, 0.1); + background: var(--accent-subtle); } /* Messages area */ diff --git a/apps/desktop/src/renderer/styles/global.css b/apps/desktop/src/renderer/styles/global.css index 049319a5..09b396e3 100644 --- a/apps/desktop/src/renderer/styles/global.css +++ b/apps/desktop/src/renderer/styles/global.css @@ -1511,7 +1511,7 @@ input:focus-visible { justify-content: space-between; padding: 8px 12px; margin: 0 8px 4px; - background: var(--accent-subtle, rgba(59, 130, 246, 0.08)); + background: var(--accent-subtle, rgba(94, 234, 212, 0.08)); border-radius: var(--radius-sm, 4px); font-size: var(--text-xs); color: var(--text-secondary); @@ -1526,7 +1526,7 @@ input:focus-visible { margin-left: 8px; } -.sidebar-sync-prompt-actions button:first-child { +.sidebar-sync-prompt-actions .sidebar-sync-prompt-enable { background: var(--accent); color: white; border: none; @@ -1537,11 +1537,11 @@ input:focus-visible { font-weight: 500; } -.sidebar-sync-prompt-actions button:first-child:hover { +.sidebar-sync-prompt-actions .sidebar-sync-prompt-enable:hover { opacity: 0.9; } -.sidebar-sync-prompt-actions button:last-child { +.sidebar-sync-prompt-actions .sidebar-sync-prompt-dismiss { background: none; border: none; color: var(--text-muted); @@ -1551,7 +1551,7 @@ input:focus-visible { line-height: 1; } -.sidebar-sync-prompt-actions button:last-child:hover { +.sidebar-sync-prompt-actions .sidebar-sync-prompt-dismiss:hover { color: var(--text-secondary); } diff --git a/apps/desktop/src/renderer/styles/tokens.css b/apps/desktop/src/renderer/styles/tokens.css index 59830963..49933a05 100644 --- a/apps/desktop/src/renderer/styles/tokens.css +++ b/apps/desktop/src/renderer/styles/tokens.css @@ -36,6 +36,7 @@ /* ===== COLORS - Accent ===== */ --accent: #5eead4; --accent-muted: rgba(94, 234, 212, 0.15); + --accent-subtle: rgba(94, 234, 212, 0.1); --accent-strong: #2dd4bf; /* ===== COLORS - Semantic ===== */ @@ -138,6 +139,7 @@ /* Accent (lighter versions for light theme) */ --accent: #14b8a6; + --accent-subtle: rgba(20, 184, 166, 0.1); --accent-muted: rgba(20, 184, 166, 0.15); --accent-strong: #0d9488; --accent-primary: #14b8a6; diff --git a/apps/web/.gitignore b/apps/web/.gitignore index f9ef312e..4039e06a 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -2,3 +2,4 @@ .source/ node_modules/ out/ +.vercel diff --git a/apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx b/apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx index 30b38ee9..2c2fc8d4 100644 --- a/apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx +++ b/apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx @@ -11,7 +11,7 @@ export default function AuthVerifyContent() { useEffect(() => { if (token) { - window.location.href = `readied://auth/verify?token=${token}`; + window.location.href = `readied://auth/verify?token=${encodeURIComponent(token)}`; const timer = setTimeout(() => { setShowFallback(true); @@ -39,8 +39,8 @@ export default function AuthVerifyContent() {
-

Invalid Link

-

+

Invalid Link

+

This verification link is incomplete or has expired. Please request a new magic link from the Readied app.

@@ -61,8 +61,8 @@ export default function AuthVerifyContent() { {!showFallback ? ( <>
-

Opening Readied...

-

The app should open automatically. Hang tight.

+

Opening Readied...

+

The app should open automatically. Hang tight.

) : ( <> @@ -79,27 +79,27 @@ export default function AuthVerifyContent() {
-

Almost there

-

+

Almost there

+

The app didn't open automatically. Try clicking the button below.

Open in Readied
-

+

Opened this on the wrong device?

-

+

Open this same link on the device where Readied is installed. The magic link is valid for 15 minutes.

-

+

Don't have Readied yet?{' '} Download now diff --git a/apps/web/app/(marketing)/pricing/page.tsx b/apps/web/app/(marketing)/pricing/page.tsx index f381ca5e..adff55f9 100644 --- a/apps/web/app/(marketing)/pricing/page.tsx +++ b/apps/web/app/(marketing)/pricing/page.tsx @@ -14,7 +14,6 @@ import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { BorderBeam } from '@/components/magicui/border-beam'; -import { NumberTicker } from '@/components/magicui/number-ticker'; import { Accordion, AccordionItem, @@ -27,9 +26,8 @@ export default function PricingPage() { const { plans, guarantees, trialDays, trialDescription } = config; const proPricing = plans.pro.pricing!; - // Extract numeric values from price labels for NumberTicker - const monthlyPrice = proPricing.intervals.monthly.amountCents / 100; - const annualPrice = proPricing.intervals.annual.amountCents / 100; + const monthlyLabel = proPricing.intervals.monthly.label; + const annualLabel = proPricing.intervals.annual.label; const faqs = [ { q: 'What if you stop developing Readied?', a: guarantees.freeTierForever.description }, @@ -109,27 +107,13 @@ export default function PricingPage() {

{plans.pro.name}
-
- $ - - /mo -
+ + {monthlyLabel} + or -
- $ - - - /year - -
+ + {annualLabel} + Save {proPricing.annualSavings} diff --git a/apps/web/app/docs/[[...slug]]/page.tsx b/apps/web/app/docs/[[...slug]]/page.tsx index 64a54b4d..4db6a23e 100644 --- a/apps/web/app/docs/[[...slug]]/page.tsx +++ b/apps/web/app/docs/[[...slug]]/page.tsx @@ -1,31 +1,9 @@ import { source } from '@/lib/source'; import { notFound } from 'next/navigation'; import { DocsPage, DocsBody, DocsTitle, DocsDescription } from 'fumadocs-ui/page'; -import defaultMdxComponents 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'; +import { useMDXComponents } from '@/mdx-components'; -const mdxComponents = { - ...defaultMdxComponents, - Card, - Cards, - Callout, - Step, - Steps, - Tab, - Tabs, - Accordion, - Accordions, - File, - Folder, - Files, - TypeTable, -}; +const mdxComponents = useMDXComponents({}); export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; diff --git a/apps/web/app/docs/layout.tsx b/apps/web/app/docs/layout.tsx index 1bfcc343..ad924e52 100644 --- a/apps/web/app/docs/layout.tsx +++ b/apps/web/app/docs/layout.tsx @@ -1,7 +1,7 @@ +import type { ReactNode } from 'react'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { source } from '@/lib/source'; import { baseOptions } from '@/lib/layout.shared'; -import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 4f6fafc2..1fb5c9e0 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -27,22 +27,39 @@ /* ─── Fumadocs theme alignment ─── */ /* Override fumadocs CSS variables to match our violet/zinc design system */ :root, .dark { + --rd-violet: #8b5cf6; + --rd-violet-light: #a78bfa; + --rd-violet-lighter: #c4b5fd; + --rd-violet-glow-10: rgba(139, 92, 246, 0.1); + --rd-violet-glow-12: rgba(139, 92, 246, 0.12); + --rd-violet-glow-20: rgba(139, 92, 246, 0.2); + --rd-violet-glow-30: rgba(139, 92, 246, 0.3); + --rd-violet-glow-08: rgba(139, 92, 246, 0.08); + --rd-surface: #111113; + --rd-inset: #0c0c0e; + --rd-foreground: #fafafa; + --rd-muted-foreground: #a1a1aa; + --rd-subtle-foreground: #e4e4e7; + --rd-faint: #52525b; + --rd-border-subtle: rgba(255, 255, 255, 0.06); + --rd-border: rgba(255, 255, 255, 0.08); + --color-fd-background: #09090b; - --color-fd-foreground: #fafafa; - --color-fd-muted: #111113; - --color-fd-muted-foreground: #a1a1aa; - --color-fd-popover: #111113; - --color-fd-popover-foreground: #e4e4e7; - --color-fd-card: #111113; - --color-fd-card-foreground: #fafafa; - --color-fd-border: rgba(255, 255, 255, 0.08); - --color-fd-primary: #8b5cf6; + --color-fd-foreground: var(--rd-foreground); + --color-fd-muted: var(--rd-surface); + --color-fd-muted-foreground: var(--rd-muted-foreground); + --color-fd-popover: var(--rd-surface); + --color-fd-popover-foreground: var(--rd-subtle-foreground); + --color-fd-card: var(--rd-surface); + --color-fd-card-foreground: var(--rd-foreground); + --color-fd-border: var(--rd-border); + --color-fd-primary: var(--rd-violet); --color-fd-primary-foreground: #ffffff; --color-fd-secondary: #1a1a1f; - --color-fd-secondary-foreground: #e4e4e7; - --color-fd-accent: rgba(139, 92, 246, 0.12); - --color-fd-accent-foreground: #e4e4e7; - --color-fd-ring: #8b5cf6; + --color-fd-secondary-foreground: var(--rd-subtle-foreground); + --color-fd-accent: var(--rd-violet-glow-12); + --color-fd-accent-foreground: var(--rd-subtle-foreground); + --color-fd-ring: var(--rd-violet); } /* ─── Fumadocs component overrides ─── */ @@ -54,26 +71,26 @@ /* Sidebar links — subtle hover */ .fd-sidebar [data-active='true'] { - color: #8b5cf6 !important; + color: var(--rd-violet) !important; } /* Docs nav bar — glass effect */ nav[data-fumadocs] { - border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-bottom: 1px solid var(--rd-border-subtle); } /* Code blocks — darker with violet accents */ pre:has(code) { - background: #0c0c0e !important; - border: 1px solid rgba(255, 255, 255, 0.06); + background: var(--rd-inset) !important; + border: 1px solid var(--rd-border-subtle); border-radius: 0.75rem; } /* Inline code */ :not(pre) > code { - background: rgba(139, 92, 246, 0.1) !important; - color: #c4b5fd !important; - border: 1px solid rgba(139, 92, 246, 0.2); + background: var(--rd-violet-glow-10) !important; + color: var(--rd-violet-lighter) !important; + border: 1px solid var(--rd-violet-glow-20); border-radius: 0.375rem; padding: 0.125rem 0.375rem; font-size: 0.875em; @@ -81,51 +98,51 @@ pre:has(code) { /* Table of contents — active item */ [data-toc] a[data-active='true'] { - color: #8b5cf6; - border-left-color: #8b5cf6; + color: var(--rd-violet); + border-left-color: var(--rd-violet); } /* Card links in docs */ .fd-card { - background: #111113; - border-color: rgba(255, 255, 255, 0.08); + background: var(--rd-surface); + border-color: var(--rd-border); transition: border-color 0.2s, box-shadow 0.2s; } .fd-card:hover { - border-color: rgba(139, 92, 246, 0.3); - box-shadow: 0 0 30px rgba(139, 92, 246, 0.08); + border-color: var(--rd-violet-glow-30); + box-shadow: 0 0 30px var(--rd-violet-glow-08); } /* Search dialog */ [data-fumadocs-search] { - --color-fd-background: #0c0c0e; - --color-fd-popover: #111113; + --color-fd-background: var(--rd-inset); + --color-fd-popover: var(--rd-surface); } /* Breadcrumbs */ nav[aria-label='Breadcrumb'] { - color: #52525b; + color: var(--rd-faint); } nav[aria-label='Breadcrumb'] a:hover { - color: #8b5cf6; + color: var(--rd-violet); } /* Headings in docs content — slightly brighter */ .fd-page h1, .fd-page h2, .fd-page h3, .fd-page h4 { - color: #fafafa; + color: var(--rd-foreground); } /* Links in docs content */ .fd-page a:not([class]) { - color: #a78bfa; - text-decoration-color: rgba(139, 92, 246, 0.3); + color: var(--rd-violet-light); + text-decoration-color: var(--rd-violet-glow-30); } .fd-page a:not([class]):hover { - color: #c4b5fd; - text-decoration-color: #8b5cf6; + color: var(--rd-violet-lighter); + text-decoration-color: var(--rd-violet); } /* Marketing page utilities */ @@ -272,5 +289,6 @@ nav[aria-label='Breadcrumb'] a:hover { .animate-fade-in-up { animation: none !important; transform: none !important; + transition: none !important; } } diff --git a/apps/web/components/FaqAccordion.tsx b/apps/web/components/FaqAccordion.tsx index 61d08d55..252e9a85 100644 --- a/apps/web/components/FaqAccordion.tsx +++ b/apps/web/components/FaqAccordion.tsx @@ -120,30 +120,40 @@ export default function FaqAccordion(props: Props) {
{/* Category tabs */} -
- {categories.map(cat => ( - - ))} +
+ {categories.map(cat => { + const isActive = !isSearching && activeTab === cat.category; + return ( + + ); + })}
{/* Results */} {isSearching && visibleItems.length === 0 ? (

No questions match your search.

) : ( -
+
)} diff --git a/apps/web/components/Footer.tsx b/apps/web/components/Footer.tsx index d29d3eac..3c917bd3 100644 --- a/apps/web/components/Footer.tsx +++ b/apps/web/components/Footer.tsx @@ -129,25 +129,11 @@ export default function Footer() {
{/* Bottom bar */} -
+
© {year} Readied. Built with ♥ in Argentina. -
- {socialLinks.map(social => ( - - {social.icon} - - ))} -
diff --git a/apps/web/components/landing/Testimonials.tsx b/apps/web/components/landing/Testimonials.tsx index aa010341..2c3ffd00 100644 --- a/apps/web/components/landing/Testimonials.tsx +++ b/apps/web/components/landing/Testimonials.tsx @@ -56,9 +56,9 @@ const secondRow = reviews.slice(reviews.length / 2); function Stars({ count }: { count: number }) { return ( -
+
{Array.from({ length: count }).map((_, i) => ( - +
); @@ -101,7 +101,7 @@ function ReviewCard({
{text}
-

{date}

+
{date}
); } diff --git a/apps/web/components/landing/WhyLocal.tsx b/apps/web/components/landing/WhyLocal.tsx index bdac47d3..e1123e78 100644 --- a/apps/web/components/landing/WhyLocal.tsx +++ b/apps/web/components/landing/WhyLocal.tsx @@ -194,12 +194,15 @@ function DataFlowDiagram() { export default function WhyLocal() { return ( -
+
{/* Header */}
Why Local -

+

Your notes should live on your machine {' — '}not someone else's server.

diff --git a/apps/web/components/magicui/animated-beam.tsx b/apps/web/components/magicui/animated-beam.tsx index b824e4e9..b892a6ac 100644 --- a/apps/web/components/magicui/animated-beam.tsx +++ b/apps/web/components/magicui/animated-beam.tsx @@ -1,6 +1,6 @@ 'use client'; -import { type RefObject, useEffect, useId, useState } from 'react'; +import { type RefObject, useEffect, useId, useMemo, useState } from 'react'; import { motion } from 'framer-motion'; import { cn } from '@/lib/utils'; @@ -32,7 +32,7 @@ export const AnimatedBeam: React.FC = ({ toRef, curvature = 0, reverse = false, - duration = Math.random() * 3 + 4, + duration: durationProp, delay = 0, pathColor = 'gray', pathWidth = 2, @@ -45,6 +45,7 @@ export const AnimatedBeam: React.FC = ({ endYOffset = 0, }) => { const id = useId(); + const duration = useMemo(() => durationProp ?? Math.random() * 3 + 4, [durationProp]); const [pathD, setPathD] = useState(''); const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 }); diff --git a/apps/web/components/magicui/animated-grid-pattern.tsx b/apps/web/components/magicui/animated-grid-pattern.tsx index 777dac64..170c8337 100644 --- a/apps/web/components/magicui/animated-grid-pattern.tsx +++ b/apps/web/components/magicui/animated-grid-pattern.tsx @@ -32,8 +32,16 @@ export function AnimatedGridPattern({ }: AnimatedGridPatternProps) { const id = useId(); const containerRef = useRef(null); + const mountedRef = useRef(true); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + const getPos = useCallback(() => { return [ Math.floor((Math.random() * dimensions.width) / width), @@ -53,19 +61,22 @@ export function AnimatedGridPattern({ 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 - ) - ); - }; + const updateSquarePosition = useCallback( + (id: number) => { + if (!mountedRef.current) return; + setSquares(currentSquares => + currentSquares.map(sq => + sq.id === id + ? { + ...sq, + pos: getPos(), + } + : sq + ) + ); + }, + [getPos] + ); // Update squares to animate in useEffect(() => { @@ -78,6 +89,7 @@ export function AnimatedGridPattern({ useEffect(() => { const currentRef = containerRef.current; const resizeObserver = new ResizeObserver(entries => { + if (!mountedRef.current) return; for (const entry of entries) { setDimensions({ width: entry.contentRect.width, diff --git a/apps/web/components/magicui/dot-pattern.tsx b/apps/web/components/magicui/dot-pattern.tsx index bfc57468..ca67a2b4 100644 --- a/apps/web/components/magicui/dot-pattern.tsx +++ b/apps/web/components/magicui/dot-pattern.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useId, useRef, useState } from 'react'; +import React, { useEffect, useId, useMemo, useRef, useState } from 'react'; import { motion } from 'framer-motion'; import { cn } from '@/lib/utils'; @@ -50,23 +50,27 @@ export function DotPattern({ 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, - }; - } + const dots = useMemo( + () => + 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, + }; + } + ), + [dimensions.width, dimensions.height, safeWidth, safeHeight, cx, cy, x, y] ); return ( diff --git a/apps/web/components/magicui/hero-video-dialog.tsx b/apps/web/components/magicui/hero-video-dialog.tsx index d73dc0bf..20c3dff7 100644 --- a/apps/web/components/magicui/hero-video-dialog.tsx +++ b/apps/web/components/magicui/hero-video-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Play, XIcon } from 'lucide-react'; import { AnimatePresence, motion } from 'framer-motion'; @@ -75,7 +75,23 @@ export function HeroVideoDialog({ className, }: HeroVideoProps) { const [isVideoOpen, setIsVideoOpen] = useState(false); + const [isClosing, setIsClosing] = useState(false); const selectedAnimation = animationVariants[animationStyle]; + const closeButtonRef = useRef(null); + + const closeVideo = useCallback(() => { + setIsClosing(true); + setIsVideoOpen(false); + }, []); + + useEffect(() => { + if (!isVideoOpen) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') closeVideo(); + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isVideoOpen, closeVideo]); return (
@@ -106,7 +122,7 @@ export function HeroVideoDialog({
- + setIsClosing(false)}> {isVideoOpen && ( { - if (e.key === 'Escape') { - setIsVideoOpen(false); - } - }} - onClick={() => setIsVideoOpen(false)} + onClick={closeVideo} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md" > @@ -129,16 +140,18 @@ export function HeroVideoDialog({ className="relative mx-4 aspect-video w-full max-w-4xl md:mx-0" > setIsVideoOpen(false)} + autoFocus + onClick={closeVideo} 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" >