From 5b9075c6c2ea0a90bfd0bb2e314c978dfb675de6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 13:27:02 +0000 Subject: [PATCH 1/2] feat: add shadcn MCP server configuration Enable AI-assisted component management for the frontend by adding the shadcn MCP server config. This allows browsing, searching, and installing shadcn components using natural language prompts. --- .mcp.json | 8 ++++++++ CLAUDE.md | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..9d30c31 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": ["shadcn@latest", "mcp"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 0601eff..fdff601 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,3 +97,20 @@ Pre-commit hooks auto-run on commit (ruff format, ruff check, import sorting). - Services: MariaDB 10.6, Redis (ports 13000, 11000) - Python 3.14, Node 24 - Runs: `bench --site test_site run-tests --app hazelnode` + +## MCP Servers + +### shadcn MCP + +The project includes shadcn MCP server configuration (`.mcp.json`) for AI-assisted component management. This enables: + +- Browse and search shadcn components +- Install components using natural language +- Works with Claude Code, Cursor, and VS Code + +**Example prompts:** +- "Show me all available components in the shadcn registry" +- "Add the button, dialog and card components to my project" +- "Create a contact form using components from the shadcn registry" + +**Configuration:** `frontend/components.json` (style: new-york, baseColor: slate) From 3ca12f918a9f7c2bcb3b1e8e281ce74f03481a4b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 13:34:47 +0000 Subject: [PATCH 2/2] refactor: migrate dialogs from HeadlessUI to shadcn/Radix - Replace catalyst-dialog (HeadlessUI) with shadcn dialog (Radix) - Update create-dialog, set-trigger-dialog, confirm-dialog components - Add data-testid attributes for stable E2E test selectors - Update workflow-list.page.ts E2E page object for new dialog selectors - Remove unused catalyst-dialog.tsx --- e2e/pages/workflow-list.page.ts | 16 +-- .../src/components/common/confirm-dialog.tsx | 56 ++++---- .../src/components/ui/catalyst-dialog.tsx | 134 ------------------ .../components/workflows/create-dialog.tsx | 42 +++--- .../workflows/set-trigger-dialog.tsx | 63 ++++---- 5 files changed, 96 insertions(+), 215 deletions(-) delete mode 100644 frontend/src/components/ui/catalyst-dialog.tsx diff --git a/e2e/pages/workflow-list.page.ts b/e2e/pages/workflow-list.page.ts index de9554c..34f8bb2 100644 --- a/e2e/pages/workflow-list.page.ts +++ b/e2e/pages/workflow-list.page.ts @@ -29,13 +29,13 @@ export class WorkflowListPage { this.loadingSkeleton = page.locator('[class*="skeleton"]'); this.errorMessage = page.locator('text=Error loading workflows'); - // Create dialog elements - HeadlessUI dialog panel - // Use content-based selectors as HeadlessUI transitions can leave role="dialog" in DOM - this.createDialog = page.locator('text=Create new workflow'); - // Input has id="title" - this.titleInput = page.locator('input#title'); - this.createButton = page.locator('button:has-text("Create"):not(:has-text("Creating"))'); - this.cancelButton = page.locator('button:has-text("Cancel")'); + // Create dialog elements - shadcn/Radix dialog + // Use data-testid for stable selectors with Radix dialogs + this.createDialog = page.locator('[data-testid="create-workflow-dialog"]'); + // Input has id="title" within the dialog + this.titleInput = this.createDialog.locator('input#title'); + this.createButton = this.createDialog.locator('button:has-text("Create"):not(:has-text("Creating"))'); + this.cancelButton = this.createDialog.locator('button:has-text("Cancel")'); } /** @@ -74,7 +74,7 @@ export class WorkflowListPage { await this.newWorkflowButton.waitFor({ state: 'visible', timeout: 10000 }); await expect(this.newWorkflowButton).toBeEnabled(); await this.newWorkflowButton.click(); - // Wait for dialog title to appear (HeadlessUI transitions) + // Wait for dialog to appear (Radix uses data-state="open") await this.createDialog.waitFor({ state: 'visible', timeout: 15000 }); // Wait for input to be visible and interactable await this.titleInput.waitFor({ state: 'visible', timeout: 5000 }); diff --git a/frontend/src/components/common/confirm-dialog.tsx b/frontend/src/components/common/confirm-dialog.tsx index 80d7fa0..2c2e7d2 100644 --- a/frontend/src/components/common/confirm-dialog.tsx +++ b/frontend/src/components/common/confirm-dialog.tsx @@ -1,10 +1,12 @@ import { createContext, useCallback, useRef, useState } from 'react'; import { Dialog, - DialogActions, + DialogContent, DialogDescription, + DialogFooter, + DialogHeader, DialogTitle, -} from '@/components/ui/catalyst-dialog'; +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; interface ConfirmOptions { @@ -47,33 +49,35 @@ export function ConfirmDialogProvider({ {children} fn.current && fn.current(false)} + onOpenChange={(open) => !open && fn.current && fn.current(false)} > - {state.title} + + + {state.title} + {state.description && ( + {state.description} + )} + - {state.description && ( - {state.description} - )} - - - - - + + + + + ); diff --git a/frontend/src/components/ui/catalyst-dialog.tsx b/frontend/src/components/ui/catalyst-dialog.tsx deleted file mode 100644 index 423253d..0000000 --- a/frontend/src/components/ui/catalyst-dialog.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { - Description as HeadlessDescription, - Dialog as HeadlessDialog, - DialogPanel as HeadlessDialogPanel, - DialogTitle as HeadlessDialogTitle, - Transition as HeadlessTransition, - TransitionChild as HeadlessTransitionChild, - type DialogProps as HeadlessDialogProps, -} from '@headlessui/react'; -import clsx from 'clsx'; -import type React from 'react'; -import { Fragment } from 'react'; -import { Text } from './text'; - -const sizes = { - xs: 'sm:max-w-xs', - sm: 'sm:max-w-sm', - md: 'sm:max-w-md', - lg: 'sm:max-w-lg', - xl: 'sm:max-w-xl', - '2xl': 'sm:max-w-2xl', - '3xl': 'sm:max-w-3xl', - '4xl': 'sm:max-w-4xl', - '5xl': 'sm:max-w-5xl', -}; - -export function Dialog({ - open, - onClose, - size = 'lg', - className, - children, - ...props -}: { - size?: keyof typeof sizes; - children: React.ReactNode; -} & HeadlessDialogProps) { - return ( - - - -
- - - -
- - {children} - -
-
- - - ); -} - -export function DialogTitle({ - className, - ...props -}: React.ComponentPropsWithoutRef<'div'>) { - return ( - - ); -} - -export function DialogDescription({ - className, - ...props -}: React.ComponentPropsWithoutRef<'div'>) { - return ( - - ); -} - -export function DialogBody({ - className, - ...props -}: React.ComponentPropsWithoutRef<'div'>) { - return
; -} - -export function DialogActions({ - className, - ...props -}: React.ComponentPropsWithoutRef<'div'>) { - return ( -
- ); -} diff --git a/frontend/src/components/workflows/create-dialog.tsx b/frontend/src/components/workflows/create-dialog.tsx index f5c3631..64b9ca0 100644 --- a/frontend/src/components/workflows/create-dialog.tsx +++ b/frontend/src/components/workflows/create-dialog.tsx @@ -4,25 +4,24 @@ import { useFrappeCreateDoc } from 'frappe-react-sdk'; import { Dialog, - DialogActions, - DialogBody, + DialogContent, + DialogFooter, + DialogHeader, DialogTitle, -} from '@/components/ui/catalyst-dialog'; +} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { useNavigate } from '@tanstack/react-router'; -import { type DialogProps as HeadlessDialogProps } from '@headlessui/react'; export default function CreateWorkflowDialog({ open, onClose, - ...props }: { open: boolean | undefined; onClose: (isOpen: boolean) => void; -} & HeadlessDialogProps) { +}) { const [workflowTitle, setWorkflowTitle] = useState(''); const navigate = useNavigate(); @@ -53,11 +52,16 @@ export default function CreateWorkflowDialog({ } return ( - - Create new workflow + + + + Create new workflow + - -
+
- - - - - + + + + +
); } diff --git a/frontend/src/components/workflows/set-trigger-dialog.tsx b/frontend/src/components/workflows/set-trigger-dialog.tsx index 9a5ca82..b649bad 100644 --- a/frontend/src/components/workflows/set-trigger-dialog.tsx +++ b/frontend/src/components/workflows/set-trigger-dialog.tsx @@ -3,11 +3,11 @@ import { useFrappeGetDocList, useFrappeUpdateDoc } from 'frappe-react-sdk'; import { Dialog, + DialogContent, DialogDescription, - DialogBody, + DialogHeader, DialogTitle, -} from '@/components/ui/catalyst-dialog'; -import { type DialogProps as HeadlessDialogProps } from '@headlessui/react'; +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; @@ -20,7 +20,7 @@ export default function SetTriggerDialog({ }: { open: boolean | undefined; onClose: (isOpen: boolean) => void; -} & HeadlessDialogProps) { +}) { const editorStore = useEditorStore((state) => ({ appendNode: state.appendNode, removeNode: state.removeNode, @@ -58,32 +58,39 @@ export default function SetTriggerDialog({ } return ( - - Select a trigger - - The event that will trigger a run of this workflow - + + + + Select a trigger + + The event that will trigger a run of this workflow + + - - {isLoading && ( - - )} - {error && ( - Error fetching list of triggers... - )} +
+ {isLoading && ( + + )} + {error && ( + Error fetching list of triggers... + )} - {!error && triggers && ( -
    - {triggers.map((trigger) => ( -
  1. - -
  2. - ))} -
- )} - + {!error && triggers && ( +
    + {triggers.map((trigger) => ( +
  1. + +
  2. + ))} +
+ )} +
+
); }