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)
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}
);
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 (
-