diff --git a/packages/components/.gitignore b/packages/components/.gitignore new file mode 100644 index 00000000..f52343a9 --- /dev/null +++ b/packages/components/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*storybook.log +storybook-static diff --git a/packages/components/.storybook/main.ts b/packages/components/.storybook/main.ts new file mode 100644 index 00000000..546a447f --- /dev/null +++ b/packages/components/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + "stories": [ + "../src/**/*.mdx", + "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" + ], + "addons": [ + "@chromatic-com/storybook", + "@storybook/addon-docs", + "@storybook/addon-a11y", + "@storybook/addon-vitest" + ], + "framework": { + "name": "@storybook/react-vite", + "options": {} + } +}; +export default config; \ No newline at end of file diff --git a/packages/components/.storybook/preview.ts b/packages/components/.storybook/preview.ts new file mode 100644 index 00000000..7f2713ae --- /dev/null +++ b/packages/components/.storybook/preview.ts @@ -0,0 +1,15 @@ +import type { Preview } from '@storybook/react-vite' +import '../src/index.css' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/packages/components/README.md b/packages/components/README.md new file mode 100644 index 00000000..7959ce42 --- /dev/null +++ b/packages/components/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/packages/components/components.json b/packages/components/components.json new file mode 100644 index 00000000..13e1db0b --- /dev/null +++ b/packages/components/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/components/eslint.config.js b/packages/components/eslint.config.js new file mode 100644 index 00000000..bc656236 --- /dev/null +++ b/packages/components/eslint.config.js @@ -0,0 +1,26 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from "eslint-plugin-storybook"; + +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +], storybook.configs["flat/recommended"]); diff --git a/packages/components/package.json b/packages/components/package.json new file mode 100644 index 00000000..f4fd0956 --- /dev/null +++ b/packages/components/package.json @@ -0,0 +1,57 @@ +{ + "name": "@green-ecolution/components", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "storybook dev -p 6006", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@fontsource/lato": "^5.2.6", + "@fontsource/nunito-sans": "^5.2.6", + "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/vite": "^4.1.11", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.503.0", + "react": "catalog:react19", + "react-dom": "catalog:react19", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11" + }, + "devDependencies": { + "@chromatic-com/storybook": "^4.0.1", + "@eslint/js": "^9.30.1", + "@storybook/addon-a11y": "^9.0.16", + "@storybook/addon-docs": "^9.0.16", + "@storybook/addon-vitest": "^9.0.16", + "@storybook/react-vite": "^9.0.16", + "@types/node": "^24.0.13", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "@vitest/browser": "^3.2.4", + "@vitest/coverage-v8": "^3.2.4", + "eslint": "^9.30.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "eslint-plugin-storybook": "^9.0.16", + "globals": "^16.3.0", + "playwright": "^1.54.1", + "react-hook-form": "^7.60.0", + "storybook": "^9.0.16", + "tw-animate-css": "^1.3.5", + "typescript": "~5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^7.0.4", + "vitest": "^3.2.4", + "zod": "^3.25.76" + } +} diff --git a/packages/components/src/App.tsx b/packages/components/src/App.tsx new file mode 100644 index 00000000..3d7ded3f --- /dev/null +++ b/packages/components/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/packages/components/src/components/cards/dashboard.tsx b/packages/components/src/components/cards/dashboard.tsx new file mode 100644 index 00000000..05e783ae --- /dev/null +++ b/packages/components/src/components/cards/dashboard.tsx @@ -0,0 +1,38 @@ +import { MoveRight } from 'lucide-react' +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../ui/card' +import { cn } from '@/lib/utils' + +interface DashboardCard { + headline: string + description: string + linkLabel: string + theme: 'dark' | 'light' | 'white' +} + +const themeClasses = { + dark: 'border-green-dark bg-green-dark/5 hover:bg-green-dark/10', + light: 'border-green-light bg-green-light/5 hover:bg-green-light/10', + white: 'border-dark bg-white hover:bg-dark/5', +} + +const DashboardCard = ({ headline, description, linkLabel, theme }: DashboardCard) => { + return ( + + + {headline} + + {description} + + {linkLabel} + + + + ) +} + +export default DashboardCard diff --git a/packages/components/src/components/notice.tsx b/packages/components/src/components/notice.tsx new file mode 100644 index 00000000..01cde794 --- /dev/null +++ b/packages/components/src/components/notice.tsx @@ -0,0 +1,22 @@ +import { Info } from 'lucide-react' +import { Alert, AlertDescription, AlertTitle } from './ui/alert' +import type { ReactNode } from 'react' + +interface NoticeProps { + title: string + description: string + icon?: ReactNode + variant?: 'default' | 'destructive' +} + +const Notice = ({ title, description, variant, icon = }: NoticeProps) => { + return ( + + {icon} + {title} + {description} + + ) +} + +export default Notice diff --git a/packages/components/src/components/ui/alert.tsx b/packages/components/src/components/ui/alert.tsx new file mode 100644 index 00000000..bce15006 --- /dev/null +++ b/packages/components/src/components/ui/alert.tsx @@ -0,0 +1,63 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current shadow-lg border-dark/15', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-destructive/10 [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/packages/components/src/components/ui/badge.tsx b/packages/components/src/components/ui/badge.tsx new file mode 100644 index 00000000..66492e0c --- /dev/null +++ b/packages/components/src/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-4 py-1 text-sm font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return +} + +export { Badge, badgeVariants } diff --git a/packages/components/src/components/ui/button.tsx b/packages/components/src/components/ui/button.tsx new file mode 100644 index 00000000..1211dc62 --- /dev/null +++ b/packages/components/src/components/ui/button.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow-xs hover:bg-secondary', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-5 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/packages/components/src/components/ui/card.tsx b/packages/components/src/components/ui/card.tsx new file mode 100644 index 00000000..d05bbc6c --- /dev/null +++ b/packages/components/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/packages/components/src/components/ui/form.tsx b/packages/components/src/components/ui/form.tsx new file mode 100644 index 00000000..7d7474cc --- /dev/null +++ b/packages/components/src/components/ui/form.tsx @@ -0,0 +1,165 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +