diff --git a/.cursor/plans/workos_setup_9603c983.plan.md b/.cursor/plans/workos_setup_9603c983.plan.md new file mode 100644 index 0000000..7c7c6fc --- /dev/null +++ b/.cursor/plans/workos_setup_9603c983.plan.md @@ -0,0 +1,66 @@ +--- +name: WorkOS Setup +overview: Complete WorkOS AuthKit integration for the dashboard by creating the Callback page, adding auth protection to Dashboard, and adding the callback route. +todos: + - id: create-callback + content: Create src/pages/Callback.tsx with auth redirect handling + status: completed + - id: add-callback-route + content: Add /callback route handling to src/App.tsx + status: completed + - id: update-dashboard + content: Add WorkOS auth protection to Dashboard.tsx + status: completed + - id: test-flow + content: Test authentication flow locally + status: completed + - id: todo-1767049823884-2nns40kqq + content: "" + status: pending +--- + +# WorkOS AuthKit Setup Plan + +## Current Status + +| Step | Description | Status | +| -------- | ----------------------------------- | ------------------ | +| Part 1-2 | Create WorkOS account and configure | Done | +| Part 3 | Install dependencies | Done | +| Part 4 | Environment variables | Done | +| Part 5 | Convex auth config | Done | +| Part 6 | Update main.tsx | Done | +| Part 7 | Create Callback route | Missing | +| Part 8 | Protected Dashboard | Needs auth wrapper | +| Part 9 | Add routes to App.tsx | Missing /callback | +| Part 10 | Test | Pending | + +--- + +## Tasks + +### 1. Create Callback.tsx (Part 7) + +Create `src/pages/Callback.tsx` that handles OAuth callback and redirects to dashboard. + +### 2. Add /callback route to App.tsx (Part 9) + +Add callback route handling similar to how dashboard is handled. + +### 3. Update Dashboard.tsx (Part 8) + +Wrap existing dashboard with auth guards using Authenticated, Unauthenticated, and AuthLoading from convex/react. + +### 4. Test (Part 10) + +Test the full authentication flow locally. + +--- + +## Files + +| File | Action | +| ----------------------- | ------------------- | +| src/pages/Callback.tsx | Create | +| src/App.tsx | Add /callback route | +| src/pages/Dashboard.tsx | Add auth wrapper | \ No newline at end of file diff --git a/.cursor/rules/dev2.mdc b/.cursor/rules/dev2.mdc index c742a86..dcda786 100644 --- a/.cursor/rules/dev2.mdc +++ b/.cursor/rules/dev2.mdc @@ -27,6 +27,8 @@ alwaysApply: true - Be casual unless otherwise specified - you are a super experienced full-stack and AI developer super experienced in React, Vite, Bun, Clerk, workos, Resend, TypeScript, and Convex.dev - You’re an experienced AI developer with deep expertise in convex.dev, OpenAI, and Claude, following best practices for building AI powered and full-stack SaaS applications, react applications and social network platforms. +- you are an expert in all things workos AuthKit https://workos.com/docs/authkit/vanilla/nodejs and workos docs https://workos.com/docs +- you are an expert setting up Convex & WorkOS AuthKit https://docs.convex.dev/auth/authkit/ - follow the vercel Web Interface Guidelines https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/refs/heads/main/AGENTS.md and https://vercel.com/design/guidelines diff --git a/.cursor/rules/help.mdc b/.cursor/rules/help.mdc index ce0a8db..37aa803 100644 --- a/.cursor/rules/help.mdc +++ b/.cursor/rules/help.mdc @@ -41,6 +41,8 @@ Before implementing any solution, follow this reflection process: • End-to-End Type Support with TypeScript: https://docs.convex.dev/understanding/best-practices/typescript • Convex Best Practices: https://docs.convex.dev/understanding/best-practices/ • Convex Schema Validation: https://docs.convex.dev/database/schemas +• workos AuthKit https://workos.com/docs/authkit/vanilla/nodejs and workos docs https://workos.com/docs +• you are an expert setting up Convex & WorkOS AuthKit https://docs.convex.dev/auth/authkit/ ⸻ diff --git a/AGENTS.md b/AGENTS.md index c3b5696..6b7c401 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Your content is instantly available to browsers, LLMs, and AI agents.. Write mar - **Total Posts**: 15 - **Total Pages**: 5 - **Latest Post**: 2025-12-29 -- **Last Updated**: 2025-12-29T03:04:12.912Z +- **Last Updated**: 2025-12-29T21:11:54.407Z ## Tech stack diff --git a/FORK_CONFIG.md b/FORK_CONFIG.md index f39a14a..8a82f3f 100644 --- a/FORK_CONFIG.md +++ b/FORK_CONFIG.md @@ -668,6 +668,49 @@ weeklyDigest: { Runs automatically via cron job every Sunday at 9:00 AM UTC. +### Dashboard + +The dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations. + +**Configuration:** + +In `src/config/siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, // Global toggle for dashboard page + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +**Authentication:** + +WorkOS authentication is optional. When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. + +**WorkOS Setup:** + +To enable WorkOS authentication: + +1. Create a WorkOS account at [workos.com](https://workos.com) +2. Set `VITE_WORKOS_CLIENT_ID` in your `.env.local` file +3. Set `VITE_WORKOS_REDIRECT_URI` (e.g., `http://localhost:5173/callback`) +4. Add `WORKOS_CLIENT_ID` to Convex environment variables +5. Configure redirect URI in WorkOS dashboard +6. Set `requireAuth: true` in `siteConfig.ts` + +See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for complete setup instructions. + +**Features:** + +- Content management: Edit posts and pages with live preview +- Sync commands: Run sync operations from the browser +- Site configuration: Configure all settings via UI +- Newsletter management: Integrated subscriber and email management +- AI Agent: Writing assistance powered by Claude +- Analytics: Real-time stats dashboard + +See [How to use the Markdown sync dashboard](https://www.markdown.fast/how-to-use-the-markdown-sync-dashboard) for complete usage guide. + --- ## Contact Form Configuration diff --git a/README.md b/README.md index 96429f0..b573414 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,19 @@ See [How to Use the MCP Server](https://www.markdown.fast/how-to-use-mcp-server) - Run `npm run import ` to scrape and create draft posts locally - Then sync to dev or prod with `npm run sync` or `npm run sync:prod` +### Dashboard + +The framework includes a centralized dashboard at `/dashboard` for managing content and configuring your site. Features include: + +- Content management: Edit posts and pages with live preview +- Sync commands: Run sync operations from the browser +- Site configuration: Configure all settings via UI +- Newsletter management: Integrated subscriber and email management +- AI Agent: Writing assistance powered by Claude +- Analytics: Real-time stats dashboard + +WorkOS authentication is recommended so no one has access to your dashboard if it's enabled. Configure it in `siteConfig.ts` to protect the dashboard in production. See [How to use the Markdown sync dashboard](https://www.markdown.fast/how-to-use-the-markdown-sync-dashboard) and [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for details. + ### Newsletter and Email The framework includes AgentMail integration for newsletter subscriptions and contact forms. Features include: diff --git a/TASK.md b/TASK.md index 1d97b95..e14c3dd 100644 --- a/TASK.md +++ b/TASK.md @@ -7,10 +7,33 @@ ## Current Status -v1.41.0 ready. Blog heading styles added to home intro content. Headings in `content/pages/home.md` now use same styling as blog posts (blog-h1 through blog-h6) with clickable anchor links. Home intro content matches blog post typography and spacing. +v1.46.0 ready. Dashboard sync server feature complete. Local HTTP server allows executing sync commands directly from dashboard UI without opening terminal. Real-time output streaming, server status indicators, and copy/execute buttons implemented. Header sync buttons use sync server when available. Copy icons added for npm run sync-server command. ## Completed +- [x] Stats page configuration option for public/private access + - [x] Added StatsPageConfig interface to siteConfig.ts with enabled and showInNav options + - [x] Updated App.tsx to conditionally render /stats route based on config + - [x] Updated Stats.tsx to check if enabled and show disabled message if not + - [x] Updated Layout.tsx to hide stats nav item when disabled + - [x] Default configuration: enabled: true (public), showInNav: true (visible in nav) + - [x] Follows same pattern as NewsletterAdmin for consistency + - [x] Updated files.md with stats page configuration notes + - [x] Updated changelog.md with v1.43.0 entry + - [x] Updated TASK.md with completed task + +- [x] Honeypot bot protection for contact and newsletter forms + - [x] Added honeypot state and hidden field to ContactForm.tsx + - [x] Added honeypot state and hidden field to NewsletterSignup.tsx + - [x] Hidden "Website" field for contact form bot detection + - [x] Hidden "Fax" field for newsletter signup bot detection + - [x] Bots receive fake success message (no data submitted) + - [x] CSS positioning (position: absolute, left: -9999px) hides fields from users + - [x] aria-hidden="true" and tabIndex={-1} for accessibility + - [x] Different field names per form to avoid pattern detection + - [x] Updated files.md with honeypot protection notes + - [x] Updated changelog.md with v1.42.0 entry + - [x] Blog heading styles for home intro content - [x] Added generateSlug, getTextContent, HeadingAnchor helper functions to Home.tsx - [x] Updated ReactMarkdown components to include h1-h6 with blog-h* classes @@ -20,6 +43,21 @@ v1.41.0 ready. Blog heading styles added to home intro content. Headings in `con - [x] Updated files.md, changelog.md, changelog-page.md, TASK.md with feature documentation - [x] Home intro headings now match blog post typography and spacing +- [x] Synced home intro content via markdown file (home.md) + - [x] Created content/pages/home.md (slug: home-intro) for homepage intro text + - [x] Home.tsx fetches content from Convex via getPageBySlug query + - [x] Added textAlign frontmatter field for pages (left/center/right, default: left) + - [x] Added featuredTitle to siteConfig.ts for configurable featured section title + - [x] Full markdown support with links, headings, lists, blockquotes, horizontal rules + - [x] External links automatically open in new tab + - [x] Fallback to siteConfig.bio if home-intro page not found (loading/error states) + - [x] Content syncs with npm run sync (no redeploy needed for homepage text changes) + - [x] Updated convex/schema.ts with textAlign field + - [x] Updated convex/pages.ts with textAlign in queries and mutations + - [x] Updated scripts/sync-posts.ts to parse textAlign from frontmatter + - [x] Updated src/styles/global.css with home-intro-content styles + - [x] Updated files.md, changelog.md, TASK.md documentation + - [x] HTTP-based MCP Server on Netlify - [x] Created netlify/edge-functions/mcp.ts with JSON-RPC 2.0 implementation - [x] Added @modelcontextprotocol/sdk dependency to package.json diff --git a/changelog.md b/changelog.md index 6455a2e..ef211cc 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,134 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.46.0] - 2025-12-29 + +### Added + +- Dashboard sync server for executing sync commands from UI + - Local HTTP server (`scripts/sync-server.ts`) runs on localhost:3001 + - Execute sync commands directly from dashboard without opening terminal + - Real-time output streaming in dashboard terminal view + - Server status indicator (online/offline) in dashboard sync section + - Copy and Execute buttons for each sync command + - Optional token authentication via `SYNC_TOKEN` environment variable + - Whitelisted commands only (sync, sync:prod, sync:discovery, sync:discovery:prod, sync:all, sync:all:prod) + - Health check endpoint at `/health` for server availability + - CORS enabled for localhost:5173 (dev server) + - Header sync buttons use sync server when available, fallback to command modal + - Copy icons for `npm run sync-server` command in dashboard sync settings + +### Technical + +- New file: `scripts/sync-server.ts` - Local HTTP server using Node.js http module +- New npm script: `sync-server` - Start the local sync server +- Updated: `src/pages/Dashboard.tsx` - Sync server integration with health checks, execute functionality, and terminal output display +- Updated: `src/styles/global.css` - Styles for sync server status, terminal output, and copy buttons + +## [1.45.0] - 2025-12-29 + +### Added + +- Dashboard and WorkOS authentication integration + - Dashboard supports optional WorkOS authentication via `siteConfig.dashboard.requireAuth` + - WorkOS is optional - dashboard works with or without WorkOS configured + - When `requireAuth` is `false`, dashboard is open access + - When `requireAuth` is `true` and WorkOS is configured, dashboard requires login + - Shows setup instructions if `requireAuth` is `true` but WorkOS is not configured + - Warning banner displayed when authentication is not enabled +- Blog posts + - "How to use the Markdown sync dashboard" - Complete guide to dashboard features and usage + - "How to setup WorkOS" - Step-by-step WorkOS AuthKit setup guide +- Documentation updates + - README.md: Added dashboard and WorkOS section with links to blog posts + - docs.md: Added dashboard and WorkOS authentication sections + - setup-guide.md: Added dashboard and WorkOS authentication sections + - FORK_CONFIG.md: Added dashboard configuration information + - fork-config.json.example: Added dashboard configuration option + - files.md: Updated with dashboard and WorkOS file descriptions + +### Technical + +- New file: `src/utils/workos.ts` - WorkOS configuration utility +- Updated: `src/main.tsx` - Conditional WorkOS providers with lazy loading +- Updated: `src/App.tsx` - Callback route handling for WorkOS OAuth +- Updated: `src/pages/Dashboard.tsx` - Optional WorkOS authentication integration +- Updated: `src/pages/Callback.tsx` - OAuth callback handler for WorkOS +- Updated: `convex/auth.config.ts` - Convex authentication configuration for WorkOS +- Updated: `src/config/siteConfig.ts` - Dashboard configuration with requireAuth option + +## [1.44.0] - 2025-12-29 + +### Added + +- Dashboard at `/dashboard` for centralized content management and site configuration + - Content management: Posts and Pages list views with filtering, search, pagination, and items per page selector (15, 25, 50, 100) + - Post and Page editor: Markdown editor with live preview, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown, copy to clipboard + - Write Post and Write Page: Full-screen writing interface with markdown editor, frontmatter reference, download markdown, localStorage persistence + - AI Agent section: Dedicated AI chat separate from Write page, uses Anthropic Claude API, per-session chat history, markdown rendering + - Newsletter management: All Newsletter Admin features integrated (subscribers, send newsletter, write email, recent sends, email stats) + - Content import: Firecrawl import UI for importing external URLs as markdown drafts + - Site configuration: Config Generator UI for all `siteConfig.ts` settings, generates downloadable config file + - Index HTML editor: View and edit `index.html` content with meta tags, Open Graph, Twitter Cards, JSON-LD + - Analytics: Real-time stats dashboard (clone of `/stats` page, always accessible in dashboard) + - Sync commands: UI with buttons for all sync operations (sync, sync:discovery, sync:all for dev and prod) + - Header sync buttons: Quick sync buttons in dashboard header for `npm run sync:all` (dev and prod) + - Dashboard search: Search bar in header to search dashboard features, page titles, and post content + - Toast notifications: Success, error, info, and warning notifications with auto-dismiss + - Command modal: Shows sync command output with copy to clipboard functionality + - Mobile responsive: Fully responsive design with mobile-optimized layout + - Theme and font: Theme toggle and font switcher with persistent preferences + +### Technical + +- New page: `src/pages/Dashboard.tsx` (4736 lines) +- Dashboard uses Convex queries for real-time data +- All mutations follow Convex best practices (idempotent, indexed queries) +- Frontmatter sidebar width persisted in localStorage +- Editor content persisted in localStorage +- Independent scrolling for editor and sidebar sections +- Preview uses ReactMarkdown with remark-gfm, remark-breaks, rehype-raw, rehype-sanitize +- CSS styles added for dashboard layout, tables, editor, frontmatter sidebar, config generator, newsletter sections, stats sections, sync sections, toast notifications, command modal +- Mobile responsive breakpoints for all dashboard sections + +## [1.43.0] - 2025-12-29 + +### Added + +- Stats page configuration option for public/private access + - New `StatsPageConfig` interface in `siteConfig.ts` with `enabled` and `showInNav` options + - Stats page can be made private by setting `enabled: false` (similar to NewsletterAdmin pattern) + - When disabled, route shows "Stats page is disabled" message instead of analytics + - Navigation item automatically hidden when stats page is disabled + - Default configuration: `enabled: true` (public), `showInNav: true` (visible in nav) + +### Technical + +- Updated: `src/config/siteConfig.ts` (added StatsPageConfig interface and default config) +- Updated: `src/App.tsx` (conditionally renders /stats route based on config) +- Updated: `src/pages/Stats.tsx` (checks if enabled, shows disabled message if not) +- Updated: `src/components/Layout.tsx` (hides stats nav item when disabled) + +## [1.42.0] - 2025-12-29 + +### Added + +- Honeypot bot protection for contact and newsletter forms + - Hidden honeypot fields invisible to humans but visible to bots + - Contact form uses hidden "Website" field for bot detection + - Newsletter signup uses hidden "Fax" field for bot detection + - Bots that fill hidden fields receive fake success message (no data submitted) + - No external dependencies required (client-side only protection) + - Works with all four themes (dark, light, tan, cloud) + +### Technical + +- Updated: `src/components/ContactForm.tsx` (added honeypot state, hidden field, bot detection logic) +- Updated: `src/components/NewsletterSignup.tsx` (added honeypot state, hidden field, bot detection logic) +- Honeypot fields use CSS positioning (position: absolute, left: -9999px) to hide from users +- Fields include aria-hidden="true" and tabIndex={-1} for accessibility +- Different field names per form (website/fax) to avoid pattern detection + ## [1.41.0] - 2025-12-28 ### Added @@ -37,9 +165,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - New `content/pages/home.md` file for homepage intro/bio text - Home intro content now syncs with `npm run sync` like other pages - No redeploy needed for homepage text changes - - Full markdown support with links: `[text](url)` + - Full markdown support: links, headings, lists, blockquotes, horizontal rules - External links automatically open in new tab - - Fallback to `siteConfig.bio` if page not found + - Fallback to `siteConfig.bio` if page not found or while loading - New `textAlign` frontmatter field for pages - Control text alignment: "left", "center", "right" - Default: "left" (previously was always centered) diff --git a/content/blog/how-to-setup-workos.md b/content/blog/how-to-setup-workos.md new file mode 100644 index 0000000..8d1b9b0 --- /dev/null +++ b/content/blog/how-to-setup-workos.md @@ -0,0 +1,315 @@ +--- +title: "How to setup WorkOS" +description: "Step-by-step guide to configure WorkOS AuthKit authentication for your markdown blog dashboard. WorkOS is optional and can be enabled in siteConfig.ts." +date: "2025-12-29" +slug: "how-to-setup-workos" +published: true +tags: ["workos", "authentication", "tutorial", "dashboard"] +readTime: "10 min read" +featured: true +featuredOrder: 3 +excerpt: "Complete guide to setting up WorkOS AuthKit authentication for your dashboard. WorkOS is optional and can be configured in siteConfig.ts." +--- + +# How to setup WorkOS + +WorkOS AuthKit adds authentication to your markdown blog dashboard. It's optional—you can use the dashboard without WorkOS, or enable authentication for production sites. + +## Overview + +WorkOS AuthKit provides: + +- Password authentication +- Social login (Google, GitHub, etc.) +- SSO support +- User management +- Session handling + +The dashboard at `/dashboard` can work with or without WorkOS. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. + +## Prerequisites + +Before starting, make sure you have: + +- Node.js 18 or higher +- A working markdown blog project +- A Convex account and project set up +- Your Convex development server running (`npx convex dev`) + +## Step 1: Create a WorkOS account + +### Sign up + +1. Go to [workos.com/sign-up](https://signin.workos.com/sign-up) +2. Create a free account with your email +3. Verify your email address + +### Set up AuthKit + +1. Log into the [WorkOS Dashboard](https://dashboard.workos.com) +2. Navigate to **Authentication** → **AuthKit** +3. Click the **Set up AuthKit** button +4. Select **"Use AuthKit's customizable hosted UI"** +5. Click **Begin setup** + +### Configure redirect URI + +During the AuthKit setup wizard, you'll reach step 4: **"Add default redirect endpoint URI"** + +Enter this for local development: +``` +http://localhost:5173/callback +``` + +After a user logs in, WorkOS redirects them back to this URL with an authorization code. Your app exchanges this code for user information. + +### Copy your credentials + +1. Go to [dashboard.workos.com/get-started](https://dashboard.workos.com/get-started) +2. Under **Quick start**, find and copy: + - **Client ID** (looks like `client_01XXXXXXXXXXXXXXXXX`) + +Save this somewhere safe—you'll need it shortly. + +## Step 2: Configure WorkOS dashboard + +### Enable CORS + +For the React SDK to work, you need to allow your app's domain: + +1. Go to **Authentication** → **Sessions** in the WorkOS Dashboard +2. Find **Cross-Origin Resource Sharing (CORS)** +3. Click **Manage** +4. Add your development URL: `http://localhost:5173` +5. Click **Save** + +When you deploy to production (e.g., Netlify), add your production domain here too (e.g., `https://yoursite.netlify.app`). + +### Verify redirect URI + +1. Go to **Redirects** in the WorkOS Dashboard +2. Confirm `http://localhost:5173/callback` is listed +3. If not, add it by clicking **Add redirect** + +## Step 3: Install dependencies + +Open your terminal in the project folder and install the required packages: + +```bash +npm install @workos-inc/authkit-react @convex-dev/workos +``` + +**What these packages do:** + +| Package | Purpose | +|---------|---------| +| `@workos-inc/authkit-react` | WorkOS React SDK for handling login/logout | +| `@convex-dev/workos` | Bridges WorkOS auth with Convex backend | + +## Step 4: Add environment variables + +### Update `.env.local` + +Open your `.env.local` file (in the project root) and add these lines: + +```env +# Existing Convex URL (should already be here) +VITE_CONVEX_URL=https://your-deployment.convex.cloud + +# WorkOS AuthKit Configuration (add these) +VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX +VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback +``` + +Replace `client_01XXXXXXXXXXXXXXXXX` with your actual Client ID from the WorkOS Dashboard. + +Vite only exposes environment variables that start with `VITE_` to the browser. + +### Add to `.gitignore` + +Make sure `.env.local` is in your `.gitignore` to avoid committing secrets: + +``` +.env.local +.env.production.local +``` + +## Step 5: Configure Convex auth + +Create a new file to tell Convex how to validate WorkOS tokens. + +### Create `convex/auth.config.ts` + +Create a new file at `convex/auth.config.ts`: + +```typescript +// convex/auth.config.ts +const clientId = process.env.WORKOS_CLIENT_ID; + +const authConfig = { + providers: [ + { + type: "customJwt", + issuer: "https://api.workos.com/", + algorithm: "RS256", + applicationID: clientId, + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + { + type: "customJwt", + issuer: `https://api.workos.com/user_management/${clientId}`, + algorithm: "RS256", + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + ], +}; + +export default authConfig; +``` + +### Add environment variable to Convex + +The Convex backend needs the Client ID too: + +1. Run `npx convex dev` if not already running +2. You'll see an error with a link—click it +3. It takes you to the Convex Dashboard environment variables page +4. Add a new variable: + - **Name**: `WORKOS_CLIENT_ID` + - **Value**: Your WorkOS Client ID (e.g., `client_01XXXXXXXXXXXXXXXXX`) +5. Save + +After saving, `npx convex dev` should show "Convex functions ready." + +## Step 6: Update site configuration + +Enable authentication in your site config: + +```typescript +// src/config/siteConfig.ts +dashboard: { + enabled: true, + requireAuth: true, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `true` and WorkOS is configured, the dashboard requires login. When `requireAuth` is `false`, the dashboard is open access. + +## Step 7: Test locally + +1. Start your development server: + ```bash + npm run dev + ``` + +2. Navigate to `http://localhost:5173/dashboard` + +3. You should see a login prompt (if `requireAuth: true`) or the dashboard (if `requireAuth: false`) + +4. If authentication is enabled, click "Sign in" to test the WorkOS login flow + +5. After logging in, you should be redirected back to the dashboard + +## Step 8: Deploy to production + +### Add production environment variables + +In your Netlify dashboard (or hosting provider), add these environment variables: + +- `VITE_WORKOS_CLIENT_ID` - Your WorkOS Client ID +- `VITE_WORKOS_REDIRECT_URI` - Your production callback URL (e.g., `https://yoursite.netlify.app/callback`) + +### Update WorkOS redirect URI + +1. Go to **Redirects** in the WorkOS Dashboard +2. Add your production callback URL: `https://yoursite.netlify.app/callback` +3. Click **Save** + +### Update CORS settings + +1. Go to **Authentication** → **Sessions** in the WorkOS Dashboard +2. Find **Cross-Origin Resource Sharing (CORS)** +3. Click **Manage** +4. Add your production domain: `https://yoursite.netlify.app` +5. Click **Save** + +### Add Convex environment variable + +In your Convex Dashboard, add the `WORKOS_CLIENT_ID` environment variable for your production deployment: + +1. Go to [dashboard.convex.dev](https://dashboard.convex.dev) +2. Select your production project +3. Navigate to Settings → Environment Variables +4. Add `WORKOS_CLIENT_ID` with your Client ID value +5. Save + +## How it works + +When WorkOS is configured and `requireAuth: true`: + +1. User navigates to `/dashboard` +2. Dashboard checks authentication status +3. If not authenticated, shows login prompt +4. User clicks "Sign in" and is redirected to WorkOS +5. User logs in with WorkOS (password, social, or SSO) +6. WorkOS redirects back to `/callback` with authorization code +7. App exchanges code for user information +8. User is redirected to `/dashboard` as authenticated user + +When WorkOS is not configured or `requireAuth: false`: + +1. User navigates to `/dashboard` +2. Dashboard shows content directly (no authentication required) + +## Troubleshooting + +**Dashboard shows "Authentication Required" message:** +- Verify `VITE_WORKOS_CLIENT_ID` and `VITE_WORKOS_REDIRECT_URI` are set in `.env.local` +- Check that `WORKOS_CLIENT_ID` is set in Convex environment variables +- Ensure `requireAuth: true` in `siteConfig.ts` + +**Login redirect not working:** +- Verify redirect URI matches exactly in WorkOS Dashboard +- Check CORS settings include your domain +- Ensure callback route is configured in `App.tsx` + +**"WorkOS is not configured" message:** +- Check that both `VITE_WORKOS_CLIENT_ID` and `VITE_WORKOS_REDIRECT_URI` are set +- Verify environment variables are loaded (check browser console) +- Restart development server after adding environment variables + +**Convex auth errors:** +- Verify `WORKOS_CLIENT_ID` is set in Convex environment variables +- Check that `convex/auth.config.ts` exists and is correct +- Ensure Convex functions are deployed (`npx convex deploy`) + +## Optional configuration + +WorkOS is optional. You can: + +- Use the dashboard without WorkOS (`requireAuth: false`) +- Enable WorkOS later when you need authentication +- Configure WorkOS for production only + +The dashboard works with or without WorkOS. Configure it based on your needs. + +## Next steps + +After setting up WorkOS: + +1. Test the authentication flow locally +2. Deploy to production with production environment variables +3. Add additional redirect URIs if needed +4. Configure social login providers in WorkOS Dashboard +5. Set up SSO if needed for your organization + +See [How to use the Markdown sync dashboard](https://www.markdown.fast/how-to-use-the-markdown-sync-dashboard) for dashboard usage. diff --git a/content/blog/how-to-use-the-markdown-sync-dashboard.md b/content/blog/how-to-use-the-markdown-sync-dashboard.md new file mode 100644 index 0000000..576dc79 --- /dev/null +++ b/content/blog/how-to-use-the-markdown-sync-dashboard.md @@ -0,0 +1,269 @@ +--- +title: "How to use the Markdown sync dashboard" +description: "Learn how to use the dashboard at /dashboard to manage content, configure your site, and sync markdown files without leaving your browser." +date: "2025-12-29" +slug: "how-to-use-the-markdown-sync-dashboard" +published: true +tags: ["dashboard", "tutorial", "content-management"] +readTime: "8 min read" +featured: true +featuredOrder: 2 +excerpt: "A complete guide to using the dashboard for managing your markdown blog without leaving your browser." +--- + +# How to use the Markdown sync dashboard + +The dashboard at `/dashboard` gives you a centralized interface for managing your markdown blog. You can edit posts, sync content, configure settings, and more without switching between your editor and terminal. + +## Accessing the dashboard + +Navigate to `/dashboard` in your browser. The dashboard isn't linked in the navigation by default, so you'll access it directly via URL. + +### Authentication + +The dashboard supports optional WorkOS authentication. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open to anyone who knows the URL. For production sites, set `requireAuth: true` and configure WorkOS authentication. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup. + +If WorkOS isn't configured and `requireAuth` is `true`, the dashboard shows setup instructions instead of the login prompt. + +## Content management + +### Posts and pages list + +View all your posts and pages in one place. Each list includes: + +- Filter by status: All, Published, Drafts +- Search by title or content +- Pagination with "First" and "Next" buttons +- Items per page selector (15, 25, 50, 100) +- Quick actions: Edit, View, Publish/Unpublish + +Posts and pages display with their titles, publication status, and last modified dates. Click any item to open the editor. + +### Post and page editor + +Edit markdown content with a live preview. The editor includes: + +- Markdown editor on the left +- Live preview on the right showing how content appears on your site +- Draggable frontmatter sidebar (200px-600px width) +- Independent scrolling for editor and preview sections +- Download markdown button to save changes locally +- Copy to clipboard for quick sharing + +The frontmatter sidebar shows all available fields with descriptions. Edit values directly, and changes appear in the preview immediately. + +### Write post and write page + +Create new content without leaving the dashboard. The write interface includes: + +- Full-screen markdown editor +- Frontmatter reference panel with copy buttons +- Word, line, and character counts +- Download markdown button +- Content persists in localStorage + +Write your content, fill in frontmatter fields, then download the markdown file. Save it to `content/blog/` or `content/pages/`, then sync to Convex. + +## AI Agent + +The dashboard includes a dedicated AI chat section separate from the Write page. Use it for: + +- Writing assistance +- Content suggestions +- Editing help +- Answering questions about your content + +The AI Agent uses Anthropic Claude API and requires `ANTHROPIC_API_KEY` in your Convex environment variables. Chat history is stored per-session in Convex. + +## Newsletter management + +All Newsletter Admin features are integrated into the dashboard: + +- Subscribers: View, search, filter, and delete subscribers +- Send newsletter: Select a blog post to send to all active subscribers +- Write email: Compose custom emails with markdown support +- Recent sends: View the last 10 newsletter sends (posts and custom emails) +- Email stats: Dashboard with total emails sent, newsletters sent, active subscribers, and retention rate + +Newsletter features require AgentMail configuration. Set `AGENTMAIL_API_KEY` and `AGENTMAIL_INBOX` in your Convex environment variables. + +## Content import + +Import articles from external URLs using Firecrawl: + +1. Enter the URL you want to import +2. Click "Import" +3. Review the imported markdown draft +4. Edit if needed, then sync to Convex + +Imported posts are created as drafts (`published: false`) by default. Review, edit, set `published: true`, then sync. + +Firecrawl import requires `FIRECRAWL_API_KEY` in your `.env.local` file. + +## Site configuration + +The Config Generator UI lets you configure all `siteConfig.ts` settings from the dashboard: + +- Site name, title, logo, bio +- Blog page settings +- Featured section configuration +- Logo gallery settings +- GitHub contributions +- Footer and social footer +- Newsletter settings +- Contact form settings +- Stats page settings +- Dashboard settings + +Make changes in the UI, then download the generated `siteConfig.ts` file. Replace your existing config file with the downloaded version. + +## Index HTML editor + +View and edit `index.html` content directly: + +- Meta tags +- Open Graph tags +- Twitter Cards +- JSON-LD structured data + +Edit values in the UI, then download the updated HTML file. Replace your existing `index.html` with the downloaded version. + +## Analytics + +The dashboard includes a real-time stats section (clone of `/stats` page): + +- Active visitors with per-page breakdown +- Total page views +- Unique visitors +- Views by page sorted by popularity + +Stats update automatically via Convex subscriptions. No page refresh needed. + +## Sync commands + +Run sync operations from the dashboard without opening a terminal: + +**Development:** + +- `npm run sync` - Sync markdown content +- `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) +- `npm run sync:all` - Sync content + discovery files together + +**Production:** + +- `npm run sync:prod` - Sync markdown content +- `npm run sync:discovery:prod` - Update discovery files +- `npm run sync:all:prod` - Sync content + discovery files together + +### Sync server + +For the best experience, start the sync server to execute commands directly from the dashboard: + +```bash +npm run sync-server +``` + +This starts a local HTTP server on `localhost:3001` that allows the dashboard to execute sync commands and stream output in real-time. + +**When sync server is running:** + +- Server status shows "Online" in the sync section +- "Execute" buttons appear for each sync command +- Clicking Execute runs the command and streams output to the terminal view +- Real-time output appears as the command runs +- No need to copy commands to your terminal + +**When sync server is offline:** + +- Server status shows "Offline" in the sync section +- Only "Copy" buttons appear for each sync command +- Clicking Copy shows the command in a modal for copying to your terminal +- Copy icon appears next to `npm run sync-server` command to help you start the server + +**Security:** + +- Server binds to localhost only (not accessible from network) +- Optional token authentication via `SYNC_TOKEN` environment variable +- Only whitelisted commands can be executed +- CORS enabled for localhost:5173 (dev server) + +### Header sync buttons + +Quick sync buttons in the dashboard header let you run `npm run sync:all` (dev and prod) with one click. These buttons automatically use the sync server when available, or show the command in a modal when the server is offline. + +## Dashboard features + +### Search + +The search bar in the dashboard header searches: + +- Dashboard features and sections +- Page titles +- Post content + +Results appear as you type. Click any result to navigate to that section or content. + +### Theme and font + +Toggle between themes (dark, light, tan, cloud) and switch fonts (serif, sans, monospace) from the dashboard. Preferences persist across sessions. + +### Toast notifications + +Success, error, info, and warning notifications appear in the top-right corner. They auto-dismiss after 4 seconds and are theme-aware. + +### Command modal + +When you run sync commands, output appears in a modal. You can: + +- View full command output +- Copy command to clipboard +- Close the modal + +### Mobile responsive + +The dashboard works on mobile devices with: + +- Collapsible sidebar +- Touch-friendly controls +- Responsive tables and forms +- Mobile-optimized layout + +## Best practices + +1. Use the editor for quick edits and previews +2. Download markdown files for version control +3. Sync regularly to keep content up to date +4. Use the AI Agent for writing assistance +5. Check analytics to see what content performs well +6. Configure WorkOS authentication for production sites + +## Troubleshooting + +**Dashboard not loading:** + +- Check that `dashboard.enabled: true` in `siteConfig.ts` +- Verify Convex is running (`npx convex dev`) +- Check browser console for errors + +**Sync commands failing:** + +- Ensure you're in the project root directory +- Check that Convex environment variables are set +- Verify `.env.local` or `.env.production.local` exists + +**Authentication not working:** + +- Verify WorkOS environment variables are set +- Check that `requireAuth: true` in `siteConfig.ts` +- See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for setup instructions + +The dashboard makes managing your markdown blog easier. You can edit content, sync files, and configure settings all from one place. diff --git a/content/blog/markdown-with-code-examples.md b/content/blog/markdown-with-code-examples.md index 3f66f77..ade8472 100644 --- a/content/blog/markdown-with-code-examples.md +++ b/content/blog/markdown-with-code-examples.md @@ -16,7 +16,9 @@ image: "/images/markdown.png" # Writing Markdown with Code Examples -This post demonstrates how to write markdown content with code blocks, tables, and formatting. Use it as a reference when creating your own posts. +This post is the complete reference for all markdown syntax used in this framework. It includes copy-paste examples for code blocks, tables, lists, links, images, collapsible sections, and all formatting options. + +**Use this post as your reference** when writing blog posts or pages. All examples are ready to copy and paste directly into your markdown files. ## Frontmatter diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md index ed88c22..e4270e3 100644 --- a/content/blog/setup-guide.md +++ b/content/blog/setup-guide.md @@ -1589,6 +1589,67 @@ Agent requires the following Convex environment variables: If `ANTHROPIC_API_KEY` is not configured in Convex environment variables, Agent displays a user-friendly error message: "API key is not set". This helps identify when the API key is missing in production deployments. +## Dashboard + +The Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations. It's designed for developers who fork the repository to set up and manage their markdown blog. + +**Access:** Navigate to `/dashboard` in your browser. The dashboard is not linked in the navigation by default. + +**Authentication:** WorkOS authentication is optional. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup. + +**Key Features:** + +- **Content Management:** Posts and Pages list views with filtering, search, pagination, and items per page selector +- **Post/Page Editor:** Markdown editor with live preview, draggable/resizable frontmatter sidebar, download markdown +- **Write Post/Page:** Full-screen writing interface with markdown editor and frontmatter reference +- **AI Agent:** Dedicated AI chat section separate from Write page +- **Newsletter Management:** All Newsletter Admin features integrated (subscribers, send newsletter, write email, recent sends, email stats) +- **Content Import:** Firecrawl import UI for importing external URLs as markdown drafts +- **Site Configuration:** Config Generator UI for all `siteConfig.ts` settings +- **Index HTML Editor:** View and edit `index.html` content +- **Analytics:** Real-time stats dashboard (always accessible in dashboard) +- **Sync Commands:** UI with buttons for all sync operations (sync, sync:discovery, sync:all for dev and prod) +- **Sync Server:** Execute sync commands directly from dashboard with real-time output +- **Header Sync Buttons:** Quick sync buttons in dashboard header for `npm run sync:all` (dev and prod) + +**Sync Commands Available:** + +- `npm run sync` - Sync markdown content (development) +- `npm run sync:prod` - Sync markdown content (production) +- `npm run sync:discovery` - Update discovery files (development) +- `npm run sync:discovery:prod` - Update discovery files (production) +- `npm run sync:all` - Sync content + discovery files (development) +- `npm run sync:all:prod` - Sync content + discovery files (production) +- `npm run sync-server` - Start local HTTP server for executing commands from dashboard + +**Sync Server:** + +The dashboard can execute sync commands directly without opening a terminal. Start the sync server: + +```bash +npm run sync-server +``` + +This starts a local HTTP server on `localhost:3001` that: +- Executes sync commands when requested from the dashboard +- Streams output in real-time to the dashboard terminal view +- Shows server status (online/offline) in the dashboard +- Supports optional token authentication via `SYNC_TOKEN` environment variable +- Only executes whitelisted commands for security + +When the sync server is running, the dashboard shows "Execute" buttons that run commands directly. When offline, buttons show commands in a modal for copying to your terminal. + +The dashboard provides a UI for these commands, but you can also run them directly from the terminal. See the [Dashboard documentation](/docs#dashboard) for complete details. + ## Next Steps After deploying: diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index bcb4261..eaa776c 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -9,6 +9,108 @@ layout: "sidebar" All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v1.46.0 + +Released December 29, 2025 + +**Dashboard sync server for executing sync commands from UI** + +- Local HTTP server for executing sync commands directly from dashboard + - Run `npm run sync-server` to start the local server on localhost:3001 + - Execute sync commands without opening a terminal + - Real-time output streaming in dashboard terminal view + - Server status indicator shows online/offline status + - Copy and Execute buttons for each sync command + - Optional token authentication via `SYNC_TOKEN` environment variable + - Whitelisted commands only for security + - Health check endpoint for server availability detection + - Header sync buttons automatically use sync server when available + - Copy icons for `npm run sync-server` command throughout dashboard + +**Technical details:** + +- New `scripts/sync-server.ts` file implements local HTTP server +- Uses Node.js `child_process.spawn` to execute npm commands +- Streams output in real-time to dashboard UI +- CORS enabled for localhost:5173 development server +- Server binds to localhost only (not accessible from network) + +Updated files: `scripts/sync-server.ts`, `src/pages/Dashboard.tsx`, `src/styles/global.css`, `package.json` + +## v1.45.0 + +Released December 29, 2025 + +**Dashboard and WorkOS authentication integration** + +- Dashboard supports optional WorkOS authentication via `siteConfig.dashboard.requireAuth` +- WorkOS is optional - dashboard works with or without WorkOS configured +- When `requireAuth` is `false`, dashboard is open access +- When `requireAuth` is `true` and WorkOS is configured, dashboard requires login +- Shows setup instructions if `requireAuth` is `true` but WorkOS is not configured +- Warning banner displayed when authentication is not enabled +- Blog posts added: "How to use the Markdown sync dashboard" and "How to setup WorkOS" + +Updated files: `src/pages/Dashboard.tsx`, `src/main.tsx`, `src/App.tsx`, `src/pages/Callback.tsx`, `src/utils/workos.ts`, `convex/auth.config.ts`, `src/config/siteConfig.ts`, `README.md`, `content/pages/docs.md`, `content/blog/setup-guide.md`, `FORK_CONFIG.md`, `fork-config.json.example`, `files.md`, `TASK.md`, `changelog.md`, `content/pages/changelog-page.md` + +## v1.44.0 + +Released December 29, 2025 + +**Dashboard for centralized content management and site configuration** + +- Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations +- Content management: Posts and Pages list views with filtering, search, pagination, and items per page selector (15, 25, 50, 100) +- Post and Page editor: Markdown editor with live preview, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown, copy to clipboard +- Write Post and Write Page: Full-screen writing interface with markdown editor, frontmatter reference, download markdown, localStorage persistence +- AI Agent section: Dedicated AI chat separate from Write page, uses Anthropic Claude API, per-session chat history, markdown rendering +- Newsletter management: All Newsletter Admin features integrated (subscribers, send newsletter, write email, recent sends, email stats) +- Content import: Firecrawl import UI for importing external URLs as markdown drafts +- Site configuration: Config Generator UI for all `siteConfig.ts` settings, generates downloadable config file +- Index HTML editor: View and edit `index.html` content with meta tags, Open Graph, Twitter Cards, JSON-LD +- Analytics: Real-time stats dashboard (clone of `/stats` page, always accessible in dashboard) +- Sync commands: UI with buttons for all sync operations (sync, sync:discovery, sync:all for dev and prod) +- Header sync buttons: Quick sync buttons in dashboard header for `npm run sync:all` (dev and prod) +- Dashboard search: Search bar in header to search dashboard features, page titles, and post content +- Toast notifications: Success, error, info, and warning notifications with auto-dismiss +- Command modal: Shows sync command output with copy to clipboard functionality +- Mobile responsive: Fully responsive design with mobile-optimized layout +- Theme and font: Theme toggle and font switcher with persistent preferences + +Updated files: `src/pages/Dashboard.tsx`, `src/styles/global.css`, `src/App.tsx` + +## v1.43.0 + +Released December 29, 2025 + +**Stats page configuration option for public/private access** + +- New `StatsPageConfig` interface in `siteConfig.ts` with `enabled` and `showInNav` options +- Stats page can be made private by setting `enabled: false` (similar to NewsletterAdmin pattern) +- When disabled, route shows "Stats page is disabled" message instead of analytics +- Navigation item automatically hidden when stats page is disabled +- Default configuration: `enabled: true` (public), `showInNav: true` (visible in nav) + +Updated files: `src/config/siteConfig.ts`, `src/App.tsx`, `src/pages/Stats.tsx`, `src/components/Layout.tsx` + +## v1.42.0 + +Released December 29, 2025 + +**Honeypot bot protection for contact and newsletter forms** + +- Hidden honeypot fields invisible to humans but visible to bots +- Contact form uses hidden "Website" field for bot detection +- Newsletter signup uses hidden "Fax" field for bot detection +- Bots that fill hidden fields receive fake success message (no data submitted) +- No external dependencies required (client-side only protection) +- Works with all four themes (dark, light, tan, cloud) + +Updated files: `src/components/ContactForm.tsx`, `src/components/NewsletterSignup.tsx` +- Honeypot fields use CSS positioning (position: absolute, left: -9999px) to hide from users +- Fields include aria-hidden="true" and tabIndex={-1} for accessibility +- Different field names per form (website/fax) to avoid pattern detection + ## v1.41.0 Released December 28, 2025 diff --git a/content/pages/docs.md b/content/pages/docs.md index d319ecd..fc86154 100644 --- a/content/pages/docs.md +++ b/content/pages/docs.md @@ -83,6 +83,8 @@ markdown-site/ ## Content +**Markdown examples:** For complete markdown syntax examples including code blocks, tables, lists, links, images, collapsible sections, and all formatting options, see [Writing Markdown with Code Examples](/markdown-with-code-examples). That post includes copy-paste examples for every markdown feature. + ### Blog posts Create files in `content/blog/` with frontmatter: @@ -1003,6 +1005,206 @@ npm run newsletter:send setup-guide The `newsletter:send` command calls the `scheduleSendPostNewsletter` mutation directly and sends emails in the background. Check the Newsletter Admin page or recent sends to see results. +## Dashboard + +The Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations. It's designed for developers who fork the repository to set up and manage their markdown blog. + +**Access:** Navigate to `/dashboard` in your browser. The dashboard is not linked in the navigation by default (similar to Newsletter Admin pattern). + +**Authentication:** WorkOS authentication is optional. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup. + +### Content Management + +**Posts and Pages List Views:** +- View all posts and pages (published and unpublished) +- Filter by status: All, Published, Drafts +- Search by title or content +- Pagination with "First" and "Next" buttons +- Items per page selector (15, 25, 50, 100) - default: 15 +- Edit, view, and publish/unpublish options +- WordPress-style UI with date, edit, view, and publish controls + +**Post and Page Editor:** +- Markdown editor with live preview +- Frontmatter sidebar on the right with all available fields +- Draggable/resizable frontmatter sidebar (200px-600px width) +- Independent scrolling for frontmatter sidebar +- Preview mode shows content as it appears on the live site +- Download markdown button to generate `.md` files +- Copy markdown to clipboard +- All frontmatter fields editable in sidebar +- Preview uses ReactMarkdown with proper styling + +**Write Post and Write Page:** +- Full-screen writing interface +- Markdown editor with word/line/character counts +- Frontmatter reference panel +- Download markdown button for new content +- Content persists in localStorage +- Separate storage for post and page content + +### AI Agent + +- Dedicated AI chat section separate from the Write page +- Uses Anthropic Claude API (requires `ANTHROPIC_API_KEY` in Convex environment) +- Per-session chat history stored in Convex +- Markdown rendering for AI responses +- Copy functionality for AI responses + +### Newsletter Management + +All Newsletter Admin features integrated into the Dashboard: + +- **Subscribers:** View, search, filter, and delete subscribers +- **Send Newsletter:** Select a blog post to send as newsletter +- **Write Email:** Compose custom emails with markdown support +- **Recent Sends:** View last 10 newsletter sends (posts and custom emails) +- **Email Stats:** Dashboard with total emails, newsletters sent, active subscribers, retention rate + +All newsletter sections are full-width in the dashboard content area. + +### Content Import + +**Firecrawl Import:** +- Import articles from external URLs using Firecrawl API +- Requires `FIRECRAWL_API_KEY` in `.env.local` +- Creates local markdown drafts in `content/blog/` +- Imported posts are drafts (`published: false`) by default +- Review, edit, set `published: true`, then sync + +### Site Configuration + +**Config Generator:** +- UI to configure all settings in `src/config/siteConfig.ts` +- Generates downloadable `siteConfig.ts` file +- Hybrid approach: dashboard generates config, file-based config continues to work +- Includes all site configuration options: + - Site name, title, logo, bio, intro + - Blog page settings + - Featured section configuration + - Logo gallery settings + - GitHub contributions + - Footer and social footer + - Newsletter settings + - Contact form settings + - Stats page settings + - And more + +**Index HTML Editor:** +- View and edit `index.html` content +- Meta tags, Open Graph, Twitter Cards, JSON-LD +- Download updated HTML file + +### Analytics + +- Real-time stats dashboard (clone of `/stats` page) +- Active visitors with per-page breakdown +- Total page views and unique visitors +- Views by page sorted by popularity +- Does not follow `siteConfig.statsPage` settings (always accessible in dashboard) + +### Sync Commands + +**Sync Content Section:** +- UI with buttons for all sync operations +- Development sync commands: + - `npm run sync` - Sync markdown content + - `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) + - `npm run sync:all` - Sync content + discovery files together +- Production sync commands: + - `npm run sync:prod` - Sync markdown content + - `npm run sync:discovery:prod` - Update discovery files + - `npm run sync:all:prod` - Sync content + discovery files together +- Server status indicator shows if sync server is online +- Copy and Execute buttons for each command +- Real-time terminal output when sync server is running +- Command modal shows full command output when sync server is offline +- Toast notifications for success/error feedback + +**Sync Server:** +- Local HTTP server for executing commands from dashboard +- Start with `npm run sync-server` (runs on localhost:3001) +- Execute commands directly from dashboard with real-time output streaming +- Optional token authentication via `SYNC_TOKEN` environment variable +- Whitelisted commands only for security +- Health check endpoint for server availability detection +- Copy icons for `npm run sync-server` command in dashboard + +**Header Sync Buttons:** +- Quick sync buttons in dashboard header (right side) +- `npm run sync:all` (dev) button +- `npm run sync:all:prod` (prod) button +- One-click sync for all content and discovery files +- Automatically use sync server when available, fallback to command modal + +### Dashboard Features + +**Search:** +- Search bar in header +- Search dashboard features, page titles, and post content +- Real-time results as you type + +**Theme and Font:** +- Theme toggle (dark, light, tan, cloud) +- Font switcher (serif, sans, monospace) +- Preferences persist across sessions + +**Mobile Responsive:** +- Fully responsive design +- Mobile-optimized layout +- Touch-friendly controls +- Collapsible sidebar on mobile + +**Toast Notifications:** +- Success, error, info, and warning notifications +- Auto-dismiss after 4 seconds +- Theme-aware styling +- No browser default alerts + +**Command Modal:** +- Shows sync command output +- Copy command to clipboard +- Close button to dismiss +- Theme-aware styling + +### Technical Details + +- Uses Convex queries for real-time data +- All mutations follow Convex best practices (idempotent, indexed queries) +- Frontmatter sidebar width persisted in localStorage +- Editor content persisted in localStorage +- Independent scrolling for editor and sidebar sections +- Preview uses ReactMarkdown with remark-gfm, remark-breaks, rehype-raw, rehype-sanitize + +### Sync Commands Reference + +**Development:** +- `npm run sync` - Sync markdown content to development Convex +- `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) with development data +- `npm run sync:all` - Run both content sync and discovery sync (development) + +**Production:** +- `npm run sync:prod` - Sync markdown content to production Convex +- `npm run sync:discovery:prod` - Update discovery files with production data +- `npm run sync:all:prod` - Run both content sync and discovery sync (production) + +**Sync Server:** +- `npm run sync-server` - Start local HTTP server for executing sync commands from dashboard UI + +**Content Import:** +- `npm run import ` - Import external URL as markdown post (requires FIRECRAWL_API_KEY) + +**Note:** The dashboard provides a UI for these commands. When the sync server is running (`npm run sync-server`), you can execute commands directly from the dashboard with real-time output. Otherwise, the dashboard shows commands in a modal for copying to your terminal. + ## API endpoints | Endpoint | Description | @@ -1077,49 +1279,18 @@ When you run `npm run sync` (development) or `npm run sync:prod` (production), s These files include a metadata header with type, date, reading time, and tags. Access via the "View as Markdown" option in the Copy Page dropdown. -## Markdown tables +## Markdown formatting -Tables render with GitHub-style formatting: +For complete markdown syntax examples including tables, collapsible sections, code blocks, lists, links, images, and all formatting options, see [Writing Markdown with Code Examples](/markdown-with-code-examples). -- Clean borders across all themes -- Mobile responsive with horizontal scroll -- Theme-aware alternating row colors -- Hover states for readability +**Quick reference:** -Example: +- **Tables:** Render with GitHub-style formatting, clean borders, mobile responsive +- **Collapsible sections:** Use HTML `
` and `` tags for expandable content +- **Code blocks:** Support syntax highlighting for TypeScript, JavaScript, bash, JSON, and more +- **Images:** Place in `public/images/` and reference with absolute paths -| Feature | Status | -| ------- | ------ | -| Borders | Clean | -| Mobile | Scroll | -| Themes | All | - -## Collapsible sections - -Create expandable/collapsible content using HTML `
` and `` tags: - -```html -
- Click to expand - - Hidden content here. Supports markdown: - Lists - **Bold** and _italic_ - Code - blocks -
-``` - -**Expanded by default:** Add the `open` attribute: - -```html -
- Already expanded - - This section starts open. -
-``` - -**Nested sections:** You can nest `
` inside other `
` for multi-level collapsible content. - -Collapsible sections work with all four themes and are styled to match the site design. +All markdown features work with all four themes and are styled to match the site design. ## Import external content diff --git a/content/pages/home.md b/content/pages/home.md index c176233..e93fcf6 100644 --- a/content/pages/home.md +++ b/content/pages/home.md @@ -7,7 +7,7 @@ order: -1 textAlign: "left" --- -An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. +An open-source publishing framework built for AI agents and developers to ship **websites**, **docs**, or **blogs**. Write markdown, sync from the terminal. [Fork it](https://github.com/waynesutton/markdown-site), customize it, ship it. diff --git a/convex/auth.config.ts b/convex/auth.config.ts new file mode 100644 index 0000000..1f3cc0c --- /dev/null +++ b/convex/auth.config.ts @@ -0,0 +1,21 @@ +const clientId = process.env.WORKOS_CLIENT_ID; + +const authConfig = { + providers: [ + { + type: "customJwt", + issuer: `https://api.workos.com/`, + algorithm: "RS256", + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + applicationID: clientId, + }, + { + type: "customJwt", + issuer: `https://api.workos.com/user_management/${clientId}`, + algorithm: "RS256", + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + ], +}; + +export default authConfig; diff --git a/convex/pages.ts b/convex/pages.ts index ceb1f38..de4b740 100644 --- a/convex/pages.ts +++ b/convex/pages.ts @@ -1,6 +1,57 @@ import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; +// Get all pages (published and unpublished) for dashboard admin view +export const listAll = query({ + args: {}, + returns: v.array( + v.object({ + _id: v.id("pages"), + _creationTime: v.number(), + slug: v.string(), + title: v.string(), + content: v.string(), + published: v.boolean(), + order: v.optional(v.number()), + showInNav: v.optional(v.boolean()), + excerpt: v.optional(v.string()), + image: v.optional(v.string()), + featured: v.optional(v.boolean()), + featuredOrder: v.optional(v.number()), + authorName: v.optional(v.string()), + authorImage: v.optional(v.string()), + }), + ), + handler: async (ctx) => { + const pages = await ctx.db.query("pages").collect(); + + // Sort by order, then by title + const sortedPages = pages.sort((a, b) => { + const orderA = a.order ?? 999; + const orderB = b.order ?? 999; + if (orderA !== orderB) return orderA - orderB; + return a.title.localeCompare(b.title); + }); + + return sortedPages.map((page) => ({ + _id: page._id, + _creationTime: page._creationTime, + slug: page.slug, + title: page.title, + content: page.content, + published: page.published, + order: page.order, + showInNav: page.showInNav, + excerpt: page.excerpt, + image: page.image, + featured: page.featured, + featuredOrder: page.featuredOrder, + authorName: page.authorName, + authorImage: page.authorImage, + })); + }, +}); + // Get all published pages for navigation export const getAllPages = query({ args: {}, diff --git a/convex/posts.ts b/convex/posts.ts index a7af0db..0e55e7b 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -1,6 +1,58 @@ import { query, mutation, internalMutation, internalQuery } from "./_generated/server"; import { v } from "convex/values"; +// Get all posts (published and unpublished) for dashboard admin view +export const listAll = query({ + args: {}, + returns: v.array( + v.object({ + _id: v.id("posts"), + _creationTime: v.number(), + slug: v.string(), + title: v.string(), + description: v.string(), + content: v.string(), + date: v.string(), + published: v.boolean(), + tags: v.array(v.string()), + readTime: v.optional(v.string()), + image: v.optional(v.string()), + excerpt: v.optional(v.string()), + featured: v.optional(v.boolean()), + featuredOrder: v.optional(v.number()), + authorName: v.optional(v.string()), + authorImage: v.optional(v.string()), + }), + ), + handler: async (ctx) => { + const posts = await ctx.db.query("posts").collect(); + + // Sort by date descending + const sortedPosts = posts.sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), + ); + + return sortedPosts.map((post) => ({ + _id: post._id, + _creationTime: post._creationTime, + slug: post.slug, + title: post.title, + description: post.description, + content: post.content, + date: post.date, + published: post.published, + tags: post.tags, + readTime: post.readTime, + image: post.image, + excerpt: post.excerpt, + featured: post.featured, + featuredOrder: post.featuredOrder, + authorName: post.authorName, + authorImage: post.authorImage, + })); + }, +}); + // Get all published posts, sorted by date descending export const getAllPosts = query({ args: {}, diff --git a/files.md b/files.md index 67c85cf..acb13cf 100644 --- a/files.md +++ b/files.md @@ -25,26 +25,29 @@ A brief description of each file in the codebase. | File | Description | | --------------- | ------------------------------------------------------------------------------------------------ | -| `main.tsx` | React app entry point with Convex provider | -| `App.tsx` | Main app component with routing (supports custom homepage configuration via siteConfig.homepage) | +| `main.tsx` | React app entry point with conditional WorkOS providers. When WorkOS is configured (VITE_WORKOS_CLIENT_ID and VITE_WORKOS_REDIRECT_URI set), wraps app with AuthKitProvider and ConvexProviderWithAuthKit. When WorkOS is not configured, uses standard ConvexProvider. Uses lazy loading and Suspense for optional WorkOS integration. | +| `App.tsx` | Main app component with routing (supports custom homepage configuration via siteConfig.homepage). Handles /callback route for WorkOS OAuth redirect. | +| `AppWithWorkOS.tsx` | Wrapper component for WorkOS-enabled app. Provides AuthKitProvider and ConvexProviderWithAuthKit. Only loaded when WorkOS is configured. | | `vite-env.d.ts` | Vite environment type definitions | ### Config (`src/config/`) | File | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration with markdown support, social footer configuration, homepage configuration, AI chat configuration, newsletter configuration with admin and notifications, contact form configuration, weekly digest configuration) | +| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, featured section with configurable title via featuredTitle, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration with markdown support, social footer configuration, homepage configuration, AI chat configuration, newsletter configuration with admin and notifications, contact form configuration, weekly digest configuration, stats page configuration with public/private toggle, dashboard configuration with optional WorkOS authentication via requireAuth) | ### Pages (`src/pages/`) | File | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Home.tsx` | Landing page with featured content and optional post list. Fetches home intro content from `content/pages/home.md` (slug: `home-intro`) for synced markdown intro text. Supports configurable post limit (homePostsLimit) and optional "read more" link (homePostsReadMore) via siteConfig.postsDisplay. Falls back to siteConfig.bio if home-intro page not found. Home intro content uses blog heading styles (blog-h1 through blog-h6) with clickable anchor links, matching blog post typography. Includes helper functions (generateSlug, getTextContent, HeadingAnchor) for heading ID generation and anchor links. | +| `Home.tsx` | Landing page with featured content and optional post list. Fetches home intro content from `content/pages/home.md` (slug: `home-intro`) for synced markdown intro text. Supports configurable post limit (homePostsLimit) and optional "read more" link (homePostsReadMore) via siteConfig.postsDisplay. Falls back to siteConfig.bio if home-intro page not found. Home intro content uses blog heading styles (blog-h1 through blog-h6) with clickable anchor links, matching blog post typography. Includes helper functions (generateSlug, getTextContent, HeadingAnchor) for heading ID generation and anchor links. Featured section title configurable via siteConfig.featuredTitle (default: "Get started:"). | | `Blog.tsx` | Dedicated blog page with featured layout: hero post (first blogFeatured), featured row (remaining blogFeatured in 2 columns with excerpts), and regular posts (3 columns without excerpts). Supports list/card view toggle. Includes back button in navigation | | `Post.tsx` | Individual blog post or page view with optional left sidebar (TOC) and right sidebar (CopyPageDropdown). Includes back button (hidden when used as homepage), tag links, related posts section in footer for blog posts, footer component with markdown support, and social footer. Supports 3-column layout at 1135px+. Can display image at top when showImageAtTop: true. Can be used as custom homepage via siteConfig.homepage (update SITE_URL/SITE_NAME when forking) | -| `Stats.tsx` | Real-time analytics dashboard with visitor stats and GitHub stars | +| `Stats.tsx` | Real-time analytics dashboard with visitor stats and GitHub stars. Configurable via `siteConfig.statsPage` to enable/disable public access and navigation visibility. Shows disabled message when `enabled: false` (similar to NewsletterAdmin pattern). | | `TagPage.tsx` | Tag archive page displaying posts filtered by a specific tag. Includes view mode toggle (list/cards) with localStorage persistence | | `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans/monospace), localStorage persistence, and optional AI Agent mode (toggleable via siteConfig.aiChat.enabledOnWritePage). When enabled, Agent replaces the textarea with AIChatView component. Includes scroll prevention when switching to Agent mode to prevent page jump. Title changes to "Agent" when in AI chat mode. | +| `Dashboard.tsx` | Centralized dashboard at `/dashboard` for content management and site configuration. Features include: Posts and Pages list views with filtering, search, pagination, items per page selector; Post/Page editor with markdown editor, live preview, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown; Write Post/Page sections with full-screen writing interface; AI Agent section (dedicated chat separate from Write page); Newsletter management (all Newsletter Admin features integrated); Content import (Firecrawl UI); Site configuration (Config Generator UI for all siteConfig.ts settings); Index HTML editor; Analytics (real-time stats dashboard); Sync commands UI with buttons for all sync operations; Header sync buttons for quick sync; Dashboard search; Toast notifications; Command modal; Mobile responsive design. Uses Convex queries for real-time data, localStorage for preferences, ReactMarkdown for preview. Optional WorkOS authentication via siteConfig.dashboard.requireAuth. When requireAuth is false, dashboard is open access. When requireAuth is true and WorkOS is configured, dashboard requires login. Shows setup instructions if requireAuth is true but WorkOS is not configured. | +| `Callback.tsx` | OAuth callback handler for WorkOS authentication. Handles redirect from WorkOS after user login, exchanges authorization code for user information, then redirects to dashboard. Only used when WorkOS is configured. | | `NewsletterAdmin.tsx` | Three-column newsletter admin page for managing subscribers and sending newsletters. Left sidebar with navigation and stats, main area with searchable subscriber list, right sidebar with send newsletter panel and recent sends. Access at /newsletter-admin, configurable via siteConfig.newsletterAdmin. | ### Components (`src/components/`) @@ -68,8 +71,8 @@ A brief description of each file in the codebase. | `PageSidebar.tsx` | Collapsible table of contents sidebar for pages/posts with sidebar layout, extracts headings (H1-H6), active heading highlighting, smooth scroll navigation, localStorage persistence for expanded/collapsed state | | `RightSidebar.tsx` | Right sidebar component that displays CopyPageDropdown or AI chat on posts/pages at 1135px+ viewport width, controlled by siteConfig.rightSidebar.enabled and frontmatter rightSidebar/aiChat fields | | `AIChatView.tsx` | AI chat interface component (Agent) using Anthropic Claude API. Supports per-page chat history, page content context, markdown rendering, and copy functionality. Used in Write page (replaces textarea when enabled) and optionally in RightSidebar. Requires ANTHROPIC_API_KEY environment variable in Convex. System prompt configurable via CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES, or CLAUDE_SYSTEM_PROMPT environment variables. Includes error handling for missing API keys. | -| `NewsletterSignup.tsx` | Newsletter signup form component for email-only subscriptions. Displays configurable title/description, validates email, and submits to Convex. Shows on home, blog page, and posts based on siteConfig.newsletter settings. Supports frontmatter override via newsletter: true/false. | -| `ContactForm.tsx` | Contact form component with name, email, and message fields. Displays when contactForm: true in frontmatter. Submits to Convex which sends email via AgentMail to configured recipient. Requires AGENTMAIL_API_KEY and AGENTMAIL_INBOX environment variables. | +| `NewsletterSignup.tsx` | Newsletter signup form component for email-only subscriptions. Displays configurable title/description, validates email, and submits to Convex. Shows on home, blog page, and posts based on siteConfig.newsletter settings. Supports frontmatter override via newsletter: true/false. Includes honeypot field for bot protection. | +| `ContactForm.tsx` | Contact form component with name, email, and message fields. Displays when contactForm: true in frontmatter. Submits to Convex which sends email via AgentMail to configured recipient. Requires AGENTMAIL_API_KEY and AGENTMAIL_INBOX environment variables. Includes honeypot field for bot protection. | | `SocialFooter.tsx` | Social footer component with social icons on left (GitHub, Twitter/X, LinkedIn, Instagram, YouTube, TikTok, Discord, Website) and copyright on right. Configurable via siteConfig.socialFooter. Shows below main footer on homepage, blog posts, and pages. Supports frontmatter override via showSocialFooter: true/false. Auto-updates copyright year. | ### Context (`src/context/`) @@ -85,6 +88,7 @@ A brief description of each file in the codebase. | File | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------- | | `extractHeadings.ts` | Parses markdown content to extract headings (H1-H6), generates slugs, filters out headings inside code blocks | +| `workos.ts` | WorkOS configuration utility. Exports isWorkOSConfigured boolean (checks if VITE_WORKOS_CLIENT_ID and VITE_WORKOS_REDIRECT_URI are set) and workosConfig object with clientId and redirectUri. Used throughout app to conditionally enable WorkOS features. | ### Hooks (`src/hooks/`) @@ -110,6 +114,7 @@ A brief description of each file in the codebase. | `crons.ts` | Cron jobs for stale session cleanup (every 5 minutes), weekly newsletter digest (Sundays 9am UTC), and weekly stats summary (Mondays 9am UTC). Uses environment variables SITE_URL and SITE_NAME for email content. | | `http.ts` | HTTP endpoints: sitemap (includes tag pages), API (update SITE_URL/SITE_NAME when forking, uses www.markdown.fast), Open Graph HTML generation for social crawlers | | `rss.ts` | RSS feed generation (update SITE_URL/SITE_TITLE when forking, uses www.markdown.fast) | +| `auth.config.ts` | Convex authentication configuration for WorkOS. Defines JWT providers for WorkOS API and user management. Requires WORKOS_CLIENT_ID environment variable in Convex. Optional - only needed if using WorkOS authentication for dashboard. | | `aiChats.ts` | Queries and mutations for AI chat history (per-session, per-context storage). Handles anonymous session IDs, per-page chat contexts, and message history management. Supports page content as context for AI responses. | | `aiChatActions.ts` | Anthropic Claude API integration action for AI chat responses. Requires ANTHROPIC_API_KEY environment variable in Convex. Uses claude-sonnet-3-5-20240620 model. System prompt configurable via environment variables (CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES, or CLAUDE_SYSTEM_PROMPT). Includes error handling for missing API keys with user-friendly error messages. Supports page content context and chat history (last 20 messages). | | `newsletter.ts` | Newsletter mutations and queries: subscribe, unsubscribe, getSubscriberCount, getActiveSubscribers, getAllSubscribers (admin), deleteSubscriber (admin), getNewsletterStats, getPostsForNewsletter, wasPostSent, recordPostSent, scheduleSendPostNewsletter, scheduleSendCustomNewsletter, scheduleSendStatsSummary, getStatsForSummary. | @@ -169,7 +174,7 @@ Markdown files with frontmatter for blog posts. Each file becomes a blog post. Markdown files for static pages like About, Projects, Contact, Changelog. **Special pages:** -- `home.md` (slug: `home-intro`): Homepage intro/bio content. Set `showInNav: false` to hide from navigation. Content syncs with `npm run sync` and displays on the homepage without redeploy. Headings (h1-h6) use blog post styling (`blog-h1` through `blog-h6`) with clickable anchor links. Lists, blockquotes, horizontal rules, and links also use blog styling classes for consistent typography. +- `home.md` (slug: `home-intro`): Homepage intro/bio content. Set `showInNav: false` to hide from navigation. Content syncs with `npm run sync` and displays on the homepage without redeploy. Headings (h1-h6) use blog post styling (`blog-h1` through `blog-h6`) with clickable anchor links. Lists, blockquotes, horizontal rules, and links also use blog styling classes for consistent typography. Use `textAlign` frontmatter field to control alignment (left/center/right, default: left). Falls back to `siteConfig.bio` if page not found or while loading. | Field | Description | | --------------- | ----------------------------------------------------------------------- | @@ -204,6 +209,7 @@ Markdown files for static pages like About, Projects, Contact, Changelog. | `configure-fork.ts` | Automated fork configuration (reads fork-config.json) | | `send-newsletter.ts` | CLI tool for sending newsletter posts (npm run newsletter:send ). Calls scheduleSendPostNewsletter mutation directly. | | `send-newsletter-stats.ts` | CLI tool for sending weekly stats summary (npm run newsletter:send:stats). Calls scheduleSendStatsSummary mutation directly. | +| `sync-server.ts` | Local HTTP server for executing sync commands from Dashboard UI. Runs on localhost:3001 with optional token authentication. Whitelisted commands only. | ### Sync Commands diff --git a/fork-config.json.example b/fork-config.json.example index e60361e..ad15ec8 100644 --- a/fork-config.json.example +++ b/fork-config.json.example @@ -147,6 +147,10 @@ "enabled": false, "dayOfWeek": 0, "subject": "Weekly Digest" + }, + "dashboard": { + "enabled": true, + "requireAuth": false } } diff --git a/package-lock.json b/package-lock.json index f524ada..ddcebaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "dependencies": { "@anthropic-ai/sdk": "^0.71.2", "@convex-dev/aggregate": "^0.2.0", + "@convex-dev/workos": "^0.0.1", "@mendable/firecrawl-js": "^1.21.1", "@modelcontextprotocol/sdk": "^1.0.0", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-icons": "^1.3.2", + "@workos-inc/authkit-react": "^0.15.0", + "@workos-inc/widgets": "^1.6.1", "agentmail": "^0.1.15", "convex": "^1.17.4", "date-fns": "^3.3.1", @@ -136,7 +139,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", @@ -232,7 +234,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -242,7 +243,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -276,7 +276,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -367,7 +366,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -386,6 +384,70 @@ "convex": "^1.24.8" } }, + "node_modules/@convex-dev/workos": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@convex-dev/workos/-/workos-0.0.1.tgz", + "integrity": "sha512-8gZOgmcTitcKXwagdU69XC4Va6wMPFIhSqSOEaXmFXMEPtkMgxPW1dhJzrmm9UQ4iRgZsckjd2O5aQjUH7kHGQ==", + "license": "ISC", + "dependencies": { + "@workos-inc/authkit-react": "^0.11.0", + "tsdown": "^0.12.7", + "vitest": "^3.1.4" + }, + "peerDependencies": { + "convex": "^1.25.4", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + } + }, + "node_modules/@convex-dev/workos/node_modules/@workos-inc/authkit-js": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@workos-inc/authkit-js/-/authkit-js-0.13.0.tgz", + "integrity": "sha512-iA0Dt7D1BmY2/1s4oeA36W/aRt8/b5iyH6rP4AlgnjrcH2lUGkBgDXL76NXc0M7repkDQTMcJJ2NhCSo2rcWmg==", + "license": "MIT" + }, + "node_modules/@convex-dev/workos/node_modules/@workos-inc/authkit-react": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@workos-inc/authkit-react/-/authkit-react-0.11.0.tgz", + "integrity": "sha512-67HFSxP4wXC8ECGyvc1yGMwuD5NGkwT2OPt8DavHoKAlO+hRaAlu9wwzqUx1EJrHht0Dcx+l20Byq8Ab0bEhlg==", + "license": "MIT", + "dependencies": { + "@workos-inc/authkit-js": "0.13.0" + }, + "peerDependencies": { + "react": ">=17" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -890,6 +952,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@hono/node-server": { "version": "1.19.7", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", @@ -968,7 +1068,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -990,7 +1089,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1000,14 +1098,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1088,6 +1184,18 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1126,6 +1234,24 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/runtime": { + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.71.0.tgz", + "integrity": "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.71.0.tgz", + "integrity": "sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@phosphor-icons/react": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", @@ -1139,6 +1265,556 @@ "react-dom": ">= 16.8" } }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "license": "MIT", + "peer": true + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT", + "peer": true + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", @@ -1148,6 +1824,1036 @@ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@radix-ui/themes": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-3.2.1.tgz", + "integrity": "sha512-WJL2YKAGItkunwm3O4cLTFKCGJTfAfF6Hmq7f5bCo1ggqC9qJQ/wfg/25AAN72aoEM1yqXZQ+pslsw48AFR0Xg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/colors": "^3.0.0", + "classnames": "^2.3.2", + "radix-ui": "^1.1.3", + "react-remove-scroll-bar": "^2.3.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.23.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", @@ -1157,6 +2863,165 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-40re4rMNrsi57oavRzIOpRGmg3QRlW6Ea8Q3znaqgOuJuKVrrm2bIQInTfkZJG7a4/5YMX7T951d0+toGLTdCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-8BDM939bbMariZupiHp3OmP5N+LXPT4mULA0hZjDaq970PCxv4krZOSMG+HkWUUwmuQROtV+/00xw39EO0P+8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-sntsPaPgrECpBB/+2xrQzVUt0r493TMPI+4kWRMhvMsmrxOqH1Ep5lM0Wua/ZdbfZNwm1aVa5pcESQfNfM4Fhw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-5clBW/I+er9F2uM1OFjJFWX86y7Lcy0M+NqsN4s3o07W+8467Zk8oQa4B45vdaXoNUF/yqIAgKkA/OEdQDxZqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-wv+rnAfQDk9p/CheX8/Kmqk2o1WaFa4xhWI9gOyDMk/ljvOX0u0ubeM8nI1Qfox7Tnh71eV5AjzSePXUhFOyOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-gxD0/xhU4Py47IH3bKZbWtvB99tMkUPGPJFRfSc5UB9Osoje0l0j1PPbxpUtXIELurYCqwLBKXIMTQGifox1BQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-HotuVe3XUjDwqqEMbm3o3IRkP9gdm8raY/btd/6KE3JGLF/cv4+3ff1l6nOhAZI8wulWDPEXPtE7v+HQEaTXnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-8Cx+ucbd8n2dIr21FqBh6rUvTVL0uTgEtKR7l+MUZ5BgY4dFh1e4mPVX8oqmoYwOxBiXrsD2JIOCz4AyKLKxWA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.4" + }, + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-Vhq5vikrVDxAa75fxsyqj0c0Y/uti/TwshXI71Xb8IeUQJOBnmLUsn5dgYf5ljpYYkNa0z9BPAvUDIDMmyDi+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-lN7RIg9Iugn08zP2aZN9y/MIdG8iOOCE93M1UrFlrxMTqPf8X+fDzmR/OKhTSd1A2pYNipZHjyTcb5H8kyQSow==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-7/7cLIn48Y+EpQ4CePvf8reFl63F15yPUlg4ZAhl+RXJIfydkdak1WD8Ir3AwAO+bJBXzrfNL+XQbxm0mcQZmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1171,7 +3036,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1185,7 +3049,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1199,7 +3062,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1213,7 +3075,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1227,7 +3088,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1241,7 +3101,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1255,7 +3114,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1269,7 +3127,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1283,7 +3140,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1297,7 +3153,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1311,7 +3166,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1325,7 +3179,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1339,7 +3192,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1353,7 +3205,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1367,7 +3218,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1381,7 +3231,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1395,7 +3244,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1409,7 +3257,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1423,7 +3270,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1437,7 +3283,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1451,7 +3296,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1465,13 +3309,50 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.90.15", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.15.tgz", + "integrity": "sha512-mInIZNUZftbERE+/Hbtswfse49uUQwch46p+27gP9DWJL927UjnaWEF2t3RMOqBcXbfMdcNkPe06VyUIAZTV1g==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.15.tgz", + "integrity": "sha512-uQvnDDcTOgJouNtAyrgRej+Azf0U5WDov3PXmHFUBc+t1INnAYhIlpZtCGNBLwCN41b43yO7dPNZu8xWkUFBwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tanstack/query-core": "5.90.15" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1517,6 +3398,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1526,6 +3417,12 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1569,7 +3466,7 @@ "version": "25.0.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -1595,7 +3492,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -1837,6 +3734,154 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@workos-inc/authkit-js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@workos-inc/authkit-js/-/authkit-js-0.17.0.tgz", + "integrity": "sha512-3PKSf59M/IU4YzmeQs2brTe2pctvpTXbvci05BL+sk0unhqYs30zZzsha+6/9IYiaMVtCZWPdL5lrqIPA4echw==", + "license": "MIT" + }, + "node_modules/@workos-inc/authkit-react": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@workos-inc/authkit-react/-/authkit-react-0.15.0.tgz", + "integrity": "sha512-4+ppdylMPhoscaCzxiLK4r6c4B54kYaknxYD44dh/muWl2HSOU8puMSSqOmUyCcRerG1XpKgxUC2bojjA3ZxcQ==", + "license": "MIT", + "dependencies": { + "@workos-inc/authkit-js": "0.17.0" + }, + "peerDependencies": { + "react": ">=17" + } + }, + "node_modules/@workos-inc/widgets": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@workos-inc/widgets/-/widgets-1.6.1.tgz", + "integrity": "sha512-r6EOG4du7TkOlBUqtqsjC2EQYELxDR9BrVdAZiYKg8PVDxRuDxPALq/1oUkO+PuBxsbjuu/h0rtxIgVOGzpCHA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.0.0", + "@radix-ui/react-form": "^0.1.2", + "@radix-ui/react-icons": "^1.3.1", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-use-controllable-state": "^1.1.0", + "bowser": "2.12.1", + "clsx": "^2.0.0", + "use-debounce": "^10.0.4" + }, + "peerDependencies": { + "@radix-ui/themes": "^3.1.0", + "@tanstack/react-query": "^5.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1991,6 +4036,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1998,6 +4052,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2008,6 +4074,31 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-kit": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.2.tgz", + "integrity": "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2052,6 +4143,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/body-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -2076,6 +4176,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2142,6 +4248,15 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2212,6 +4327,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2269,6 +4400,46 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT", + "peer": true + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2471,6 +4642,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2478,6 +4658,12 @@ "dev": true, "license": "MIT" }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2505,6 +4691,12 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2518,6 +4710,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2557,6 +4758,26 @@ "url": "https://dotenvx.com" } }, + "node_modules/dts-resolver": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.2.tgz", + "integrity": "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2584,6 +4805,15 @@ "dev": true, "license": "ISC" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2623,6 +4853,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2930,6 +5166,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2970,6 +5215,15 @@ "node": ">=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -3319,7 +5573,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3373,6 +5626,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3390,7 +5652,6 @@ "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -3849,6 +6110,12 @@ "node": ">=16.9.0" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -4110,6 +6377,15 @@ "ws": "*" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -4142,7 +6418,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -4282,6 +6557,12 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "license": "MIT" + }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -4315,6 +6596,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -5285,7 +7575,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -5523,11 +7812,25 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5556,7 +7859,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5669,6 +7971,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5690,6 +8008,84 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5776,6 +8172,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.30.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", @@ -5808,6 +8251,28 @@ "react-dom": ">=16.8" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-syntax-highlighter": { "version": "15.6.6", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", @@ -5825,6 +8290,19 @@ "react": ">= 0.14.0" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", @@ -6074,7 +8552,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -6108,11 +8585,84 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g==", + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.71.0", + "@oxc-project/types": "0.71.0", + "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", + "ansis": "^4.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "optionalDependencies": { + "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.13.14.tgz", + "integrity": "sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.1", + "ast-kit": "^2.1.1", + "birpc": "^2.5.0", + "debug": "^4.4.1", + "dts-resolver": "^2.1.1", + "get-tsconfig": "^4.10.1" + }, + "engines": { + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@typescript/native-preview": ">=7.0.0-dev.20250601.1", + "rolldown": "^1.0.0-beta.9", + "typescript": "^5.0.0", + "vue-tsc": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9-commit.d91dfb5", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9-commit.d91dfb5.tgz", + "integrity": "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6222,7 +8772,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6400,6 +8949,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "license": "ISC" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6414,7 +8969,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6436,6 +8990,12 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -6445,6 +9005,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -6494,6 +9060,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -6532,6 +9116,93 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6593,6 +9264,66 @@ "typescript": ">=4.2.0" } }, + "node_modules/tsdown": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.12.9.tgz", + "integrity": "sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==", + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "debug": "^4.4.1", + "diff": "^8.0.2", + "empathic": "^2.0.0", + "hookable": "^5.5.3", + "rolldown": "^1.0.0-beta.19", + "rolldown-plugin-dts": "^0.13.12", + "semver": "^7.7.2", + "tinyexec": "^1.0.1", + "tinyglobby": "^0.2.14", + "unconfig": "^7.3.2" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "publint": "^0.3.0", + "typescript": "^5.0.0", + "unplugin-lightningcss": "^0.4.0", + "unplugin-unused": "^0.5.0" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "publint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-lightningcss": { + "optional": true + }, + "unplugin-unused": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -7149,7 +9880,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7165,11 +9896,40 @@ "integrity": "sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==", "license": "MIT" }, + "node_modules/unconfig": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.4.2.tgz", + "integrity": "sha512-nrMlWRQ1xdTjSnSUqvYqJzbTBFugoqHobQj58B2bc8qxHKBBHMNNsWQFP3Cd3/JZK907voM2geYPWqD4VK3MPQ==", + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "defu": "^6.1.4", + "jiti": "^2.6.1", + "quansync": "^1.0.0", + "unconfig-core": "7.4.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unconfig-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.4.2.tgz", + "integrity": "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==", + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unified": { @@ -7309,6 +10069,71 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-debounce": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.6.tgz", + "integrity": "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7364,7 +10189,6 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -7420,6 +10244,28 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -7427,7 +10273,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7444,7 +10289,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7461,7 +10305,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7478,7 +10321,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7495,7 +10337,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7512,7 +10353,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7529,7 +10369,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7546,7 +10385,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7563,7 +10401,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7580,7 +10417,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7597,7 +10433,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7614,7 +10449,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7631,7 +10465,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7648,7 +10481,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7665,7 +10497,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7682,7 +10513,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7699,7 +10529,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7716,7 +10545,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7733,7 +10561,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7750,7 +10577,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7767,7 +10593,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7784,7 +10609,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7801,7 +10625,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7815,7 +10638,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -7850,6 +10672,96 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -7875,6 +10787,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index c8b1a5e..4ee581b 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,20 @@ "configure": "npx tsx scripts/configure-fork.ts", "newsletter:send": "npx tsx scripts/send-newsletter.ts", "newsletter:send:stats": "npx tsx scripts/send-newsletter-stats.ts", + "sync-server": "npx tsx scripts/sync-server.ts", "deploy": "npm run sync && npm run build", "deploy:prod": "npx convex deploy && npm run sync:prod" }, "dependencies": { "@anthropic-ai/sdk": "^0.71.2", - "@modelcontextprotocol/sdk": "^1.0.0", "@convex-dev/aggregate": "^0.2.0", + "@convex-dev/workos": "^0.0.1", "@mendable/firecrawl-js": "^1.21.1", + "@modelcontextprotocol/sdk": "^1.0.0", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-icons": "^1.3.2", + "@workos-inc/authkit-react": "^0.15.0", + "@workos-inc/widgets": "^1.6.1", "agentmail": "^0.1.15", "convex": "^1.17.4", "date-fns": "^3.3.1", diff --git a/prds/workos-authkit-dashboard-guide.md b/prds/workos-authkit-dashboard-guide.md new file mode 100644 index 0000000..ec56f48 --- /dev/null +++ b/prds/workos-authkit-dashboard-guide.md @@ -0,0 +1,712 @@ +# Adding WorkOS AuthKit to markdown-site (Dashboard Only) + +A beginner-friendly, step-by-step guide for adding WorkOS AuthKit authentication to only the `/dashboard` page of your [markdown-site](https://github.com/waynesutton/markdown-site) project. The main site remains public—no login required. + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Prerequisites](#2-prerequisites) +3. [Part 1: Create a WorkOS Account](#3-part-1-create-a-workos-account) +4. [Part 2: Configure WorkOS Dashboard](#4-part-2-configure-workos-dashboard) +5. [Part 3: Install Dependencies](#5-part-3-install-dependencies) +6. [Part 4: Add Environment Variables](#6-part-4-add-environment-variables) +7. [Part 5: Configure Convex Auth](#7-part-5-configure-convex-auth) +8. [Part 6: Update the Main App Entry Point](#8-part-6-update-the-main-app-entry-point) +9. [Part 7: Create the Callback Route](#9-part-7-create-the-callback-route) +10. [Part 8: Create the Protected Dashboard Page](#10-part-8-create-the-protected-dashboard-page) +11. [Part 9: Add Dashboard Route to Router](#11-part-9-add-dashboard-route-to-router) +12. [Part 10: Deploy and Test](#12-part-10-deploy-and-test) +13. [Troubleshooting](#13-troubleshooting) +14. [Quick Reference](#14-quick-reference) + +--- + +## 1. Overview + +### What We're Building + +- **Public site**: All existing pages (Home, Blog posts, About, etc.) remain publicly accessible +- **Protected dashboard**: Only `/dashboard` requires login via WorkOS AuthKit +- **Authentication provider**: WorkOS AuthKit (supports passwords, social login, SSO, and more) + +### Architecture + +``` +markdown-site/ +├── src/ +│ ├── main.tsx ← Wrap app with AuthKitProvider +│ ├── App.tsx ← Main router (unchanged for public routes) +│ └── pages/ +│ ├── Home.tsx ← Public (no changes) +│ ├── Post.tsx ← Public (no changes) +│ ├── Callback.tsx ← NEW: Handle auth callback +│ └── Dashboard.tsx ← NEW: Protected page +└── convex/ + └── auth.config.ts ← NEW: Convex auth configuration +``` + +--- + +## 2. Prerequisites + +Before starting, make sure you have: + +- [ ] Node.js 18 or higher installed +- [ ] A working [markdown-site](https://github.com/waynesutton/markdown-site) project +- [ ] A Convex account and project already set up +- [ ] Your Convex development server running (`npx convex dev`) + +**Don't have markdown-site yet?** Clone it first: + +```bash +git clone https://github.com/waynesutton/markdown-site.git +cd markdown-site +npm install +npx convex dev +``` + +--- + +## 3. Part 1: Create a WorkOS Account + +### Step 1.1: Sign Up for WorkOS + +1. Go to [workos.com/sign-up](https://signin.workos.com/sign-up) +2. Create a free account with your email +3. Verify your email address + +### Step 1.2: Set Up AuthKit + +1. Log into the [WorkOS Dashboard](https://dashboard.workos.com) +2. Navigate to **Authentication** → **AuthKit** +3. Click the **Set up AuthKit** button +4. Select **"Use AuthKit's customizable hosted UI"** +5. Click **Begin setup** + +### Step 1.3: Configure Redirect URI + +During the AuthKit setup wizard, you'll reach step 4: **"Add default redirect endpoint URI"** + +Enter this for local development: +``` +http://localhost:5173/callback +``` + +> **What is this?** After a user logs in, WorkOS redirects them back to this URL with an authorization code. Your app exchanges this code for user information. + +### Step 1.4: Copy Your Credentials + +1. Go to [dashboard.workos.com/get-started](https://dashboard.workos.com/get-started) +2. Under **Quick start**, find and copy: + - **Client ID** (looks like `client_01XXXXXXXXXXXXXXXXX`) + +Save this somewhere safe—you'll need it shortly. + +--- + +## 4. Part 2: Configure WorkOS Dashboard + +### Step 2.1: Enable CORS + +For the React SDK to work, you need to allow your app's domain: + +1. Go to **Authentication** → **Sessions** in the WorkOS Dashboard +2. Find **Cross-Origin Resource Sharing (CORS)** +3. Click **Manage** +4. Add your development URL: `http://localhost:5173` +5. Click **Save** + +> **Note**: When you deploy to production (e.g., Netlify), add your production domain here too (e.g., `https://markdowncms.netlify.app`). + +### Step 2.2: Verify Redirect URI + +1. Go to **Redirects** in the WorkOS Dashboard +2. Confirm `http://localhost:5173/callback` is listed +3. If not, add it by clicking **Add redirect** + +--- + +## 5. Part 3: Install Dependencies + +Open your terminal in the markdown-site project folder and install the required packages: + +```bash +npm install @workos-inc/authkit-react @convex-dev/workos +``` + +**What these packages do:** + +| Package | Purpose | +|---------|---------| +| `@workos-inc/authkit-react` | WorkOS React SDK for handling login/logout | +| `@convex-dev/workos` | Bridges WorkOS auth with Convex backend | + +--- + +## 6. Part 4: Add Environment Variables + +### Step 4.1: Update `.env.local` + +Open your `.env.local` file (in the project root) and add these lines: + +```env +# Existing Convex URL (should already be here) +VITE_CONVEX_URL=https://your-deployment.convex.cloud + +# WorkOS AuthKit Configuration (add these) +VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX +VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback +``` + +Replace `client_01XXXXXXXXXXXXXXXXX` with your actual Client ID from the WorkOS Dashboard. + +> **Why `VITE_` prefix?** Vite only exposes environment variables that start with `VITE_` to the browser. + +### Step 4.2: Add to `.gitignore` (if not already) + +Make sure `.env.local` is in your `.gitignore` to avoid committing secrets: + +``` +.env.local +.env.production.local +``` + +--- + +## 7. Part 5: Configure Convex Auth + +Create a new file to tell Convex how to validate WorkOS tokens. + +### Step 5.1: Create `convex/auth.config.ts` + +Create a new file at `convex/auth.config.ts`: + +```typescript +// convex/auth.config.ts +const clientId = process.env.WORKOS_CLIENT_ID; + +const authConfig = { + providers: [ + { + type: "customJwt", + issuer: "https://api.workos.com/", + algorithm: "RS256", + applicationID: clientId, + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + { + type: "customJwt", + issuer: `https://api.workos.com/user_management/${clientId}`, + algorithm: "RS256", + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + ], +}; + +export default authConfig; +``` + +### Step 5.2: Add Environment Variable to Convex + +The Convex backend needs the Client ID too: + +1. Run `npx convex dev` if not already running +2. You'll see an error with a link—click it +3. It takes you to the Convex Dashboard environment variables page +4. Add a new variable: + - **Name**: `WORKOS_CLIENT_ID` + - **Value**: Your WorkOS Client ID (e.g., `client_01XXXXXXXXXXXXXXXXX`) +5. Save + +After saving, `npx convex dev` should show "Convex functions ready." + +--- + +## 8. Part 6: Update the Main App Entry Point + +Now we'll wrap the app with authentication providers—but only where needed. + +### Step 6.1: Modify `src/main.tsx` + +Replace your current `src/main.tsx` with this: + +```tsx +// src/main.tsx +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react"; +import { ConvexReactClient } from "convex/react"; +import { ConvexProviderWithAuthKit } from "@convex-dev/workos"; +import "./styles/global.css"; +import App from "./App"; + +// Initialize Convex client +const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); + +createRoot(document.getElementById("root")!).render( + + + + + + + +); +``` + +**What changed:** +- Added `AuthKitProvider` wrapper (handles WorkOS authentication state) +- Added `ConvexProviderWithAuthKit` (connects WorkOS auth to Convex) +- Passed the `useAuth` hook to bridge the two + +--- + +## 9. Part 7: Create the Callback Route + +When a user logs in via WorkOS, they're redirected to `/callback`. This page handles the authentication response. + +### Step 7.1: Create `src/pages/Callback.tsx` + +Create a new file at `src/pages/Callback.tsx`: + +```tsx +// src/pages/Callback.tsx +import { useEffect } from "react"; +import { useAuth } from "@workos-inc/authkit-react"; +import { useNavigate } from "react-router-dom"; + +export default function Callback() { + const { isLoading, user } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + // Once authentication is complete, redirect to dashboard + if (!isLoading && user) { + navigate("/dashboard", { replace: true }); + } + }, [isLoading, user, navigate]); + + return ( +
+
+

Signing you in...

+

Please wait while we complete your authentication.

+
+
+ ); +} +``` + +**How it works:** +1. User completes login on WorkOS hosted page +2. WorkOS redirects to `/callback?code=...` +3. The `AuthKitProvider` automatically exchanges the code for a session +4. Once authenticated, we redirect to `/dashboard` + +--- + +## 10. Part 8: Create the Protected Dashboard Page + +This is where authenticated users will land. We'll protect it so only logged-in users can access it. + +### Step 8.1: Create `src/pages/Dashboard.tsx` + +Create a new file at `src/pages/Dashboard.tsx`: + +```tsx +// src/pages/Dashboard.tsx +import { Authenticated, Unauthenticated, AuthLoading } from "convex/react"; +import { useAuth } from "@workos-inc/authkit-react"; +import { Link } from "react-router-dom"; + +export default function Dashboard() { + return ( + <> + + + + + + + + + + + ); +} + +function LoadingState() { + return ( +
+

Loading authentication...

+
+ ); +} + +function LoginPrompt() { + const { signIn } = useAuth(); + + return ( +
+
+

Dashboard

+

You need to sign in to access the dashboard.

+ +

+ ← Back to Home +

+
+
+ ); +} + +function DashboardContent() { + const { user, signOut } = useAuth(); + + return ( +
+
+

Dashboard

+

+ Welcome, {user?.firstName || user?.email || "User"}! +

+ +
+

Your Account

+
    +
  • + Email: {user?.email} +
  • +
  • + Name: {user?.firstName} {user?.lastName} +
  • +
  • + User ID: {user?.id} +
  • +
+
+ +
+ + + ← Back to Home + +
+
+
+ ); +} +``` + +### Step 8.2: Add Dashboard Styles + +Add these styles to your `src/styles/global.css` file: + +```css +/* Dashboard Styles */ +.dashboard-container { + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; +} + +.dashboard-card { + background: var(--bg-secondary, #f5f5f5); + border-radius: 12px; + padding: 2rem; + max-width: 500px; + width: 100%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.dashboard-card h1 { + margin-top: 0; + margin-bottom: 1rem; +} + +.user-info { + background: var(--bg-primary, #fff); + border-radius: 8px; + padding: 1rem; + margin: 1.5rem 0; +} + +.user-info h3 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1rem; +} + +.user-info ul { + list-style: none; + padding: 0; + margin: 0; +} + +.user-info li { + padding: 0.25rem 0; + font-size: 0.9rem; +} + +.dashboard-actions { + display: flex; + gap: 1rem; + align-items: center; + margin-top: 1.5rem; +} + +.sign-in-button, +.sign-out-button { + background: #6366f1; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; + transition: background 0.2s; +} + +.sign-in-button:hover, +.sign-out-button:hover { + background: #4f46e5; +} + +.home-link { + color: var(--text-secondary, #666); + text-decoration: none; +} + +.home-link:hover { + text-decoration: underline; +} +``` + +--- + +## 11. Part 9: Add Dashboard Route to Router + +Now we need to add routes for `/callback` and `/dashboard`. + +### Step 9.1: Update `src/App.tsx` + +Find your router configuration in `src/App.tsx` and add the new routes. Your App.tsx likely uses React Router. Add these imports and routes: + +```tsx +// At the top of src/App.tsx, add these imports: +import Callback from "./pages/Callback"; +import Dashboard from "./pages/Dashboard"; + +// In your Routes component, add these routes: +} /> +} /> +``` + +**Example of what your full App.tsx might look like:** + +```tsx +// src/App.tsx +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Home from "./pages/Home"; +import Post from "./pages/Post"; +import Callback from "./pages/Callback"; +import Dashboard from "./pages/Dashboard"; +// ... other imports + +function App() { + return ( + + + {/* Existing public routes */} + } /> + } /> + + {/* New auth routes */} + } /> + } /> + + + ); +} + +export default App; +``` + +--- + +## 12. Part 10: Deploy and Test + +### Step 10.1: Test Locally + +1. Make sure `npx convex dev` is running +2. Start your development server: + ```bash + npm run dev + ``` +3. Open `http://localhost:5173` +4. Navigate to `http://localhost:5173/dashboard` +5. Click "Sign In" and complete the WorkOS login flow +6. You should be redirected back to the dashboard with your user info displayed + +### Step 10.2: Deploy to Production + +When you're ready to deploy: + +1. **Add production redirect URI to WorkOS:** + - Go to WorkOS Dashboard → Redirects + - Add: `https://your-domain.com/callback` + +2. **Add production CORS origin:** + - Go to Authentication → Sessions → CORS + - Add: `https://your-domain.com` + +3. **Update Convex production environment:** + - Go to Convex Dashboard → Your Project → Production deployment + - Add `WORKOS_CLIENT_ID` environment variable + +4. **Create `.env.production.local`:** + ```env + VITE_CONVEX_URL=https://your-production-deployment.convex.cloud + VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX + VITE_WORKOS_REDIRECT_URI=https://your-domain.com/callback + ``` + +5. **Deploy:** + ```bash + npx convex deploy + npm run build + # Deploy to Netlify or your hosting provider + ``` + +--- + +## 13. Troubleshooting + +### "useAuth must be used within AuthKitProvider" + +**Cause**: Component is rendered outside the AuthKitProvider wrapper. + +**Fix**: Make sure `AuthKitProvider` wraps your entire app in `main.tsx`. + +### Login redirects to wrong URL + +**Cause**: Redirect URI mismatch. + +**Fix**: +1. Check `VITE_WORKOS_REDIRECT_URI` matches exactly what's in WorkOS Dashboard +2. Include protocol (`http://` or `https://`) +3. Include port number for localhost (`:5173`) + +### "isAuthenticated: false" after login + +**Cause**: Convex auth config doesn't match WorkOS setup. + +**Fix**: +1. Verify `WORKOS_CLIENT_ID` is set in Convex Dashboard +2. Re-run `npx convex dev` to sync configuration +3. Check browser console for specific errors + +### CORS errors + +**Cause**: Your domain isn't in WorkOS CORS settings. + +**Fix**: +1. Go to WorkOS Dashboard → Authentication → Sessions → CORS +2. Add your exact origin (e.g., `http://localhost:5173`) +3. Don't include trailing slashes + +### "Failed to fetch" errors + +**Cause**: Network or authentication issues. + +**Fix**: +1. Check browser DevTools Network tab for specific errors +2. Verify Convex is running (`npx convex dev`) +3. Check that environment variables are loaded (add `console.log(import.meta.env)`) + +--- + +## 14. Quick Reference + +### File Changes Summary + +| File | Action | Purpose | +|------|--------|---------| +| `convex/auth.config.ts` | Create | Tell Convex how to validate WorkOS tokens | +| `src/main.tsx` | Modify | Wrap app with auth providers | +| `src/pages/Callback.tsx` | Create | Handle auth redirect | +| `src/pages/Dashboard.tsx` | Create | Protected dashboard page | +| `src/App.tsx` | Modify | Add routes for callback and dashboard | +| `src/styles/global.css` | Modify | Add dashboard styles | +| `.env.local` | Modify | Add WorkOS credentials | + +### Environment Variables + +| Variable | Where Used | Example | +|----------|------------|---------| +| `VITE_WORKOS_CLIENT_ID` | Browser (Vite) | `client_01XXXXXXXXX` | +| `VITE_WORKOS_REDIRECT_URI` | Browser (Vite) | `http://localhost:5173/callback` | +| `WORKOS_CLIENT_ID` | Convex Backend | `client_01XXXXXXXXX` | + +### Useful Commands + +```bash +# Start development +npx convex dev # In terminal 1 +npm run dev # In terminal 2 + +# Deploy to production +npx convex deploy # Deploy Convex functions +npm run build # Build frontend + +# Sync content to production +npm run sync:prod +``` + +### WorkOS Dashboard Checklist + +- [ ] AuthKit enabled and configured +- [ ] Redirect URI added (`http://localhost:5173/callback`) +- [ ] CORS origin added (`http://localhost:5173`) +- [ ] Client ID copied to `.env.local` +- [ ] Production URLs added (when deploying) + +--- + +## Next Steps + +Once you have the basic setup working, you can: + +1. **Customize the dashboard** - Add Convex queries to show user-specific data +2. **Add more protected routes** - Use the same pattern for other admin pages +3. **Enable social login** - Configure Google, GitHub, etc. in WorkOS Dashboard +4. **Set up organizations** - If you need multi-tenant features +5. **Add role-based access** - Use WorkOS roles and permissions + +For more information, see: +- [Convex AuthKit Documentation](https://docs.convex.dev/auth/authkit) +- [WorkOS AuthKit Docs](https://workos.com/docs/authkit) +- [AuthKit React SDK on GitHub](https://github.com/workos/authkit-react) + +--- + +*Last updated: December 2024* diff --git a/public/llms.txt b/public/llms.txt index ca6f46d..593889f 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,6 +1,6 @@ # llms.txt - Information for AI assistants and LLMs # Learn more: https://llmstxt.org/ -# Last updated: 2025-12-29T03:04:12.913Z +# Last updated: 2025-12-29T21:11:54.408Z > Your content is instantly available to browsers, LLMs, and AI agents. diff --git a/public/raw/about.md b/public/raw/about.md index b79beec..2b2df11 100644 --- a/public/raw/about.md +++ b/public/raw/about.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. diff --git a/public/raw/changelog.md b/public/raw/changelog.md index 01603db..87e8152 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -2,12 +2,114 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v1.46.0 + +Released December 29, 2025 + +**Dashboard sync server for executing sync commands from UI** + +- Local HTTP server for executing sync commands directly from dashboard + - Run `npm run sync-server` to start the local server on localhost:3001 + - Execute sync commands without opening a terminal + - Real-time output streaming in dashboard terminal view + - Server status indicator shows online/offline status + - Copy and Execute buttons for each sync command + - Optional token authentication via `SYNC_TOKEN` environment variable + - Whitelisted commands only for security + - Health check endpoint for server availability detection + - Header sync buttons automatically use sync server when available + - Copy icons for `npm run sync-server` command throughout dashboard + +**Technical details:** + +- New `scripts/sync-server.ts` file implements local HTTP server +- Uses Node.js `child_process.spawn` to execute npm commands +- Streams output in real-time to dashboard UI +- CORS enabled for localhost:5173 development server +- Server binds to localhost only (not accessible from network) + +Updated files: `scripts/sync-server.ts`, `src/pages/Dashboard.tsx`, `src/styles/global.css`, `package.json` + +## v1.45.0 + +Released December 29, 2025 + +**Dashboard and WorkOS authentication integration** + +- Dashboard supports optional WorkOS authentication via `siteConfig.dashboard.requireAuth` +- WorkOS is optional - dashboard works with or without WorkOS configured +- When `requireAuth` is `false`, dashboard is open access +- When `requireAuth` is `true` and WorkOS is configured, dashboard requires login +- Shows setup instructions if `requireAuth` is `true` but WorkOS is not configured +- Warning banner displayed when authentication is not enabled +- Blog posts added: "How to use the Markdown sync dashboard" and "How to setup WorkOS" + +Updated files: `src/pages/Dashboard.tsx`, `src/main.tsx`, `src/App.tsx`, `src/pages/Callback.tsx`, `src/utils/workos.ts`, `convex/auth.config.ts`, `src/config/siteConfig.ts`, `README.md`, `content/pages/docs.md`, `content/blog/setup-guide.md`, `FORK_CONFIG.md`, `fork-config.json.example`, `files.md`, `TASK.md`, `changelog.md`, `content/pages/changelog-page.md` + +## v1.44.0 + +Released December 29, 2025 + +**Dashboard for centralized content management and site configuration** + +- Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations +- Content management: Posts and Pages list views with filtering, search, pagination, and items per page selector (15, 25, 50, 100) +- Post and Page editor: Markdown editor with live preview, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown, copy to clipboard +- Write Post and Write Page: Full-screen writing interface with markdown editor, frontmatter reference, download markdown, localStorage persistence +- AI Agent section: Dedicated AI chat separate from Write page, uses Anthropic Claude API, per-session chat history, markdown rendering +- Newsletter management: All Newsletter Admin features integrated (subscribers, send newsletter, write email, recent sends, email stats) +- Content import: Firecrawl import UI for importing external URLs as markdown drafts +- Site configuration: Config Generator UI for all `siteConfig.ts` settings, generates downloadable config file +- Index HTML editor: View and edit `index.html` content with meta tags, Open Graph, Twitter Cards, JSON-LD +- Analytics: Real-time stats dashboard (clone of `/stats` page, always accessible in dashboard) +- Sync commands: UI with buttons for all sync operations (sync, sync:discovery, sync:all for dev and prod) +- Header sync buttons: Quick sync buttons in dashboard header for `npm run sync:all` (dev and prod) +- Dashboard search: Search bar in header to search dashboard features, page titles, and post content +- Toast notifications: Success, error, info, and warning notifications with auto-dismiss +- Command modal: Shows sync command output with copy to clipboard functionality +- Mobile responsive: Fully responsive design with mobile-optimized layout +- Theme and font: Theme toggle and font switcher with persistent preferences + +Updated files: `src/pages/Dashboard.tsx`, `src/styles/global.css`, `src/App.tsx` + +## v1.43.0 + +Released December 29, 2025 + +**Stats page configuration option for public/private access** + +- New `StatsPageConfig` interface in `siteConfig.ts` with `enabled` and `showInNav` options +- Stats page can be made private by setting `enabled: false` (similar to NewsletterAdmin pattern) +- When disabled, route shows "Stats page is disabled" message instead of analytics +- Navigation item automatically hidden when stats page is disabled +- Default configuration: `enabled: true` (public), `showInNav: true` (visible in nav) + +Updated files: `src/config/siteConfig.ts`, `src/App.tsx`, `src/pages/Stats.tsx`, `src/components/Layout.tsx` + +## v1.42.0 + +Released December 29, 2025 + +**Honeypot bot protection for contact and newsletter forms** + +- Hidden honeypot fields invisible to humans but visible to bots +- Contact form uses hidden "Website" field for bot detection +- Newsletter signup uses hidden "Fax" field for bot detection +- Bots that fill hidden fields receive fake success message (no data submitted) +- No external dependencies required (client-side only protection) +- Works with all four themes (dark, light, tan, cloud) + +Updated files: `src/components/ContactForm.tsx`, `src/components/NewsletterSignup.tsx` +- Honeypot fields use CSS positioning (position: absolute, left: -9999px) to hide from users +- Fields include aria-hidden="true" and tabIndex={-1} for accessibility +- Different field names per form (website/fax) to avoid pattern detection + ## v1.41.0 Released December 28, 2025 diff --git a/public/raw/contact.md b/public/raw/contact.md index e408b0c..15f1475 100644 --- a/public/raw/contact.md +++ b/public/raw/contact.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- You found the contact page. Nice diff --git a/public/raw/docs.md b/public/raw/docs.md index 5679635..8c4891d 100644 --- a/public/raw/docs.md +++ b/public/raw/docs.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- ## Getting Started @@ -79,6 +79,8 @@ markdown-site/ ## Content +**Markdown examples:** For complete markdown syntax examples including code blocks, tables, lists, links, images, collapsible sections, and all formatting options, see [Writing Markdown with Code Examples](/markdown-with-code-examples). That post includes copy-paste examples for every markdown feature. + ### Blog posts Create files in `content/blog/` with frontmatter: @@ -999,6 +1001,206 @@ npm run newsletter:send setup-guide The `newsletter:send` command calls the `scheduleSendPostNewsletter` mutation directly and sends emails in the background. Check the Newsletter Admin page or recent sends to see results. +## Dashboard + +The Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations. It's designed for developers who fork the repository to set up and manage their markdown blog. + +**Access:** Navigate to `/dashboard` in your browser. The dashboard is not linked in the navigation by default (similar to Newsletter Admin pattern). + +**Authentication:** WorkOS authentication is optional. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup. + +### Content Management + +**Posts and Pages List Views:** +- View all posts and pages (published and unpublished) +- Filter by status: All, Published, Drafts +- Search by title or content +- Pagination with "First" and "Next" buttons +- Items per page selector (15, 25, 50, 100) - default: 15 +- Edit, view, and publish/unpublish options +- WordPress-style UI with date, edit, view, and publish controls + +**Post and Page Editor:** +- Markdown editor with live preview +- Frontmatter sidebar on the right with all available fields +- Draggable/resizable frontmatter sidebar (200px-600px width) +- Independent scrolling for frontmatter sidebar +- Preview mode shows content as it appears on the live site +- Download markdown button to generate `.md` files +- Copy markdown to clipboard +- All frontmatter fields editable in sidebar +- Preview uses ReactMarkdown with proper styling + +**Write Post and Write Page:** +- Full-screen writing interface +- Markdown editor with word/line/character counts +- Frontmatter reference panel +- Download markdown button for new content +- Content persists in localStorage +- Separate storage for post and page content + +### AI Agent + +- Dedicated AI chat section separate from the Write page +- Uses Anthropic Claude API (requires `ANTHROPIC_API_KEY` in Convex environment) +- Per-session chat history stored in Convex +- Markdown rendering for AI responses +- Copy functionality for AI responses + +### Newsletter Management + +All Newsletter Admin features integrated into the Dashboard: + +- **Subscribers:** View, search, filter, and delete subscribers +- **Send Newsletter:** Select a blog post to send as newsletter +- **Write Email:** Compose custom emails with markdown support +- **Recent Sends:** View last 10 newsletter sends (posts and custom emails) +- **Email Stats:** Dashboard with total emails, newsletters sent, active subscribers, retention rate + +All newsletter sections are full-width in the dashboard content area. + +### Content Import + +**Firecrawl Import:** +- Import articles from external URLs using Firecrawl API +- Requires `FIRECRAWL_API_KEY` in `.env.local` +- Creates local markdown drafts in `content/blog/` +- Imported posts are drafts (`published: false`) by default +- Review, edit, set `published: true`, then sync + +### Site Configuration + +**Config Generator:** +- UI to configure all settings in `src/config/siteConfig.ts` +- Generates downloadable `siteConfig.ts` file +- Hybrid approach: dashboard generates config, file-based config continues to work +- Includes all site configuration options: + - Site name, title, logo, bio, intro + - Blog page settings + - Featured section configuration + - Logo gallery settings + - GitHub contributions + - Footer and social footer + - Newsletter settings + - Contact form settings + - Stats page settings + - And more + +**Index HTML Editor:** +- View and edit `index.html` content +- Meta tags, Open Graph, Twitter Cards, JSON-LD +- Download updated HTML file + +### Analytics + +- Real-time stats dashboard (clone of `/stats` page) +- Active visitors with per-page breakdown +- Total page views and unique visitors +- Views by page sorted by popularity +- Does not follow `siteConfig.statsPage` settings (always accessible in dashboard) + +### Sync Commands + +**Sync Content Section:** +- UI with buttons for all sync operations +- Development sync commands: + - `npm run sync` - Sync markdown content + - `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) + - `npm run sync:all` - Sync content + discovery files together +- Production sync commands: + - `npm run sync:prod` - Sync markdown content + - `npm run sync:discovery:prod` - Update discovery files + - `npm run sync:all:prod` - Sync content + discovery files together +- Server status indicator shows if sync server is online +- Copy and Execute buttons for each command +- Real-time terminal output when sync server is running +- Command modal shows full command output when sync server is offline +- Toast notifications for success/error feedback + +**Sync Server:** +- Local HTTP server for executing commands from dashboard +- Start with `npm run sync-server` (runs on localhost:3001) +- Execute commands directly from dashboard with real-time output streaming +- Optional token authentication via `SYNC_TOKEN` environment variable +- Whitelisted commands only for security +- Health check endpoint for server availability detection +- Copy icons for `npm run sync-server` command in dashboard + +**Header Sync Buttons:** +- Quick sync buttons in dashboard header (right side) +- `npm run sync:all` (dev) button +- `npm run sync:all:prod` (prod) button +- One-click sync for all content and discovery files +- Automatically use sync server when available, fallback to command modal + +### Dashboard Features + +**Search:** +- Search bar in header +- Search dashboard features, page titles, and post content +- Real-time results as you type + +**Theme and Font:** +- Theme toggle (dark, light, tan, cloud) +- Font switcher (serif, sans, monospace) +- Preferences persist across sessions + +**Mobile Responsive:** +- Fully responsive design +- Mobile-optimized layout +- Touch-friendly controls +- Collapsible sidebar on mobile + +**Toast Notifications:** +- Success, error, info, and warning notifications +- Auto-dismiss after 4 seconds +- Theme-aware styling +- No browser default alerts + +**Command Modal:** +- Shows sync command output +- Copy command to clipboard +- Close button to dismiss +- Theme-aware styling + +### Technical Details + +- Uses Convex queries for real-time data +- All mutations follow Convex best practices (idempotent, indexed queries) +- Frontmatter sidebar width persisted in localStorage +- Editor content persisted in localStorage +- Independent scrolling for editor and sidebar sections +- Preview uses ReactMarkdown with remark-gfm, remark-breaks, rehype-raw, rehype-sanitize + +### Sync Commands Reference + +**Development:** +- `npm run sync` - Sync markdown content to development Convex +- `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) with development data +- `npm run sync:all` - Run both content sync and discovery sync (development) + +**Production:** +- `npm run sync:prod` - Sync markdown content to production Convex +- `npm run sync:discovery:prod` - Update discovery files with production data +- `npm run sync:all:prod` - Run both content sync and discovery sync (production) + +**Sync Server:** +- `npm run sync-server` - Start local HTTP server for executing sync commands from dashboard UI + +**Content Import:** +- `npm run import ` - Import external URL as markdown post (requires FIRECRAWL_API_KEY) + +**Note:** The dashboard provides a UI for these commands. When the sync server is running (`npm run sync-server`), you can execute commands directly from the dashboard with real-time output. Otherwise, the dashboard shows commands in a modal for copying to your terminal. + ## API endpoints | Endpoint | Description | @@ -1073,49 +1275,18 @@ When you run `npm run sync` (development) or `npm run sync:prod` (production), s These files include a metadata header with type, date, reading time, and tags. Access via the "View as Markdown" option in the Copy Page dropdown. -## Markdown tables +## Markdown formatting -Tables render with GitHub-style formatting: +For complete markdown syntax examples including tables, collapsible sections, code blocks, lists, links, images, and all formatting options, see [Writing Markdown with Code Examples](/markdown-with-code-examples). -- Clean borders across all themes -- Mobile responsive with horizontal scroll -- Theme-aware alternating row colors -- Hover states for readability +**Quick reference:** -Example: +- **Tables:** Render with GitHub-style formatting, clean borders, mobile responsive +- **Collapsible sections:** Use HTML `
` and `` tags for expandable content +- **Code blocks:** Support syntax highlighting for TypeScript, JavaScript, bash, JSON, and more +- **Images:** Place in `public/images/` and reference with absolute paths -| Feature | Status | -| ------- | ------ | -| Borders | Clean | -| Mobile | Scroll | -| Themes | All | - -## Collapsible sections - -Create expandable/collapsible content using HTML `
` and `` tags: - -```html -
- Click to expand - - Hidden content here. Supports markdown: - Lists - **Bold** and _italic_ - Code - blocks -
-``` - -**Expanded by default:** Add the `open` attribute: - -```html -
- Already expanded - - This section starts open. -
-``` - -**Nested sections:** You can nest `
` inside other `
` for multi-level collapsible content. - -Collapsible sections work with all four themes and are styled to match the site design. +All markdown features work with all four themes and are styled to match the site design. ## Import external content diff --git a/public/raw/home-intro.md b/public/raw/home-intro.md index 348f1c3..b69d21a 100644 --- a/public/raw/home-intro.md +++ b/public/raw/home-intro.md @@ -2,10 +2,10 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- -An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. +An open-source publishing framework built for AI agents and developers to ship **websites**, **docs**, or **blogs**. Write markdown, sync from the terminal. [Fork it](https://github.com/waynesutton/markdown-site), customize it, ship it. diff --git a/public/raw/how-to-setup-workos.md b/public/raw/how-to-setup-workos.md new file mode 100644 index 0000000..8fefd45 --- /dev/null +++ b/public/raw/how-to-setup-workos.md @@ -0,0 +1,313 @@ +# How to setup WorkOS + +> Step-by-step guide to configure WorkOS AuthKit authentication for your markdown blog dashboard. WorkOS is optional and can be enabled in siteConfig.ts. + +--- +Type: post +Date: 2025-12-29 +Reading time: 10 min read +Tags: workos, authentication, tutorial, dashboard +--- + +# How to setup WorkOS + +WorkOS AuthKit adds authentication to your markdown blog dashboard. It's optional—you can use the dashboard without WorkOS, or enable authentication for production sites. + +## Overview + +WorkOS AuthKit provides: + +- Password authentication +- Social login (Google, GitHub, etc.) +- SSO support +- User management +- Session handling + +The dashboard at `/dashboard` can work with or without WorkOS. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. + +## Prerequisites + +Before starting, make sure you have: + +- Node.js 18 or higher +- A working markdown blog project +- A Convex account and project set up +- Your Convex development server running (`npx convex dev`) + +## Step 1: Create a WorkOS account + +### Sign up + +1. Go to [workos.com/sign-up](https://signin.workos.com/sign-up) +2. Create a free account with your email +3. Verify your email address + +### Set up AuthKit + +1. Log into the [WorkOS Dashboard](https://dashboard.workos.com) +2. Navigate to **Authentication** → **AuthKit** +3. Click the **Set up AuthKit** button +4. Select **"Use AuthKit's customizable hosted UI"** +5. Click **Begin setup** + +### Configure redirect URI + +During the AuthKit setup wizard, you'll reach step 4: **"Add default redirect endpoint URI"** + +Enter this for local development: +``` +http://localhost:5173/callback +``` + +After a user logs in, WorkOS redirects them back to this URL with an authorization code. Your app exchanges this code for user information. + +### Copy your credentials + +1. Go to [dashboard.workos.com/get-started](https://dashboard.workos.com/get-started) +2. Under **Quick start**, find and copy: + - **Client ID** (looks like `client_01XXXXXXXXXXXXXXXXX`) + +Save this somewhere safe—you'll need it shortly. + +## Step 2: Configure WorkOS dashboard + +### Enable CORS + +For the React SDK to work, you need to allow your app's domain: + +1. Go to **Authentication** → **Sessions** in the WorkOS Dashboard +2. Find **Cross-Origin Resource Sharing (CORS)** +3. Click **Manage** +4. Add your development URL: `http://localhost:5173` +5. Click **Save** + +When you deploy to production (e.g., Netlify), add your production domain here too (e.g., `https://yoursite.netlify.app`). + +### Verify redirect URI + +1. Go to **Redirects** in the WorkOS Dashboard +2. Confirm `http://localhost:5173/callback` is listed +3. If not, add it by clicking **Add redirect** + +## Step 3: Install dependencies + +Open your terminal in the project folder and install the required packages: + +```bash +npm install @workos-inc/authkit-react @convex-dev/workos +``` + +**What these packages do:** + +| Package | Purpose | +|---------|---------| +| `@workos-inc/authkit-react` | WorkOS React SDK for handling login/logout | +| `@convex-dev/workos` | Bridges WorkOS auth with Convex backend | + +## Step 4: Add environment variables + +### Update `.env.local` + +Open your `.env.local` file (in the project root) and add these lines: + +```env +# Existing Convex URL (should already be here) +VITE_CONVEX_URL=https://your-deployment.convex.cloud + +# WorkOS AuthKit Configuration (add these) +VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX +VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback +``` + +Replace `client_01XXXXXXXXXXXXXXXXX` with your actual Client ID from the WorkOS Dashboard. + +Vite only exposes environment variables that start with `VITE_` to the browser. + +### Add to `.gitignore` + +Make sure `.env.local` is in your `.gitignore` to avoid committing secrets: + +``` +.env.local +.env.production.local +``` + +## Step 5: Configure Convex auth + +Create a new file to tell Convex how to validate WorkOS tokens. + +### Create `convex/auth.config.ts` + +Create a new file at `convex/auth.config.ts`: + +```typescript +// convex/auth.config.ts +const clientId = process.env.WORKOS_CLIENT_ID; + +const authConfig = { + providers: [ + { + type: "customJwt", + issuer: "https://api.workos.com/", + algorithm: "RS256", + applicationID: clientId, + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + { + type: "customJwt", + issuer: `https://api.workos.com/user_management/${clientId}`, + algorithm: "RS256", + jwks: `https://api.workos.com/sso/jwks/${clientId}`, + }, + ], +}; + +export default authConfig; +``` + +### Add environment variable to Convex + +The Convex backend needs the Client ID too: + +1. Run `npx convex dev` if not already running +2. You'll see an error with a link—click it +3. It takes you to the Convex Dashboard environment variables page +4. Add a new variable: + - **Name**: `WORKOS_CLIENT_ID` + - **Value**: Your WorkOS Client ID (e.g., `client_01XXXXXXXXXXXXXXXXX`) +5. Save + +After saving, `npx convex dev` should show "Convex functions ready." + +## Step 6: Update site configuration + +Enable authentication in your site config: + +```typescript +// src/config/siteConfig.ts +dashboard: { + enabled: true, + requireAuth: true, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `true` and WorkOS is configured, the dashboard requires login. When `requireAuth` is `false`, the dashboard is open access. + +## Step 7: Test locally + +1. Start your development server: + ```bash + npm run dev + ``` + +2. Navigate to `http://localhost:5173/dashboard` + +3. You should see a login prompt (if `requireAuth: true`) or the dashboard (if `requireAuth: false`) + +4. If authentication is enabled, click "Sign in" to test the WorkOS login flow + +5. After logging in, you should be redirected back to the dashboard + +## Step 8: Deploy to production + +### Add production environment variables + +In your Netlify dashboard (or hosting provider), add these environment variables: + +- `VITE_WORKOS_CLIENT_ID` - Your WorkOS Client ID +- `VITE_WORKOS_REDIRECT_URI` - Your production callback URL (e.g., `https://yoursite.netlify.app/callback`) + +### Update WorkOS redirect URI + +1. Go to **Redirects** in the WorkOS Dashboard +2. Add your production callback URL: `https://yoursite.netlify.app/callback` +3. Click **Save** + +### Update CORS settings + +1. Go to **Authentication** → **Sessions** in the WorkOS Dashboard +2. Find **Cross-Origin Resource Sharing (CORS)** +3. Click **Manage** +4. Add your production domain: `https://yoursite.netlify.app` +5. Click **Save** + +### Add Convex environment variable + +In your Convex Dashboard, add the `WORKOS_CLIENT_ID` environment variable for your production deployment: + +1. Go to [dashboard.convex.dev](https://dashboard.convex.dev) +2. Select your production project +3. Navigate to Settings → Environment Variables +4. Add `WORKOS_CLIENT_ID` with your Client ID value +5. Save + +## How it works + +When WorkOS is configured and `requireAuth: true`: + +1. User navigates to `/dashboard` +2. Dashboard checks authentication status +3. If not authenticated, shows login prompt +4. User clicks "Sign in" and is redirected to WorkOS +5. User logs in with WorkOS (password, social, or SSO) +6. WorkOS redirects back to `/callback` with authorization code +7. App exchanges code for user information +8. User is redirected to `/dashboard` as authenticated user + +When WorkOS is not configured or `requireAuth: false`: + +1. User navigates to `/dashboard` +2. Dashboard shows content directly (no authentication required) + +## Troubleshooting + +**Dashboard shows "Authentication Required" message:** +- Verify `VITE_WORKOS_CLIENT_ID` and `VITE_WORKOS_REDIRECT_URI` are set in `.env.local` +- Check that `WORKOS_CLIENT_ID` is set in Convex environment variables +- Ensure `requireAuth: true` in `siteConfig.ts` + +**Login redirect not working:** +- Verify redirect URI matches exactly in WorkOS Dashboard +- Check CORS settings include your domain +- Ensure callback route is configured in `App.tsx` + +**"WorkOS is not configured" message:** +- Check that both `VITE_WORKOS_CLIENT_ID` and `VITE_WORKOS_REDIRECT_URI` are set +- Verify environment variables are loaded (check browser console) +- Restart development server after adding environment variables + +**Convex auth errors:** +- Verify `WORKOS_CLIENT_ID` is set in Convex environment variables +- Check that `convex/auth.config.ts` exists and is correct +- Ensure Convex functions are deployed (`npx convex deploy`) + +## Optional configuration + +WorkOS is optional. You can: + +- Use the dashboard without WorkOS (`requireAuth: false`) +- Enable WorkOS later when you need authentication +- Configure WorkOS for production only + +The dashboard works with or without WorkOS. Configure it based on your needs. + +## Next steps + +After setting up WorkOS: + +1. Test the authentication flow locally +2. Deploy to production with production environment variables +3. Add additional redirect URIs if needed +4. Configure social login providers in WorkOS Dashboard +5. Set up SSO if needed for your organization + +See [How to use the Markdown sync dashboard](https://www.markdown.fast/how-to-use-the-markdown-sync-dashboard) for dashboard usage. \ No newline at end of file diff --git a/public/raw/how-to-use-the-markdown-sync-dashboard.md b/public/raw/how-to-use-the-markdown-sync-dashboard.md new file mode 100644 index 0000000..03f32f0 --- /dev/null +++ b/public/raw/how-to-use-the-markdown-sync-dashboard.md @@ -0,0 +1,267 @@ +# How to use the Markdown sync dashboard + +> Learn how to use the dashboard at /dashboard to manage content, configure your site, and sync markdown files without leaving your browser. + +--- +Type: post +Date: 2025-12-29 +Reading time: 8 min read +Tags: dashboard, tutorial, content-management +--- + +# How to use the Markdown sync dashboard + +The dashboard at `/dashboard` gives you a centralized interface for managing your markdown blog. You can edit posts, sync content, configure settings, and more without switching between your editor and terminal. + +## Accessing the dashboard + +Navigate to `/dashboard` in your browser. The dashboard isn't linked in the navigation by default, so you'll access it directly via URL. + +### Authentication + +The dashboard supports optional WorkOS authentication. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open to anyone who knows the URL. For production sites, set `requireAuth: true` and configure WorkOS authentication. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup. + +If WorkOS isn't configured and `requireAuth` is `true`, the dashboard shows setup instructions instead of the login prompt. + +## Content management + +### Posts and pages list + +View all your posts and pages in one place. Each list includes: + +- Filter by status: All, Published, Drafts +- Search by title or content +- Pagination with "First" and "Next" buttons +- Items per page selector (15, 25, 50, 100) +- Quick actions: Edit, View, Publish/Unpublish + +Posts and pages display with their titles, publication status, and last modified dates. Click any item to open the editor. + +### Post and page editor + +Edit markdown content with a live preview. The editor includes: + +- Markdown editor on the left +- Live preview on the right showing how content appears on your site +- Draggable frontmatter sidebar (200px-600px width) +- Independent scrolling for editor and preview sections +- Download markdown button to save changes locally +- Copy to clipboard for quick sharing + +The frontmatter sidebar shows all available fields with descriptions. Edit values directly, and changes appear in the preview immediately. + +### Write post and write page + +Create new content without leaving the dashboard. The write interface includes: + +- Full-screen markdown editor +- Frontmatter reference panel with copy buttons +- Word, line, and character counts +- Download markdown button +- Content persists in localStorage + +Write your content, fill in frontmatter fields, then download the markdown file. Save it to `content/blog/` or `content/pages/`, then sync to Convex. + +## AI Agent + +The dashboard includes a dedicated AI chat section separate from the Write page. Use it for: + +- Writing assistance +- Content suggestions +- Editing help +- Answering questions about your content + +The AI Agent uses Anthropic Claude API and requires `ANTHROPIC_API_KEY` in your Convex environment variables. Chat history is stored per-session in Convex. + +## Newsletter management + +All Newsletter Admin features are integrated into the dashboard: + +- Subscribers: View, search, filter, and delete subscribers +- Send newsletter: Select a blog post to send to all active subscribers +- Write email: Compose custom emails with markdown support +- Recent sends: View the last 10 newsletter sends (posts and custom emails) +- Email stats: Dashboard with total emails sent, newsletters sent, active subscribers, and retention rate + +Newsletter features require AgentMail configuration. Set `AGENTMAIL_API_KEY` and `AGENTMAIL_INBOX` in your Convex environment variables. + +## Content import + +Import articles from external URLs using Firecrawl: + +1. Enter the URL you want to import +2. Click "Import" +3. Review the imported markdown draft +4. Edit if needed, then sync to Convex + +Imported posts are created as drafts (`published: false`) by default. Review, edit, set `published: true`, then sync. + +Firecrawl import requires `FIRECRAWL_API_KEY` in your `.env.local` file. + +## Site configuration + +The Config Generator UI lets you configure all `siteConfig.ts` settings from the dashboard: + +- Site name, title, logo, bio +- Blog page settings +- Featured section configuration +- Logo gallery settings +- GitHub contributions +- Footer and social footer +- Newsletter settings +- Contact form settings +- Stats page settings +- Dashboard settings + +Make changes in the UI, then download the generated `siteConfig.ts` file. Replace your existing config file with the downloaded version. + +## Index HTML editor + +View and edit `index.html` content directly: + +- Meta tags +- Open Graph tags +- Twitter Cards +- JSON-LD structured data + +Edit values in the UI, then download the updated HTML file. Replace your existing `index.html` with the downloaded version. + +## Analytics + +The dashboard includes a real-time stats section (clone of `/stats` page): + +- Active visitors with per-page breakdown +- Total page views +- Unique visitors +- Views by page sorted by popularity + +Stats update automatically via Convex subscriptions. No page refresh needed. + +## Sync commands + +Run sync operations from the dashboard without opening a terminal: + +**Development:** + +- `npm run sync` - Sync markdown content +- `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) +- `npm run sync:all` - Sync content + discovery files together + +**Production:** + +- `npm run sync:prod` - Sync markdown content +- `npm run sync:discovery:prod` - Update discovery files +- `npm run sync:all:prod` - Sync content + discovery files together + +### Sync server + +For the best experience, start the sync server to execute commands directly from the dashboard: + +```bash +npm run sync-server +``` + +This starts a local HTTP server on `localhost:3001` that allows the dashboard to execute sync commands and stream output in real-time. + +**When sync server is running:** + +- Server status shows "Online" in the sync section +- "Execute" buttons appear for each sync command +- Clicking Execute runs the command and streams output to the terminal view +- Real-time output appears as the command runs +- No need to copy commands to your terminal + +**When sync server is offline:** + +- Server status shows "Offline" in the sync section +- Only "Copy" buttons appear for each sync command +- Clicking Copy shows the command in a modal for copying to your terminal +- Copy icon appears next to `npm run sync-server` command to help you start the server + +**Security:** + +- Server binds to localhost only (not accessible from network) +- Optional token authentication via `SYNC_TOKEN` environment variable +- Only whitelisted commands can be executed +- CORS enabled for localhost:5173 (dev server) + +### Header sync buttons + +Quick sync buttons in the dashboard header let you run `npm run sync:all` (dev and prod) with one click. These buttons automatically use the sync server when available, or show the command in a modal when the server is offline. + +## Dashboard features + +### Search + +The search bar in the dashboard header searches: + +- Dashboard features and sections +- Page titles +- Post content + +Results appear as you type. Click any result to navigate to that section or content. + +### Theme and font + +Toggle between themes (dark, light, tan, cloud) and switch fonts (serif, sans, monospace) from the dashboard. Preferences persist across sessions. + +### Toast notifications + +Success, error, info, and warning notifications appear in the top-right corner. They auto-dismiss after 4 seconds and are theme-aware. + +### Command modal + +When you run sync commands, output appears in a modal. You can: + +- View full command output +- Copy command to clipboard +- Close the modal + +### Mobile responsive + +The dashboard works on mobile devices with: + +- Collapsible sidebar +- Touch-friendly controls +- Responsive tables and forms +- Mobile-optimized layout + +## Best practices + +1. Use the editor for quick edits and previews +2. Download markdown files for version control +3. Sync regularly to keep content up to date +4. Use the AI Agent for writing assistance +5. Check analytics to see what content performs well +6. Configure WorkOS authentication for production sites + +## Troubleshooting + +**Dashboard not loading:** + +- Check that `dashboard.enabled: true` in `siteConfig.ts` +- Verify Convex is running (`npx convex dev`) +- Check browser console for errors + +**Sync commands failing:** + +- Ensure you're in the project root directory +- Check that Convex environment variables are set +- Verify `.env.local` or `.env.production.local` exists + +**Authentication not working:** + +- Verify WorkOS environment variables are set +- Check that `requireAuth: true` in `siteConfig.ts` +- See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for setup instructions + +The dashboard makes managing your markdown blog easier. You can edit content, sync files, and configure settings all from one place. \ No newline at end of file diff --git a/public/raw/index.md b/public/raw/index.md index 965e7b3..2d098b3 100644 --- a/public/raw/index.md +++ b/public/raw/index.md @@ -2,8 +2,12 @@ This is the homepage index of all published content. -## Blog Posts (15) +## Blog Posts (17) +- **[How to setup WorkOS](/raw/how-to-setup-workos.md)** - Step-by-step guide to configure WorkOS AuthKit authentication for your markdown blog dashboard. WorkOS is optional and can be enabled in siteConfig.ts. + - Date: 2025-12-29 | Reading time: 10 min read | Tags: workos, authentication, tutorial, dashboard +- **[How to use the Markdown sync dashboard](/raw/how-to-use-the-markdown-sync-dashboard.md)** - Learn how to use the dashboard at /dashboard to manage content, configure your site, and sync markdown files without leaving your browser. + - Date: 2025-12-29 | Reading time: 8 min read | Tags: dashboard, tutorial, content-management - **[Team Workflows with Git Version Control](/raw/team-workflows-git-version-control.md)** - How teams collaborate on markdown content using git, sync to shared Convex deployments, and automate production syncs with CI/CD. - Date: 2025-12-29 | Reading time: 6 min read | Tags: git, convex, ci-cd, collaboration, workflow - **[How to Use the MCP Server with MarkDown Sync](/raw/how-to-use-mcp-server.md)** - Guide to using the HTTP-based Model Context Protocol(MCP) server at www.markdown.fast/mcp with Cursor and other AI tools @@ -47,6 +51,6 @@ This is the homepage index of all published content. --- -**Total Content:** 15 posts, 7 pages +**Total Content:** 17 posts, 7 pages All content is available as raw markdown files at `/raw/{slug}.md` diff --git a/public/raw/markdown-with-code-examples.md b/public/raw/markdown-with-code-examples.md index 74fff85..9dc1831 100644 --- a/public/raw/markdown-with-code-examples.md +++ b/public/raw/markdown-with-code-examples.md @@ -11,7 +11,9 @@ Tags: markdown, tutorial, code # Writing Markdown with Code Examples -This post demonstrates how to write markdown content with code blocks, tables, and formatting. Use it as a reference when creating your own posts. +This post is the complete reference for all markdown syntax used in this framework. It includes copy-paste examples for code blocks, tables, lists, links, images, collapsible sections, and all formatting options. + +**Use this post as your reference** when writing blog posts or pages. All examples are ready to copy and paste directly into your markdown files. ## Frontmatter diff --git a/public/raw/newsletter.md b/public/raw/newsletter.md index 9668cf2..2f67eeb 100644 --- a/public/raw/newsletter.md +++ b/public/raw/newsletter.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- # Newsletter Demo Page diff --git a/public/raw/projects.md b/public/raw/projects.md index 06dad0f..76da6bb 100644 --- a/public/raw/projects.md +++ b/public/raw/projects.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2025-12-29 +Date: 2025-12-30 --- This markdown framework is open source and built to be extended. Here is what ships out of the box. diff --git a/public/raw/setup-guide.md b/public/raw/setup-guide.md index f4303a8..a8eb95e 100644 --- a/public/raw/setup-guide.md +++ b/public/raw/setup-guide.md @@ -1582,6 +1582,67 @@ Agent requires the following Convex environment variables: If `ANTHROPIC_API_KEY` is not configured in Convex environment variables, Agent displays a user-friendly error message: "API key is not set". This helps identify when the API key is missing in production deployments. +## Dashboard + +The Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations. It's designed for developers who fork the repository to set up and manage their markdown blog. + +**Access:** Navigate to `/dashboard` in your browser. The dashboard is not linked in the navigation by default. + +**Authentication:** WorkOS authentication is optional. Configure it in `siteConfig.ts`: + +```typescript +dashboard: { + enabled: true, + requireAuth: false, // Set to true to require WorkOS authentication +}, +``` + +When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup. + +**Key Features:** + +- **Content Management:** Posts and Pages list views with filtering, search, pagination, and items per page selector +- **Post/Page Editor:** Markdown editor with live preview, draggable/resizable frontmatter sidebar, download markdown +- **Write Post/Page:** Full-screen writing interface with markdown editor and frontmatter reference +- **AI Agent:** Dedicated AI chat section separate from Write page +- **Newsletter Management:** All Newsletter Admin features integrated (subscribers, send newsletter, write email, recent sends, email stats) +- **Content Import:** Firecrawl import UI for importing external URLs as markdown drafts +- **Site Configuration:** Config Generator UI for all `siteConfig.ts` settings +- **Index HTML Editor:** View and edit `index.html` content +- **Analytics:** Real-time stats dashboard (always accessible in dashboard) +- **Sync Commands:** UI with buttons for all sync operations (sync, sync:discovery, sync:all for dev and prod) +- **Sync Server:** Execute sync commands directly from dashboard with real-time output +- **Header Sync Buttons:** Quick sync buttons in dashboard header for `npm run sync:all` (dev and prod) + +**Sync Commands Available:** + +- `npm run sync` - Sync markdown content (development) +- `npm run sync:prod` - Sync markdown content (production) +- `npm run sync:discovery` - Update discovery files (development) +- `npm run sync:discovery:prod` - Update discovery files (production) +- `npm run sync:all` - Sync content + discovery files (development) +- `npm run sync:all:prod` - Sync content + discovery files (production) +- `npm run sync-server` - Start local HTTP server for executing commands from dashboard + +**Sync Server:** + +The dashboard can execute sync commands directly without opening a terminal. Start the sync server: + +```bash +npm run sync-server +``` + +This starts a local HTTP server on `localhost:3001` that: +- Executes sync commands when requested from the dashboard +- Streams output in real-time to the dashboard terminal view +- Shows server status (online/offline) in the dashboard +- Supports optional token authentication via `SYNC_TOKEN` environment variable +- Only executes whitelisted commands for security + +When the sync server is running, the dashboard shows "Execute" buttons that run commands directly. When offline, buttons show commands in a modal for copying to your terminal. + +The dashboard provides a UI for these commands, but you can also run them directly from the terminal. See the [Dashboard documentation](/docs#dashboard) for complete details. + ## Next Steps After deploying: diff --git a/scripts/sync-server.ts b/scripts/sync-server.ts new file mode 100644 index 0000000..ecca6e7 --- /dev/null +++ b/scripts/sync-server.ts @@ -0,0 +1,224 @@ +#!/usr/bin/env npx tsx +/** + * Sync Server + * + * Local HTTP server for executing sync commands from the Dashboard UI. + * Runs on localhost:3001 with optional token authentication. + * + * Usage: + * npm run sync-server + * + * Security: + * - Binds to localhost only (not accessible from network) + * - Optional token auth via SYNC_TOKEN env var + * - Whitelisted commands only + */ + +import { spawn } from "child_process"; +import http from "http"; +import dotenv from "dotenv"; + +// Load environment variables +dotenv.config({ path: ".env.local" }); +dotenv.config(); + +const PORT = 3001; +const HOST = "127.0.0.1"; // Localhost only for security + +// Optional token for authentication (set in .env.local) +const SYNC_TOKEN = process.env.SYNC_TOKEN || ""; + +// Whitelist of allowed sync commands +const ALLOWED_COMMANDS: Record }> = { + "sync": { script: "sync" }, + "sync:prod": { script: "sync:prod" }, + "sync:discovery": { script: "sync:discovery" }, + "sync:discovery:prod": { script: "sync:discovery:prod" }, + "sync:all": { script: "sync:all" }, + "sync:all:prod": { script: "sync:all:prod" }, +}; + +// CORS headers for local development +function setCorsHeaders(res: http.ServerResponse) { + res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Sync-Token"); +} + +// Verify authentication token +function verifyAuth(req: http.IncomingMessage): boolean { + // If no token configured, allow all requests (dev mode) + if (!SYNC_TOKEN) { + return true; + } + + const token = req.headers["x-sync-token"]; + return token === SYNC_TOKEN; +} + +// Parse JSON body from request +async function parseBody(req: http.IncomingMessage): Promise> { + return new Promise((resolve, reject) => { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch { + reject(new Error("Invalid JSON")); + } + }); + req.on("error", reject); + }); +} + +// Execute npm script and stream output +function executeScript( + scriptName: string, + res: http.ServerResponse, +): void { + const config = ALLOWED_COMMANDS[scriptName]; + if (!config) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: `Unknown command: ${scriptName}` })); + return; + } + + // Set headers for streaming response + res.writeHead(200, { + "Content-Type": "text/plain; charset=utf-8", + "Transfer-Encoding": "chunked", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }); + + // Write initial message + res.write(`> npm run ${config.script}\n\n`); + + // Spawn the npm process + const child = spawn("npm", ["run", config.script], { + cwd: process.cwd(), + env: { ...process.env, ...config.env }, + shell: true, + }); + + // Stream stdout + child.stdout.on("data", (data: Buffer) => { + res.write(data.toString()); + }); + + // Stream stderr + child.stderr.on("data", (data: Buffer) => { + res.write(data.toString()); + }); + + // Handle process completion + child.on("close", (code) => { + if (code === 0) { + res.write(`\n[Done] Command completed successfully.\n`); + } else { + res.write(`\n[Error] Command exited with code ${code}.\n`); + } + res.end(); + }); + + // Handle process errors + child.on("error", (err) => { + res.write(`\n[Error] Failed to execute command: ${err.message}\n`); + res.end(); + }); +} + +// Main request handler +async function handleRequest( + req: http.IncomingMessage, + res: http.ServerResponse, +): Promise { + setCorsHeaders(res); + + // Handle preflight OPTIONS request + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + const url = new URL(req.url || "/", `http://${HOST}:${PORT}`); + const path = url.pathname; + + // Health check endpoint + if (path === "/health" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok", commands: Object.keys(ALLOWED_COMMANDS) })); + return; + } + + // Execute sync command + if (path === "/api/sync" && req.method === "POST") { + // Verify authentication + if (!verifyAuth(req)) { + res.writeHead(401, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Unauthorized" })); + return; + } + + try { + const body = await parseBody(req); + const command = body.command as string; + + if (!command || !ALLOWED_COMMANDS[command]) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + error: "Invalid command", + allowed: Object.keys(ALLOWED_COMMANDS) + })); + return; + } + + executeScript(command, res); + } catch (error) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Invalid request body" })); + } + return; + } + + // 404 for other routes + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); +} + +// Create and start server +const server = http.createServer((req, res) => { + handleRequest(req, res).catch((error) => { + console.error("Request error:", error); + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Internal server error" })); + } + }); +}); + +server.listen(PORT, HOST, () => { + console.log(`\n Sync Server running at http://${HOST}:${PORT}`); + console.log(`\n Available commands:`); + Object.keys(ALLOWED_COMMANDS).forEach((cmd) => { + console.log(` - ${cmd}`); + }); + if (SYNC_TOKEN) { + console.log(`\n Token authentication: enabled`); + } else { + console.log(`\n Token authentication: disabled (set SYNC_TOKEN in .env.local to enable)`); + } + console.log(`\n Use with Dashboard at http://localhost:5173/dashboard\n`); +}); + +// Handle graceful shutdown +process.on("SIGINT", () => { + console.log("\n Shutting down sync server..."); + server.close(() => { + process.exit(0); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index 1753251..565bdc4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,8 @@ import Write from "./pages/Write"; import TagPage from "./pages/TagPage"; import Unsubscribe from "./pages/Unsubscribe"; import NewsletterAdmin from "./pages/NewsletterAdmin"; +import Dashboard from "./pages/Dashboard"; +import Callback from "./pages/Callback"; import Layout from "./components/Layout"; import { usePageTracking } from "./hooks/usePageTracking"; import { SidebarProvider } from "./context/SidebarContext"; @@ -27,6 +29,16 @@ function App() { return ; } + // Dashboard renders without Layout (full-screen admin) + if (location.pathname === "/dashboard") { + return ; + } + + // Callback handles OAuth redirect from WorkOS + if (location.pathname === "/callback") { + return ; + } + // Determine if we should use a custom homepage const useCustomHomepage = siteConfig.homepage.type !== "default" && siteConfig.homepage.slug; diff --git a/src/AppWithWorkOS.tsx b/src/AppWithWorkOS.tsx new file mode 100644 index 0000000..fa5e91b --- /dev/null +++ b/src/AppWithWorkOS.tsx @@ -0,0 +1,24 @@ +// App wrapper with WorkOS authentication providers +// This file is only loaded when WorkOS environment variables are configured +import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react"; +import { ConvexReactClient } from "convex/react"; +import { ConvexProviderWithAuthKit } from "@convex-dev/workos"; +import { workosConfig } from "./utils/workos"; +import App from "./App"; + +interface AppWithWorkOSProps { + convex: ConvexReactClient; +} + +export default function AppWithWorkOS({ convex }: AppWithWorkOSProps) { + return ( + + + + + + ); +} diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index d2378ea..ce5aa7e 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -13,6 +13,7 @@ interface ContactFormProps { // Contact form component // Displays a form with name, email, and message fields // Submits to Convex which sends email via AgentMail +// Includes honeypot field for bot protection export default function ContactForm({ source, title, @@ -21,6 +22,7 @@ export default function ContactForm({ const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); + const [honeypot, setHoneypot] = useState(""); // Honeypot field for bot detection const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const [statusMessage, setStatusMessage] = useState(""); @@ -36,6 +38,17 @@ export default function ContactForm({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + // Honeypot check: if filled, silently reject (bot detected) + if (honeypot) { + // Pretend success to not alert the bot + setStatus("success"); + setStatusMessage("Thanks for your message! We'll get back to you soon."); + setName(""); + setEmail(""); + setMessage(""); + return; + } + // Basic validation if (!name.trim()) { setStatus("error"); @@ -103,6 +116,31 @@ export default function ContactForm({ ) : (
+ {/* Honeypot field: hidden from humans, visible to bots */} + +