From f76034b9471f80e87c2ddc1a8f5687972657e720 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 23 Dec 2025 22:24:02 +0100 Subject: [PATCH 001/108] Add ShadCN MCP --- .mcp.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.mcp.json b/.mcp.json index 399d235a0..e3b4db813 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,6 +4,10 @@ "command": "dotnet", "args": ["run", "--project", "developer-cli", "mcp"] }, + "shadcn": { + "command": "npx", + "args": ["-y", "shadcn@latest", "mcp"] + }, "aspire": { "url": "http://localhost:9096/mcp", "type": "http" From 40d8043003b3eaa5dbeb766e171513b47b2c80d3 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 23 Dec 2025 22:28:20 +0100 Subject: [PATCH 002/108] Add BaseUI and ShadCN component configuraiton --- application/package-lock.json | 123 +++++++++++++- application/package.json | 3 + application/shared-webapp/ui/components.json | 22 +++ .../shared-webapp/ui/components/AppLayout.tsx | 2 +- application/shared-webapp/ui/tailwind.css | 40 +++-- application/shared-webapp/ui/theme.css | 160 ++++++++++-------- application/shared-webapp/ui/tsconfig.json | 6 +- .../shared-webapp/ui/{cn.ts => utils.ts} | 0 8 files changed, 275 insertions(+), 81 deletions(-) create mode 100644 application/shared-webapp/ui/components.json rename application/shared-webapp/ui/{cn.ts => utils.ts} (100%) diff --git a/application/package-lock.json b/application/package-lock.json index fe9d04152..40eadb429 100644 --- a/application/package-lock.json +++ b/application/package-lock.json @@ -13,6 +13,7 @@ "shared-webapp/*" ], "dependencies": { + "@base-ui/react": "1.0.0", "@fontsource/inter": "5.2.8", "@lingui/core": "5.7.0", "@lingui/macro": "5.7.0", @@ -25,6 +26,8 @@ "@spectrum-icons/illustrations": "3.6.27", "@tanstack/react-query": "5.90.16", "@tanstack/react-router": "1.145.7", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "lucide-react": "0.562.0", "openapi-fetch": "0.15.0", "openapi-react-query": "0.5.1", @@ -383,6 +386,60 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/react": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.0.0.tgz", + "integrity": "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@base-ui/utils": "0.2.3", + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "reselect": "^5.1.1", + "tabbable": "^6.3.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@base-ui/utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@floating-ui/utils": "^0.2.10", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@biomejs/biome": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz", @@ -1031,6 +1088,44 @@ "npm": ">=10" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@fontsource/inter": { "version": "5.2.8", "license": "OFL-1.1", @@ -5119,7 +5214,7 @@ }, "node_modules/@types/react": { "version": "19.2.7", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -5476,6 +5571,18 @@ "fsevents": "~2.3.1" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "dev": true, @@ -5693,7 +5800,7 @@ }, "node_modules/csstype": { "version": "3.2.3", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -7419,6 +7526,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "devOptional": true, @@ -7784,6 +7897,12 @@ "node": ">= 10" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "license": "MIT", diff --git a/application/package.json b/application/package.json index 7f2c2645a..541f1ef64 100644 --- a/application/package.json +++ b/application/package.json @@ -18,6 +18,7 @@ "lint": "turbo lint" }, "dependencies": { + "@base-ui/react": "1.0.0", "@fontsource/inter": "5.2.8", "@lingui/core": "5.7.0", "@lingui/macro": "5.7.0", @@ -30,6 +31,8 @@ "@spectrum-icons/illustrations": "3.6.27", "@tanstack/react-query": "5.90.16", "@tanstack/react-router": "1.145.7", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "lucide-react": "0.562.0", "openapi-fetch": "0.15.0", "openapi-react-query": "0.5.1", diff --git a/application/shared-webapp/ui/components.json b/application/shared-webapp/ui/components.json new file mode 100644 index 000000000..bd7591dc9 --- /dev/null +++ b/application/shared-webapp/ui/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-vega", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "tailwind.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/utils", + "ui": "@/components", + "lib": "@", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/application/shared-webapp/ui/components/AppLayout.tsx b/application/shared-webapp/ui/components/AppLayout.tsx index 73111153d..618589ccd 100644 --- a/application/shared-webapp/ui/components/AppLayout.tsx +++ b/application/shared-webapp/ui/components/AppLayout.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; -import { cn } from "../cn"; import { useSideMenuLayout } from "../hooks/useSideMenuLayout"; +import { cn } from "../utils"; type AppLayoutVariant = "full" | "center"; diff --git a/application/shared-webapp/ui/tailwind.css b/application/shared-webapp/ui/tailwind.css index f426b15ae..5016c40a3 100644 --- a/application/shared-webapp/ui/tailwind.css +++ b/application/shared-webapp/ui/tailwind.css @@ -33,26 +33,40 @@ } @theme inline { + --font-sans: "Inter", sans-serif; --color-background: var(--background); --color-foreground: var(--foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-border: var(--border); - --color-input: var(--input); --color-card: var(--card); --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + /* Custom color variables */ --color-hover-background: var(--hover-background); --color-active-background: var(--active-background); --color-selected-hover-background: var(--selected-hover-background); @@ -65,10 +79,14 @@ --color-warning-foreground: var(--warning-foreground); --color-info: var(--info); --color-info-foreground: var(--info-foreground); - - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); + /* Radius scale */ --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); } /* Custom dialog width utilities */ @@ -90,10 +108,10 @@ @layer base { * { - @apply border-border; + @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; + @apply font-sans bg-background text-foreground; font-feature-settings: "rlig" 1, "calt" 1; } html { diff --git a/application/shared-webapp/ui/theme.css b/application/shared-webapp/ui/theme.css index ea3537bed..52fa58b1e 100644 --- a/application/shared-webapp/ui/theme.css +++ b/application/shared-webapp/ui/theme.css @@ -1,74 +1,102 @@ :root, .light { - --background: hsl(200 13% 95%); - --foreground: hsl(219 42% 11%); - --muted: hsl(210 40% 96.1%); - --muted-foreground: hsl(215.4 16.3% 46.9%); - --popover: hsl(200 13% 95%); /* Same as background */ - --popover-foreground: hsl(219 42% 11%); - --border: hsl(214.3 31.8% 91.4%); - --input: hsl(214.3 31.8% 91.4%); - --card: hsl(200 13% 95%); /* Same as background */ - --card-foreground: hsl(219 42% 11%); - --primary: hsl(222.2 47.4% 11.2%); - --primary-foreground: hsl(210 40% 98%); - --secondary: hsl(220 17% 85%); - --secondary-foreground: hsl(219 42% 11%); - --accent: hsl(210 40% 90%); - --accent-foreground: hsl(219 42% 11%); - --destructive: hsl(1 75% 55.6%); - --destructive-foreground: hsl(210 40% 98%); - --ring: hsl(215 20.2% 65.1%); - --sidebar: hsl(200 13% 95%); /* Same as background */ - --hover-background: hsl(210 10% 91%); - --active-background: hsl(0 0% 100%); /* Same as input-background */ - --selected-hover-background: hsl(210 10% 91%); /* Same as hover-background */ - --input-background: hsl(0 0% 100%); - --danger: hsl(1 75% 55.6%); - --danger-foreground: hsl(210 40% 98%); - --success: hsl(143 64% 24%); - --success-foreground: hsl(210 40% 98%); - --warning: hsl(38 92% 50%); - --warning-foreground: hsl(210 40% 98%); - --info: hsl(226 71% 40%); - --info-foreground: hsl(210 40% 98%); + color-scheme: light; + --background: oklch(0.97 0.003 264); + --foreground: oklch(0.13 0.028 261.692); + --card: oklch(1 0 0); + --card-foreground: oklch(0.13 0.028 261.692); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.13 0.028 261.692); + --primary: oklch(0.20 0.02 264); + --primary-foreground: oklch(0.97 0.003 264); + --secondary: oklch(0.95 0.003 264); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.95 0.003 264); + --muted-foreground: oklch(0.45 0.027 264.364); + --accent: oklch(0.95 0.003 264); + --accent-foreground: oklch(0.21 0.034 264.665); + --destructive: oklch(0.58 0.19 22); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.90 0.006 264); + --input: oklch(0.90 0.006 264); + --ring: oklch(0.707 0.022 261.325); + --chart-1: oklch(0.88 0.15 92); + --chart-2: oklch(0.77 0.16 70); + --chart-3: oklch(0.67 0.16 58); + --chart-4: oklch(0.56 0.15 49); + --chart-5: oklch(0.47 0.12 46); + --sidebar: oklch(0.97 0.003 264); + --sidebar-foreground: oklch(0.13 0.028 261.692); + --sidebar-primary: oklch(0.20 0.02 264); + --sidebar-primary-foreground: oklch(0.97 0.003 264); + --sidebar-accent: oklch(0.95 0.003 264); + --sidebar-accent-foreground: oklch(0.21 0.034 264.665); + --sidebar-border: oklch(0.90 0.006 264); + --sidebar-ring: oklch(0.707 0.022 261.325); + --radius: 0.625rem; + /* Custom variables */ + --hover-background: oklch(0.93 0.003 264); + --active-background: oklch(1 0 0); + --selected-hover-background: oklch(0.93 0.003 264); + --input-background: oklch(1 0 0); + --danger: var(--destructive); + --danger-foreground: var(--destructive-foreground); + --success: oklch(0.45 0.15 145); + --success-foreground: oklch(0.985 0 0); + --warning: oklch(0.75 0.18 70); + --warning-foreground: oklch(0.13 0.028 261.692); + --info: oklch(0.55 0.2 260); + --info-foreground: oklch(0.985 0 0); --panel-opacity: 0.5; - --radius: 0.5rem; } .dark { - --background: hsl(222 29% 12%); /* Same as background */ - --foreground: hsl(210 40% 98%); - --muted: hsl(223 15% 18%); - --muted-foreground: hsl(217.9 10.6% 64.9%); - --popover: hsl(222 29% 12%); /* Same as background */ - --popover-foreground: hsl(210 40% 98%); - --border: hsl(217 19% 20%); - --input: hsl(217 19% 27%); /* Same as border */ - --card: hsl(222 29% 12%); /* Same as background */ - --card-foreground: hsl(210 40% 98%); - --primary: hsl(210 40% 98%); - --primary-foreground: hsl(222.2 47.4% 11.2%); - --secondary: hsl(217 19% 27%); - --secondary-foreground: hsl(210 40% 98%); - --accent: hsl(217.2 32.6% 17.5%); - --accent-foreground: hsl(210 40% 98%); - --destructive: hsl(1 75% 55.6%); - --destructive-foreground: hsl(210 40% 98%); - --ring: hsl(0 0% 100%); - --sidebar: hsl(222 29% 12%); /* Same as background */ - --hover-background: hsl(219 31% 15%); - --active-background: hsl(222 45% 8%); /* Same as input-background */ - --selected-hover-background: hsl(219 31% 15%); /* Same as hover-background */ - --input-background: hsl(222 45% 8%); - --danger: hsl(1 75% 55.6%); - --danger-foreground: hsl(0 0% 100%); - --success: hsl(143 64% 24%); - --success-foreground: hsl(0 0% 100%); - --warning: hsl(38 92% 50%); - --warning-foreground: hsl(0 0% 100%); - --info: hsl(217 91% 60%); - --info-foreground: hsl(0 0% 100%); + color-scheme: dark; + --background: oklch(0.18 0.02 261); + --foreground: oklch(0.985 0.002 247.839); + --card: oklch(0.22 0.025 261); + --card-foreground: oklch(0.985 0.002 247.839); + --popover: oklch(0.22 0.025 261); + --popover-foreground: oklch(0.985 0.002 247.839); + --primary: oklch(0.95 0.003 264); + --primary-foreground: oklch(0.18 0.02 261); + --secondary: oklch(0.25 0.02 261); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.30 0.02 261); + --muted-foreground: oklch(0.65 0.02 261); + --accent: oklch(0.30 0.02 261); + --accent-foreground: oklch(0.985 0.002 247.839); + --destructive: oklch(0.58 0.19 22); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.30 0.02 261); + --input: oklch(0.30 0.02 261); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.88 0.15 92); + --chart-2: oklch(0.77 0.16 70); + --chart-3: oklch(0.67 0.16 58); + --chart-4: oklch(0.56 0.15 49); + --chart-5: oklch(0.47 0.12 46); + --sidebar: oklch(0.18 0.02 261); + --sidebar-foreground: oklch(0.985 0.002 247.839); + --sidebar-primary: oklch(0.95 0.003 264); + --sidebar-primary-foreground: oklch(0.18 0.02 261); + --sidebar-accent: oklch(0.25 0.02 261); + --sidebar-accent-foreground: oklch(0.985 0.002 247.839); + --sidebar-border: oklch(0.30 0.02 261); + --sidebar-ring: oklch(0.551 0.027 264.364); + --radius: 0.625rem; + /* Custom variables */ + --hover-background: oklch(0.22 0.02 261); + --active-background: oklch(0.15 0.02 261); + --selected-hover-background: oklch(0.22 0.02 261); + --input-background: oklch(0.14 0.02 261); + --danger: var(--destructive); + --danger-foreground: var(--destructive-foreground); + --success: oklch(0.50 0.15 145); + --success-foreground: oklch(0.985 0 0); + --warning: oklch(0.75 0.18 70); + --warning-foreground: oklch(0.13 0.028 261.692); + --info: oklch(0.65 0.2 260); + --info-foreground: oklch(0.985 0 0); --panel-opacity: 0.5; - --radius: 0.5rem; } diff --git a/application/shared-webapp/ui/tsconfig.json b/application/shared-webapp/ui/tsconfig.json index 241384c92..7d92771b8 100644 --- a/application/shared-webapp/ui/tsconfig.json +++ b/application/shared-webapp/ui/tsconfig.json @@ -4,7 +4,11 @@ "extends": "@repo/config/typescript/react-library.json", "compilerOptions": { "rootDir": ".", - "outDir": "dist" + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } }, "exclude": ["node_modules", "dist"] } diff --git a/application/shared-webapp/ui/cn.ts b/application/shared-webapp/ui/utils.ts similarity index 100% rename from application/shared-webapp/ui/cn.ts rename to application/shared-webapp/ui/utils.ts From 4ffdd8055a8506a76d263bfe2ae6c8052fa92fbe Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 24 Dec 2025 11:18:57 +0100 Subject: [PATCH 003/108] Update frontend rules to document ShadCN 2.0 with BaseUI patterns --- .agent/rules/frontend/form-with-validation.md | 12 +++--- .agent/rules/frontend/frontend.md | 38 ++++++++++++------- .../tanstack-query-api-integration.md | 4 +- .agent/rules/frontend/translations.md | 2 +- .../mcp-configs/frontend-mcp-config.json | 4 +- .../rules/frontend/form-with-validation.md | 12 +++--- .claude/rules/frontend/frontend.md | 36 +++++++++++------- .../tanstack-query-api-integration.md | 4 +- .claude/rules/frontend/translations.md | 2 +- .../rules/frontend/form-with-validation.mdc | 12 +++--- .cursor/rules/frontend/frontend.mdc | 38 ++++++++++++------- .../tanstack-query-api-integration.mdc | 4 +- .cursor/rules/frontend/translations.mdc | 2 +- .../rules/frontend/form-with-validation.md | 10 ++--- .github/copilot/rules/frontend/frontend.md | 38 ++++++++++++------- .../tanstack-query-api-integration.md | 4 +- .../copilot/rules/frontend/translations.md | 2 +- .../rules/frontend/form-with-validation.md | 12 +++--- .windsurf/rules/frontend/frontend.md | 38 ++++++++++++------- .../tanstack-query-api-integration.md | 4 +- .windsurf/rules/frontend/translations.md | 2 +- 21 files changed, 164 insertions(+), 116 deletions(-) diff --git a/.agent/rules/frontend/form-with-validation.md b/.agent/rules/frontend/form-with-validation.md index a9d4ae5c0..69bce6903 100644 --- a/.agent/rules/frontend/form-with-validation.md +++ b/.agent/rules/frontend/form-with-validation.md @@ -1,7 +1,7 @@ --- trigger: glob globs: **/*.tsx -description: Rules for forms with validation using React Aria Components +description: Rules for forms with validation using ShadCN 2.0 components --- # Form With Validation @@ -9,11 +9,11 @@ Guidelines for implementing forms with validation in the frontend, covering UI c ## Implementation -1. Use React Aria Components from `@repo/ui/components` for form elements +1. Use ShadCN components from `@repo/ui/components` for form elements 2. Use `api.useMutation` or TanStack's `useMutation` for form submissions 3. Use the custom `mutationSubmitter` to handle form submission and data mapping 4. Handle validation errors using the `validationErrors` prop from the mutation error -5. Show loading state in submit buttons +5. Show loading state in submit buttons using `disabled={mutation.isPending}` 6. For complex scenarios with multiple API calls, create a custom mutation with a `mutationFn` ## Anti-patterns @@ -66,7 +66,7 @@ export function UserProfileForm({ user }) { defaultValue={user?.title} /> - @@ -105,7 +105,7 @@ function BadUserProfileForm({ user }) { - @@ -160,7 +160,7 @@ export function UserProfileWithAvatarForm({ user, onSuccess, onClose }) { > {/* Form fields */} - diff --git a/.agent/rules/frontend/frontend.md b/.agent/rules/frontend/frontend.md index f93261a11..ca14b25e0 100644 --- a/.agent/rules/frontend/frontend.md +++ b/.agent/rules/frontend/frontend.md @@ -37,6 +37,17 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v - Server state lives in TanStack Query only - Use `queryClient.invalidateQueries()` to refresh data after mutations +4. **ShadCN 2.0 with BaseUI** (not Radix UI): + - **BaseUI** (`@base-ui/react`): Headless primitives providing accessibility and behavior + - **ShadCN 2.0**: Pre-styled components built on BaseUI, using class-variance-authority (cva) + - Install components via `npx shadcn add ` - never copy manually + - After installing: change `@/utils` to `../utils` and rename file to PascalCase (e.g., `button.tsx` to `Button.tsx`) + - **Never modify ShadCN components** beyond these fixes - keep them stock + - Create an adapter component if you need different behavior + - Import from `@repo/ui/components/`, never from BaseUI directly + - Only create custom components when no ShadCN equivalent exists (edge cases) + - **Use BaseUI `render` prop** to customize underlying elements (not Radix's `asChild`): `}>Close` + ## Implementation 1. Follow these code style and pattern conventions: @@ -51,27 +62,26 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v - Don't use acronyms (e.g., use `errorMessage` not `errMsg`, `button` not `btn`, `authentication` not `auth`) - Prioritize code readability and maintainability - Don't introduce new npm dependencies - - Use React Aria Components instead of native HTML elements like ``, `