diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5be46dd5..0c7f278f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -79,7 +79,13 @@ "Bash(git log:*)", "Bash(npm run preview:*)", "Bash(npx:*)", - "Bash(kill:*)" + "Bash(kill:*)", + "WebFetch(domain:mui.com)", + "WebFetch(domain:v6.mui.com)", + "WebFetch(domain:daisyui.com)", + "WebFetch(domain:headlessui.com)", + "Bash(gh pr:*)", + "Bash(git stash:*)" ], "deny": [] } diff --git a/CLAUDE.md b/CLAUDE.md index 71f3eba0..d22aae40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,6 +89,49 @@ Key tables with security considerations: - Primary brand color: `#3E6FF3` - Custom fonts: Geist and Inter +### UI Component Libraries +**IMPORTANT**: Do NOT use deprecated Radix UI components. Use these modern alternatives: + +#### Primary UI Libraries: +- **DaisyUI**: Pre-styled Tailwind components for basic UI elements +- **Headless UI**: Unstyled behavioral components for complex interactions +- **Documentation**: See `/docs/UI_LIBRARY_GUIDE.md` and `/docs/RADIX_MIGRATION_PLAN.md` + +#### Component Guidelines: +- Use DaisyUI for: buttons, inputs, modals, alerts, forms, navigation +- Use Headless UI for: complex dropdowns, comboboxes, tabs with state +- Never import from `@radix-ui/*` packages (deprecated) +- All UI components must be accessible and React 19 compatible + +#### Installation: +```bash +bun add @headlessui/react +bun add -D daisyui@latest +``` + +#### Example Usage: +```tsx +// DaisyUI Button + + +// DaisyUI Modal +
+
+

Title

+

Content

+
+
+ +// Headless UI Dropdown +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' + + Options + + Item 1 + + +``` + ### Development Environment - Uses lovable-tagger in development mode - Images stored in `public/lovable-uploads/` diff --git a/bun.lock b/bun.lock index 94b98402..4511b55c 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "@e2b/code-interpreter": "1.5.1", "@eslint/plugin-kit": "^0.3.5", "@geist-ui/core": "^2.3.8", + "@headlessui/react": "^2.2.7", "@hookform/resolvers": "^5.2.1", "@lottiefiles/dotlottie-react": "^0.14.4", "@microsoft/eslint-formatter-sarif": "3.1.0", @@ -106,6 +107,7 @@ "@vitejs/plugin-react-swc": "^4.0.0", "autoprefixer": "^10.4.21", "concurrently": "^9.2.0", + "daisyui": "^5.0.50", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", @@ -439,12 +441,16 @@ "@floating-ui/dom": ["@floating-ui/dom@1.6.11", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.8" } }, "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ=="], + "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.8", "", {}, "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="], "@geist-ui/core": ["@geist-ui/core@2.3.8", "", { "dependencies": { "@babel/runtime": "^7.16.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OKwGgTA4+fBM41eQbqDoUj4XBycZbYH7Ynrn6LPO5yKX7zeWPu/R7HN3vB4/oHt34VTDQI5sDNb1SirHvNyB5w=="], + "@headlessui/react": ["@headlessui/react@2.2.7", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -663,6 +669,20 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@react-aria/focus": ["@react-aria/focus@3.21.0", "", { "dependencies": { "@react-aria/interactions": "^3.25.4", "@react-aria/utils": "^3.30.0", "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA=="], + + "@react-aria/interactions": ["@react-aria/interactions@3.25.4", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.30.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg=="], + + "@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="], + + "@react-aria/utils": ["@react-aria/utils@3.30.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.10.8", "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw=="], + + "@react-stately/flags": ["@react-stately/flags@3.1.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="], + + "@react-stately/utils": ["@react-stately/utils@3.10.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g=="], + + "@react-types/shared": ["@react-types/shared@3.31.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.8.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.30", "", {}, "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw=="], @@ -767,6 +787,8 @@ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + "@swc/types": ["@swc/types@0.1.23", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], @@ -805,6 +827,10 @@ "@tanstack/react-query": ["@tanstack/react-query@5.85.0", "", { "dependencies": { "@tanstack/query-core": "5.83.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-t1HMfToVMGfwEJRya6GG7gbK0luZJd+9IySFNePL1BforU1F3LqQ3tBC2Rpvr88bOrlU6PXyMLgJD0Yzn4ztUw=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], + + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], @@ -1163,6 +1189,8 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "daisyui": ["daisyui@5.0.50", "", {}, "sha512-c1PweK5RI1C76q58FKvbS4jzgyNJSP6CGTQ+KkZYzADdJoERnOxFoeLfDHmQgxLpjEzlYhFMXCeodQNLCC9bow=="], + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -2035,6 +2063,8 @@ "swr": ["swr@2.3.4", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg=="], + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], diff --git a/docs/RADIX_MIGRATION_PLAN.md b/docs/RADIX_MIGRATION_PLAN.md new file mode 100644 index 00000000..69267c7c --- /dev/null +++ b/docs/RADIX_MIGRATION_PLAN.md @@ -0,0 +1,354 @@ +# Radix UI Migration Plan + +## Current Usage Analysis + +Your codebase uses 31 files with Radix UI components. Here's the migration strategy: + +## Phase 1: High Priority (Most Used Components) + +### 1. Slot Component (3 uses) +**Current**: `@radix-ui/react-slot` +**Replacement**: Use React's built-in `forwardRef` and `cloneElement` + +```tsx +// Old Radix approach +import { Slot } from "@radix-ui/react-slot" + +// New approach - Custom implementation +import { forwardRef, cloneElement, isValidElement } from 'react' + +interface SlotProps extends React.HTMLAttributes { + asChild?: boolean; + children?: React.ReactNode; +} + +const Slot = forwardRef(({ asChild, children, ...props }, ref) => { + if (asChild && isValidElement(children)) { + return cloneElement(children, { + ...props, + ...children.props, + ref, + }); + } + return {children}; +}); +``` + +### 2. Label Component (2 uses) +**Current**: `@radix-ui/react-label` +**Replacement**: DaisyUI with semantic HTML + +```tsx +// Old +import * as LabelPrimitive from "@radix-ui/react-label" + +// New + + +// Or for form inputs +
+ + +
+``` + +## Phase 2: Dialog & Overlay Components + +### Dialog/Modal +**Current**: `@radix-ui/react-dialog` +**Replacement**: DaisyUI Modal + +```tsx +// Replace all dialog components with DaisyUI modal +
+
+

Modal Title

+

Content

+
+ +
+
+
+``` + +### Alert Dialog +**Current**: `@radix-ui/react-alert-dialog` +**Replacement**: DaisyUI Modal with alert styling + +```tsx +
+
+
+ Are you sure you want to delete this item? +
+
+ + +
+
+
+``` + +### Popover +**Current**: `@radix-ui/react-popover` +**Replacement**: Headless UI Popover + +```tsx +import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' + + + Options + +
Content
+
+
+``` + +## Phase 3: Form Components + +### Checkbox +**Current**: `@radix-ui/react-checkbox` +**Replacement**: DaisyUI Checkbox + +```tsx +// Old complex Radix implementation +// New simple DaisyUI + + +``` + +### Radio Group +**Current**: `@radix-ui/react-radio-group` +**Replacement**: DaisyUI Radio + +```tsx +
+ +
+``` + +### Switch +**Current**: `@radix-ui/react-switch` +**Replacement**: DaisyUI Toggle + +```tsx + +``` + +### Select +**Current**: `@radix-ui/react-select` +**Replacement**: DaisyUI Select or Headless UI Listbox + +```tsx +// Simple select + + +// Complex select with search +import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' +``` + +### Slider +**Current**: `@radix-ui/react-slider` +**Replacement**: DaisyUI Range + +```tsx + +``` + +## Phase 4: Navigation Components + +### Tabs +**Current**: `@radix-ui/react-tabs` +**Replacement**: DaisyUI Tabs or Headless UI Tab + +```tsx +// Simple tabs +
+ Tab 1 + Tab 2 +
+ +// Complex tabs with panels +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react' +``` + +### Dropdown Menu +**Current**: `@radix-ui/react-dropdown-menu` +**Replacement**: DaisyUI Dropdown + +```tsx +
+
Click
+ +
+``` + +### Navigation Menu +**Current**: `@radix-ui/react-navigation-menu` +**Replacement**: DaisyUI Navbar + +```tsx +
+
+ Brand +
+
+ +
+
+``` + +## Phase 5: Utility Components + +### Tooltip +**Current**: `@radix-ui/react-tooltip` +**Replacement**: DaisyUI Tooltip + +```tsx +
+ +
+``` + +### Progress +**Current**: `@radix-ui/react-progress` +**Replacement**: DaisyUI Progress + +```tsx + +``` + +### Avatar +**Current**: `@radix-ui/react-avatar` +**Replacement**: DaisyUI Avatar + +```tsx +
+
+ Avatar +
+
+ +// With placeholder +
+
+ K +
+
+``` + +### Separator +**Current**: `@radix-ui/react-separator` +**Replacement**: DaisyUI Divider + +```tsx +
OR
+
OR
+``` + +### Scroll Area +**Current**: `@radix-ui/react-scroll-area` +**Replacement**: CSS or custom scrollbar styling + +```tsx +// Use regular div with custom scrollbar +
+ {/* Content */} +
+``` + +### Collapsible +**Current**: `@radix-ui/react-collapsible` +**Replacement**: DaisyUI Collapse + +```tsx +
+ +
+ Click to expand +
+
+

Content goes here

+
+
+``` + +### Accordion +**Current**: `@radix-ui/react-accordion` +**Replacement**: DaisyUI Collapse (multiple) + +```tsx +
+
+ +
Section 1
+
Content 1
+
+
+ +
Section 2
+
Content 2
+
+
+``` + +## Migration Steps + +1. **Install dependencies**: + ```bash + npm install @headlessui/react + npm install -D daisyui@latest + ``` + +2. **Update Tailwind config** to include DaisyUI: + ```js + module.exports = { + plugins: [require("daisyui")], + } + ``` + +3. **Create wrapper components** for complex Radix replacements + +4. **Update imports** file by file, starting with most used components + +5. **Test thoroughly** - DaisyUI/Headless UI have better accessibility but different APIs + +6. **Remove Radix dependencies** once migration is complete + +## Benefits After Migration + +- **Smaller bundle size**: DaisyUI + Headless UI are more lightweight +- **Better performance**: Less JavaScript overhead +- **Active maintenance**: Both libraries are actively developed +- **Better React 19 compatibility**: Built for modern React +- **Easier theming**: DaisyUI's theme system is more flexible +- **Consistent design**: DaisyUI provides design consistency out of the box + +## Timeline Estimate + +- **Phase 1**: 1-2 days (Slot, Label) +- **Phase 2**: 3-4 days (Dialogs, Popovers) +- **Phase 3**: 2-3 days (Form components) +- **Phase 4**: 2-3 days (Navigation) +- **Phase 5**: 1-2 days (Utilities) +- **Testing & cleanup**: 2-3 days + +**Total**: ~2 weeks for complete migration \ No newline at end of file diff --git a/docs/UI_LIBRARY_GUIDE.md b/docs/UI_LIBRARY_GUIDE.md new file mode 100644 index 00000000..0a0a47b2 --- /dev/null +++ b/docs/UI_LIBRARY_GUIDE.md @@ -0,0 +1,339 @@ +# UI Library Guide for ZapDev + +## Migration from Radix UI to Modern Alternatives + +**IMPORTANT**: Do not use deprecated Radix UI components. Use DaisyUI + Headless UI instead. + +## Primary UI Libraries + +### 1. DaisyUI (Styled Components) +- **Installation**: Already integrated with Tailwind CSS +- **Use for**: Basic UI components with built-in styling +- **Documentation**: https://daisyui.com/components/ + +### 2. Headless UI (Behavior Components) +- **Installation**: `npm install @headlessui/react` +- **Use for**: Complex interactive components needing custom styling +- **Documentation**: https://headlessui.com/ + +## Component Migration Map + +### ❌ DEPRECATED - Do Not Use These Radix Components: +```tsx +// DON'T USE THESE +import * from "@radix-ui/react-dialog" +import * from "@radix-ui/react-dropdown-menu" +import * from "@radix-ui/react-popover" +// ... any @radix-ui imports +``` + +### ✅ USE THESE INSTEAD: + +#### Buttons +```tsx +// DaisyUI Button + + + + + + +// Sizes + + + + + +// States + + +``` + +#### Modal/Dialog +```tsx +// DaisyUI Modal +import { useState } from 'react' + +function Modal() { + const [isOpen, setIsOpen] = useState(false) + + return ( + <> + + + {isOpen && ( +
+
+

Modal Title

+

Modal content goes here

+
+ +
+
+
setIsOpen(false)} /> +
+ )} + + ) +} +``` + +#### Dropdown Menu +```tsx +// DaisyUI Dropdown +
+
+ Click +
+ +
+ +// Or Headless UI for complex dropdowns +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' + + + Options + + + {({ focus }) => ( + + Account settings + + )} + + + {({ focus }) => ( + + Support + + )} + + + +``` + +#### Form Controls + +```tsx +// Checkbox + + + + +// Radio + + + +// Toggle/Switch + + + +// Select + + +// Range Slider + +``` + +#### Tabs +```tsx +// DaisyUI Tabs +
+ Tab 1 + Tab 2 + Tab 3 +
+ +// Or Headless UI for complex tab behavior +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react' + + + + Tab 1 + Tab 2 + Tab 3 + + + Content 1 + Content 2 + Content 3 + + +``` + +#### Toast/Alert +```tsx +// DaisyUI Alert +
+ + New software update available. +
+ +
+ Your purchase has been confirmed! +
+ +
+ Warning: Invalid email address! +
+ +
+ Error! Task failed successfully. +
+``` + +#### Tooltip +```tsx +// DaisyUI Tooltip +
+ +
+ +
+ +
+``` + +#### Progress Bar +```tsx +// DaisyUI Progress + + + +``` + +#### Loading States +```tsx +// DaisyUI Loading + + + + + + + +// Different sizes + + + + +``` + +## Complex Components (Use Headless UI) + +### Popover +```tsx +import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' + + + Open popover + +
+

Popover content

+

Your popover content goes here

+
+
+
+``` + +### Combobox (Autocomplete) +```tsx +import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from '@headlessui/react' +import { useState } from 'react' + +function AutoComplete() { + const [selectedPerson, setSelectedPerson] = useState(null) + const [query, setQuery] = useState('') + + const people = [ + { id: 1, name: 'Durward Reynolds' }, + { id: 2, name: 'Kenton Towne' }, + { id: 3, name: 'Therese Wunsch' }, + ] + + const filteredPeople = query === '' + ? people + : people.filter((person) => + person.name.toLowerCase().includes(query.toLowerCase()) + ) + + return ( + + person?.name} + onChange={(event) => setQuery(event.target.value)} + /> + + {filteredPeople.map((person) => ( + + {({ focus, selected }) => ( +
  • + {person.name} +
  • + )} +
    + ))} +
    +
    + ) +} +``` + +## Theme Integration + +DaisyUI works with your existing Tailwind setup and offers multiple themes: + +```tsx +// Set theme on html element + + + + +// ... many more themes available +``` + +## Installation Commands + +Add to your project: + +```bash +# DaisyUI (already in your tailwind.config) +npm install -D daisyui@latest + +# Headless UI for complex components +npm install @headlessui/react + +# Optional: Heroicons for consistent icons +npm install @heroicons/react +``` + +## Best Practices + +1. **Use DaisyUI for**: Simple components (buttons, inputs, cards, alerts) +2. **Use Headless UI for**: Complex interactions (combobox, popover, complex menus) +3. **Combine both**: Use DaisyUI classes within Headless UI components for styling +4. **Always prefer**: These modern alternatives over deprecated Radix UI +5. **Testing**: Both libraries offer excellent accessibility and React 19 compatibility + +## Migration Priority + +1. **High Priority**: Dialog, Dropdown, Popover, Tabs (most commonly used) +2. **Medium Priority**: Form controls, Progress, Tooltip +3. **Low Priority**: Advanced components like Accordion, Navigation Menu + +Remember: DaisyUI + Headless UI provides better performance, smaller bundle size, and active maintenance compared to Radix UI. \ No newline at end of file diff --git a/package.json b/package.json index 7884d9b7..d1121504 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@e2b/code-interpreter": "1.5.1", "@eslint/plugin-kit": "^0.3.5", "@geist-ui/core": "^2.3.8", + "@headlessui/react": "^2.2.7", "@hookform/resolvers": "^5.2.1", "@lottiefiles/dotlottie-react": "^0.14.4", "@microsoft/eslint-formatter-sarif": "3.1.0", @@ -133,6 +134,7 @@ "@vitejs/plugin-react-swc": "^4.0.0", "autoprefixer": "^10.4.21", "concurrently": "^9.2.0", + "daisyui": "^5.0.50", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx index 8500708b..d08de9e9 100644 --- a/src/components/ChatInterface.tsx +++ b/src/components/ChatInterface.tsx @@ -198,8 +198,14 @@ const ChatInterface: React.FC = () => { ); // Memoize normalized results to prevent useEffect dependencies from changing on every render - const chats = React.useMemo(() => chatsData?.chats ?? [], [chatsData?.chats]); - const messages = React.useMemo(() => messagesData?.messages ?? [], [messagesData?.messages]); + const chats = React.useMemo(() => { + const chatsArray = chatsData?.chats; + return Array.isArray(chatsArray) ? chatsArray : []; + }, [chatsData?.chats]); + const messages = React.useMemo(() => { + const messagesArray = messagesData?.messages; + return Array.isArray(messagesArray) ? messagesArray : []; + }, [messagesData?.messages]); const createChat = useMutation(api.chats.createChat); const updateChat = useMutation(api.chats.updateChat); const createMessage = useMutation(api.messages.createMessage); @@ -325,8 +331,18 @@ const ChatInterface: React.FC = () => { error: error instanceof Error ? error.message : String(error), title: 'New chat' }); - Sentry.captureException(error); - toast.error('Failed to create chat'); + + const errorMessage = error instanceof Error ? error.message : String(error); + + // Handle specific error types with helpful messages + if (errorMessage.includes('Free plan limit reached')) { + toast.error('Free plan limit reached! You can create up to 5 chats. Upgrade to Pro for unlimited chats.'); + } else if (errorMessage.includes('Rate limit exceeded')) { + toast.error('Please wait a moment before creating another chat.'); + } else { + Sentry.captureException(error); + toast.error('Failed to create chat'); + } } } ); diff --git a/src/components/EnhancedChatInterface.tsx b/src/components/EnhancedChatInterface.tsx index 6369131c..d0b4ed03 100644 --- a/src/components/EnhancedChatInterface.tsx +++ b/src/components/EnhancedChatInterface.tsx @@ -34,7 +34,9 @@ import { ArrowUp, Mic, Paperclip, - Settings + Settings, + Github, + GitBranch } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useQuery, useMutation } from 'convex/react'; @@ -52,6 +54,9 @@ import { toast } from 'sonner'; import * as Sentry from '@sentry/react'; import WebContainerFailsafe from './WebContainerFailsafe'; import { DECISION_PROMPT_NEXT } from '@/lib/decisionPrompt'; +import { GitHubIntegration } from '@/components/GitHubIntegration'; +import type { GitHubRepo } from '@/lib/github-service'; +import { githubService } from '@/lib/github-service'; const { logger } = Sentry; @@ -152,17 +157,33 @@ const EnhancedChatInterface: React.FC = () => { // Enhanced animations const [messageAnimations, setMessageAnimations] = useState<{ [key: string]: boolean }>({}); + + // GitHub Integration state + const [selectedRepo, setSelectedRepo] = useState(null); + const [githubContext, setGithubContext] = useState(''); + const [isGithubMode, setIsGithubMode] = useState(false); const textareaRef = useRef(null); const messagesEndRef = useRef(null); const inputContainerRef = useRef(null); // Convex queries and mutations - const chats = useQuery(api.chats.getUserChats); - const messages = useQuery( + const chatsData = useQuery(api.chats.getUserChats); + const messagesData = useQuery( api.messages.getChatMessages, selectedChatId ? { chatId: selectedChatId as Id<'chats'> } : "skip" ); + + // Ensure arrays are properly handled + const chats = React.useMemo(() => { + const chatsArray = chatsData?.chats; + return Array.isArray(chatsArray) ? chatsArray : []; + }, [chatsData?.chats]); + + const messages = React.useMemo(() => { + const messagesArray = messagesData?.messages; + return Array.isArray(messagesArray) ? messagesArray : []; + }, [messagesData?.messages]); const createChatMutation = useMutation(api.chats.createChat); const addMessageMutation = useMutation(api.messages.createMessage); @@ -209,6 +230,9 @@ const EnhancedChatInterface: React.FC = () => { const sanitizedInput = sanitizeText(input); if (!sanitizedInput.trim()) return; + + // Enhance message with GitHub context if available + const enhancedInput = await enhanceMessageWithGitHub(sanitizedInput); if (!sessionStarted) { setSessionStarted(true); @@ -228,9 +252,12 @@ const EnhancedChatInterface: React.FC = () => { // Add user message await addMessageMutation({ chatId: currentChatId as Id<'chats'>, - content: sanitizedInput, + content: sanitizedInput, // Store original user input role: 'user', - metadata: {} + metadata: { + githubMode: isGithubMode, + repository: selectedRepo?.full_name + } }); setInput(''); @@ -238,7 +265,7 @@ const EnhancedChatInterface: React.FC = () => { // Generate AI response with enhanced error handling try { - const stream = await streamAIResponse(sanitizedInput); + const stream = await streamAIResponse(enhancedInput); // Use enhanced input for AI let assistantResponse = ''; // Add assistant message placeholder @@ -301,7 +328,17 @@ const EnhancedChatInterface: React.FC = () => { } catch (error) { logger.error('Chat submission failed:', error); - toast.error('Failed to send message. Please try again.'); + + const errorMessage = error instanceof Error ? error.message : String(error); + + // Handle specific error types with helpful messages + if (errorMessage.includes('Free plan limit reached')) { + toast.error('Free plan limit reached! You can create up to 5 chats. Upgrade to Pro for unlimited chats.'); + } else if (errorMessage.includes('Rate limit exceeded')) { + toast.error('Please wait a moment before creating another chat.'); + } else { + toast.error('Failed to send message. Please try again.'); + } } finally { setIsTyping(false); } @@ -430,6 +467,78 @@ const EnhancedChatInterface: React.FC = () => { }); }; + // GitHub Integration Functions + const handleRepoSelected = (repo: GitHubRepo) => { + setSelectedRepo(repo); + setIsGithubMode(true); + + const repoContext = `Repository: ${repo.full_name}\n` + + `Description: ${repo.description || 'No description'}\n` + + `Language: ${repo.language || 'Unknown'}\n` + + `Default Branch: ${repo.default_branch}\n` + + `Type: ${repo.private ? 'Private' : 'Public'} repository\n\n`; + + setGithubContext(repoContext); + + // Add context to the current input + setInput(prev => { + const newInput = `I'm working with this GitHub repository:\n\n${repoContext}` + + `Please help me analyze and suggest improvements for this codebase. ` + + `${prev ? '\n\n' + prev : ''}`; + return newInput; + }); + + toast.success(`Repository ${repo.full_name} loaded for AI analysis!`); + }; + + const handlePullRequestCreated = (prUrl: string, repo: GitHubRepo) => { + toast.success('Pull request created successfully!'); + + // Add success message to chat + if (selectedChatId) { + addMessageMutation({ + chatId: selectedChatId as Id<'chats'>, + content: `✅ **Pull Request Created Successfully!**\n\n` + + `Repository: ${repo.full_name}\n` + + `Pull Request: [View PR](${prUrl})\n\n` + + `The changes have been applied and are ready for review. ` + + `You can now review the pull request on GitHub and merge it when ready.`, + role: 'assistant', + metadata: { + type: 'github_success', + prUrl, + repository: repo.full_name + } + }).catch(error => { + logger.error('Failed to add PR success message:', error); + }); + } + }; + + const detectGithubUrls = (text: string): string[] => { + const githubUrlRegex = /https?:\/\/github\.com\/[\w\-.]+\/[\w\-.]+(?:\/[^\s]*)?/g; + return text.match(githubUrlRegex) || []; + }; + + const enhanceMessageWithGitHub = async (message: string): Promise => { + let enhancedMessage = message; + + // If GitHub mode is active, add repository context + if (isGithubMode && selectedRepo && githubContext) { + enhancedMessage = githubContext + '\n' + message; + } + + // Detect and suggest GitHub integration + const githubUrls = detectGithubUrls(message); + if (githubUrls.length > 0 && !isGithubMode) { + enhancedMessage += '\n\n[Assistant Note: I detected GitHub repository URLs in your message. ' + + 'Would you like to use the GitHub integration to analyze the repository, ' + + 'make changes, and create pull requests directly?]'; + } + + return enhancedMessage; + }; + if (authLoading) { return (
    @@ -1048,6 +1157,64 @@ const EnhancedChatInterface: React.FC = () => {
    + {/* GitHub Mode Indicator */} + + {isGithubMode && selectedRepo && ( + +
    +
    +
    +
    + + GitHub Mode Active +
    +
    + {selectedRepo.owner.login} + {selectedRepo.full_name} + + {selectedRepo.language || 'Unknown'} + +
    +
    +
    + + +
    +
    +
    +
    + )} +
    + {/* Enhanced Input Area */}
    @@ -1092,6 +1259,11 @@ const EnhancedChatInterface: React.FC = () => { Clone +
    diff --git a/src/components/GitHubIntegration.tsx b/src/components/GitHubIntegration.tsx new file mode 100644 index 00000000..fcb582ab --- /dev/null +++ b/src/components/GitHubIntegration.tsx @@ -0,0 +1,658 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { Label } from '@/components/ui/label'; +import { + GitBranch, + GitFork, + GitPullRequest, + Github, + ExternalLink, + Loader2, + Check, + AlertCircle, + Settings, + Key, + FileText, + Folder, + Plus +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { toast } from 'sonner'; +import { githubService, initializeGitHub, type GitHubRepo, type FileChange, type CreatePullRequestOptions } from '@/lib/github-service'; +import { setGitHubToken, clearGitHubToken } from '@/lib/github-token-storage'; +import * as Sentry from '@sentry/react'; + +const { logger } = Sentry; + +export interface GitHubIntegrationProps { + onRepoSelected?: (repo: GitHubRepo) => void; + onPullRequestCreated?: (prUrl: string, repo: GitHubRepo) => void; + className?: string; +} + +interface GitHubOperationStatus { + stage: 'idle' | 'parsing' | 'forking' | 'creating-branch' | 'applying-changes' | 'creating-pr' | 'completed' | 'error'; + message: string; + progress: number; +} + +export function GitHubIntegration({ + onRepoSelected, + onPullRequestCreated, + className = '' +}: GitHubIntegrationProps) { + const [isOpen, setIsOpen] = useState(false); + const [githubUrl, setGithubUrl] = useState(''); + const [isTokenSetup, setIsTokenSetup] = useState(false); + const [githubToken, setGithubToken] = useState(''); + const [currentRepo, setCurrentRepo] = useState(null); + const [operationStatus, setOperationStatus] = useState({ + stage: 'idle', + message: 'Ready to start', + progress: 0 + }); + + // Pull Request creation fields + const [prTitle, setPrTitle] = useState(''); + const [prDescription, setPrDescription] = useState(''); + const [changes, setChanges] = useState([]); + const [showPRForm, setShowPRForm] = useState(false); + + // Token setup + const [showTokenSetup, setShowTokenSetup] = useState(false); + + useEffect(() => { + checkGitHubSetup(); + }, []); + + const checkGitHubSetup = async () => { + const isSetup = await initializeGitHub(); + setIsTokenSetup(isSetup); + }; + + const saveGitHubToken = async () => { + if (!githubToken.trim()) { + toast.error('Please enter a valid GitHub token'); + return; + } + + if (!githubService.validateGitHubToken(githubToken)) { + toast.error('Invalid GitHub token format. Please check your token.'); + return; + } + + try { + await setGitHubToken(githubToken.trim()); + githubService.setToken(githubToken.trim()); + + // Clear token from component state immediately after use + setGithubToken(''); + + setIsTokenSetup(true); + setShowTokenSetup(false); + toast.success('GitHub token saved securely!'); + } catch (error) { + logger.error('Failed to save GitHub token:', error); + toast.error('Failed to save GitHub token. Please try again.'); + } + }; + + const parseAndLoadRepo = async () => { + if (!githubUrl.trim()) { + toast.error('Please enter a GitHub repository URL'); + return; + } + + if (!isTokenSetup) { + toast.error('Please configure your GitHub token first'); + setShowTokenSetup(true); + return; + } + + try { + setOperationStatus({ + stage: 'parsing', + message: 'Parsing GitHub URL...', + progress: 10 + }); + + const parsed = await githubService.parseRepoUrl(githubUrl); + if (!parsed) { + throw new Error('Invalid GitHub URL format'); + } + + setOperationStatus({ + stage: 'parsing', + message: `Loading repository ${parsed.owner}/${parsed.repo}...`, + progress: 30 + }); + + const repo = await githubService.getRepo(parsed.owner, parsed.repo); + setCurrentRepo(repo); + + setOperationStatus({ + stage: 'completed', + message: `Repository loaded successfully!`, + progress: 100 + }); + + if (onRepoSelected) { + onRepoSelected(repo); + } + + toast.success(`Repository ${repo.full_name} loaded successfully!`); + } catch (error) { + logger.error('Error loading repository:', error); + setOperationStatus({ + stage: 'error', + message: error instanceof Error ? error.message : 'Failed to load repository', + progress: 0 + }); + toast.error(error instanceof Error ? error.message : 'Failed to load repository'); + } + }; + + const createPullRequest = async () => { + if (!currentRepo || changes.length === 0) { + toast.error('No changes to commit'); + return; + } + + if (!prTitle.trim()) { + toast.error('Please enter a pull request title'); + return; + } + + try { + const originalOwner = currentRepo.owner.login; + const originalRepo = currentRepo.name; + + setOperationStatus({ + stage: 'forking', + message: 'Forking repository...', + progress: 20 + }); + + // Fork the repository + const forkedRepo = await githubService.forkRepo(originalOwner, originalRepo); + const userLogin = forkedRepo.owner.login; + + setOperationStatus({ + stage: 'creating-branch', + message: 'Creating feature branch...', + progress: 40 + }); + + // Create a new branch + const branchName = githubService.generateBranchName(prTitle); + await githubService.createBranch(userLogin, originalRepo, branchName, currentRepo.default_branch); + + setOperationStatus({ + stage: 'applying-changes', + message: 'Applying changes...', + progress: 60 + }); + + // Apply changes + await githubService.updateFiles( + userLogin, + originalRepo, + branchName, + changes, + prTitle + ); + + setOperationStatus({ + stage: 'creating-pr', + message: 'Creating pull request...', + progress: 80 + }); + + // Create pull request using options object pattern + const prOptions: CreatePullRequestOptions = { + owner: originalOwner, + repo: originalRepo, + title: prTitle, + body: prDescription, + headBranch: branchName, + baseBranch: currentRepo.default_branch, + originalOwner: originalOwner + }; + + const pr = await githubService.createPullRequest(prOptions); + + setOperationStatus({ + stage: 'completed', + message: 'Pull request created successfully!', + progress: 100 + }); + + if (onPullRequestCreated) { + onPullRequestCreated(pr.html_url, currentRepo); + } + + toast.success(`Pull request created: ${pr.html_url}`); + + // Reset form + setShowPRForm(false); + setPrTitle(''); + setPrDescription(''); + setChanges([]); + + } catch (error) { + logger.error('Error creating pull request:', error); + setOperationStatus({ + stage: 'error', + message: error instanceof Error ? error.message : 'Failed to create pull request', + progress: 0 + }); + toast.error(error instanceof Error ? error.message : 'Failed to create pull request'); + } + }; + + const addFileChange = () => { + setChanges([...changes, { path: '', content: '', action: 'create' }]); + }; + + const updateFileChange = (index: number, field: keyof FileChange, value: string) => { + const updatedChanges = [...changes]; + updatedChanges[index] = { ...updatedChanges[index], [field]: value }; + setChanges(updatedChanges); + }; + + const removeFileChange = (index: number) => { + setChanges(changes.filter((_, i) => i !== index)); + }; + + const getStatusIcon = () => { + switch (operationStatus.stage) { + case 'parsing': + case 'forking': + case 'creating-branch': + case 'applying-changes': + case 'creating-pr': + return ; + case 'completed': + return ; + case 'error': + return ; + default: + return ; + } + }; + + return ( +
    + + + + + + + + + GitHub Integration + + + +
    + {/* GitHub Token Setup */} + + + + + GitHub Token Setup + {isTokenSetup && Configured} + + + + {!isTokenSetup ? ( +
    +

    + You need a GitHub Personal Access Token to fork repositories and create pull requests. +

    +
    + + +
    + + {showTokenSetup && ( + + + setGithubToken(e.target.value)} + className="font-mono" + /> +

    + Required scopes: repo, workflow, write:packages +

    +
    + + +
    +
    + )} +
    + ) : ( +
    + + GitHub token configured successfully +
    + + +
    +
    + )} +
    +
    + + {/* Repository Input */} + + + + + Repository Selection + + + +
    + +
    + setGithubUrl(e.target.value)} + disabled={operationStatus.stage !== 'idle' && operationStatus.stage !== 'completed' && operationStatus.stage !== 'error'} + /> + +
    +
    + + {/* Operation Status */} + + {operationStatus.stage !== 'idle' && ( + +
    + {getStatusIcon()} + {operationStatus.message} +
    +
    +
    +
    + + )} + + + {/* Repository Info */} + + {currentRepo && ( + +
    + {currentRepo.owner.login} +
    +

    {currentRepo.full_name}

    +

    {currentRepo.description}

    +
    +
    + {currentRepo.language && ( + {currentRepo.language} + )} + + {currentRepo.private ? 'Private' : 'Public'} + +
    +
    + +
    + + +
    +
    + )} +
    + + + + {/* Pull Request Form */} + + {showPRForm && currentRepo && ( + + + + + + Create Pull Request + + + +
    +
    + + setPrTitle(e.target.value)} + /> +
    + +
    + +