diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a4a195..9cb0102 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: @@ -39,8 +37,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: @@ -60,8 +56,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: @@ -81,8 +75,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: @@ -108,8 +100,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..caaffdd --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +pnpm-lock.yaml +bun.lock +storybook-static +*.log diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..554f2a3 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "printWidth": 100 +} diff --git a/CLAUDE.md b/CLAUDE.md index 6637022..0e33589 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,17 +44,18 @@ packages/ ### Component Organization Components in `packages/design-system/src/components/` follow atomic design: + - `atoms/` - Basic building blocks (Button, Input, Badge, etc.) - `molecules/` - Combined components (Card, Tabs, Dialog, etc.) - `organisms/` - Complex sections (Sidebar, NavigationMenu, AppShell, etc.) ### Key Files -| File | Purpose | -|------|---------| -| `packages/design-system/agents.md` | Component usage rules (MUST READ) | -| `packages/design-system/src/styles/globals.css` | CSS variables, theming, dark mode | -| `packages/design-system/lib/utils.ts` | `cn()` utility (clsx + tailwind-merge) | +| File | Purpose | +| ----------------------------------------------- | -------------------------------------- | +| `packages/design-system/agents.md` | Component usage rules (MUST READ) | +| `packages/design-system/src/styles/globals.css` | CSS variables, theming, dark mode | +| `packages/design-system/lib/utils.ts` | `cn()` utility (clsx + tailwind-merge) | ## Critical: Component Styling Rules @@ -105,7 +106,7 @@ const buttonVariants = cva('...', { variants: { variant: { // ... existing variants - warning: 'bg-yellow-500 text-white', // Add new variant + warning: 'bg-yellow-500 text-white', // Add new variant }, }, }); diff --git a/README.md b/README.md index c37e5b5..cc7f215 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,14 @@ This starts Storybook at http://localhost:6006 ## Commands -| Command | Description | -|---------|-------------| +| Command | Description | +| ---------------- | -------------------------- | | `pnpm storybook` | Start Storybook dev server | -| `pnpm build` | Build all packages | -| `pnpm typecheck` | Type check all packages | -| `pnpm lint` | Lint all packages | -| `pnpm test` | Run tests | -| `pnpm format` | Format code with Prettier | +| `pnpm build` | Build all packages | +| `pnpm typecheck` | Type check all packages | +| `pnpm lint` | Lint all packages | +| `pnpm test` | Run tests | +| `pnpm format` | Format code with Prettier | ## Building diff --git a/apps/example/eslint.config.mjs b/apps/example/eslint.config.mjs index c85fb67..5b67bd0 100644 --- a/apps/example/eslint.config.mjs +++ b/apps/example/eslint.config.mjs @@ -1,6 +1,6 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -9,8 +9,6 @@ const compat = new FlatCompat({ baseDirectory: __dirname, }); -const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), -]; +const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')]; export default eslintConfig; diff --git a/apps/example/next.config.ts b/apps/example/next.config.ts index e9ffa30..5e891cf 100644 --- a/apps/example/next.config.ts +++ b/apps/example/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ diff --git a/apps/example/postcss.config.mjs b/apps/example/postcss.config.mjs index 61e3684..297374d 100644 --- a/apps/example/postcss.config.mjs +++ b/apps/example/postcss.config.mjs @@ -1,6 +1,6 @@ const config = { plugins: { - "@tailwindcss/postcss": {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/apps/example/src/app/(app)/controls/page.tsx b/apps/example/src/app/(app)/controls/page.tsx index 7349523..0e84f42 100644 --- a/apps/example/src/app/(app)/controls/page.tsx +++ b/apps/example/src/app/(app)/controls/page.tsx @@ -90,7 +90,7 @@ export default function ControlsPage() { return allControlCategories.filter( (category) => category.name.toLowerCase().includes(query) || - category.description.toLowerCase().includes(query) + category.description.toLowerCase().includes(query), ); }, [searchQuery]); @@ -107,8 +107,12 @@ export default function ControlsPage() { - {passedControls} of {totalControls} controls passing - {totalControls > 0 ? Math.round((passedControls / totalControls) * 100) : 0}% complete + + {passedControls} of {totalControls} controls passing + + + {totalControls > 0 ? Math.round((passedControls / totalControls) * 100) : 0}% complete + diff --git a/apps/example/src/app/(app)/cybersecurity/page.tsx b/apps/example/src/app/(app)/cybersecurity/page.tsx index f36345a..dcd9e66 100644 --- a/apps/example/src/app/(app)/cybersecurity/page.tsx +++ b/apps/example/src/app/(app)/cybersecurity/page.tsx @@ -48,9 +48,21 @@ const securityMetrics = [ const recentThreats = [ { id: 1, type: 'Phishing Attempt', severity: 'high', time: '2 hours ago', status: 'blocked' }, - { id: 2, type: 'Suspicious Login', severity: 'medium', time: '5 hours ago', status: 'investigating' }, + { + id: 2, + type: 'Suspicious Login', + severity: 'medium', + time: '5 hours ago', + status: 'investigating', + }, { id: 3, type: 'Malware Detection', severity: 'high', time: '1 day ago', status: 'resolved' }, - { id: 4, type: 'Data Exfiltration Attempt', severity: 'critical', time: '2 days ago', status: 'blocked' }, + { + id: 4, + type: 'Data Exfiltration Attempt', + severity: 'critical', + time: '2 days ago', + status: 'blocked', + }, ]; export default function CybersecurityPage() { @@ -63,16 +75,26 @@ export default function CybersecurityPage() { - {metric.title} + + {metric.title} + - {metric.value} + + {metric.value} + {metric.suffix && {metric.suffix}} - {metric.status === 'good' && } + {metric.status === 'good' && ( + + )} {metric.status === 'warning' && } - {metric.status === 'critical' && } - {metric.change} from last week + {metric.status === 'critical' && ( + + )} + + {metric.change} from last week + @@ -90,25 +112,40 @@ export default function CybersecurityPage() { {recentThreats.map((threat) => ( -
+
-
+
- {threat.type} - {threat.time} + + {threat.type} + + + {threat.time} + - + {threat.status}
@@ -126,28 +163,36 @@ export default function CybersecurityPage() { Network Security - 92% + + 92% + Endpoint Protection - 88% + + 88% + Identity & Access - 95% + + 95% + Data Protection - 78% + + 78% + diff --git a/apps/example/src/app/(app)/design/loading/page.tsx b/apps/example/src/app/(app)/design/loading/page.tsx index 37d315a..a443a23 100644 --- a/apps/example/src/app/(app)/design/loading/page.tsx +++ b/apps/example/src/app/(app)/design/loading/page.tsx @@ -27,10 +27,7 @@ export default function LoadingStatePage() { This page demonstrates the loading state of PageLayout - @@ -38,8 +35,9 @@ export default function LoadingStatePage() { - The PageLayout component supports a loading prop - that displays skeleton placeholders while content is being fetched. + The PageLayout component supports a{' '} + loading prop that displays + skeleton placeholders while content is being fetched.
@@ -49,7 +47,9 @@ export default function LoadingStatePage() { View your metrics - Quick access to your dashboard analytics and insights. + + Quick access to your dashboard analytics and insights. + @@ -59,7 +59,9 @@ export default function LoadingStatePage() { Generate reports - Create and export detailed compliance reports. + + Create and export detailed compliance reports. + @@ -69,7 +71,9 @@ export default function LoadingStatePage() { Configure options - Customize your workspace and preferences. + + Customize your workspace and preferences. +
diff --git a/apps/example/src/app/(app)/design/page.tsx b/apps/example/src/app/(app)/design/page.tsx index 0d87083..1cf7276 100644 --- a/apps/example/src/app/(app)/design/page.tsx +++ b/apps/example/src/app/(app)/design/page.tsx @@ -97,7 +97,7 @@ export default function DesignPage() { {/* Buttons */}

Buttons

- +

Variants

@@ -125,9 +125,15 @@ export default function DesignPage() {

With Icons

- - - + + +
@@ -160,7 +166,7 @@ export default function DesignPage() { {/* Form Controls */}

Form Controls

- +
@@ -234,7 +240,9 @@ export default function DesignPage() {
- +
diff --git a/apps/example/src/app/(app)/integrations/page.tsx b/apps/example/src/app/(app)/integrations/page.tsx index f5d65fc..0cdcee9 100644 --- a/apps/example/src/app/(app)/integrations/page.tsx +++ b/apps/example/src/app/(app)/integrations/page.tsx @@ -4,11 +4,5 @@ import { PageHeader, PageLayout } from '@trycompai/design-system'; export default function IntegrationsLoadingPage() { // Permanently loading state to debug PageLayout skeleton - return ( - } - /> - ); + return } />; } diff --git a/apps/example/src/app/(app)/page.tsx b/apps/example/src/app/(app)/page.tsx index da746ca..16b9345 100644 --- a/apps/example/src/app/(app)/page.tsx +++ b/apps/example/src/app/(app)/page.tsx @@ -185,7 +185,11 @@ export default function OverviewPage() { [], ); const completedTasks = React.useMemo( - () => (Object.keys(STAGE_TASKS) as Soc2StageValue[]).reduce((acc, key) => acc + (taskState[key]?.filter(Boolean).length ?? 0), 0), + () => + (Object.keys(STAGE_TASKS) as Soc2StageValue[]).reduce( + (acc, key) => acc + (taskState[key]?.filter(Boolean).length ?? 0), + 0, + ), [taskState], ); const roadmapPercent = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; @@ -205,7 +209,10 @@ export default function OverviewPage() { const stageMeta = SOC2_STAGES.find((s) => s.value === stage) ?? SOC2_STAGES[0]; const tasks = STAGE_TASKS[stage]; - const stageIndex = Math.max(0, SOC2_STAGES.findIndex((s) => s.value === stage)); + const stageIndex = Math.max( + 0, + SOC2_STAGES.findIndex((s) => s.value === stage), + ); const blockingStage = SOC2_STAGES.slice(0, stageIndex).find((s) => !completedStages[s.value]); const isLocked = Boolean(blockingStage); @@ -236,128 +243,128 @@ export default function OverviewPage() { {/* Roadmap Summary + Phases */} - - - - - - - - SOC 2 Type II roadmap - - On track - Est. {formatMinutes(remainingMinutes)} remaining + + + + + + + + SOC 2 Type II roadmap + + On track + + Est. {formatMinutes(remainingMinutes)} remaining + + + + Target audit window: Nov 12–Nov 26, 2025 + + + - - Target audit window: Nov 12–Nov 26, 2025 - + + + + + Roadmap completed + + + {roadmapPercent}% + + + + + {completedTasks} of {totalTasks} tasks + + - - + + - - - - Roadmap completed - - - {roadmapPercent}% - - - - - {completedTasks} of {totalTasks} tasks - - + + - - - - - - - - {isLocked && ( - - )} + + {isLocked && ( + + )} -
- - {tasks.map((task, idx) => { - const checked = taskState[stage]?.[idx] ?? false; - const actionIcon = - task.actionKind === 'video' ? ( - - ) : task.actionKind === 'link' ? ( - - ) : ( - - ); - - return ( - - - - - {checked ? : idx + 1} - - - - - - - {task.title} - {task.estimate} - - - {task.description} - - - {checked ? ( - Done +
+ + {tasks.map((task, idx) => { + const checked = taskState[stage]?.[idx] ?? false; + const actionIcon = + task.actionKind === 'video' ? ( + + ) : task.actionKind === 'link' ? ( + ) : ( - - )} - - - ); - })} + + ); + + return ( + + + + + {checked ? : idx + 1} + + + + + + + {task.title} + {task.estimate} + + + {task.description} + + + {checked ? ( + Done + ) : ( + + )} + + + ); + })} + +
-
-
diff --git a/apps/example/src/app/(app)/policies/[id]/page.tsx b/apps/example/src/app/(app)/policies/[id]/page.tsx index c7e7adf..f19d433 100644 --- a/apps/example/src/app/(app)/policies/[id]/page.tsx +++ b/apps/example/src/app/(app)/policies/[id]/page.tsx @@ -26,22 +26,26 @@ import Link from 'next/link'; import { use } from 'react'; // Mock data - in real app would fetch based on id -const policiesData: Record = { +const policiesData: Record< + string, + { + id: number; + name: string; + status: string; + owner: string; + lastUpdated: string; + description: string; + controls: { id: number; name: string; status: string }[]; + } +> = { '1': { id: 1, name: 'Access Control Policy', status: 'approved', owner: 'Sarah Chen', lastUpdated: 'Jan 15, 2024', - description: 'This policy defines the requirements for controlling access to organizational information systems and data.', + description: + 'This policy defines the requirements for controlling access to organizational information systems and data.', controls: [ { id: 1, name: 'User Access Management', status: 'compliant' }, { id: 2, name: 'Password Requirements', status: 'compliant' }, @@ -55,7 +59,8 @@ const policiesData: Record - + @@ -41,16 +40,15 @@ export default function SettingsGeneralPage() {
- Only lowercase letters, numbers, and hyphens allowed. + + Only lowercase letters, numbers, and hyphens allowed. +
-
+
@@ -62,10 +60,7 @@ export default function SettingsGeneralPage() {
-
+
Delete Organization diff --git a/apps/example/src/app/(app)/settings/security/page.tsx b/apps/example/src/app/(app)/settings/security/page.tsx index af37e2f..89a6443 100644 --- a/apps/example/src/app/(app)/settings/security/page.tsx +++ b/apps/example/src/app/(app)/settings/security/page.tsx @@ -20,10 +20,7 @@ export default function SettingsSecurityPage() { -
+
diff --git a/apps/example/src/app/(app)/team/page.tsx b/apps/example/src/app/(app)/team/page.tsx index f6d86b5..63ebcb8 100644 --- a/apps/example/src/app/(app)/team/page.tsx +++ b/apps/example/src/app/(app)/team/page.tsx @@ -122,9 +122,7 @@ export default function TeamPage() {
{member.name} - - {member.role} - + {member.role}
diff --git a/apps/example/src/app/(app)/vendors/[vendorId]/page.tsx b/apps/example/src/app/(app)/vendors/[vendorId]/page.tsx index 070460b..388e0ab 100644 --- a/apps/example/src/app/(app)/vendors/[vendorId]/page.tsx +++ b/apps/example/src/app/(app)/vendors/[vendorId]/page.tsx @@ -63,71 +63,70 @@ export default function VendorDetailPage() { - That vendor doesn't exist (or the URL is wrong). + That vendor doesn't exist (or the URL is wrong). ) : ( <> - - -
- - - - - {vendor.name.slice(0, 2).toUpperCase()} - - - - - - {vendor.name} - - ID: {vendor.id} - - - - {getRiskBadge(vendor.riskLevel)} - {getStatusBadge(vendor.status)} - - Last assessed: {vendor.lastAssessment} - - - - - -
- - - Category - - - - {vendor.category} - - - - - - - Risk - - {getRiskBadge(vendor.riskLevel)} - - - - - Status - - {getStatusBadge(vendor.status)} - -
-
-
-
- - + +
+ + + + + {vendor.name.slice(0, 2).toUpperCase()} + + + + + + {vendor.name} + + ID: {vendor.id} + + + + {getRiskBadge(vendor.riskLevel)} + {getStatusBadge(vendor.status)} + + Last assessed: {vendor.lastAssessment} + + + + + +
+ + + Category + + + + {vendor.category} + + + + + + + Risk + + {getRiskBadge(vendor.riskLevel)} + + + + + Status + + {getStatusBadge(vendor.status)} + +
+
+
+
+ + Assessments @@ -138,9 +137,9 @@ export default function VendorDetailPage() { - +
- + Documents @@ -151,11 +150,10 @@ export default function VendorDetailPage() { - + )} ); } - diff --git a/apps/example/src/app/(app)/vendors/page.tsx b/apps/example/src/app/(app)/vendors/page.tsx index e42e0d6..f8abab2 100644 --- a/apps/example/src/app/(app)/vendors/page.tsx +++ b/apps/example/src/app/(app)/vendors/page.tsx @@ -45,8 +45,7 @@ export default function VendorsPage() { const query = searchQuery.toLowerCase(); return allVendors.filter( (vendor) => - vendor.name.toLowerCase().includes(query) || - vendor.category.toLowerCase().includes(query) + vendor.name.toLowerCase().includes(query) || vendor.category.toLowerCase().includes(query), ); }, [searchQuery]); @@ -92,87 +91,87 @@ export default function VendorsPage() { - - - Vendor - Risk - Status - Last assessment - -
- Actions -
-
-
-
- - {vendors.map((vendor) => ( - goToVendor(vendor.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - goToVendor(vendor.id); - } - }} - > - - - - - {vendor.name.slice(0, 2).toUpperCase()} - - - - {vendor.name} - - - {vendor.category} - - - - - {getRiskBadge(vendor.riskLevel)} - {getStatusBadge(vendor.status)} - - - {vendor.lastAssessment} - - - -
- - - - + + + Vendor + Risk + Status + Last assessment + +
+ Actions
- +
- ))} - -
+ + + {vendors.map((vendor) => ( + goToVendor(vendor.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + goToVendor(vendor.id); + } + }} + > + + + + + {vendor.name.slice(0, 2).toUpperCase()} + + + + {vendor.name} + + + {vendor.category} + + + + + {getRiskBadge(vendor.riskLevel)} + {getStatusBadge(vendor.status)} + + + {vendor.lastAssessment} + + + +
+ + + + +
+
+
+ ))} +
+ ); diff --git a/apps/example/src/app/(app)/vendors/vendors.data.ts b/apps/example/src/app/(app)/vendors/vendors.data.ts index 3646485..86a1f3a 100644 --- a/apps/example/src/app/(app)/vendors/vendors.data.ts +++ b/apps/example/src/app/(app)/vendors/vendors.data.ts @@ -74,4 +74,3 @@ export const vendors: Vendor[] = [ export function getVendorById(id: number): Vendor | undefined { return vendors.find((v) => v.id === id); } - diff --git a/apps/example/src/app/(app)/vendors/vendors.ui.tsx b/apps/example/src/app/(app)/vendors/vendors.ui.tsx index e8658c1..8e67140 100644 --- a/apps/example/src/app/(app)/vendors/vendors.ui.tsx +++ b/apps/example/src/app/(app)/vendors/vendors.ui.tsx @@ -43,4 +43,3 @@ export function getStatusBadge(status: string) { ); } } - diff --git a/apps/example/src/components/shell/app-shell-client.tsx b/apps/example/src/components/shell/app-shell-client.tsx index 6b70480..14f766c 100644 --- a/apps/example/src/components/shell/app-shell-client.tsx +++ b/apps/example/src/components/shell/app-shell-client.tsx @@ -41,13 +41,7 @@ import { Text, ThemeToggle, } from '@trycompai/design-system'; -import { - Add, - Logout, - Notification, - Settings, - User, -} from '@carbon/icons-react'; +import { Add, Logout, Notification, Settings, User } from '@carbon/icons-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import * as React from 'react'; @@ -158,10 +152,10 @@ function NotificationsPopover() {
- Notifications - {unreadCount > 0 && ( - {unreadCount} new - )} + + Notifications + + {unreadCount > 0 && {unreadCount} new}
{notifications.map((notification) => ( @@ -173,15 +167,21 @@ function NotificationsPopover() { >
- {notification.title} + + {notification.title} + {notification.unread && (
)}
- {notification.description} + + {notification.description} +
- {notification.time} + + {notification.time} +
))} @@ -234,8 +234,12 @@ function UserMenu() {
- John Doe - john@example.com + + John Doe + + + john@example.com +
@@ -322,18 +326,15 @@ function SidebarNavItemWithChildren({ item, pathname }: SidebarNavItemWithChildr // If item has children, render collapsible if (item.children && item.children.length > 0) { return ( - + {item.children.map((child) => { - const childActive = child.href === '/' ? pathname === '/' : pathname === child.href || pathname.startsWith(child.href + '/'); + const childActive = + child.href === '/' + ? pathname === '/' + : pathname === child.href || pathname.startsWith(child.href + '/'); return ( - - {child.label} - + {child.label} ); })} @@ -401,16 +402,10 @@ export function AppShellClient({ children }: AppShellClientProps) { - + {sidebarConfig.items.map((item) => ( - + ))} {sidebarConfig.footer && ( diff --git a/apps/example/src/components/shell/nav-config.tsx b/apps/example/src/components/shell/nav-config.tsx index 1ef3f9f..be7e252 100644 --- a/apps/example/src/components/shell/nav-config.tsx +++ b/apps/example/src/components/shell/nav-config.tsx @@ -126,9 +126,7 @@ export const sidebarConfigs: Record = { href: '/integrations', }, ], - footer: [ - { id: 'settings', label: 'Settings', href: '/settings', icon: }, - ], + footer: [{ id: 'settings', label: 'Settings', href: '/settings', icon: }], }, cybersecurity: { title: 'Cybersecurity', @@ -138,9 +136,7 @@ export const sidebarConfigs: Record = { { id: 'vulnerabilities', label: 'Vulnerabilities', href: '#', icon: }, { id: 'assets', label: 'Assets', href: '#', icon: }, ], - footer: [ - { id: 'help', label: 'Help', href: '#', icon: }, - ], + footer: [{ id: 'help', label: 'Help', href: '#', icon: }], }, analytics: { title: 'Analytics', @@ -150,9 +146,7 @@ export const sidebarConfigs: Record = { { id: 'compliance-reports', label: 'Compliance Reports', href: '#', icon: }, { id: 'audit-logs', label: 'Audit Logs', href: '#', icon: }, ], - footer: [ - { id: 'help', label: 'Help', href: '#', icon: }, - ], + footer: [{ id: 'help', label: 'Help', href: '#', icon: }], }, settings: { title: 'Settings', @@ -184,18 +178,16 @@ export const sidebarConfigs: Record = { icon: , }, ], - footer: [ - { id: 'help', label: 'Help & Support', href: '#', icon: }, - ], + footer: [{ id: 'help', label: 'Help & Support', href: '#', icon: }], }, }; // Helper to get active rail item from pathname export function getActiveRailItem(pathname: string): string { for (const item of railItems) { - if (item.activePaths.some(path => - path === '/' ? pathname === '/' : pathname.startsWith(path) - )) { + if ( + item.activePaths.some((path) => (path === '/' ? pathname === '/' : pathname.startsWith(path))) + ) { return item.id; } } @@ -215,7 +207,7 @@ export function isNavItemActive(item: NavItemWithChildren, pathname: string): bo return pathname === item.href || pathname.startsWith(item.href + '/'); } if (item.children) { - return item.children.some(child => { + return item.children.some((child) => { if (child.href === '/') return pathname === '/'; return pathname === child.href || pathname.startsWith(child.href + '/'); }); diff --git a/apps/example/src/components/soc2-timeline.tsx b/apps/example/src/components/soc2-timeline.tsx index 9a6177b..0a6ce83 100644 --- a/apps/example/src/components/soc2-timeline.tsx +++ b/apps/example/src/components/soc2-timeline.tsx @@ -1,12 +1,6 @@ 'use client'; -import { - Tabs, - TabsList, - TabsContent, - TabsTrigger, - Text, -} from '@trycompai/design-system'; +import { Tabs, TabsList, TabsContent, TabsTrigger, Text } from '@trycompai/design-system'; export type Soc2StageValue = 'trust' | 'team' | 'evidence' | 'audit_ready'; @@ -30,7 +24,8 @@ export const SOC2_STAGES: Soc2Stage[] = [ { value: 'evidence', title: 'Evidence collection', - summary: 'Start evidence collection and automate where possible to stay continuously compliant.', + summary: + 'Start evidence collection and automate where possible to stay continuously compliant.', }, { value: 'audit_ready', @@ -80,4 +75,3 @@ export function Soc2TimelineControlled({ ); } - diff --git a/apps/example/tsconfig.json b/apps/example/tsconfig.json index 759e6d7..cf9c65d 100644 --- a/apps/example/tsconfig.json +++ b/apps/example/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": [ @@ -36,7 +30,5 @@ ".next/dev/types/**/*.ts", "**/*.mts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 5fbbb74..b077b22 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -37,33 +37,33 @@ DS_REPO_ROOT=/path/to/design-system npx @trycompai/design-system-mcp ### Component Documentation (Recommended for Agents) -| Tool | Description | -|------|-------------| -| `get_component_docs` | **RECOMMENDED** - Get comprehensive docs for a component including props, variants, and usage examples | -| `list_components` | List all component source files (atoms/molecules/organisms) | -| `get_component_source` | Fetch raw component source by id (e.g., `molecules/card`) | -| `search` | Search component ids and optionally source content | +| Tool | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | +| `get_component_docs` | **RECOMMENDED** - Get comprehensive docs for a component including props, variants, and usage examples | +| `list_components` | List all component source files (atoms/molecules/organisms) | +| `get_component_source` | Fetch raw component source by id (e.g., `molecules/card`) | +| `search` | Search component ids and optionally source content | ### Usage Guidelines -| Tool | Description | -|------|-------------| +| Tool | Description | +| ---------------------- | ------------------------------------------------------------------- | | `get_usage_guidelines` | Get usage rules - **IMPORTANT: Components do NOT accept className** | -| `installation` | Get framework-specific installation instructions | +| `installation` | Get framework-specific installation instructions | ### Design Tokens -| Tool | Description | -|------|-------------| +| Tool | Description | +| ----------- | ------------------------------------------------------- | | `get_theme` | Get CSS variables and design tokens for light/dark mode | ### Storybook Stories -| Tool | Description | -|------|-------------| -| `list_stories` | List all Storybook story files | -| `get_story_source` | Fetch story source by name (e.g., `Card`) | -| `suggest_story_for_component` | Best-guess story name for a component id | +| Tool | Description | +| ----------------------------- | ----------------------------------------- | +| `list_stories` | List all Storybook story files | +| `get_story_source` | Fetch story source by name (e.g., `Card`) | +| `suggest_story_for_component` | Best-guess story name for a component id | ## Critical Usage Note diff --git a/apps/mcp/src/design-system-index.ts b/apps/mcp/src/design-system-index.ts index 2bd266d..3b96372 100644 --- a/apps/mcp/src/design-system-index.ts +++ b/apps/mcp/src/design-system-index.ts @@ -1,7 +1,7 @@ -import path from "node:path"; +import path from 'node:path'; -import { pathExists, walkFiles } from "./fs-utils.js"; -import type { RepoPaths } from "./paths.js"; +import { pathExists, walkFiles } from './fs-utils.js'; +import type { RepoPaths } from './paths.js'; export type ComponentEntry = { /** e.g. "atoms/button" */ @@ -14,24 +14,22 @@ export type ComponentEntry = { filePath: string; }; -const CATEGORY_DIRS = ["atoms", "molecules", "organisms"] as const; +const CATEGORY_DIRS = ['atoms', 'molecules', 'organisms'] as const; -export async function listDesignSystemComponents( - repoPaths: RepoPaths, -): Promise { +export async function listDesignSystemComponents(repoPaths: RepoPaths): Promise { const baseDir = repoPaths.designSystemSrcComponentsDir; if (!(await pathExists(baseDir))) return []; const files = await walkFiles(baseDir, { - includeExtensions: [".ts", ".tsx"], + includeExtensions: ['.ts', '.tsx'], ignore: (relPath) => { const parts = relPath.split(path.sep); // ignore re-export barrels - if (parts.at(-1) === "index.ts") return true; + if (parts.at(-1) === 'index.ts') return true; // ignore ui/index.ts (handled via atoms/molecules/organisms) - if (parts[0] === "ui") return true; + if (parts[0] === 'ui') return true; return false; }, @@ -43,7 +41,7 @@ export async function listDesignSystemComponents( const category = relParts[0]; if (!category || !CATEGORY_DIRS.includes(category as any)) continue; - const stem = path.basename(file.relPath).replace(/\.(ts|tsx)$/, ""); + const stem = path.basename(file.relPath).replace(/\.(ts|tsx)$/, ''); out.push({ id: `${category}/${stem}`, category, @@ -59,6 +57,5 @@ export async function listDesignSystemComponents( export function bestGuessStoryNameFromComponentFileStem(stem: string) { // kebab-case / snake_case -> PascalCase-ish const parts = stem.split(/[-_]/g).filter(Boolean); - return parts.map((p) => p[0]?.toUpperCase() + p.slice(1)).join(""); + return parts.map((p) => p[0]?.toUpperCase() + p.slice(1)).join(''); } - diff --git a/apps/mcp/src/fs-utils.ts b/apps/mcp/src/fs-utils.ts index 4ab260a..d6d3640 100644 --- a/apps/mcp/src/fs-utils.ts +++ b/apps/mcp/src/fs-utils.ts @@ -1,5 +1,5 @@ -import fs from "node:fs/promises"; -import path from "node:path"; +import fs from 'node:fs/promises'; +import path from 'node:path'; export async function pathExists(p: string) { try { @@ -11,7 +11,7 @@ export async function pathExists(p: string) { } export async function readTextFile(filePath: string) { - return await fs.readFile(filePath, "utf8"); + return await fs.readFile(filePath, 'utf8'); } export type WalkEntry = { @@ -55,4 +55,3 @@ export async function walkFiles( await walk(baseDir); return out; } - diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index bb09d06..7bc7f8c 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -1,23 +1,20 @@ #!/usr/bin/env node -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; -import { readTextFile } from "./fs-utils.js"; +import { readTextFile } from './fs-utils.js'; import { bestGuessStoryNameFromComponentFileStem, listDesignSystemComponents, -} from "./design-system-index.js"; -import { listStorybookStories, findStoryByName } from "./storybook-index.js"; -import { getRepoPaths } from "./paths.js"; +} from './design-system-index.js'; +import { listStorybookStories, findStoryByName } from './storybook-index.js'; +import { getRepoPaths } from './paths.js'; // ============================================================================ // Helper Functions @@ -29,79 +26,88 @@ import { getRepoPaths } from "./paths.js"; */ const COMPONENT_TAGS: Record = { // Atoms - "atoms/button": ["action", "submit", "click", "cta", "primary", "secondary", "destructive", "link"], - "atoms/input": ["text", "form", "field", "type", "entry", "textbox"], - "atoms/textarea": ["multiline", "text", "form", "field", "long text", "description"], - "atoms/checkbox": ["toggle", "boolean", "form", "select", "check", "tick"], - "atoms/switch": ["toggle", "boolean", "on off", "setting", "preference"], - "atoms/label": ["form", "field", "text", "name"], - "atoms/badge": ["tag", "status", "indicator", "pill", "chip", "count"], - "atoms/avatar": ["user", "profile", "image", "photo", "initials", "person"], - "atoms/progress": ["loading", "bar", "percentage", "completion", "status"], - "atoms/spinner": ["loading", "wait", "busy", "activity"], - "atoms/skeleton": ["loading", "placeholder", "shimmer"], - "atoms/separator": ["divider", "line", "hr", "horizontal rule"], - "atoms/heading": ["title", "h1", "h2", "h3", "h4", "header", "typography"], - "atoms/text": ["paragraph", "body", "content", "typography", "p"], - "atoms/kbd": ["keyboard", "shortcut", "key", "hotkey"], - "atoms/slider": ["range", "value", "number", "input"], - "atoms/toggle": ["switch", "button", "on off", "state"], - "atoms/container": ["wrapper", "layout", "max-width", "center"], - "atoms/stack": ["layout", "flex", "vertical", "horizontal", "spacing", "gap"], - "atoms/logo": ["brand", "identity", "image"], - "atoms/aspect-ratio": ["image", "video", "ratio", "responsive"], + 'atoms/button': [ + 'action', + 'submit', + 'click', + 'cta', + 'primary', + 'secondary', + 'destructive', + 'link', + ], + 'atoms/input': ['text', 'form', 'field', 'type', 'entry', 'textbox'], + 'atoms/textarea': ['multiline', 'text', 'form', 'field', 'long text', 'description'], + 'atoms/checkbox': ['toggle', 'boolean', 'form', 'select', 'check', 'tick'], + 'atoms/switch': ['toggle', 'boolean', 'on off', 'setting', 'preference'], + 'atoms/label': ['form', 'field', 'text', 'name'], + 'atoms/badge': ['tag', 'status', 'indicator', 'pill', 'chip', 'count'], + 'atoms/avatar': ['user', 'profile', 'image', 'photo', 'initials', 'person'], + 'atoms/progress': ['loading', 'bar', 'percentage', 'completion', 'status'], + 'atoms/spinner': ['loading', 'wait', 'busy', 'activity'], + 'atoms/skeleton': ['loading', 'placeholder', 'shimmer'], + 'atoms/separator': ['divider', 'line', 'hr', 'horizontal rule'], + 'atoms/heading': ['title', 'h1', 'h2', 'h3', 'h4', 'header', 'typography'], + 'atoms/text': ['paragraph', 'body', 'content', 'typography', 'p'], + 'atoms/kbd': ['keyboard', 'shortcut', 'key', 'hotkey'], + 'atoms/slider': ['range', 'value', 'number', 'input'], + 'atoms/toggle': ['switch', 'button', 'on off', 'state'], + 'atoms/container': ['wrapper', 'layout', 'max-width', 'center'], + 'atoms/stack': ['layout', 'flex', 'vertical', 'horizontal', 'spacing', 'gap'], + 'atoms/logo': ['brand', 'identity', 'image'], + 'atoms/aspect-ratio': ['image', 'video', 'ratio', 'responsive'], // Molecules - "molecules/card": ["container", "box", "panel", "content", "wrapper"], - "molecules/tabs": ["navigation", "switch", "panels", "sections"], - "molecules/accordion": ["expand", "collapse", "faq", "disclosure"], - "molecules/alert": ["message", "notification", "warning", "error", "info", "success"], - "molecules/popover": ["tooltip", "overlay", "dropdown", "floating"], - "molecules/tooltip": ["hint", "help", "info", "hover"], - "molecules/select": ["dropdown", "picker", "choice", "option", "form"], - "molecules/field": ["form", "input", "label", "error", "help text"], - "molecules/table": ["data", "grid", "list", "rows", "columns"], - "molecules/pagination": ["pages", "navigation", "next", "previous"], - "molecules/breadcrumb": ["navigation", "path", "location", "crumbs"], - "molecules/radio-group": ["choice", "option", "form", "select one"], - "molecules/toggle-group": ["buttons", "options", "select", "multiple"], - "molecules/button-group": ["actions", "toolbar", "buttons"], - "molecules/input-group": ["form", "addon", "prefix", "suffix"], - "molecules/scroll-area": ["overflow", "scrollbar", "container"], - "molecules/collapsible": ["expand", "collapse", "toggle", "disclosure"], - "molecules/hover-card": ["preview", "popup", "info", "details"], - "molecules/page-header": ["title", "description", "actions", "header"], - "molecules/section": ["group", "container", "panel"], - "molecules/settings": ["preferences", "config", "options"], - "molecules/theme-switcher": ["dark mode", "light mode", "theme", "toggle"], - "molecules/command-search": ["search", "spotlight", "quick", "cmd k"], - "molecules/input-otp": ["code", "verification", "2fa", "pin"], - "molecules/grid": ["layout", "columns", "responsive"], - "molecules/empty": ["no data", "placeholder", "zero state"], - "molecules/item": ["list item", "row", "entry"], - "molecules/resizable": ["resize", "split", "panels"], - "molecules/ai-chat": ["chatbot", "assistant", "ai", "conversation"], - "molecules/data-table-header": ["table", "filter", "search", "actions"], + 'molecules/card': ['container', 'box', 'panel', 'content', 'wrapper'], + 'molecules/tabs': ['navigation', 'switch', 'panels', 'sections'], + 'molecules/accordion': ['expand', 'collapse', 'faq', 'disclosure'], + 'molecules/alert': ['message', 'notification', 'warning', 'error', 'info', 'success'], + 'molecules/popover': ['tooltip', 'overlay', 'dropdown', 'floating'], + 'molecules/tooltip': ['hint', 'help', 'info', 'hover'], + 'molecules/select': ['dropdown', 'picker', 'choice', 'option', 'form'], + 'molecules/field': ['form', 'input', 'label', 'error', 'help text'], + 'molecules/table': ['data', 'grid', 'list', 'rows', 'columns'], + 'molecules/pagination': ['pages', 'navigation', 'next', 'previous'], + 'molecules/breadcrumb': ['navigation', 'path', 'location', 'crumbs'], + 'molecules/radio-group': ['choice', 'option', 'form', 'select one'], + 'molecules/toggle-group': ['buttons', 'options', 'select', 'multiple'], + 'molecules/button-group': ['actions', 'toolbar', 'buttons'], + 'molecules/input-group': ['form', 'addon', 'prefix', 'suffix'], + 'molecules/scroll-area': ['overflow', 'scrollbar', 'container'], + 'molecules/collapsible': ['expand', 'collapse', 'toggle', 'disclosure'], + 'molecules/hover-card': ['preview', 'popup', 'info', 'details'], + 'molecules/page-header': ['title', 'description', 'actions', 'header'], + 'molecules/section': ['group', 'container', 'panel'], + 'molecules/settings': ['preferences', 'config', 'options'], + 'molecules/theme-switcher': ['dark mode', 'light mode', 'theme', 'toggle'], + 'molecules/command-search': ['search', 'spotlight', 'quick', 'cmd k'], + 'molecules/input-otp': ['code', 'verification', '2fa', 'pin'], + 'molecules/grid': ['layout', 'columns', 'responsive'], + 'molecules/empty': ['no data', 'placeholder', 'zero state'], + 'molecules/item': ['list item', 'row', 'entry'], + 'molecules/resizable': ['resize', 'split', 'panels'], + 'molecules/ai-chat': ['chatbot', 'assistant', 'ai', 'conversation'], + 'molecules/data-table-header': ['table', 'filter', 'search', 'actions'], // Organisms - "organisms/sidebar": ["navigation", "menu", "nav", "drawer", "panel", "left", "collapsible"], - "organisms/app-shell": ["layout", "structure", "page", "navbar", "sidebar", "main"], - "organisms/dialog": ["modal", "popup", "overlay", "form", "confirm"], - "organisms/sheet": ["drawer", "slide", "panel", "side", "overlay"], - "organisms/drawer": ["slide", "panel", "side", "overlay", "mobile"], - "organisms/dropdown-menu": ["menu", "actions", "context", "options"], - "organisms/context-menu": ["right click", "menu", "actions"], - "organisms/command": ["search", "palette", "cmd k", "spotlight", "keyboard"], - "organisms/navigation-menu": ["nav", "header", "links", "menu"], - "organisms/menubar": ["menu", "actions", "toolbar", "file menu"], - "organisms/alert-dialog": ["confirm", "warning", "destructive", "modal"], - "organisms/calendar": ["date", "picker", "schedule", "events"], - "organisms/combobox": ["autocomplete", "search", "select", "typeahead"], - "organisms/carousel": ["slider", "gallery", "images", "slides"], - "organisms/chart": ["graph", "visualization", "data", "analytics"], - "organisms/organization-selector": ["workspace", "team", "account", "switcher"], - "organisms/page-layout": ["layout", "structure", "container", "center"], - "organisms/sonner": ["toast", "notification", "alert", "message"], + 'organisms/sidebar': ['navigation', 'menu', 'nav', 'drawer', 'panel', 'left', 'collapsible'], + 'organisms/app-shell': ['layout', 'structure', 'page', 'navbar', 'sidebar', 'main'], + 'organisms/dialog': ['modal', 'popup', 'overlay', 'form', 'confirm'], + 'organisms/sheet': ['drawer', 'slide', 'panel', 'side', 'overlay'], + 'organisms/drawer': ['slide', 'panel', 'side', 'overlay', 'mobile'], + 'organisms/dropdown-menu': ['menu', 'actions', 'context', 'options'], + 'organisms/context-menu': ['right click', 'menu', 'actions'], + 'organisms/command': ['search', 'palette', 'cmd k', 'spotlight', 'keyboard'], + 'organisms/navigation-menu': ['nav', 'header', 'links', 'menu'], + 'organisms/menubar': ['menu', 'actions', 'toolbar', 'file menu'], + 'organisms/alert-dialog': ['confirm', 'warning', 'destructive', 'modal'], + 'organisms/calendar': ['date', 'picker', 'schedule', 'events'], + 'organisms/combobox': ['autocomplete', 'search', 'select', 'typeahead'], + 'organisms/carousel': ['slider', 'gallery', 'images', 'slides'], + 'organisms/chart': ['graph', 'visualization', 'data', 'analytics'], + 'organisms/organization-selector': ['workspace', 'team', 'account', 'switcher'], + 'organisms/page-layout': ['layout', 'structure', 'container', 'center'], + 'organisms/sonner': ['toast', 'notification', 'alert', 'message'], }; /** @@ -144,15 +150,17 @@ function parseComponentDocs(source: string, componentId: string) { if (exportBlockMatch && exportBlockMatch[1]) { const exportedNames = exportBlockMatch[1] .split(',') - .map(s => s.trim().replace(/\n/g, '')) - .filter(s => s && !s.includes(' as ') && !s.toLowerCase().includes('variants')); + .map((s) => s.trim().replace(/\n/g, '')) + .filter((s) => s && !s.includes(' as ') && !s.toLowerCase().includes('variants')); docs.exports.push(...exportedNames); } // FALLBACK: If no export block, look for inline exports if (docs.exports.length === 0) { // Extract exported function/const names (including forwardRef) - const exportMatches = source.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*(?:React\.)?forwardRef/g); + const exportMatches = source.matchAll( + /(?:export\s+)?const\s+(\w+)\s*=\s*(?:React\.)?forwardRef/g, + ); for (const match of exportMatches) { if (match[1] && !docs.exports.includes(match[1])) { docs.exports.push(match[1]); @@ -160,7 +168,9 @@ function parseComponentDocs(source: string, componentId: string) { } // Also get regular exports - const regularExportMatches = source.matchAll(/export\s+(?:function|const)\s+(\w+)(?!\s*=\s*(?:React\.)?forwardRef)/g); + const regularExportMatches = source.matchAll( + /export\s+(?:function|const)\s+(\w+)(?!\s*=\s*(?:React\.)?forwardRef)/g, + ); for (const match of regularExportMatches) { if (match[1] && !match[1].includes('Variants') && !docs.exports.includes(match[1])) { docs.exports.push(match[1]); @@ -170,7 +180,9 @@ function parseComponentDocs(source: string, componentId: string) { // Extract type definitions with properties (handles both type and interface) // Pattern 1: type XProps = BaseProps & { ... } - const typePropsMatches = source.matchAll(/type\s+(\w+(?:Props|Context(?:Props)?))\s*=\s*(?:[^{]*&\s*)?\{([^}]+)\}/gs); + const typePropsMatches = source.matchAll( + /type\s+(\w+(?:Props|Context(?:Props)?))\s*=\s*(?:[^{]*&\s*)?\{([^}]+)\}/gs, + ); for (const match of typePropsMatches) { const typeName = match[1]; const body = match[2]; @@ -187,14 +199,16 @@ function parseComponentDocs(source: string, componentId: string) { } // Pattern 2: Inline props in function signature - Omit<...> & { ... } - const inlinePropMatches = source.matchAll(/function\s+(\w+)\s*\(\s*\{[^}]*\}\s*:\s*(?:Omit<[^>]+>\s*&\s*)?\{([^}]+)\}/g); + const inlinePropMatches = source.matchAll( + /function\s+(\w+)\s*\(\s*\{[^}]*\}\s*:\s*(?:Omit<[^>]+>\s*&\s*)?\{([^}]+)\}/g, + ); for (const match of inlinePropMatches) { const funcName = match[1]; const body = match[2]; if (!funcName || !body) continue; const properties = parsePropertiesFromBody(body); - if (properties.length > 0 && !docs.props.find(p => p.name === funcName)) { + if (properties.length > 0 && !docs.props.find((p) => p.name === funcName)) { docs.props.push({ name: funcName, interface: `${funcName}Props`, @@ -204,7 +218,9 @@ function parseComponentDocs(source: string, componentId: string) { } // Extract ALL CVA variants from the file - const cvaMatches = source.matchAll(/const\s+(\w+Variants)\s*=\s*cva\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`|\[[^\]]*\]|[^,]+)\s*,\s*\{([\s\S]*?)\}\s*\)/g); + const cvaMatches = source.matchAll( + /const\s+(\w+Variants)\s*=\s*cva\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`|\[[^\]]*\]|[^,]+)\s*,\s*\{([\s\S]*?)\}\s*\)/g, + ); for (const cvaMatch of cvaMatches) { const variantName = cvaMatch[1]; const configBlock = cvaMatch[2]; @@ -212,7 +228,9 @@ function parseComponentDocs(source: string, componentId: string) { if (!variantName || !configBlock) continue; // Find the variants section - const variantsSection = configBlock.match(/variants:\s*\{([\s\S]*?)\}\s*(?:,\s*(?:defaultVariants|compoundVariants)|$)/); + const variantsSection = configBlock.match( + /variants:\s*\{([\s\S]*?)\}\s*(?:,\s*(?:defaultVariants|compoundVariants)|$)/, + ); if (!variantsSection || !variantsSection[1]) continue; const variantsBlock = variantsSection[1]; @@ -256,8 +274,11 @@ function parseComponentDocs(source: string, componentId: string) { /** * Helper to parse properties from a TypeScript object body */ -function parsePropertiesFromBody(body: string): Array<{ name: string; type: string; optional: boolean; description?: string }> { - const properties: Array<{ name: string; type: string; optional: boolean; description?: string }> = []; +function parsePropertiesFromBody( + body: string, +): Array<{ name: string; type: string; optional: boolean; description?: string }> { + const properties: Array<{ name: string; type: string; optional: boolean; description?: string }> = + []; const lines = body.split('\n'); let currentComment = ''; @@ -297,10 +318,15 @@ function parsePropertiesFromBody(body: string): Array<{ name: string; type: stri function formatComponentDocsForAgent( docs: ReturnType, storySource: string | null, - componentSource: string + componentSource: string, ): string { // Use the component ID to derive a nice title - const componentName = docs.id.split('/')[1]?.split('-').map(s => s[0]?.toUpperCase() + s.slice(1)).join('') || docs.id; + const componentName = + docs.id + .split('/')[1] + ?.split('-') + .map((s) => s[0]?.toUpperCase() + s.slice(1)) + .join('') || docs.id; let output = `# ${componentName} Component\n\n`; // Critical warning @@ -316,9 +342,9 @@ function formatComponentDocsForAgent( const subExports = docs.exports.slice(1); output += `**Main:** \`${mainExport}\`\n\n`; output += `**Sub-components:**\n`; - output += subExports.map(e => `- \`${e}\``).join('\n') + '\n\n'; + output += subExports.map((e) => `- \`${e}\``).join('\n') + '\n\n'; } else { - output += docs.exports.map(e => `- \`${e}\``).join('\n') + '\n\n'; + output += docs.exports.map((e) => `- \`${e}\``).join('\n') + '\n\n'; } } @@ -327,7 +353,7 @@ function formatComponentDocsForAgent( output += `## Available Variants\n`; for (const v of docs.variants) { output += `### ${v.component} - ${v.variantName}\n`; - output += `Options: ${v.options.map(o => `\`${o}\``).join(', ')}\n\n`; + output += `Options: ${v.options.map((o) => `\`${o}\``).join(', ')}\n\n`; } } @@ -350,7 +376,9 @@ function formatComponentDocsForAgent( // Usage example from story if (storySource) { // Extract a simple usage example from the story - const storyMatch = storySource.match(/export\s+const\s+\w+:\s*Story\s*=\s*\{[\s\S]*?render:\s*\([^)]*\)\s*=>\s*\(([\s\S]*?)\),?\s*\}/); + const storyMatch = storySource.match( + /export\s+const\s+\w+:\s*Story\s*=\s*\{[\s\S]*?render:\s*\([^)]*\)\s*=>\s*\(([\s\S]*?)\),?\s*\}/, + ); if (storyMatch && storyMatch[1]) { output += `## Usage Example\n\`\`\`tsx\n${storyMatch[1].trim()}\n\`\`\`\n\n`; } @@ -431,24 +459,36 @@ function parseDesignTokens(cssSource: string) { function getInstallationInstructions(framework: string) { const baseSteps = [ { - title: "Install the package", - command: "pnpm add @trycompai/design-system", + title: 'Install the package', + command: 'pnpm add @trycompai/design-system', }, { - title: "Import global styles", - description: "Add this import to your root layout or entry file:", + title: 'Import global styles', + description: 'Add this import to your root layout or entry file:', code: `import '@trycompai/design-system/styles/globals.css';`, }, ]; - const frameworkMap: Record }> = { - "next-app": { - framework: "Next.js App Router", + const frameworkMap: Record< + string, + { + framework: string; + steps: Array<{ + title: string; + description?: string; + command?: string; + code?: string; + file?: string; + }>; + } + > = { + 'next-app': { + framework: 'Next.js App Router', steps: [ ...baseSteps, { - title: "Setup root layout", - file: "app/layout.tsx", + title: 'Setup root layout', + file: 'app/layout.tsx', code: `import '@trycompai/design-system/styles/globals.css'; import { cn } from '@trycompai/design-system'; @@ -464,13 +504,13 @@ export default function RootLayout({ children }: { children: React.ReactNode }) }, ], }, - "next-pages": { - framework: "Next.js Pages Router", + 'next-pages': { + framework: 'Next.js Pages Router', steps: [ ...baseSteps, { - title: "Setup _app.tsx", - file: "pages/_app.tsx", + title: 'Setup _app.tsx', + file: 'pages/_app.tsx', code: `import '@trycompai/design-system/styles/globals.css'; import type { AppProps } from 'next/app'; @@ -481,12 +521,12 @@ export default function App({ Component, pageProps }: AppProps) { ], }, vite: { - framework: "Vite + React", + framework: 'Vite + React', steps: [ ...baseSteps, { - title: "Setup main.tsx", - file: "src/main.tsx", + title: 'Setup main.tsx', + file: 'src/main.tsx', code: `import '@trycompai/design-system/styles/globals.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; @@ -501,15 +541,15 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ], }, general: { - framework: "General", + framework: 'General', steps: [ ...baseSteps, { - title: "Import styles in your entry file", - description: "Make sure globals.css is imported before any component usage.", + title: 'Import styles in your entry file', + description: 'Make sure globals.css is imported before any component usage.', }, { - title: "Use components", + title: 'Use components', code: `import { Button, Card, Stack, Text } from '@trycompai/design-system'; // IMPORTANT: Components do NOT accept className @@ -542,31 +582,31 @@ function getRepoPathsResolved() { repoRoot: repoRootOverride, designSystemSrcComponentsDir: path.join( repoRootOverride, - "packages", - "design-system", - "src", - "components", + 'packages', + 'design-system', + 'src', + 'components', ), - storybookStoriesDir: path.join(repoRootOverride, "apps", "storybook", "stories"), + storybookStoriesDir: path.join(repoRootOverride, 'apps', 'storybook', 'stories'), globalsStylesPath: path.join( repoRootOverride, - "packages", - "design-system", - "src", - "styles", - "globals.css", + 'packages', + 'design-system', + 'src', + 'styles', + 'globals.css', ), - agentsMdPath: path.join(repoRootOverride, "packages", "design-system", "agents.md"), - claudeMdPath: path.join(repoRootOverride, "CLAUDE.md"), + agentsMdPath: path.join(repoRootOverride, 'packages', 'design-system', 'agents.md'), + claudeMdPath: path.join(repoRootOverride, 'CLAUDE.md'), }; } // appDir is .../apps/mcp/src - return getRepoPaths(path.join(getAppDir(), "..")); + return getRepoPaths(path.join(getAppDir(), '..')); } const zListComponentsArgs = z.object({ - category: z.enum(["atoms", "molecules", "organisms"]).optional(), + category: z.enum(['atoms', 'molecules', 'organisms']).optional(), }); const zGetComponentSourceArgs = z.object({ @@ -589,14 +629,14 @@ const zSuggestStoryForComponentArgs = z.object({ }); function jsonText(payload: unknown) { - return [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }]; + return [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }]; } async function main() { const repoPaths = getRepoPathsResolved(); const server = new Server( - { name: "design-system-mcp", version: "0.1.0" }, + { name: 'design-system-mcp', version: '0.1.0' }, { capabilities: { tools: {} } }, ); @@ -604,114 +644,113 @@ async function main() { return { tools: [ { - name: "list_components", - description: - "List all design system component source files (atoms/molecules/organisms).", + name: 'list_components', + description: 'List all design system component source files (atoms/molecules/organisms).', inputSchema: { - type: "object", + type: 'object', properties: { category: { - type: "string", - enum: ["atoms", "molecules", "organisms"], - description: "Optional filter.", + type: 'string', + enum: ['atoms', 'molecules', 'organisms'], + description: 'Optional filter.', }, }, }, }, { - name: "get_component_source", + name: 'get_component_source', description: "Fetch the source for a design system component by id (e.g. 'molecules/card').", inputSchema: { - type: "object", + type: 'object', properties: { - id: { type: "string" }, + id: { type: 'string' }, }, - required: ["id"], + required: ['id'], }, }, { - name: "search", - description: - "Search component ids (and optionally component source) for a query string.", + name: 'search', + description: 'Search component ids (and optionally component source) for a query string.', inputSchema: { - type: "object", + type: 'object', properties: { - query: { type: "string" }, - limit: { type: "number", minimum: 1, maximum: 50 }, + query: { type: 'string' }, + limit: { type: 'number', minimum: 1, maximum: 50 }, includeSource: { - type: "boolean", - description: "If true, searches within file contents too (slower).", + type: 'boolean', + description: 'If true, searches within file contents too (slower).', }, }, - required: ["query"], + required: ['query'], }, }, { - name: "list_stories", - description: "List Storybook story files in apps/storybook/stories.", - inputSchema: { type: "object", properties: {} }, + name: 'list_stories', + description: 'List Storybook story files in apps/storybook/stories.', + inputSchema: { type: 'object', properties: {} }, }, { - name: "get_story_source", + name: 'get_story_source', description: "Fetch a Storybook story source by story name (e.g. 'Card' for Card.stories.tsx).", inputSchema: { - type: "object", - properties: { name: { type: "string" } }, - required: ["name"], + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'], }, }, { - name: "suggest_story_for_component", + name: 'suggest_story_for_component', description: - "Best-guess the Storybook story name for a component id (e.g. molecules/card -> Card).", + 'Best-guess the Storybook story name for a component id (e.g. molecules/card -> Card).', inputSchema: { - type: "object", - properties: { componentId: { type: "string" } }, - required: ["componentId"], + type: 'object', + properties: { componentId: { type: 'string' } }, + required: ['componentId'], }, }, { - name: "get_theme", + name: 'get_theme', description: - "Get the design system theme including CSS variables, color tokens, and design tokens for light/dark mode. Returns parsed tokens from globals.css.", - inputSchema: { type: "object", properties: {} }, + 'Get the design system theme including CSS variables, color tokens, and design tokens for light/dark mode. Returns parsed tokens from globals.css.', + inputSchema: { type: 'object', properties: {} }, }, { - name: "get_usage_guidelines", + name: 'get_usage_guidelines', description: - "Get the usage guidelines and rules for the design system. IMPORTANT: Components do NOT accept className - use variants and props only. This returns the agents.md file with all usage patterns.", - inputSchema: { type: "object", properties: {} }, + 'Get the usage guidelines and rules for the design system. IMPORTANT: Components do NOT accept className - use variants and props only. This returns the agents.md file with all usage patterns.', + inputSchema: { type: 'object', properties: {} }, }, { - name: "installation", + name: 'installation', description: - "Get installation and setup instructions for using the design system in a project.", + 'Get installation and setup instructions for using the design system in a project.', inputSchema: { - type: "object", + type: 'object', properties: { framework: { - type: "string", - enum: ["next-app", "next-pages", "vite", "general"], + type: 'string', + enum: ['next-app', 'next-pages', 'vite', 'general'], description: "The framework you're using.", }, }, }, }, { - name: "get_component_docs", + name: 'get_component_docs', description: - "Get comprehensive documentation for a component including props, variants, and usage examples. THIS IS THE RECOMMENDED TOOL for understanding how to use a component. CRITICAL: Components do NOT accept className - use variants only.", + 'Get comprehensive documentation for a component including props, variants, and usage examples. THIS IS THE RECOMMENDED TOOL for understanding how to use a component. CRITICAL: Components do NOT accept className - use variants only.', inputSchema: { - type: "object", + type: 'object', properties: { id: { - type: "string", - description: "Component id (e.g., 'atoms/button', 'molecules/card'). Use list_components to see all available ids.", + type: 'string', + description: + "Component id (e.g., 'atoms/button', 'molecules/card'). Use list_components to see all available ids.", }, }, - required: ["id"], + required: ['id'], }, }, ], @@ -723,17 +762,15 @@ async function main() { const name = request.params.name; const args = request.params.arguments ?? {}; - if (name === "list_components") { + if (name === 'list_components') { const { category } = zListComponentsArgs.parse(args); const components = await listDesignSystemComponents(repoPaths); - const filtered = category - ? components.filter((c) => c.category === category) - : components; + const filtered = category ? components.filter((c) => c.category === category) : components; return { content: jsonText({ repoPaths, components: filtered }) }; } - if (name === "get_component_source") { + if (name === 'get_component_source') { const { id } = zGetComponentSourceArgs.parse(args); const components = await listDesignSystemComponents(repoPaths); const comp = components.find((c) => c.id.toLowerCase() === id.toLowerCase()); @@ -741,7 +778,7 @@ async function main() { return { content: jsonText({ error: `Component not found: ${id}`, - hint: "Use list_components to see valid ids.", + hint: 'Use list_components to see valid ids.', }), }; } @@ -750,15 +787,21 @@ async function main() { return { content: jsonText({ component: comp, source }) }; } - if (name === "search") { + if (name === 'search') { const { query, limit, includeSource } = zSearchArgs.parse(args); const q = query.toLowerCase(); const queryWords = q.split(/\s+/).filter(Boolean); const components = await listDesignSystemComponents(repoPaths); const hits: Array< - | { kind: "component"; id: string; filePath: string; match: "id" | "tags" | "source"; tags?: string[] } - | { kind: "story"; name: string; filePath: string; match: "name" } + | { + kind: 'component'; + id: string; + filePath: string; + match: 'id' | 'tags' | 'source'; + tags?: string[]; + } + | { kind: 'story'; name: string; filePath: string; match: 'name' } > = []; const seenIds = new Set(); @@ -766,7 +809,7 @@ async function main() { for (const c of components) { if (c.id.toLowerCase().includes(q) || c.fileStem.toLowerCase().includes(q)) { if (!seenIds.has(c.id)) { - hits.push({ kind: "component", id: c.id, filePath: c.filePath, match: "id" }); + hits.push({ kind: 'component', id: c.id, filePath: c.filePath, match: 'id' }); seenIds.add(c.id); } if (hits.length >= limit) break; @@ -779,16 +822,16 @@ async function main() { if (seenIds.has(c.id)) continue; const tags = getComponentTags(c.id); - const matchingTags = tags.filter(tag => - queryWords.some(word => tag.includes(word) || word.includes(tag)) + const matchingTags = tags.filter((tag) => + queryWords.some((word) => tag.includes(word) || word.includes(tag)), ); if (matchingTags.length > 0) { hits.push({ - kind: "component", + kind: 'component', id: c.id, filePath: c.filePath, - match: "tags", + match: 'tags', tags: matchingTags, }); seenIds.add(c.id); @@ -805,10 +848,10 @@ async function main() { const source = await readTextFile(c.filePath); if (source.toLowerCase().includes(q)) { hits.push({ - kind: "component", + kind: 'component', id: c.id, filePath: c.filePath, - match: "source", + match: 'source', }); seenIds.add(c.id); if (hits.length >= limit) break; @@ -821,7 +864,7 @@ async function main() { const stories = await listStorybookStories(repoPaths); for (const s of stories) { if (s.name.toLowerCase().includes(q)) { - hits.push({ kind: "story", name: s.name, filePath: s.filePath, match: "name" }); + hits.push({ kind: 'story', name: s.name, filePath: s.filePath, match: 'name' }); if (hits.length >= limit) break; } } @@ -830,20 +873,20 @@ async function main() { return { content: jsonText({ query, limit, hits }) }; } - if (name === "list_stories") { + if (name === 'list_stories') { zListStoriesArgs.parse(args); const stories = await listStorybookStories(repoPaths); return { content: jsonText({ repoPaths, stories }) }; } - if (name === "get_story_source") { + if (name === 'get_story_source') { const { name: storyName } = zGetStorySourceArgs.parse(args); const story = await findStoryByName(repoPaths, storyName); if (!story) { return { content: jsonText({ error: `Story not found: ${storyName}`, - hint: "Use list_stories to see valid names.", + hint: 'Use list_stories to see valid names.', }), }; } @@ -851,17 +894,15 @@ async function main() { return { content: jsonText({ story, source }) }; } - if (name === "suggest_story_for_component") { + if (name === 'suggest_story_for_component') { const { componentId } = zSuggestStoryForComponentArgs.parse(args); const components = await listDesignSystemComponents(repoPaths); - const comp = components.find( - (c) => c.id.toLowerCase() === componentId.toLowerCase(), - ); + const comp = components.find((c) => c.id.toLowerCase() === componentId.toLowerCase()); if (!comp) { return { content: jsonText({ error: `Component not found: ${componentId}`, - hint: "Use list_components to see valid ids.", + hint: 'Use list_components to see valid ids.', }), }; } @@ -879,13 +920,13 @@ async function main() { }; } - if (name === "get_theme") { + if (name === 'get_theme') { const cssSource = await readTextFile(repoPaths.globalsStylesPath); const tokens = parseDesignTokens(cssSource); return { content: [ { - type: "text" as const, + type: 'text' as const, text: `Design System Theme Tokens Use these semantic tokens for consistent theming. The design system uses Tailwind CSS v4 with CSS variables. @@ -899,35 +940,35 @@ Key token categories: Dark mode is handled automatically via .dark class selector.`, }, - { type: "text" as const, text: JSON.stringify(tokens, null, 2) }, + { type: 'text' as const, text: JSON.stringify(tokens, null, 2) }, ], }; } - if (name === "get_usage_guidelines") { + if (name === 'get_usage_guidelines') { const agentsMd = await readTextFile(repoPaths.agentsMdPath); return { content: [ { - type: "text" as const, + type: 'text' as const, text: `CRITICAL: Components do NOT accept className or style props. Use variants and props only. This design system enforces strict styling through class-variance-authority (cva). For layout concerns (width, margins, grid positioning), use wrapper elements. Below are the complete usage guidelines:`, }, - { type: "text" as const, text: agentsMd }, + { type: 'text' as const, text: agentsMd }, ], }; } - if (name === "installation") { - const framework = (args as { framework?: string }).framework || "general"; + if (name === 'installation') { + const framework = (args as { framework?: string }).framework || 'general'; const instructions = getInstallationInstructions(framework); return { content: jsonText(instructions) }; } - if (name === "get_component_docs") { + if (name === 'get_component_docs') { const { id } = zGetComponentSourceArgs.parse(args); const components = await listDesignSystemComponents(repoPaths); const comp = components.find((c) => c.id.toLowerCase() === id.toLowerCase()); @@ -936,8 +977,8 @@ Below are the complete usage guidelines:`, return { content: jsonText({ error: `Component not found: ${id}`, - hint: "Use list_components to see valid ids.", - availableComponents: components.map(c => c.id), + hint: 'Use list_components to see valid ids.', + availableComponents: components.map((c) => c.id), }), }; } @@ -961,18 +1002,22 @@ Below are the complete usage guidelines:`, return { content: [ - { type: "text" as const, text: formattedDocs }, + { type: 'text' as const, text: formattedDocs }, { - type: "text" as const, - text: JSON.stringify({ - componentId: comp.id, - category: comp.category, - exports: parsedDocs.exports, - variants: parsedDocs.variants, - props: parsedDocs.props, - hasStory: Boolean(story), - storyName: story?.name, - }, null, 2), + type: 'text' as const, + text: JSON.stringify( + { + componentId: comp.id, + category: comp.category, + exports: parsedDocs.exports, + variants: parsedDocs.variants, + props: parsedDocs.props, + hasStory: Boolean(story), + storyName: story?.name, + }, + null, + 2, + ), }, ], }; @@ -998,4 +1043,3 @@ Below are the complete usage guidelines:`, // eslint-disable-next-line no-void void main(); - diff --git a/apps/mcp/src/paths.ts b/apps/mcp/src/paths.ts index f057cb4..69508e7 100644 --- a/apps/mcp/src/paths.ts +++ b/apps/mcp/src/paths.ts @@ -1,8 +1,8 @@ -import path from "node:path"; +import path from 'node:path'; export function repoRootFromAppDir(appDir: string) { // apps/design-system-mcp -> repo root - return path.resolve(appDir, "..", ".."); + return path.resolve(appDir, '..', '..'); } export type RepoPaths = { @@ -21,22 +21,21 @@ export function getRepoPaths(appDir: string): RepoPaths { repoRoot, designSystemSrcComponentsDir: path.join( repoRoot, - "packages", - "design-system", - "src", - "components", + 'packages', + 'design-system', + 'src', + 'components', ), - storybookStoriesDir: path.join(repoRoot, "apps", "storybook", "stories"), + storybookStoriesDir: path.join(repoRoot, 'apps', 'storybook', 'stories'), globalsStylesPath: path.join( repoRoot, - "packages", - "design-system", - "src", - "styles", - "globals.css", + 'packages', + 'design-system', + 'src', + 'styles', + 'globals.css', ), - agentsMdPath: path.join(repoRoot, "packages", "design-system", "agents.md"), - claudeMdPath: path.join(repoRoot, "CLAUDE.md"), + agentsMdPath: path.join(repoRoot, 'packages', 'design-system', 'agents.md'), + claudeMdPath: path.join(repoRoot, 'CLAUDE.md'), }; } - diff --git a/apps/mcp/src/smoke.ts b/apps/mcp/src/smoke.ts index 209fe79..8ec085f 100644 --- a/apps/mcp/src/smoke.ts +++ b/apps/mcp/src/smoke.ts @@ -1,20 +1,20 @@ -import path from "node:path"; -import process from "node:process"; +import path from 'node:path'; +import process from 'node:process'; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; async function main() { const client = new Client( - { name: "design-system-mcp-smoke", version: "0.0.0" }, + { name: 'design-system-mcp-smoke', version: '0.0.0' }, { capabilities: {} }, ); const transport = new StdioClientTransport({ command: process.execPath, - args: [path.join(process.cwd(), "dist", "index.js")], + args: [path.join(process.cwd(), 'dist', 'index.js')], env: Object.fromEntries( - Object.entries(process.env).filter(([, v]) => typeof v === "string"), + Object.entries(process.env).filter(([, v]) => typeof v === 'string'), ) as Record, }); @@ -22,93 +22,87 @@ async function main() { const tools = await client.listTools(); const toolNames = tools.tools.map((t) => t.name).sort(); - console.log("tools:", toolNames); + console.log('tools:', toolNames); const listResult = await client.callTool({ - name: "list_components", + name: 'list_components', arguments: {}, }); - console.log( - "list_components:", - (listResult as any).content?.[0]?.type, - ); + console.log('list_components:', (listResult as any).content?.[0]?.type); const sourceResult = await client.callTool({ - name: "get_component_source", - arguments: { id: "molecules/card" }, + name: 'get_component_source', + arguments: { id: 'molecules/card' }, }); - console.log( - "get_component_source:", - (sourceResult as any).content?.[0]?.type, - ); + console.log('get_component_source:', (sourceResult as any).content?.[0]?.type); // Test new tools const themeResult = await client.callTool({ - name: "get_theme", + name: 'get_theme', arguments: {}, }); const themeContent = (themeResult as any).content?.[1]?.text; const themeTokens = themeContent ? JSON.parse(themeContent) : {}; console.log( - "get_theme:", + 'get_theme:', `light=${Object.keys(themeTokens.light || {}).length} tokens, dark=${Object.keys(themeTokens.dark || {}).length} tokens`, ); const guidelinesResult = await client.callTool({ - name: "get_usage_guidelines", + name: 'get_usage_guidelines', arguments: {}, }); - const guidelinesText = (guidelinesResult as any).content?.[1]?.text || ""; + const guidelinesText = (guidelinesResult as any).content?.[1]?.text || ''; console.log( - "get_usage_guidelines:", - `${guidelinesText.length} chars, includes className rule: ${guidelinesText.includes("className")}`, + 'get_usage_guidelines:', + `${guidelinesText.length} chars, includes className rule: ${guidelinesText.includes('className')}`, ); const installResult = await client.callTool({ - name: "installation", - arguments: { framework: "next-app" }, + name: 'installation', + arguments: { framework: 'next-app' }, }); const installContent = (installResult as any).content?.[0]?.text; const installData = installContent ? JSON.parse(installContent) : {}; console.log( - "installation:", + 'installation:', `framework=${installData.framework}, steps=${installData.steps?.length || 0}`, ); // Test the new comprehensive docs tool with button (inline exports) const docsResult = await client.callTool({ - name: "get_component_docs", - arguments: { id: "atoms/button" }, + name: 'get_component_docs', + arguments: { id: 'atoms/button' }, }); - const docsText = (docsResult as any).content?.[0]?.text || ""; + const docsText = (docsResult as any).content?.[0]?.text || ''; const docsJson = (docsResult as any).content?.[1]?.text; const docsData = docsJson ? JSON.parse(docsJson) : {}; console.log( - "get_component_docs (button):", + 'get_component_docs (button):', `exports=${docsData.exports?.length || 0}, variants=${docsData.variants?.length || 0}, hasStory=${docsData.hasStory}`, ); - console.log(" -> includes className warning:", docsText.includes("className")); + console.log(' -> includes className warning:', docsText.includes('className')); // Test compound component with multiline export block (sidebar) const sidebarDocsResult = await client.callTool({ - name: "get_component_docs", - arguments: { id: "organisms/sidebar" }, + name: 'get_component_docs', + arguments: { id: 'organisms/sidebar' }, }); const sidebarDocsJson = (sidebarDocsResult as any).content?.[1]?.text; const sidebarDocsData = sidebarDocsJson ? JSON.parse(sidebarDocsJson) : {}; console.log( - "get_component_docs (sidebar):", + 'get_component_docs (sidebar):', `exports=${sidebarDocsData.exports?.length || 0}, variants=${sidebarDocsData.variants?.length || 0}`, ); // Test semantic search const searchResult = await client.callTool({ - name: "search", - arguments: { query: "navigation menu" }, + name: 'search', + arguments: { query: 'navigation menu' }, }); - const searchData = JSON.parse((searchResult as any).content?.[0]?.text || "{}"); + const searchData = JSON.parse((searchResult as any).content?.[0]?.text || '{}'); console.log( - "search (navigation menu):", + 'search (navigation menu):', `hits=${searchData.hits?.length || 0}`, searchData.hits?.slice(0, 3).map((h: any) => h.id || h.name), ); @@ -120,4 +114,3 @@ main().catch((err) => { console.error(err); process.exit(1); }); - diff --git a/apps/mcp/src/storybook-index.ts b/apps/mcp/src/storybook-index.ts index 5a5a263..7e96fbf 100644 --- a/apps/mcp/src/storybook-index.ts +++ b/apps/mcp/src/storybook-index.ts @@ -1,7 +1,7 @@ -import path from "node:path"; +import path from 'node:path'; -import { pathExists, walkFiles } from "./fs-utils.js"; -import type { RepoPaths } from "./paths.js"; +import { pathExists, walkFiles } from './fs-utils.js'; +import type { RepoPaths } from './paths.js'; export type StoryEntry = { /** e.g. "Card" */ @@ -12,20 +12,18 @@ export type StoryEntry = { filePath: string; }; -export async function listStorybookStories( - repoPaths: RepoPaths, -): Promise { +export async function listStorybookStories(repoPaths: RepoPaths): Promise { const baseDir = repoPaths.storybookStoriesDir; if (!(await pathExists(baseDir))) return []; const files = await walkFiles(baseDir, { - includeExtensions: [".ts", ".tsx"], - ignore: (relPath) => !relPath.endsWith(".stories.tsx"), + includeExtensions: ['.ts', '.tsx'], + ignore: (relPath) => !relPath.endsWith('.stories.tsx'), }); const out: StoryEntry[] = files.map((f) => { const filename = path.basename(f.relPath); - const name = filename.replace(/\.stories\.tsx$/, ""); + const name = filename.replace(/\.stories\.tsx$/, ''); return { name, filename, filePath: f.absPath }; }); @@ -37,4 +35,3 @@ export async function findStoryByName(repoPaths: RepoPaths, name: string) { const stories = await listStorybookStories(repoPaths); return stories.find((s) => s.name.toLowerCase() === name.toLowerCase()); } - diff --git a/apps/mcp/tsconfig.json b/apps/mcp/tsconfig.json index a55ee47..d21eaa7 100644 --- a/apps/mcp/tsconfig.json +++ b/apps/mcp/tsconfig.json @@ -10,4 +10,3 @@ "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] } - diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index d5425df..8e14156 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -1,29 +1,26 @@ import type { StorybookConfig } from '@storybook/react-vite'; -import { dirname } from "path" +import { dirname } from 'path'; -import { fileURLToPath } from "url" +import { fileURLToPath } from 'url'; /** -* This function is used to resolve the absolute path of a package. -* It is needed in projects that use Yarn PnP or are set up within a monorepo. -*/ + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ function getAbsolutePath(value: string): any { - return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))) + return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))); } const config: StorybookConfig = { - "stories": [ - "../stories/**/*.mdx", - "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)" - ], - "addons": [ + stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ getAbsolutePath('@chromatic-com/storybook'), getAbsolutePath('@storybook/addon-vitest'), getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('@storybook/addon-docs'), getAbsolutePath('@storybook/addon-onboarding'), - getAbsolutePath('@storybook/addon-mcp') + getAbsolutePath('@storybook/addon-mcp'), ], - "framework": getAbsolutePath('@storybook/react-vite') + framework: getAbsolutePath('@storybook/react-vite'), }; -export default config; \ No newline at end of file +export default config; diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx index 8738d20..2a37fc0 100644 --- a/apps/storybook/.storybook/preview.tsx +++ b/apps/storybook/.storybook/preview.tsx @@ -28,7 +28,9 @@ const preview: Preview = { const isFullscreen = context.parameters?.layout === 'fullscreen'; return (
-
+
diff --git a/apps/storybook/agents.md b/apps/storybook/agents.md index 4ee2daa..21cb877 100644 --- a/apps/storybook/agents.md +++ b/apps/storybook/agents.md @@ -105,11 +105,12 @@ Don't wrap components in divs with Tailwind classes for spacing or layout. ## Layout Component Reference ### Stack & HStack + For spacing between elements: ```tsx // 4px - // 8px + // 8px // 12px // 16px // 24px @@ -129,6 +130,7 @@ For spacing between elements: ``` ### PageLayout + For page-level structure: ```tsx @@ -157,6 +159,7 @@ For page-level structure: ``` ### Container + For max-width sections within a page: ```tsx @@ -165,6 +168,7 @@ For max-width sections within a page: ``` ### Grid + For grid layouts: ```tsx @@ -178,6 +182,7 @@ For grid layouts: Only use raw `
` when absolutely necessary: 1. **Complex CSS Grid layouts** (when Grid component doesn't fit): + ```tsx
{items.map(...)} @@ -211,9 +216,7 @@ export const MyStory: Story = { - - {/* Content */} - + {/* Content */} @@ -272,7 +275,7 @@ import { Grid, Container, PageLayout, - + // Page structure PageHeader, PageHeaderDescription, @@ -281,7 +284,7 @@ import { SectionHeader, SectionTitle, SectionContent, - + // Components Card, CardHeader, @@ -297,14 +300,14 @@ import { ## Summary -| Need | Solution | -|------|----------| -| Spacing between items | `` or `` | -| Page structure | `` with `container`, `maxWidth`, `padding` props | -| Centered content | `` or `` | -| Full-width layout | `` | -| Grid layout | `` | -| Text styling | `` | -| Component styling | Use component's variant/size props | -| Missing prop | Add it to the design system component | -| New pattern | Create a new design system component | +| Need | Solution | +| --------------------- | ------------------------------------------------------------ | +| Spacing between items | `` or `` | +| Page structure | `` with `container`, `maxWidth`, `padding` props | +| Centered content | `` or `` | +| Full-width layout | `` | +| Grid layout | `` | +| Text styling | `` | +| Component styling | Use component's variant/size props | +| Missing prop | Add it to the design system component | +| New pattern | Create a new design system component | diff --git a/apps/storybook/stories/Accordion.stories.tsx b/apps/storybook/stories/Accordion.stories.tsx index 0d63886..ffea06d 100644 --- a/apps/storybook/stories/Accordion.stories.tsx +++ b/apps/storybook/stories/Accordion.stories.tsx @@ -1,5 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@trycompai/design-system'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@trycompai/design-system'; const meta = { title: 'Molecules/Accordion', diff --git a/apps/storybook/stories/AppShell.stories.tsx b/apps/storybook/stories/AppShell.stories.tsx index 849baa8..9b35fad 100644 --- a/apps/storybook/stories/AppShell.stories.tsx +++ b/apps/storybook/stories/AppShell.stories.tsx @@ -85,7 +85,9 @@ const OrgSelector = () => ( @@ -163,7 +165,9 @@ const Logo = () => { return ( - / + + / + ); @@ -175,8 +179,18 @@ const searchGroups = [ label: 'Pages', items: [ { id: 'overview', label: 'Overview', icon: , shortcut: '⌘1' }, - { id: 'dashboard', label: 'Dashboard', icon: , shortcut: '⌘2' }, - { id: 'settings', label: 'Settings', icon: , shortcut: '⌘,' }, + { + id: 'dashboard', + label: 'Dashboard', + icon: , + shortcut: '⌘2', + }, + { + id: 'settings', + label: 'Settings', + icon: , + shortcut: '⌘,', + }, ], }, { @@ -199,13 +213,12 @@ const searchGroups = [ const SidebarNav = () => ( <> - } - title="Compliance" - /> + } title="Compliance" /> - } isActive>Overview + } isActive> + Overview + }>Quickstart @@ -224,13 +237,12 @@ const SidebarNav = () => ( const RailSidebarNav = () => ( <> - } - title="HR" - /> + } title="HR" /> - } isActive>Employees + } isActive> + Employees + }>Recruiting }>Learning @@ -331,7 +343,9 @@ export const Default: Story = { - Track your progress towards SOC 2 compliance. + + Track your progress towards SOC 2 compliance. + {/* SOC 2 Progress Section */} @@ -343,10 +357,16 @@ export const Default: Story = { - 67% - complete + + 67% + + + complete + - 98 of 146 controls + + 98 of 146 controls + @@ -359,22 +379,34 @@ export const Default: Story = {
- Complete security awareness training - 3 team members haven't completed annual training + + Complete security awareness training + + + 3 team members haven't completed annual training +
- Review and approve access policies - 2 policies pending approval from admin + + Review and approve access policies + + + 2 policies pending approval from admin +
- Connect your cloud infrastructure - AWS and GCP integrations available + + Connect your cloud infrastructure + + + AWS and GCP integrations available +
@@ -424,9 +456,13 @@ export const WithBreadcrumbs: Story = { startContent={ - / + + / + Dashboard - / + + / + Settings } @@ -541,12 +577,75 @@ export const WithRail: Story = { - Manage your team members and their information. + + Manage your team members and their information. + - Rippling-style layout with app rail on the left. Use ⌘\ to toggle the sidebar. + + Rippling-style layout with app rail on the left. Use ⌘\ to toggle the + sidebar. + + + + + + ), +}; + +export const WithRailLinks: Story = { + parameters: { + docs: { + description: { + story: + 'Pass `render` to swap the rail item’s ` ), - width: "md", + width: 'md', children: ( @@ -137,17 +134,14 @@ export const CompoundComponents: Story = { Advanced Layout - - Using compound components for full control - + Using compound components for full control New - Use compound components when you need custom layouts or multiple - content sections. + Use compound components when you need custom layouts or multiple content sections. @@ -160,21 +154,17 @@ export const CompoundComponents: Story = { export const ContentOnly: Story = { args: { - width: "sm", - children: ( - - A minimal card with just content, no header or footer. - - ), + width: 'sm', + children: A minimal card with just content, no header or footer., }, }; export const Small: Story = { args: { - size: "sm", - width: "sm", - title: "Compact Card", - description: "Smaller padding for compact layouts.", + size: 'sm', + width: 'sm', + title: 'Compact Card', + description: 'Smaller padding for compact layouts.', children: This card uses the small size variant., }, }; @@ -188,9 +178,7 @@ export const Shadows: Story = { title="No shadow" description="Flat surface against the background." > - - Use for inline lists, nested cards, or grid items. - + Use for inline lists, nested cards, or grid items. - - Use for hero sections, trust-portal cards, and empty states. - + Use for hero sections, trust-portal cards, and empty states. - This card has longer content so it will be wider, but still shrinks - to fit. + This card has longer content so it will be wider, but still shrinks to fit. @@ -285,14 +270,8 @@ export const AutoWidth: Story = { export const FullWidth: Story = { render: () => (
- - - This card takes up the full width of its container (600px here). - + + This card takes up the full width of its container (600px here).
), diff --git a/apps/storybook/stories/Checkbox.stories.tsx b/apps/storybook/stories/Checkbox.stories.tsx index 114d0dd..f7e764b 100644 --- a/apps/storybook/stories/Checkbox.stories.tsx +++ b/apps/storybook/stories/Checkbox.stories.tsx @@ -63,4 +63,3 @@ export const CheckboxGroup: Story = {
), }; - diff --git a/apps/storybook/stories/Dialog.stories.tsx b/apps/storybook/stories/Dialog.stories.tsx index 35f3b17..85b01ef 100644 --- a/apps/storybook/stories/Dialog.stories.tsx +++ b/apps/storybook/stories/Dialog.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { expect, within, userEvent } from 'storybook/test'; +import { expect, waitFor, within, userEvent } from 'storybook/test'; import { Button, Dialog, @@ -59,9 +59,7 @@ export const Default: Story = { // Verify dialog title and description await expect(within(dialog).getByText('Dialog Title')).toBeInTheDocument(); - await expect( - within(dialog).getByText(/This is the dialog description/) - ).toBeInTheDocument(); + await expect(within(dialog).getByText(/This is the dialog description/)).toBeInTheDocument(); // Close the dialog using the close button const closeButton = within(dialog).getByRole('button', { name: /close/i }); @@ -155,7 +153,10 @@ export const WithTextarea: Story = { // Find the textarea and verify it's visible and can receive input const textarea = within(dialog).getByRole('textbox'); await expect(textarea).toBeInTheDocument(); - await expect(textarea).toBeVisible(); + // DialogContent fades in (`data-open:fade-in-0`, `duration-100`), so the textarea is + // mounted at opacity 0 for a beat. jest-dom counts zero opacity as not visible, so this + // has to outlast the animation rather than sample it once. + await waitFor(() => expect(textarea).toBeVisible()); // Type in the textarea await userEvent.type(textarea, 'Approved after security review'); diff --git a/apps/storybook/stories/Menubar.stories.tsx b/apps/storybook/stories/Menubar.stories.tsx index 67aa8cf..9d7ff7e 100644 --- a/apps/storybook/stories/Menubar.stories.tsx +++ b/apps/storybook/stories/Menubar.stories.tsx @@ -85,9 +85,7 @@ export const Default: Story = { View Always Show Bookmarks Bar - - Always Show Full URLs - + Always Show Full URLs Reload ⌘R @@ -118,4 +116,3 @@ export const Default: Story = { ), }; - diff --git a/apps/storybook/stories/OrganizationSelector.stories.tsx b/apps/storybook/stories/OrganizationSelector.stories.tsx index db765c7..3bfbad5 100644 --- a/apps/storybook/stories/OrganizationSelector.stories.tsx +++ b/apps/storybook/stories/OrganizationSelector.stories.tsx @@ -1,10 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { - OrganizationSelector, - type Organization, - Stack, - Label, -} from '@trycompai/design-system'; +import { OrganizationSelector, type Organization, Stack, Label } from '@trycompai/design-system'; import * as React from 'react'; const meta = { @@ -29,19 +24,59 @@ const organizations: Organization[] = [ ]; const organizationsWithLogos: Organization[] = [ - { id: 'org_acme123', name: 'Acme Corp', createdAt: '2024-01-05', logoUrl: 'https://img.logo.dev/airbnb.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, - { id: 'org_beta456', name: 'Beta Inc', createdAt: '2023-12-12', logoUrl: 'https://img.logo.dev/stripe.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, - { id: 'org_gamma789', name: 'Gamma LLC', createdAt: '2024-03-22', logoUrl: 'https://img.logo.dev/figma.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, - { id: 'org_delta012', name: 'Delta Systems', createdAt: '2022-11-08', logoUrl: 'https://img.logo.dev/linear.app?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, - { id: 'org_epsilon', name: 'Epsilon Technologies', createdAt: '2024-06-14', logoUrl: 'https://img.logo.dev/notion.so?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, + { + id: 'org_acme123', + name: 'Acme Corp', + createdAt: '2024-01-05', + logoUrl: 'https://img.logo.dev/airbnb.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, + { + id: 'org_beta456', + name: 'Beta Inc', + createdAt: '2023-12-12', + logoUrl: 'https://img.logo.dev/stripe.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, + { + id: 'org_gamma789', + name: 'Gamma LLC', + createdAt: '2024-03-22', + logoUrl: 'https://img.logo.dev/figma.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, + { + id: 'org_delta012', + name: 'Delta Systems', + createdAt: '2022-11-08', + logoUrl: 'https://img.logo.dev/linear.app?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, + { + id: 'org_epsilon', + name: 'Epsilon Technologies', + createdAt: '2024-06-14', + logoUrl: 'https://img.logo.dev/notion.so?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, ]; const organizationsWithFallbacks: Organization[] = [ - { id: 'org_acme123', name: 'Acme Corp', createdAt: '2024-01-05', logoUrl: 'https://img.logo.dev/airbnb.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, + { + id: 'org_acme123', + name: 'Acme Corp', + createdAt: '2024-01-05', + logoUrl: 'https://img.logo.dev/airbnb.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, { id: 'org_beta456', name: 'Beta Inc', createdAt: '2023-12-12' }, - { id: 'org_gamma789', name: 'Gamma LLC', createdAt: '2024-03-22', logoUrl: 'https://img.logo.dev/figma.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, + { + id: 'org_gamma789', + name: 'Gamma LLC', + createdAt: '2024-03-22', + logoUrl: 'https://img.logo.dev/figma.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, { id: 'org_delta012', name: 'Delta Systems', createdAt: '2022-11-08' }, - { id: 'org_epsilon', name: 'Epsilon Technologies', createdAt: '2024-06-14', logoUrl: 'https://img.logo.dev/notion.so?token=pk_AZatYxV5QDSfWpRDaBxzRQ' }, + { + id: 'org_epsilon', + name: 'Epsilon Technologies', + createdAt: '2024-06-14', + logoUrl: 'https://img.logo.dev/notion.so?token=pk_AZatYxV5QDSfWpRDaBxzRQ', + }, ]; // Generate many organizations for testing large lists @@ -56,10 +91,7 @@ export const Default: Story = { args: { organizations }, render: () => (
- +
), }; @@ -68,10 +100,7 @@ export const WithDefaultValue: Story = { args: { organizations }, render: () => (
- +
), }; @@ -104,10 +133,7 @@ export const WithLabel: Story = {
- +
), @@ -117,11 +143,7 @@ export const SmallSize: Story = { args: { organizations }, render: () => (
- +
), }; @@ -130,10 +152,7 @@ export const WithLogos: Story = { args: { organizations: organizationsWithLogos }, render: () => (
- +
), }; @@ -142,10 +161,7 @@ export const WithLogoFallbacks: Story = { args: { organizations: organizationsWithFallbacks }, render: () => (
- +
), }; @@ -154,11 +170,7 @@ export const Disabled: Story = { args: { organizations }, render: () => (
- +
), }; @@ -181,8 +193,8 @@ export const SearchByID: Story = { render: () => (

- Try searching by organization ID (e.g., "org_001") or name - (e.g., "Organization 15") + Try searching by organization ID (e.g., "org_001") or name (e.g., + "Organization 15")

- Click the trigger or press ⌘O to open in a centered modal dialog. + Click the trigger or press{' '} + ⌘O to open in a + centered modal dialog.

Section {index + 1} - - Placeholder content for section {index + 1}. - + Placeholder content for section {index + 1}. ))} diff --git a/apps/storybook/stories/Pagination.stories.tsx b/apps/storybook/stories/Pagination.stories.tsx index c646825..20ed4ef 100644 --- a/apps/storybook/stories/Pagination.stories.tsx +++ b/apps/storybook/stories/Pagination.stories.tsx @@ -32,7 +32,9 @@ export const Default: Story = { 1 - 2 + + 2 + 3 @@ -65,7 +67,9 @@ export const WithEllipsis: Story = { 4 - 5 + + 5 + 6 @@ -98,4 +102,3 @@ export const Simple: Story = { ), }; - diff --git a/apps/storybook/stories/Section.stories.tsx b/apps/storybook/stories/Section.stories.tsx index 4c46d12..80e8d42 100644 --- a/apps/storybook/stories/Section.stories.tsx +++ b/apps/storybook/stories/Section.stories.tsx @@ -68,7 +68,9 @@ export const Composable: Story = { Configure how you receive notifications.
- + @@ -103,12 +105,18 @@ export const MultipleSections: Story = {
Change Password} + actions={ + + } >
- Last password change: 30 days ago + + Last password change: 30 days ago +
diff --git a/apps/storybook/stories/Select.stories.tsx b/apps/storybook/stories/Select.stories.tsx index cda9043..e9e6bf0 100644 --- a/apps/storybook/stories/Select.stories.tsx +++ b/apps/storybook/stories/Select.stories.tsx @@ -26,7 +26,15 @@ type Story = StoryObj; export const Default: Story = { render: () => (
- diff --git a/apps/storybook/stories/Settings.stories.tsx b/apps/storybook/stories/Settings.stories.tsx index f38eddb..3e6d920 100644 --- a/apps/storybook/stories/Settings.stories.tsx +++ b/apps/storybook/stories/Settings.stories.tsx @@ -53,7 +53,11 @@ function FullSettingsDemo() { Manage email preferences} + hint={ + + Manage email preferences + + } action={} > setEmail(e.target.value)} /> @@ -112,7 +116,11 @@ function EmailSettingDemo() { Manage email preferences} + hint={ + + Manage email preferences + + } action={} > setValue(e.target.value)} /> @@ -175,11 +183,15 @@ export const NoFooter: Story = { API Calls - 12,847 / 50,000 + + 12,847 / 50,000 + Storage - 2.4 GB / 10 GB + + 2.4 GB / 10 GB + diff --git a/apps/storybook/stories/SplitButton.stories.tsx b/apps/storybook/stories/SplitButton.stories.tsx index 6bc9bfc..1a292b3 100644 --- a/apps/storybook/stories/SplitButton.stories.tsx +++ b/apps/storybook/stories/SplitButton.stories.tsx @@ -47,7 +47,13 @@ export const WithDestructiveAction: Story = { actions: [ { id: 'duplicate', label: 'Duplicate', onClick: fn() }, { id: 'template', label: 'Save as template', onClick: fn(), separator: true }, - { id: 'delete', label: 'Delete', variant: 'destructive', icon: , onClick: fn() }, + { + id: 'delete', + label: 'Delete', + variant: 'destructive', + icon: , + onClick: fn(), + }, ], }, }; @@ -55,28 +61,16 @@ export const WithDestructiveAction: Story = { export const Variants: Story = { render: () => ( - + Default - + Outline - + Secondary - + Destructive @@ -86,28 +80,16 @@ export const Variants: Story = { export const Sizes: Story = { render: () => ( - + Extra Small - + Small - + Default - + Large @@ -118,8 +100,6 @@ export const Loading: Story = { args: { children: 'Processing', loading: true, - actions: [ - { id: 'cancel', label: 'Cancel' }, - ], + actions: [{ id: 'cancel', label: 'Cancel' }], }, }; diff --git a/apps/storybook/stories/Table.stories.tsx b/apps/storybook/stories/Table.stories.tsx index cefe27c..b513c66 100644 --- a/apps/storybook/stories/Table.stories.tsx +++ b/apps/storybook/stories/Table.stories.tsx @@ -212,4 +212,3 @@ export const Bordered: Story = { ), }; - diff --git a/apps/storybook/stories/Toggle.stories.tsx b/apps/storybook/stories/Toggle.stories.tsx index 25ab59d..e685755 100644 --- a/apps/storybook/stories/Toggle.stories.tsx +++ b/apps/storybook/stories/Toggle.stories.tsx @@ -80,4 +80,3 @@ export const FormattingToolbar: Story = { ), }; - diff --git a/apps/storybook/tests/AppShell.test.tsx b/apps/storybook/tests/AppShell.test.tsx new file mode 100644 index 0000000..41d25b1 --- /dev/null +++ b/apps/storybook/tests/AppShell.test.tsx @@ -0,0 +1,123 @@ +import { render, within } from '@testing-library/react'; +import * as React from 'react'; +import { describe, expect, it } from 'vitest'; +import { AppShell, AppShellBody, AppShellRail, AppShellRailItem } from '@trycompai/design-system'; + +function Icon() { + return ; +} + +/** + * `AppShell` mirrors the rail into an always-mounted mobile drawer, so every + * rail item exists twice in the DOM. Queries are scoped to the desktop rail. + */ +function renderRail(children: React.ReactNode) { + const { container } = render( + + + {children} + + , + ); + + const rail = container.querySelector('[data-slot="app-shell-rail"]'); + expect(rail).not.toBeNull(); + return rail as HTMLElement; +} + +function getRailItem(rail: HTMLElement) { + const items = rail.querySelectorAll('[data-slot="app-shell-rail-item"]'); + expect(items).toHaveLength(1); + return items[0]; +} + +describe('AppShellRailItem', () => { + it('renders a button by default', () => { + const rail = renderRail(} />); + + const item = getRailItem(rail); + expect(item.tagName).toBe('BUTTON'); + // A bare + , ); await user.click(screen.getByRole('button')); @@ -63,7 +63,7 @@ describe('Button', () => { render( + , ); expect(screen.queryByTestId('mail-icon')).not.toBeInTheDocument(); }); @@ -77,7 +77,7 @@ describe('Button', () => { it('applies size classes', () => { render(); const button = screen.getByRole('button'); - expect(button).toHaveClass('h-9'); + expect(button).toHaveClass('h-8'); }); it('applies full width', () => { diff --git a/apps/storybook/tests/Card.test.tsx b/apps/storybook/tests/Card.test.tsx index 9ebdf8c..39d26b4 100644 --- a/apps/storybook/tests/Card.test.tsx +++ b/apps/storybook/tests/Card.test.tsx @@ -14,7 +14,7 @@ describe('Card', () => { render( Card content - + , ); expect(screen.getByText('Card content')).toBeInTheDocument(); }); @@ -28,7 +28,7 @@ describe('Card', () => { Content Footer - + , ); expect(screen.getByText('Title')).toBeInTheDocument(); @@ -62,7 +62,7 @@ describe('CardTitle', () => { My Title - + , ); expect(screen.getByText('My Title')).toBeInTheDocument(); }); @@ -75,7 +75,7 @@ describe('CardDescription', () => { My description - + , ); expect(screen.getByText('My description')).toBeInTheDocument(); }); diff --git a/apps/storybook/tests/Stack.test.tsx b/apps/storybook/tests/Stack.test.tsx index b1aa4d8..0b3813a 100644 --- a/apps/storybook/tests/Stack.test.tsx +++ b/apps/storybook/tests/Stack.test.tsx @@ -8,7 +8,7 @@ describe('Stack', () => {
Item 1
Item 2
-
+ , ); expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); @@ -33,7 +33,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('flex-row'); }); @@ -42,7 +42,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('flex-row-reverse'); }); @@ -51,7 +51,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('flex-col-reverse'); }); @@ -61,7 +61,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('gap-0'); }); @@ -70,7 +70,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('gap-1'); }); @@ -79,7 +79,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('gap-2'); }); @@ -93,7 +93,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('gap-6'); }); @@ -102,7 +102,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('gap-8'); }); @@ -113,7 +113,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('items-start'); }); @@ -122,7 +122,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('items-center'); }); @@ -131,7 +131,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('items-end'); }); @@ -145,7 +145,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('items-baseline'); }); @@ -161,7 +161,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('justify-center'); }); @@ -170,7 +170,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('justify-end'); }); @@ -179,7 +179,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('justify-between'); }); @@ -188,7 +188,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('justify-around'); }); @@ -197,7 +197,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('justify-evenly'); }); @@ -213,7 +213,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('flex-wrap'); }); @@ -222,7 +222,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack')).toHaveClass('flex-wrap-reverse'); }); @@ -238,7 +238,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack').tagName).toBe('NAV'); }); @@ -247,7 +247,7 @@ describe('Stack', () => { render( Content - + , ); expect(screen.getByTestId('stack').tagName).toBe('SECTION'); }); @@ -256,7 +256,7 @@ describe('Stack', () => { render(
  • Item
  • -
    + , ); expect(screen.getByTestId('stack').tagName).toBe('UL'); }); @@ -269,7 +269,7 @@ describe('VStack', () => {
    Item 1
    Item 2
    -
    + , ); expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); @@ -284,7 +284,7 @@ describe('VStack', () => { render( Content - + , ); expect(screen.getByTestId('vstack')).toHaveClass('gap-6'); }); @@ -293,7 +293,7 @@ describe('VStack', () => { render( Content - + , ); expect(screen.getByTestId('vstack')).toHaveClass('items-center'); }); @@ -302,7 +302,7 @@ describe('VStack', () => { render( Content - + , ); expect(screen.getByTestId('vstack')).toHaveClass('justify-between'); }); @@ -311,7 +311,7 @@ describe('VStack', () => { render( Content - + , ); expect(screen.getByTestId('vstack').tagName).toBe('SECTION'); }); @@ -323,7 +323,7 @@ describe('HStack', () => {
    Item 1
    Item 2
    -
    + , ); expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); @@ -338,7 +338,7 @@ describe('HStack', () => { render( Content - + , ); expect(screen.getByTestId('hstack')).toHaveClass('gap-8'); }); @@ -347,7 +347,7 @@ describe('HStack', () => { render( Content - + , ); expect(screen.getByTestId('hstack')).toHaveClass('items-end'); }); @@ -356,7 +356,7 @@ describe('HStack', () => { render( Content - + , ); expect(screen.getByTestId('hstack')).toHaveClass('justify-center'); }); @@ -365,7 +365,7 @@ describe('HStack', () => { render( Content - + , ); expect(screen.getByTestId('hstack')).toHaveClass('flex-wrap'); }); @@ -374,7 +374,7 @@ describe('HStack', () => { render( Content - + , ); expect(screen.getByTestId('hstack').tagName).toBe('NAV'); }); diff --git a/apps/storybook/tests/Typography.test.tsx b/apps/storybook/tests/Typography.test.tsx index 8343f61..0c64c70 100644 --- a/apps/storybook/tests/Typography.test.tsx +++ b/apps/storybook/tests/Typography.test.tsx @@ -20,33 +20,57 @@ describe('Heading', () => { }); it('renders as h2 when level="2"', () => { - render(H2); + render( + + H2 + , + ); expect(screen.getByTestId('heading').tagName).toBe('H2'); }); it('renders as h3 when level="3"', () => { - render(H3); + render( + + H3 + , + ); expect(screen.getByTestId('heading').tagName).toBe('H3'); }); it('renders as h4 when level="4"', () => { - render(H4); + render( + + H4 + , + ); expect(screen.getByTestId('heading').tagName).toBe('H4'); }); it('renders as h5 when level="5"', () => { - render(H5); + render( + + H5 + , + ); expect(screen.getByTestId('heading').tagName).toBe('H5'); }); it('renders as h6 when level="6"', () => { - render(H6); + render( + + H6 + , + ); expect(screen.getByTestId('heading').tagName).toBe('H6'); }); }); it('allows overriding element with as prop', () => { - render(Override); + render( + + Override + , + ); expect(screen.getByTestId('heading').tagName).toBe('H3'); }); @@ -57,13 +81,21 @@ describe('Heading', () => { }); it('applies muted variant', () => { - render(Muted); + render( + + Muted + , + ); expect(screen.getByTestId('heading')).toHaveClass('text-muted-foreground'); }); }); it('applies tracking variant', () => { - render(Tight); + render( + + Tight + , + ); expect(screen.getByTestId('heading')).toHaveClass('tracking-tight'); }); }); @@ -85,23 +117,39 @@ describe('Text', () => { }); it('renders as span when as="span"', () => { - render(Span); + render( + + Span + , + ); expect(screen.getByTestId('text').tagName).toBe('SPAN'); }); it('renders as div when as="div"', () => { - render(Div); + render( + + Div + , + ); expect(screen.getByTestId('text').tagName).toBe('DIV'); }); describe('size variants', () => { it('applies xs size', () => { - render(XS); + render( + + XS + , + ); expect(screen.getByTestId('text')).toHaveClass('text-xs'); }); it('applies sm size', () => { - render(SM); + render( + + SM + , + ); expect(screen.getByTestId('text')).toHaveClass('text-sm'); }); @@ -111,7 +159,11 @@ describe('Text', () => { }); it('applies lg size', () => { - render(LG); + render( + + LG + , + ); expect(screen.getByTestId('text')).toHaveClass('text-lg'); }); }); @@ -123,17 +175,29 @@ describe('Text', () => { }); it('applies muted variant', () => { - render(Muted); + render( + + Muted + , + ); expect(screen.getByTestId('text')).toHaveClass('text-muted-foreground'); }); it('applies primary variant', () => { - render(Primary); + render( + + Primary + , + ); expect(screen.getByTestId('text')).toHaveClass('text-primary'); }); it('applies destructive variant', () => { - render(Error); + render( + + Error + , + ); expect(screen.getByTestId('text')).toHaveClass('text-destructive'); }); }); @@ -145,23 +209,39 @@ describe('Text', () => { }); it('applies medium weight', () => { - render(Medium); + render( + + Medium + , + ); expect(screen.getByTestId('text')).toHaveClass('font-medium'); }); it('applies semibold weight', () => { - render(Semibold); + render( + + Semibold + , + ); expect(screen.getByTestId('text')).toHaveClass('font-semibold'); }); }); it('applies font variant', () => { - render(Monospace); + render( + + Monospace + , + ); expect(screen.getByTestId('text')).toHaveClass('font-mono'); }); it('applies leading variant', () => { - render(Relaxed); + render( + + Relaxed + , + ); expect(screen.getByTestId('text')).toHaveClass('leading-relaxed'); }); }); diff --git a/apps/storybook/vitest.config.ts b/apps/storybook/vitest.config.ts index db4debe..0e9c3e5 100644 --- a/apps/storybook/vitest.config.ts +++ b/apps/storybook/vitest.config.ts @@ -20,11 +20,7 @@ export default defineConfig({ '../../packages/design-system/src/components/**/*.tsx', '../../packages/design-system/lib/**/*.ts', ], - exclude: [ - '**/*.stories.tsx', - '**/index.ts', - '**/*.test.{ts,tsx}', - ], + exclude: ['**/*.stories.tsx', '**/index.ts', '**/*.test.{ts,tsx}'], thresholds: { statements: 70, branches: 60, diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 497dde7..a3c76db 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "@trycompai/design-system", - "version": "1.1.16", + "version": "1.1.20", "description": "Design system for Comp AI - shadcn-style components with Tailwind CSS", "type": "module", "main": "./src/index.ts", diff --git a/packages/design-system/src/components/atoms/badge.tsx b/packages/design-system/src/components/atoms/badge.tsx index 1c35d56..49c5da9 100644 --- a/packages/design-system/src/components/atoms/badge.tsx +++ b/packages/design-system/src/components/atoms/badge.tsx @@ -1,55 +1,53 @@ -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; -import { cva, type VariantProps } from "class-variance-authority"; +import { mergeProps } from '@base-ui/react/merge-props'; +import { useRender } from '@base-ui/react/use-render'; +import { cva, type VariantProps } from 'class-variance-authority'; const badgeVariants = cva( - "gap-1 font-semibold uppercase tracking-wider leading-none [text-box-trim:both] [text-box-edge:cap_alphabetic] transition-colors has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 [&>svg]:pointer-events-none inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 focus-visible:ring-ring/50 focus-visible:ring-[3px] overflow-hidden antialiased select-none", + 'gap-1 font-semibold uppercase tracking-wider leading-none [text-box-trim:both] [text-box-edge:cap_alphabetic] transition-colors has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 [&>svg]:pointer-events-none inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 focus-visible:ring-ring/50 focus-visible:ring-[3px] overflow-hidden antialiased select-none', { variants: { variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/90", - accent: "bg-primary/10 text-primary [a]:hover:bg-primary/15", - secondary: "bg-muted text-muted-foreground [a]:hover:bg-muted/80", + default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/90', + accent: 'bg-primary/10 text-primary [a]:hover:bg-primary/15', + secondary: 'bg-muted text-muted-foreground [a]:hover:bg-muted/80', destructive: - "bg-destructive/10 text-destructive [a]:hover:bg-destructive/15 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/15", - outline: - "border border-border/50 bg-transparent text-foreground [a]:hover:bg-muted/30", - ghost: - "bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground", - link: "bg-transparent text-primary underline-offset-4 hover:underline", + 'bg-destructive/10 text-destructive [a]:hover:bg-destructive/15 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/15', + outline: 'border border-border/50 bg-transparent text-foreground [a]:hover:bg-muted/30', + ghost: 'bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground', + link: 'bg-transparent text-primary underline-offset-4 hover:underline', }, shape: { - default: "rounded-sm", - pill: "rounded-full", + default: 'rounded-sm', + pill: 'rounded-full', }, size: { - default: "px-1.5 py-1 text-[10px] [&>svg]:size-2.5!", - sm: "px-1 py-0.5 text-[9px] [&>svg]:size-2.5!", - lg: "px-2 py-1.5 text-[11px] [&>svg]:size-3!", - xl: "px-2.5 py-2 text-xs [&>svg]:size-3.5!", + default: 'px-1.5 py-1 text-[10px] [&>svg]:size-2.5!', + sm: 'px-1 py-0.5 text-[9px] [&>svg]:size-2.5!', + lg: 'px-2 py-1.5 text-[11px] [&>svg]:size-3!', + xl: 'px-2.5 py-2 text-xs [&>svg]:size-3.5!', }, }, defaultVariants: { - variant: "default", - shape: "default", - size: "default", + variant: 'default', + shape: 'default', + size: 'default', }, }, ); -type BadgeProps = Omit, "className"> & +type BadgeProps = Omit, 'className'> & VariantProps; function Badge({ - variant = "default", - shape = "default", - size = "default", + variant = 'default', + shape = 'default', + size = 'default', render, ...props }: BadgeProps) { return useRender({ - defaultTagName: "span", - props: mergeProps<"span">( + defaultTagName: 'span', + props: mergeProps<'span'>( { className: badgeVariants({ variant, shape, size }), }, @@ -57,7 +55,7 @@ function Badge({ ), render, state: { - slot: "badge", + slot: 'badge', variant, shape, size, diff --git a/packages/design-system/src/components/atoms/button.tsx b/packages/design-system/src/components/atoms/button.tsx index c809b2a..063df98 100644 --- a/packages/design-system/src/components/atoms/button.tsx +++ b/packages/design-system/src/components/atoms/button.tsx @@ -1,78 +1,77 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; +import { Button as ButtonPrimitive } from '@base-ui/react/button'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; -import { Spinner } from "./spinner"; +import { Spinner } from './spinner'; const buttonVariants = cva( "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent text-[13px] font-medium leading-none [text-box-trim:both] [text-box-edge:cap_alphabetic] focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all duration-200 ease-out active:scale-[0.97] active:duration-75 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none cursor-pointer", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: 'bg-primary text-primary-foreground hover:bg-primary/90', outline: - "border-border! bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", + 'border-border! bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground', secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', ghost: - "hover:bg-accent hover:text-foreground dark:hover:bg-accent/50 aria-expanded:bg-accent aria-expanded:text-foreground", + 'hover:bg-accent hover:text-foreground dark:hover:bg-accent/50 aria-expanded:bg-accent aria-expanded:text-foreground', destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 focus-visible:border-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30", - link: "text-primary underline-offset-4 hover:underline", + 'bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 focus-visible:border-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30', + link: 'text-primary underline-offset-4 hover:underline', }, width: { - auto: "", - full: "w-full", + auto: '', + full: 'w-full', }, size: { default: - "h-7 gap-1 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", + 'h-7 gap-1 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5', xs: "h-5 gap-0.5 px-2 text-[11px] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-8 gap-1.5 px-3.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + lg: 'h-8 gap-1.5 px-3.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3', xl: "h-10 gap-2 px-5 text-sm has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4 [&_svg:not([class*='size-'])]:size-4", - icon: "size-7", - "icon-xs": "size-5 [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-6", - "icon-lg": "size-8", - "icon-xl": "size-10 [&_svg:not([class*='size-'])]:size-4", + icon: 'size-7', + 'icon-xs': "size-5 [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-6', + 'icon-lg': 'size-8', + 'icon-xl': "size-10 [&_svg:not([class*='size-'])]:size-4", // Round icon buttons - for avatar triggers and circular icons - "icon-round": "size-7 rounded-full", - "icon-round-xs": - "size-5 rounded-full [&_svg:not([class*='size-'])]:size-3", - "icon-round-sm": "size-6 rounded-full", - "icon-round-lg": "size-8 rounded-full", - "icon-round-xl": "size-10 rounded-full [&_svg:not([class*='size-'])]:size-4", + 'icon-round': 'size-7 rounded-full', + 'icon-round-xs': "size-5 rounded-full [&_svg:not([class*='size-'])]:size-3", + 'icon-round-sm': 'size-6 rounded-full', + 'icon-round-lg': 'size-8 rounded-full', + 'icon-round-xl': "size-10 rounded-full [&_svg:not([class*='size-'])]:size-4", // Calendar day button - special size for calendar day cells - "calendar-day": [ + 'calendar-day': [ // Base sizing - "relative isolate z-10 aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal", + 'relative isolate z-10 aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal', // Selection states - "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground", - "data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-middle=true]:rounded-none", - "data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius)", - "data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius)", + 'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground', + 'data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-middle=true]:rounded-none', + 'data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius)', + 'data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius)', // Focus states (from parent day cell) - "group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-[3px]", + 'group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-[3px]', // Dark mode hover - "dark:hover:text-foreground", + 'dark:hover:text-foreground', // Nested span styling for additional content - "[&>span]:text-xs [&>span]:opacity-70", - ].join(" "), + '[&>span]:text-xs [&>span]:opacity-70', + ].join(' '), }, }, defaultVariants: { - variant: "default", - width: "auto", - size: "default", + variant: 'default', + width: 'auto', + size: 'default', }, }, ); -type ButtonProps = Omit & +type ButtonProps = Omit & VariantProps & { /** Button type attribute for native button usage */ - type?: "button" | "submit" | "reset"; + type?: 'button' | 'submit' | 'reset'; /** Show loading spinner and disable button */ loading?: boolean; /** Icon to show on the left side of the button */ @@ -84,14 +83,14 @@ type ButtonProps = Omit & const Button = React.forwardRef( ( { - variant = "default", - width = "auto", - size = "default", + variant = 'default', + width = 'auto', + size = 'default', loading = false, iconLeft, iconRight, disabled, - type = "button", + type = 'button', children, render, ...props @@ -130,7 +129,7 @@ const Button = React.forwardRef( }, ); -Button.displayName = "Button"; +Button.displayName = 'Button'; export { Button, buttonVariants }; export type { ButtonProps }; diff --git a/packages/design-system/src/components/atoms/kbd.tsx b/packages/design-system/src/components/atoms/kbd.tsx index 50b9adc..c689c68 100644 --- a/packages/design-system/src/components/atoms/kbd.tsx +++ b/packages/design-system/src/components/atoms/kbd.tsx @@ -9,13 +9,7 @@ function Kbd({ ...props }: Omit, 'className'>) { } function KbdGroup({ ...props }: Omit, 'className'>) { - return ( - - ); + return ; } export { Kbd, KbdGroup }; diff --git a/packages/design-system/src/components/atoms/logo.tsx b/packages/design-system/src/components/atoms/logo.tsx index 3b563b7..a4d3928 100644 --- a/packages/design-system/src/components/atoms/logo.tsx +++ b/packages/design-system/src/components/atoms/logo.tsx @@ -11,12 +11,7 @@ function Logo({ variant = 'dark', ...props }: LogoProps) { const fill = variant === 'light' ? '#FFFFFF' : '#16171B'; return ( - + + , 'className'>) { return ( - + + + ); } diff --git a/packages/design-system/src/components/atoms/stack.tsx b/packages/design-system/src/components/atoms/stack.tsx index f4c4fed..5c814f2 100644 --- a/packages/design-system/src/components/atoms/stack.tsx +++ b/packages/design-system/src/components/atoms/stack.tsx @@ -55,11 +55,20 @@ const stackVariants = cva('flex', { }, }); -type StackElement = 'div' | 'section' | 'nav' | 'ul' | 'ol' | 'main' | 'article' | 'aside' | 'header' | 'footer'; +type StackElement = + | 'div' + | 'section' + | 'nav' + | 'ul' + | 'ol' + | 'main' + | 'article' + | 'aside' + | 'header' + | 'footer'; interface StackProps - extends Omit, 'className'>, - VariantProps { + extends Omit, 'className'>, VariantProps { as?: StackElement; } @@ -84,7 +93,9 @@ function Stack({ type VStackProps = Omit; function VStack({ gap, align, justify, wrap, ...props }: VStackProps) { - return ; + return ( + + ); } type HStackProps = Omit; diff --git a/packages/design-system/src/components/atoms/toggle.tsx b/packages/design-system/src/components/atoms/toggle.tsx index 29c8e18..894079c 100644 --- a/packages/design-system/src/components/atoms/toggle.tsx +++ b/packages/design-system/src/components/atoms/toggle.tsx @@ -28,11 +28,7 @@ function Toggle({ ...props }: Omit & VariantProps) { return ( - + ); } diff --git a/packages/design-system/src/components/molecules/ai-chat.tsx b/packages/design-system/src/components/molecules/ai-chat.tsx index 4c2c18e..030b151 100644 --- a/packages/design-system/src/components/molecules/ai-chat.tsx +++ b/packages/design-system/src/components/molecules/ai-chat.tsx @@ -75,7 +75,9 @@ function AIChat({
    Ask AI - {navigator?.platform?.includes('Mac') ? '⌘' : 'Ctrl+'} + {typeof navigator !== 'undefined' && navigator.platform?.includes('Mac') + ? '⌘' + : 'Ctrl+'} {shortcut} } /> - - {isOpen ? 'Close AI Chat' : 'Open AI Chat'} - + {isOpen ? 'Close AI Chat' : 'Open AI Chat'} ); } @@ -231,7 +233,9 @@ function AIChatDefaultContent({ onClose }: { onClose: () => void }) {
    - Press Esc to close + + Press Esc to close +
    diff --git a/packages/design-system/src/components/molecules/card.tsx b/packages/design-system/src/components/molecules/card.tsx index ec36d11..9749019 100644 --- a/packages/design-system/src/components/molecules/card.tsx +++ b/packages/design-system/src/components/molecules/card.tsx @@ -1,55 +1,54 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; const cardVariants = cva( - "bg-card text-card-foreground border border-border overflow-hidden py-4 text-sm has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 group/card flex flex-col", + 'bg-card text-card-foreground border border-border overflow-hidden py-4 text-sm has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 group/card flex flex-col', { variants: { width: { - auto: "", // shrink to content (default behavior) - full: "w-full", // fill parent container - sm: "w-sm", // 24rem (384px) - md: "w-md", // 28rem (448px) - lg: "w-lg", // 32rem (512px) - xl: "w-xl", // 36rem (576px) - "2xl": "w-2xl", // 42rem (672px) - "3xl": "w-3xl", // 48rem (768px) + auto: '', // shrink to content (default behavior) + full: 'w-full', // fill parent container + sm: 'w-sm', // 24rem (384px) + md: 'w-md', // 28rem (448px) + lg: 'w-lg', // 32rem (512px) + xl: 'w-xl', // 36rem (576px) + '2xl': 'w-2xl', // 42rem (672px) + '3xl': 'w-3xl', // 48rem (768px) }, maxWidth: { - sm: "max-w-sm", - md: "max-w-md", - lg: "max-w-lg", - xl: "max-w-xl", - "2xl": "max-w-2xl", - "3xl": "max-w-3xl", - full: "max-w-full", + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + '2xl': 'max-w-2xl', + '3xl': 'max-w-3xl', + full: 'max-w-full', }, spacing: { - default: "gap-4 data-[size=sm]:gap-3", - tight: "gap-3 data-[size=sm]:gap-2.5", - relaxed: "gap-6 data-[size=sm]:gap-4", + default: 'gap-4 data-[size=sm]:gap-3', + tight: 'gap-3 data-[size=sm]:gap-2.5', + relaxed: 'gap-6 data-[size=sm]:gap-4', }, shadow: { - none: "", - soft: "shadow-[0_1px_2px_rgba(9,9,11,.04)]", - md: "shadow-md", + none: '', + soft: 'shadow-[0_1px_2px_rgba(9,9,11,.04)]', + md: 'shadow-md', }, radius: { - md: "rounded-md *:[img:first-child]:rounded-t-md *:[img:last-child]:rounded-b-md", - lg: "rounded-lg *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg", - xl: "rounded-xl *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", - "2xl": - "rounded-2xl *:[img:first-child]:rounded-t-2xl *:[img:last-child]:rounded-b-2xl", + md: 'rounded-md *:[img:first-child]:rounded-t-md *:[img:last-child]:rounded-b-md', + lg: 'rounded-lg *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg', + xl: 'rounded-xl *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl', + '2xl': 'rounded-2xl *:[img:first-child]:rounded-t-2xl *:[img:last-child]:rounded-b-2xl', }, disabled: { - true: "opacity-60 pointer-events-none select-none", + true: 'opacity-60 pointer-events-none select-none', }, }, defaultVariants: { - width: "auto", - spacing: "default", - shadow: "none", - radius: "md", + width: 'auto', + spacing: 'default', + shadow: 'none', + radius: 'md', disabled: undefined, }, }, @@ -57,9 +56,9 @@ const cardVariants = cva( interface CardProps extends - Omit, "className" | "title">, + Omit, 'className' | 'title'>, VariantProps { - size?: "default" | "sm"; + size?: 'default' | 'sm'; /** Card title - renders in CardHeader */ title?: React.ReactNode; /** Card description - renders below title in CardHeader */ @@ -71,7 +70,7 @@ interface CardProps } function Card({ - size = "default", + size = 'default', width, maxWidth, spacing, @@ -92,20 +91,16 @@ function Card({ if (React.isValidElement(child)) { // Prefer checking component identity. `data-slot` is applied inside the component render, // so it won't exist on `child.props` unless manually passed in. - if ( - child.type === CardHeader || - child.type === CardContent || - child.type === CardFooter - ) { + if (child.type === CardHeader || child.type === CardContent || child.type === CardFooter) { return true; } // Fallback for direct DOM usage. const props = child.props as Record; return ( - props["data-slot"] === "card-header" || - props["data-slot"] === "card-content" || - props["data-slot"] === "card-footer" + props['data-slot'] === 'card-header' || + props['data-slot'] === 'card-content' || + props['data-slot'] === 'card-footer' ); } return false; @@ -115,7 +110,7 @@ function Card({
    {headerAction}} )} - {hasCompoundChildren - ? children - : children && {children}} + {hasCompoundChildren ? children : children && {children}} {footer && {footer}}
    ); } -function CardHeader({ - ...props -}: Omit, "className">) { +function CardHeader({ ...props }: Omit, 'className'>) { return (
    , "className">) { +function CardTitle({ ...props }: Omit, 'className'>) { return (
    , "className">) { - return ( -
    - ); +function CardDescription({ ...props }: Omit, 'className'>) { + return
    ; } -function CardAction({ - ...props -}: Omit, "className">) { +function CardAction({ ...props }: Omit, 'className'>) { return (
    , "className">) { +function CardContent({ ...props }: Omit, 'className'>) { return ( -
    +
    ); } -function CardFooter({ - ...props -}: Omit, "className">) { +function CardFooter({ ...props }: Omit, 'className'>) { return (
    0 - ? [{ id: 'default', label: '', items }, ...groups] - : groups; + const allGroups = items.length > 0 ? [{ id: 'default', label: '', items }, ...groups] : groups; return ( <> @@ -130,9 +128,7 @@ function CommandSearch({ > {item.icon} {item.label} - {item.shortcut && ( - {item.shortcut} - )} + {item.shortcut && {item.shortcut}} ))} diff --git a/packages/design-system/src/components/molecules/data-table-header.tsx b/packages/design-system/src/components/molecules/data-table-header.tsx index bcb280b..3bb0b1b 100644 --- a/packages/design-system/src/components/molecules/data-table-header.tsx +++ b/packages/design-system/src/components/molecules/data-table-header.tsx @@ -35,16 +35,9 @@ export interface DataTableSearchProps { onChange?: (value: string) => void; } -function DataTableSearch({ - placeholder = 'Search...', - value, - onChange, -}: DataTableSearchProps) { +function DataTableSearch({ placeholder = 'Search...', value, onChange }: DataTableSearchProps) { return ( -
    +
    +
    {children}
    ); diff --git a/packages/design-system/src/components/molecules/empty.tsx b/packages/design-system/src/components/molecules/empty.tsx index ef2d972..dd2e2e8 100644 --- a/packages/design-system/src/components/molecules/empty.tsx +++ b/packages/design-system/src/components/molecules/empty.tsx @@ -50,13 +50,7 @@ function EmptyMedia({ } function EmptyTitle({ ...props }: Omit, 'className'>) { - return ( -
    - ); + return
    ; } function EmptyDescription({ ...props }: Omit, 'className'>) { diff --git a/packages/design-system/src/components/molecules/input-otp.tsx b/packages/design-system/src/components/molecules/input-otp.tsx index d3fdd01..fb12d08 100644 --- a/packages/design-system/src/components/molecules/input-otp.tsx +++ b/packages/design-system/src/components/molecules/input-otp.tsx @@ -3,10 +3,7 @@ import * as React from 'react'; import { Subtract } from '@carbon/icons-react'; -function InputOTP({ - className: _className, - ...props -}: React.ComponentProps) { +function InputOTP({ className: _className, ...props }: React.ComponentProps) { return ( , 'className'> tabs?: React.ReactNode; } -function PageHeader({ title, actions, breadcrumbs, backHref, backLabel = 'Back', tabs, children, ...props }: PageHeaderProps) { +function PageHeader({ + title, + actions, + breadcrumbs, + backHref, + backLabel = 'Back', + tabs, + children, + ...props +}: PageHeaderProps) { const childArray = React.Children.toArray(children); const extractedActionChildren: React.ReactNode[] = []; const extractedDescriptionChildren: React.ReactNode[] = []; @@ -35,15 +44,15 @@ function PageHeader({ title, actions, breadcrumbs, backHref, backLabel = 'Back', React.isValidElement(child) && (child.type === PageHeaderDescription || (typeof child.type === 'function' && - (child.type as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot === 'description')) + (child.type as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot === + 'description')) ) { extractedDescriptionChildren.push(child); } }); const resolvedActions = - actions ?? - (extractedActionChildren.length > 0 ? extractedActionChildren : undefined); + actions ?? (extractedActionChildren.length > 0 ? extractedActionChildren : undefined); return (
    @@ -82,11 +91,7 @@ function PageHeader({ title, actions, breadcrumbs, backHref, backLabel = 'Back', {extractedDescriptionChildren.length > 0 && extractedDescriptionChildren} {/* Tabs section */} - {tabs && ( -
    - {tabs} -
    - )} + {tabs &&
    {tabs}
    }
    ); } @@ -105,6 +110,7 @@ function PageHeaderDescription({ ...props }: Omit, 'cl // Mark compound slots so PageHeader can detect them even if module instances differ. (PageHeaderActions as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot = 'actions'; -(PageHeaderDescription as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot = 'description'; +(PageHeaderDescription as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot = + 'description'; export { PageHeader, PageHeaderActions, PageHeaderDescription }; diff --git a/packages/design-system/src/components/molecules/pagination.tsx b/packages/design-system/src/components/molecules/pagination.tsx index 7788f42..9557ea8 100644 --- a/packages/design-system/src/components/molecules/pagination.tsx +++ b/packages/design-system/src/components/molecules/pagination.tsx @@ -16,9 +16,7 @@ function Pagination({ ...props }: Omit, 'className'> } function PaginationContent({ ...props }: Omit, 'className'>) { - return ( -
      - ); + return
        ; } function PaginationItem({ ...props }: Omit, 'className'>) { diff --git a/packages/design-system/src/components/molecules/popover.tsx b/packages/design-system/src/components/molecules/popover.tsx index 3fbe742..95f4a08 100644 --- a/packages/design-system/src/components/molecules/popover.tsx +++ b/packages/design-system/src/components/molecules/popover.tsx @@ -8,7 +8,9 @@ function Popover({ ...props }: PopoverPrimitive.Root.Props) { } function PopoverTrigger({ className, ...props }: PopoverPrimitive.Trigger.Props) { - return ; + return ( + + ); } function PopoverContent({ diff --git a/packages/design-system/src/components/molecules/radio-group.tsx b/packages/design-system/src/components/molecules/radio-group.tsx index 5c03bc3..9436501 100644 --- a/packages/design-system/src/components/molecules/radio-group.tsx +++ b/packages/design-system/src/components/molecules/radio-group.tsx @@ -4,13 +4,7 @@ import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group'; import { CircleFilled } from '@carbon/icons-react'; function RadioGroup({ ...props }: Omit) { - return ( - - ); + return ; } function RadioGroupItem({ ...props }: Omit) { diff --git a/packages/design-system/src/components/molecules/scroll-area.tsx b/packages/design-system/src/components/molecules/scroll-area.tsx index 76121f2..0107350 100644 --- a/packages/design-system/src/components/molecules/scroll-area.tsx +++ b/packages/design-system/src/components/molecules/scroll-area.tsx @@ -1,9 +1,6 @@ import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area'; -function ScrollArea({ - children, - ...props -}: Omit) { +function ScrollArea({ children, ...props }: Omit) { return ( , 'className'>, - VariantProps { + extends Omit, 'className'>, VariantProps { /** The setting label */ label: string; /** Optional description text */ @@ -36,14 +35,7 @@ interface SettingRowProps * A horizontal row for a single setting with label/description on the left * and a control (switch, button, select, etc.) on the right. */ -function SettingRow({ - label, - description, - disabled, - size, - children, - ...props -}: SettingRowProps) { +function SettingRow({ label, description, disabled, size, children, ...props }: SettingRowProps) { return (
        +
        -