diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 23dddeeb..b8d178ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,7 +9,7 @@ /packages/tasks/ @tomymaritano /packages/commands/ @tomymaritano /packages/embeds/ @tomymaritano -/packages/design-system/ @tomymaritano +/packages/plugin-api/ @tomymaritano # Desktop app (proprietary) - stricter review /apps/desktop/ @tomymaritano diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml new file mode 100644 index 00000000..cf55372a --- /dev/null +++ b/.github/workflows/deploy-api.yml @@ -0,0 +1,73 @@ +name: Deploy API + +on: + push: + branches: [main] + paths: + - 'packages/api/**' + - '.github/workflows/deploy-api.yml' + workflow_dispatch: + inputs: + environment: + description: 'Deploy environment' + required: true + default: 'production' + type: choice + options: + - staging + - production + +jobs: + test: + name: Test API + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - run: pnpm install + + - name: Typecheck + run: pnpm --filter @readied/api typecheck + + - name: Test + run: pnpm --filter @readied/api test + + deploy: + name: Deploy API + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - run: pnpm install + + - name: Determine environment + id: env + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "target=${{ inputs.environment }}" >> "$GITHUB_OUTPUT" + else + echo "target=production" >> "$GITHUB_OUTPUT" + fi + + - name: Deploy to Cloudflare Workers + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + DEPLOY_ENV: ${{ steps.env.outputs.target }} + run: npx wrangler deploy --env "$DEPLOY_ENV" + working-directory: packages/api diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5280569..22432863 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Readied uses an **Open Core** model: | `packages/tasks` | Task/checkbox parsing | | `packages/commands` | Command palette | | `packages/embeds` | Image/embed handling | -| `packages/design-system` | Design tokens, components | +| `packages/plugin-api` | Plugin API + theme system | ### Proprietary - Not Open for Contributions diff --git a/LICENSE b/LICENSE index 7f0a4d22..8c8c9db7 100644 --- a/LICENSE +++ b/LICENSE @@ -13,7 +13,7 @@ The following packages are licensed under the MIT License: - `packages/tasks/` - Task parsing - `packages/commands/` - Command palette logic - `packages/embeds/` - Embed handling -- `packages/design-system/` - Design tokens and components +- `packages/plugin-api/` - Plugin API and theme system - `packages/product-config/` - Product configuration See the LICENSE file in each package directory for the full MIT License text. diff --git a/README.md b/README.md index cf26b542..7e71efd0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ pnpm dev | `@readied/tasks` | Task/checkbox parsing | | `@readied/commands` | Command palette | | `@readied/embeds` | Image/embed handling | -| `@readied/design-system` | Design tokens, components | +| `@readied/plugin-api` | Plugin API + theme system | ## Contributing diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 00039487..b3e9c070 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -979,13 +979,24 @@ function registerNotebookHandlers(): void { }; }); - // Move notebook + // Move notebook (recursively updates children's depth) ipcMain.handle('notebooks:move', async (_event, id: string, newParentId: string | null) => { const notebook = await repo.get(createNotebookId(id)); if (!notebook) { throw new Error('Notebook not found'); } + // Prevent circular reference: can't move a notebook into its own descendant + if (newParentId) { + let current = await repo.get(createNotebookId(newParentId)); + while (current && current.parentId) { + if (current.parentId === notebook.id) { + throw new Error('CIRCULAR_REFERENCE'); + } + current = await repo.get(current.parentId); + } + } + let newParentDepth = 0; if (newParentId) { const parent = await repo.get(createNotebookId(newParentId)); @@ -1006,6 +1017,19 @@ function registerNotebookHandlers(): void { await repo.save(result.notebook); + // Recursively update children's depth to match the new hierarchy + const updateChildrenDepth = async (parentId: string, parentDepth: number) => { + const children = await repo.getChildren(parentId as ReturnType); + for (const child of children) { + const newChildDepth = parentDepth + 1; + if (child.depth !== newChildDepth) { + await repo.save({ ...child, depth: newChildDepth }); + await updateChildrenDepth(child.id, newChildDepth); + } + } + }; + await updateChildrenDepth(result.notebook.id, result.notebook.depth); + return { id: result.notebook.id, name: result.notebook.name, diff --git a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css index d4eda940..08dc9be1 100644 --- a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css +++ b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css @@ -82,11 +82,11 @@ } .successIcon { - color: #10b981; + color: var(--success, #10b981); } .errorIcon { - color: #ef4444; + color: var(--danger, #ef4444); } .spinner { @@ -181,6 +181,13 @@ background: var(--bg-hover); } +.errorText { + color: var(--danger, #ef4444); + font-size: 0.875rem; + text-align: center; + margin: 0 0 0.75rem; +} + .actions { display: flex; flex-direction: column; diff --git a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx index 16005d64..267f6ea4 100644 --- a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx +++ b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx @@ -4,7 +4,7 @@ * Multi-step dialog for passwordless authentication via email magic link. */ -import { useState, useCallback, FormEvent } from 'react'; +import { useState, useCallback, useEffect, FormEvent } from 'react'; import { Mail, CheckCircle, AlertCircle, X } from 'lucide-react'; import { useAuthStore } from '../../stores/authStore'; import styles from './MagicLinkFlow.module.css'; @@ -16,13 +16,28 @@ export interface MagicLinkFlowProps { type Step = 'email' | 'sent' | 'verifying' | 'success' | 'error'; -export function MagicLinkFlow({ onSuccess: _onSuccess, onCancel }: MagicLinkFlowProps) { - const { requestMagicLink, error: authError } = useAuthStore(); +export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) { + const { requestMagicLink, isAuthenticated, error: authError } = useAuthStore(); const [step, setStep] = useState('email'); const [email, setEmail] = useState(''); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + // Watch for auth success (deep link verified in background) + useEffect(() => { + if (isAuthenticated && step === 'sent') { + setStep('success'); + onSuccess(); + } + }, [isAuthenticated, step, onSuccess]); + + // Watch for verification errors from the deep link path + useEffect(() => { + if (authError && step === 'sent') { + setError(authError); + } + }, [authError, step]); + const handleSubmitEmail = useCallback( async (e: FormEvent) => { e.preventDefault(); @@ -100,10 +115,12 @@ export function MagicLinkFlow({ onSuccess: _onSuccess, onCancel }: MagicLinkFlow

Check your email

We sent a magic link to {email}. Click the link in the email to - sign in. + sign in. This window will update automatically.

+ {error &&

{error}

} +
- + + + + + + + + + + + + + {/* Plugin sidebar sections */} + + + {shouldShowPrompt && ( +
+ Sync your notes across devices +
+ + +
-
- )} + )} + setIsSyncModalOpen(true)} /> diff --git a/apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx b/apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx index 2999b78b..431cc1e0 100644 --- a/apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx +++ b/apps/desktop/src/renderer/components/sync/EnableSyncModal.tsx @@ -1,13 +1,19 @@ /** * Enable Sync Modal * - * Guides the user through enabling cloud sync with magic link auth. - * Shows value proposition → email input → waiting for link → success. + * Guides the user through enabling cloud sync with license-aware step routing. + * Computes smart initial step based on auth + license state: + * - Auth'd + pro/trial → success (already syncing) + * - Auth'd + free/expired → pricing (needs subscription) + * - Not auth'd + trial/pro → email (just needs to sign in) + * - Not auth'd + free/expired → value-prop (full flow) */ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { Cloud, Mail, CheckCircle, X, RefreshCw } from 'lucide-react'; +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { Cloud, Mail, CheckCircle, X, RefreshCw, Sparkles } from 'lucide-react'; import { useAuthStore } from '../../stores/authStore'; +import { useLicense } from '../../contexts/LicenseContext'; +import { getProductConfig } from '@readied/product-config'; import styles from './LoginModal.module.css'; interface EnableSyncModalProps { @@ -15,10 +21,23 @@ interface EnableSyncModalProps { onClose: () => void; } -type Step = 'value-prop' | 'email' | 'checking' | 'sent' | 'success'; +type Step = + | 'value-prop' + | 'pricing' + | 'waiting-payment' + | 'email' + | 'checking' + | 'sent' + | 'success'; const RESEND_COOLDOWN = 60; // seconds +const SYNC_CAPABLE_STATUSES = ['trial', 'pro_active', 'pro_grace']; + +function hasSyncCapability(status: string | undefined): boolean { + return status != null && SYNC_CAPABLE_STATUSES.includes(status); +} + export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) { const [email, setEmail] = useState(''); const [step, setStep] = useState('value-prop'); @@ -27,14 +46,51 @@ export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) { const [isResending, setIsResending] = useState(false); const timerRef = useRef | null>(null); - const { requestMagicLink, isAuthenticated } = useAuthStore(); + const { requestMagicLink, isAuthenticated, error: authError } = useAuthStore(); + const { state: licenseState, openSubscribe } = useLicense(); + const config = useMemo(() => getProductConfig(), []); + const proPricing = config.plans.pro.pricing!; + + const canSync = hasSyncCapability(licenseState?.status); + + // Compute smart initial step based on current auth + license state + // Note: checkout requires auth, so unauthenticated users always go through email first + const computeInitialStep = useCallback((): Step => { + if (isAuthenticated && canSync) return 'success'; + if (isAuthenticated && !canSync) return 'pricing'; + // Not authenticated — always need to sign in first (checkout requires auth) + return 'value-prop'; + }, [isAuthenticated, canSync]); // Watch for auth success (deep link verified in background) + // If user has sync capability → success. If not → they need to pay first. useEffect(() => { if (isAuthenticated && (step === 'sent' || step === 'checking')) { - setStep('success'); + if (canSync) { + setStep('success'); + } else { + setStep('pricing'); + } + } + }, [isAuthenticated, canSync, step]); + + // Watch for verification errors from the deep link path + useEffect(() => { + if (authError && step === 'sent') { + setError(authError); + } + }, [authError, step]); + + // Watch for license state changes while waiting for payment + useEffect(() => { + if (step === 'waiting-payment' && hasSyncCapability(licenseState?.status)) { + if (isAuthenticated) { + setStep('success'); + } else { + setStep('email'); + } } - }, [isAuthenticated, step]); + }, [licenseState?.status, step, isAuthenticated]); // Resend countdown timer useEffect(() => { @@ -54,21 +110,15 @@ export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) { } }, [resendTimer]); - // Reset state when modal closes + // Reset state when modal opens/closes useEffect(() => { - if (!isOpen) { - // Delay reset so close animation can play - const timeout = setTimeout(() => { - if (!isOpen) { - setStep('value-prop'); - setEmail(''); - setError(null); - setResendTimer(0); - } - }, 200); - return () => clearTimeout(timeout); + if (isOpen) { + setStep(computeInitialStep()); + setEmail(''); + setError(null); + setResendTimer(0); } - }, [isOpen]); + }, [isOpen, computeInitialStep]); const handleSubmitEmail = useCallback( async (e: React.FormEvent) => { @@ -103,10 +153,29 @@ export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) { } }, [email, resendTimer, isResending, requestMagicLink]); + const handleSelectPlan = useCallback( + async (plan: 'monthly' | 'annual') => { + setError(null); + setStep('waiting-payment'); + const result = await openSubscribe({ plan }); + if (!result.success) { + setError(result.error || 'Failed to open checkout'); + setStep('pricing'); + } + }, + [openSubscribe] + ); + const handleClose = useCallback(() => { onClose(); }, [onClose]); + // Where "Enable Sync" on value-prop should go + // Always go to email first — checkout requires authentication + const handleEnableSync = useCallback(() => { + setStep('email'); + }, []); + if (!isOpen) return null; return ( @@ -128,7 +197,7 @@ export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) {
- {/* Step 1: Value Proposition */} + {/* Step: Value Proposition */} {step === 'value-prop' && ( <>
@@ -147,20 +216,90 @@ export function EnableSyncModal({ isOpen, onClose }: EnableSyncModalProps) {
  • Works offline, syncs when connected
  • No account required to use Readied locally
  • - + + )} + + {/* Step: Pricing */} + {step === 'pricing' && ( + <> +
    + +
    +

    + Upgrade to Pro +

    +

    + {licenseState?.trial && !licenseState.trial.isExpired + ? config.trialDescription + : 'Get cloud sync and all Pro features'} +

    +
    + + +
    + {error &&

    {error}

    } + )} - {/* Step 2: Email Input */} + {/* Step: Waiting for Payment */} + {step === 'waiting-payment' && ( +
    +
    +

    Complete checkout in your browser...

    +

    This window will update automatically

    + +
    + )} + + {/* Step: Email Input */} {step === 'email' && ( <>
    -

    Enter your email

    -

    We'll send a magic link — no password needed.

    +

    Sign in or create account

    +

    + Enter your email and we'll send you a sign-in link. No password needed — if you're + new, your account is created automatically. +

    {error &&

    {error}

    }
    - ); -} diff --git a/packages/design-system/src/components/Card.tsx b/packages/design-system/src/components/Card.tsx deleted file mode 100644 index f8df8c11..00000000 --- a/packages/design-system/src/components/Card.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { type HTMLAttributes, type ReactNode } from 'react'; - -export interface CardProps extends HTMLAttributes { - variant?: 'default' | 'glass' | 'elevated'; - padding?: 'none' | 'sm' | 'md' | 'lg'; - children: ReactNode; -} - -export function Card({ - variant = 'default', - padding = 'md', - children, - className = '', - style, - ...props -}: CardProps) { - const baseStyles: React.CSSProperties = { - borderRadius: 'var(--radius-lg)', - overflow: 'hidden', - }; - - const paddingStyles: Record = { - none: { padding: 0 }, - sm: { padding: 'var(--space-4)' }, - md: { padding: 'var(--space-6)' }, - lg: { padding: 'var(--space-8)' }, - }; - - const variantStyles: Record = { - default: { - background: 'var(--bg-surface)', - border: '1px solid var(--border)', - }, - glass: { - background: 'var(--glass-bg)', - backdropFilter: 'var(--blur-md)', - WebkitBackdropFilter: 'var(--blur-md)', - border: '1px solid var(--glass-border)', - }, - elevated: { - background: 'var(--bg-elevated)', - border: '1px solid var(--border)', - boxShadow: 'var(--shadow-lg)', - }, - }; - - return ( -
    - {children} -
    - ); -} diff --git a/packages/design-system/src/components/Input.tsx b/packages/design-system/src/components/Input.tsx deleted file mode 100644 index f6a2ae61..00000000 --- a/packages/design-system/src/components/Input.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { type InputHTMLAttributes, forwardRef } from 'react'; - -export interface InputProps extends InputHTMLAttributes { - label?: string; - error?: string; -} - -export const Input = forwardRef( - ({ label, error, className = '', style, ...props }, ref) => { - const inputStyles: React.CSSProperties = { - width: '100%', - padding: 'var(--space-3) var(--space-4)', - fontSize: 'var(--text-base)', - color: 'var(--text-primary)', - background: 'var(--bg-inset)', - border: `1px solid ${error ? 'var(--danger)' : 'var(--border)'}`, - borderRadius: 'var(--radius-md)', - outline: 'none', - transition: 'border-color var(--duration-fast) var(--ease-out)', - ...style, - }; - - const labelStyles: React.CSSProperties = { - display: 'block', - marginBottom: 'var(--space-2)', - fontSize: 'var(--text-sm)', - fontWeight: 500, - color: 'var(--text-secondary)', - }; - - const errorStyles: React.CSSProperties = { - marginTop: 'var(--space-2)', - fontSize: 'var(--text-sm)', - color: 'var(--danger)', - }; - - return ( -
    - {label && } - - {error && {error}} -
    - ); - } -); - -Input.displayName = 'Input'; diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts deleted file mode 100644 index 58d1fc65..00000000 --- a/packages/design-system/src/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { Button, type ButtonProps } from './Button'; -export { Input, type InputProps } from './Input'; -export { Card, type CardProps } from './Card'; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts deleted file mode 100644 index bd3df3e7..00000000 --- a/packages/design-system/src/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Components -export * from './components'; - -// Token values as JS constants (for programmatic access) -export const tokens = { - colors: { - bgBase: '#0a0b0d', - bgSurface: '#111214', - bgElevated: '#18191c', - bgInset: '#0d0e10', - textPrimary: '#f4f4f5', - accent: '#5eead4', - accentStrong: '#2dd4bf', - danger: '#f87171', - warning: '#fbbf24', - success: '#34d399', - }, - spacing: { - 0: '0', - 1: '4px', - 2: '8px', - 3: '12px', - 4: '16px', - 5: '20px', - 6: '24px', - 8: '32px', - 10: '40px', - 12: '48px', - 16: '64px', - }, - radii: { - sm: '4px', - md: '6px', - lg: '8px', - xl: '12px', - full: '9999px', - }, - fonts: { - sans: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", - mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace", - }, -} as const; diff --git a/packages/design-system/src/tokens/reset.css b/packages/design-system/src/tokens/reset.css deleted file mode 100644 index aba33a7d..00000000 --- a/packages/design-system/src/tokens/reset.css +++ /dev/null @@ -1,82 +0,0 @@ -/* ============================================= - CSS RESET - Minimal reset for consistent cross-browser styling. - ============================================= */ - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; -} - -body { - font-family: var(--font-sans); - font-size: var(--text-base); - line-height: 1.5; - color: var(--text-primary); - background: var(--bg-base); -} - -a { - color: inherit; - text-decoration: none; -} - -button { - font: inherit; - color: inherit; - background: none; - border: none; - cursor: pointer; -} - -input, -textarea, -select { - font: inherit; - color: inherit; -} - -img, -svg { - display: block; - max-width: 100%; -} - -ul, -ol { - list-style: none; -} - -/* Focus visible for accessibility */ -:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} - -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--border-strong); - border-radius: var(--radius-full); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--text-muted); -} diff --git a/packages/design-system/src/tokens/tokens.css b/packages/design-system/src/tokens/tokens.css deleted file mode 100644 index 9ce8ddf8..00000000 --- a/packages/design-system/src/tokens/tokens.css +++ /dev/null @@ -1,119 +0,0 @@ -/* ============================================= - READIED DESIGN TOKENS - Shared across desktop, marketing, and docs. - ============================================= */ - -:root { - /* ===== SPACING SCALE ===== */ - --space-0: 0; - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - --space-12: 48px; - --space-16: 64px; - --space-20: 80px; - --space-24: 96px; - - /* ===== COLORS - Background ===== */ - --bg-base: #0a0b0d; - --bg-surface: #111214; - --bg-elevated: #18191c; - --bg-inset: #0d0e10; - - /* ===== COLORS - Border ===== */ - --border: rgba(255, 255, 255, 0.08); - --border-subtle: rgba(255, 255, 255, 0.04); - --border-strong: rgba(255, 255, 255, 0.12); - - /* ===== COLORS - Text ===== */ - --text-primary: #f4f4f5; - --text-secondary: rgba(255, 255, 255, 0.7); - --text-muted: rgba(255, 255, 255, 0.5); - --text-faint: rgba(255, 255, 255, 0.3); - - /* ===== COLORS - Accent (Teal) ===== */ - --accent: #5eead4; - --accent-muted: rgba(94, 234, 212, 0.15); - --accent-strong: #2dd4bf; - --accent-light: #99f6e4; - --accent-glow: rgba(94, 234, 212, 0.3); - - /* ===== COLORS - Semantic ===== */ - --danger: #f87171; - --danger-muted: rgba(248, 113, 113, 0.15); - --warning: #fbbf24; - --warning-muted: rgba(251, 191, 36, 0.15); - --success: #34d399; - --success-muted: rgba(52, 211, 153, 0.15); - - /* ===== TYPOGRAPHY - Fonts ===== */ - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'Helvetica Neue', Arial, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace; - - /* ===== TYPOGRAPHY - Sizes (Desktop) ===== */ - --text-xs: 11px; - --text-sm: 12px; - --text-base: 13px; - --text-lg: 14px; - --text-xl: 16px; - --text-2xl: 18px; - --text-3xl: 24px; - --text-4xl: 32px; - - /* ===== RADII ===== */ - --radius-sm: 4px; - --radius-md: 6px; - --radius-lg: 8px; - --radius-xl: 12px; - --radius-full: 9999px; - - /* ===== TRANSITIONS ===== */ - --duration-fast: 150ms; - --duration-normal: 200ms; - --duration-slow: 300ms; - --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); - - /* ===== GLASS MORPHISM ===== */ - --glass-bg: rgba(20, 22, 26, 0.85); - --glass-bg-dark: rgba(10, 11, 13, 0.6); - --glass-bg-accent: rgba(94, 234, 212, 0.08); - --glass-bg-elevated: rgba(30, 32, 36, 0.9); - --glass-border: rgba(255, 255, 255, 0.08); - --glass-border-strong: rgba(255, 255, 255, 0.12); - --glass-border-accent: rgba(94, 234, 212, 0.2); - --blur-sm: blur(8px); - --blur-md: blur(16px); - --blur-lg: blur(24px); - - /* ===== SHADOWS ===== */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); - --shadow-glow: 0 0 24px rgba(94, 234, 212, 0.2); - - /* ===== LAYOUT ===== */ - --nav-height: 60px; - --container-max: 1200px; - --content-max: 720px; -} - -/* ===== MARKETING SITE OVERRIDES ===== */ -@media (min-width: 768px) { - :root { - --text-xs: 12px; - --text-sm: 14px; - --text-base: 16px; - --text-lg: 18px; - --text-xl: 20px; - --text-2xl: 24px; - --text-3xl: 32px; - --text-4xl: 48px; - } -} diff --git a/packages/design-system/tsconfig.json b/packages/design-system/tsconfig.json deleted file mode 100644 index 365cfd0b..00000000 --- a/packages/design-system/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/product-config/src/facade.ts b/packages/product-config/src/facade.ts index 9a01f92a..6069542c 100644 --- a/packages/product-config/src/facade.ts +++ b/packages/product-config/src/facade.ts @@ -48,7 +48,7 @@ export interface ProductConfig { * @example * ```typescript * const config = getProductConfig(); - * console.log(config.plans.pro.pricing?.intervals.monthly.label); // '$2.99/mo' + * console.log(config.plans.pro.pricing?.intervals.monthly.label); // '€2/mo' * console.log(config.trialDays); // 14 * ``` */ @@ -85,10 +85,10 @@ export function getProductConfig(): ProductConfig { ], pricing: { intervals: { - monthly: { label: '$2.99/mo', amountCents: 299 }, - annual: { label: '$29/year', amountCents: 2900 }, + monthly: { label: '€2/mo', amountCents: 200 }, + annual: { label: '€20/year', amountCents: 2000 }, }, - annualSavings: '19%', + annualSavings: '17%', }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a83d684d..ff274669 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -391,24 +391,6 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@25.4.0)(lightningcss@1.31.1) - packages/design-system: - devDependencies: - '@types/react': - specifier: ^18.2.79 - version: 18.3.27 - '@types/react-dom': - specifier: ^18.2.25 - version: 18.3.7(@types/react@18.3.27) - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) - typescript: - specifier: ^5.7.2 - version: 5.9.3 - packages/embeds: dependencies: unist-util-visit: @@ -5186,7 +5168,6 @@ packages: libsql@0.4.7: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} - cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lie@3.3.0: