├── .eslintrc.json ├── app ├── favicon.ico ├── layout.tsx ├── test │ └── page.tsx ├── globals.css └── page.tsx ├── .vscode └── settings.json ├── public ├── codelab-cody.ico ├── codelab-cody.png ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── stories ├── assets │ ├── docs.png │ ├── assets.png │ ├── context.png │ ├── share.png │ ├── styling.png │ ├── testing.png │ ├── theming.png │ ├── figma-plugin.png │ ├── accessibility.png │ ├── addon-library.png │ ├── avif-test-image.avif │ ├── youtube.svg │ ├── tutorials.svg │ ├── accessibility.svg │ ├── discord.svg │ └── github.svg ├── Carousel │ ├── Carousel.stories.tsx │ └── Documentation.mdx ├── Progress │ ├── Progress.stories.tsx │ └── Documentation.mdx ├── DataTable │ ├── DataTable.stories.tsx │ └── Documentation.mdx ├── Alert │ ├── Alert.stories.tsx │ └── Documentation.mdx ├── Slider │ ├── Slider.stories.tsx │ └── Documentation.mdx ├── Button │ ├── Button.stories.ts │ └── Documentation.mdx ├── HoverCard │ ├── Documentation.mdx │ └── HoverCard.stories.tsx ├── Toast │ ├── Toast.stories.tsx │ └── Documentation.mdx ├── Stepper │ ├── Documentation.mdx │ └── Stepper.stories.tsx ├── Tabs │ ├── Tabs.stories.tsx │ └── Documentation.mdx ├── Card │ ├── Documentation.mdx │ └── Card.stories.tsx └── Configure.mdx ├── assets └── images │ └── first-ticket │ ├── figma-dev-mode.png │ └── button-dev-preview.png ├── next.config.ts ├── .prettierrc ├── postcss.config.mjs ├── lib └── utils.ts ├── .storybook ├── manager.ts ├── preview.ts └── main.ts ├── pull_request_template.md ├── .github └── ISSUE_TEMPLATE │ ├── documentation-update-template.md │ ├── feature_request.md │ └── bug_report.md ├── components.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── components ├── ui │ ├── input.tsx │ ├── checkbox.tsx │ ├── toaster.tsx │ ├── progress.tsx │ ├── alert.tsx │ ├── card.tsx │ ├── button.tsx │ ├── hover-card.tsx │ ├── table.tsx │ ├── stepper.tsx │ ├── tabs.tsx │ ├── toast.tsx │ ├── slider.tsx │ ├── carousel.tsx │ ├── dropdown-menu.tsx │ └── data-table.tsx └── testcomponents │ └── CarouselDemo.tsx ├── package.json ├── README.md ├── CONTRIBUTING.md ├── hooks └── use-toast.ts └── FIRST-TICKET.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.css": "tailwindcss" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/codelab-cody.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/public/codelab-cody.ico -------------------------------------------------------------------------------- /public/codelab-cody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/public/codelab-cody.png -------------------------------------------------------------------------------- /stories/assets/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/docs.png -------------------------------------------------------------------------------- /stories/assets/assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/assets.png -------------------------------------------------------------------------------- /stories/assets/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/context.png -------------------------------------------------------------------------------- /stories/assets/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/share.png -------------------------------------------------------------------------------- /stories/assets/styling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/styling.png -------------------------------------------------------------------------------- /stories/assets/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/testing.png -------------------------------------------------------------------------------- /stories/assets/theming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/theming.png -------------------------------------------------------------------------------- /stories/assets/figma-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/figma-plugin.png -------------------------------------------------------------------------------- /stories/assets/accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/accessibility.png -------------------------------------------------------------------------------- /stories/assets/addon-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/addon-library.png -------------------------------------------------------------------------------- /stories/assets/avif-test-image.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/stories/assets/avif-test-image.avif -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/first-ticket/figma-dev-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/assets/images/first-ticket/figma-dev-mode.png -------------------------------------------------------------------------------- /assets/images/first-ticket/button-dev-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codelab-Davis/codelab-ui-components/HEAD/assets/images/first-ticket/button-dev-preview.png -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": false, 5 | "trailingComma": "es5", 6 | "printWidth": 70, 7 | "bracketSpacing": true 8 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/manager-api"; 2 | import { themes } from "@storybook/theming"; 3 | 4 | addons.setConfig({ 5 | theme: { 6 | ...themes.dark, 7 | brandTitle: "CodeLab UI Components", 8 | brandImage: "codelab-cody.ico", 9 | brandUrl: "https://codelabdavis.com", 10 | textColor: "#fff", 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import "../app/globals.css"; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please provide a brief description of the changes. 4 | 5 | ## Related Issue 6 | 7 | Closes # 8 | 9 | ## Type of Change 10 | 11 | - [ ] Bug fix 12 | - [ ] New feature 13 | - [ ] Documentation update 14 | - [ ] Refactoring 15 | 16 | ## Checklist 17 | 18 | - [ ] run npm run lint to check for errors 19 | - [ ] run npm run prettier to fix styling 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-update-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Update Template 3 | about: improve documentation for code. 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Reason for making change?** 10 | What is wrong with the current documentation? 11 | 12 | **What file or part of documentation?** 13 | Write file name or part of documentation. 14 | **Additional context** 15 | Add any other context or screenshots about the change here. 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /stories/Carousel/Carousel.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CarouselDemo } from "@/components/testcomponents/CarouselDemo"; 3 | const meta = { 4 | title: "UI/Carousel", 5 | component: CarouselDemo, 6 | parameters: { 7 | layout: "centered", 8 | }, 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | variant: "default", 17 | size: "default", 18 | children: <>CarouselDemo, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/nextjs"; 2 | 3 | const config: StorybookConfig = { 4 | stories: [ 5 | "../stories/**/*.mdx", 6 | "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", 7 | ], 8 | addons: [ 9 | "@storybook/addon-onboarding", 10 | "@storybook/addon-essentials", 11 | "@chromatic-com/storybook", 12 | "@storybook/addon-interactions", 13 | ], 14 | framework: { 15 | name: "@storybook/nextjs", 16 | options: {}, 17 | }, 18 | staticDirs: ["../public"], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Create Next App", 6 | description: "Generated by create next app", 7 | }; 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: Readonly<{ 12 | children: React.ReactNode; 13 | }>) { 14 | return ( 15 | 16 | 19 | {children} 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the feature is. Is it a new component, a new developer tool, etc. 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. If is is a component, link the respective shadcn component here. If you don't plan on using shadcn. give a reasonable explanation. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /stories/assets/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | *storybook.log 44 | 45 | # test directory in app 46 | app/test 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - Browser [e.g. chrome, safari] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. If you need, delete sections that are not relevant. 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 CodeLab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ 6 | className, 7 | type, 8 | ...props 9 | }: React.ComponentProps<"input">) { 10 | return ( 11 | 22 | ); 23 | } 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /stories/assets/tutorials.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { Check } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 26 | 27 | 28 | 29 | )); 30 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 31 | 32 | export { Checkbox }; 33 | -------------------------------------------------------------------------------- /components/testcomponents/CarouselDemo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Card, CardContent } from "@/components/ui/card"; 4 | import { 5 | Carousel, 6 | CarouselContent, 7 | CarouselItem, 8 | CarouselNext, 9 | CarouselPrevious, 10 | } from "@/components/ui/carousel"; 11 | 12 | export function CarouselDemo() { 13 | return ( 14 | 15 | 16 | {Array.from({ length: 5 }).map((_, index) => ( 17 | 18 |
19 | 20 | 21 | 22 | {index + 1} 23 | 24 | 25 | 26 |
27 |
28 | ))} 29 |
30 | 31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /stories/Progress/Progress.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Progress } from "@/components/ui/progress"; 3 | 4 | const meta = { 5 | title: "UI/Progress Bar", 6 | component: Progress, 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | args: { 11 | total: 100, 12 | current: 50, 13 | value: 50, 14 | className: "w-[500px]", 15 | }, 16 | } satisfies Meta; 17 | 18 | export default meta; 19 | type Story = StoryObj; 20 | 21 | export const Default: Story = { 22 | args: { 23 | total: 100, 24 | current: 80, 25 | value: 80, 26 | }, 27 | }; 28 | 29 | export const QuarterComplete: Story = { 30 | args: { 31 | total: 100, 32 | current: 25, 33 | value: 25, 34 | }, 35 | }; 36 | 37 | export const HalfComplete: Story = { 38 | args: { 39 | total: 100, 40 | current: 50, 41 | value: 50, 42 | }, 43 | }; 44 | 45 | export const AlmostComplete: Story = { 46 | args: { 47 | total: 100, 48 | current: 90, 49 | value: 90, 50 | }, 51 | }; 52 | 53 | export const Complete: Story = { 54 | args: { 55 | total: 100, 56 | current: 100, 57 | value: 100, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useToast } from "@/hooks/use-toast"; 4 | import { 5 | Toast, 6 | ToastClose, 7 | ToastDescription, 8 | ToastProvider, 9 | ToastTitle, 10 | ToastViewport, 11 | } from "@/components/ui/toast"; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ 19 | id, 20 | title, 21 | description, 22 | action, 23 | ...props 24 | }) { 25 | return ( 26 | 27 |
28 | {title && ( 29 | {title} 30 | )} 31 | {description && ( 32 | 33 | {description} 34 | 35 | )} 36 |
37 | {action} 38 | 39 |
40 | ); 41 | })} 42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /stories/DataTable/DataTable.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { fn } from "@storybook/test"; 3 | import { DataTable, Payment } from "@/components/ui/data-table"; 4 | 5 | const dataTable_data: Payment[] = [ 6 | { 7 | id: "m5gr84i9", 8 | amount: 316, 9 | status: "success", 10 | email: "ken99@example.com", 11 | }, 12 | { 13 | id: "3u1reuv4", 14 | amount: 242, 15 | status: "success", 16 | email: "Abe45@example.com", 17 | }, 18 | { 19 | id: "derv1ws0", 20 | amount: 837, 21 | status: "processing", 22 | email: "Monserrat44@example.com", 23 | }, 24 | { 25 | id: "5kma53ae", 26 | amount: 874, 27 | status: "success", 28 | email: "Silas22@example.com", 29 | }, 30 | { 31 | id: "bhqecj4p", 32 | amount: 721, 33 | status: "failed", 34 | email: "carmella@example.com", 35 | }, 36 | ]; 37 | 38 | const meta = { 39 | title: "UI/DataTable", 40 | component: DataTable, 41 | parameters: { 42 | layout: "centered", 43 | }, 44 | args: { data: dataTable_data }, 45 | } satisfies Meta; 46 | 47 | export default meta; 48 | type Story = StoryObj; 49 | 50 | export const Default: Story = { 51 | args: { 52 | data: dataTable_data 53 | }, 54 | }; -------------------------------------------------------------------------------- /stories/Alert/Alert.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { 3 | Alert, 4 | AlertTitle, 5 | AlertDescription, 6 | } from "@/components/ui/alert"; 7 | import { TriangleAlert } from "lucide-react"; 8 | 9 | const meta = { 10 | title: "UI/Alert", 11 | component: Alert, 12 | parameters: { 13 | layout: "centered", 14 | }, 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const Default: Story = { 21 | args: { 22 | variant: "default", 23 | size: "default", 24 | children: ( 25 | <> 26 | 27 | Warning! 28 | 29 | Changes may not be saved if you leave this page. 30 | 31 | 32 | ), 33 | }, 34 | }; 35 | 36 | export const Destructive: Story = { 37 | args: { 38 | variant: "destructive", 39 | size: "default", 40 | children: ( 41 | <> 42 | 43 | Warning! 44 | 45 | Changes may not be saved if you leave this page. 46 | 47 | 48 | ), 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /stories/Slider/Slider.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { action } from "@storybook/addon-actions"; 3 | import { Slider, type SliderProps } from "@/components/ui/slider"; 4 | 5 | const meta = { 6 | title: "UI/Slider", 7 | component: Slider, 8 | 9 | args: { 10 | min: 0, 11 | max: 100, 12 | step: 1, 13 | defaultValue: [50], 14 | hasLabels: false, 15 | showTooltip: false, 16 | onValueChange: action("value changed"), 17 | }, 18 | } satisfies Meta; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const Default: Story = { 24 | args: {}, 25 | }; 26 | 27 | export const WithLabels: Story = { 28 | args: { 29 | hasLabels: true, 30 | }, 31 | }; 32 | 33 | export const WithTooltip: Story = { 34 | args: { 35 | showTooltip: true, 36 | }, 37 | }; 38 | 39 | export const Small: Story = { 40 | args: { 41 | size: "sm", 42 | }, 43 | }; 44 | 45 | export const Base: Story = { 46 | args: { 47 | size: "default", 48 | }, 49 | }; 50 | 51 | export const Large: Story = { 52 | args: { 53 | size: "lg", 54 | }, 55 | }; 56 | 57 | export const CustomRange: Story = { 58 | args: { 59 | min: 10, 60 | max: 200, 61 | step: 5, 62 | defaultValue: [50], 63 | hasLabels: true, 64 | showTooltip: true, 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /stories/assets/accessibility.svg: -------------------------------------------------------------------------------- 1 | Accessibility -------------------------------------------------------------------------------- /stories/Progress/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as ProgressStories from "./Progress.stories"; 3 | 4 | 5 | 6 | # Progress Bar 7 | 8 | A flexible and accessible progress bar component built on top of Radix UI's Progress primitive. It visually represents the completion status of a task or process, supporting dynamic updates and customizable styles. 9 | 10 | ## Table of Contents 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Props](#props) 15 | - [Variants](#variants) 16 | - [Sizes](#sizes) 17 | - [Examples](#examples) 18 | - [Best Practices](#best-practices) 19 | - [Accessibility](#accessibility) 20 | 21 | --- 22 | 23 | ## Installation 24 | 25 | To integrate the Progress Bar component into your project, import it from the components directory: 26 | 27 | ```tsx 28 | import { Progress } from "@/components/ui/progress"; 29 | ``` 30 | 31 | Using the Progress Bar component is straightforward. Here’s a basic example: 32 | 33 | ```tsx 34 | 35 | ``` 36 | 37 | 38 | 39 | The example above demonstrates a progress bar that is 80% complete. 40 | 41 | ## Props 42 | 43 | The Progress Bar component accepts the following props to control its appearance and behavior: 44 | 45 | 46 | 47 | ## Accessibility 48 | 49 | The Progress Bar component is built with accessibility in mind: 50 | 51 | - **Screen Reader Compatibility:** provides clear feedback on the current progress value for screen reader users 52 | -------------------------------------------------------------------------------- /components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | interface ProgressProps 8 | extends React.ComponentPropsWithoutRef< 9 | typeof ProgressPrimitive.Root 10 | > { 11 | total?: number; 12 | current?: number; 13 | } 14 | 15 | const Progress = React.forwardRef< 16 | React.ElementRef, 17 | ProgressProps 18 | >( 19 | ( 20 | { className, value = 0, total = 0, current = 0, ...props }, 21 | ref 22 | ) => ( 23 |
24 | 32 | 38 | 39 |

40 | Completed {current} of {total} 41 |

42 |
43 | ) 44 | ); 45 | Progress.displayName = ProgressPrimitive.Root.displayName; 46 | 47 | export { Progress }; 48 | -------------------------------------------------------------------------------- /stories/Button/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { fn } from "@storybook/test"; 3 | import { Button } from "@/components/ui/button"; 4 | 5 | const meta = { 6 | title: "UI/Button", 7 | component: Button, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | // tags: ["autodocs"], 12 | args: { onClick: fn() }, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | args: { 20 | children: "Button", 21 | variant: "default", 22 | size: "default", 23 | }, 24 | }; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | children: "Button", 29 | variant: "default", 30 | size: "default", 31 | }, 32 | }; 33 | 34 | export const Destructive: Story = { 35 | args: { 36 | children: "Delete", 37 | variant: "destructive", 38 | }, 39 | }; 40 | 41 | export const Outline: Story = { 42 | args: { 43 | children: "Outline", 44 | variant: "outline", 45 | }, 46 | }; 47 | 48 | export const Secondary: Story = { 49 | args: { 50 | children: "Secondary", 51 | variant: "secondary", 52 | }, 53 | }; 54 | 55 | export const Ghost: Story = { 56 | args: { 57 | children: "Ghost", 58 | variant: "ghost", 59 | }, 60 | }; 61 | 62 | export const Link: Story = { 63 | args: { 64 | children: "Link Button", 65 | variant: "link", 66 | }, 67 | }; 68 | 69 | export const Small: Story = { 70 | args: { 71 | children: "Small", 72 | size: "sm", 73 | }, 74 | }; 75 | 76 | export const Base: Story = { 77 | args: { 78 | children: "Default", 79 | size: "default", 80 | }, 81 | }; 82 | 83 | export const Large: Story = { 84 | args: { 85 | children: "Large", 86 | size: "lg", 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ui-codelab", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", 11 | "storybook": "storybook dev -p 6006", 12 | "build-storybook": "storybook build" 13 | }, 14 | "dependencies": { 15 | "@radix-ui/react-checkbox": "^1.1.4", 16 | "@radix-ui/react-dropdown-menu": "^2.1.6", 17 | "@radix-ui/react-hover-card": "^1.1.6", 18 | "@radix-ui/react-progress": "^1.1.2", 19 | "@radix-ui/react-slider": "^1.2.3", 20 | "@radix-ui/react-slot": "^1.1.1", 21 | "@radix-ui/react-tabs": "^1.1.8", 22 | "@radix-ui/react-toast": "^1.2.6", 23 | "@tanstack/react-table": "^8.21.2", 24 | "class-variance-authority": "^0.7.1", 25 | "clsx": "^2.1.1", 26 | "embla-carousel-react": "^8.5.2", 27 | "lucide-react": "^0.474.0", 28 | "next": "^15.1.6", 29 | "react": "^19.0.0", 30 | "react-dom": "^19.0.0", 31 | "shadcn-ui": "^0.9.4", 32 | "tailwind-merge": "^3.0.1", 33 | "tailwindcss-animate": "^1.0.7" 34 | }, 35 | "devDependencies": { 36 | "@chromatic-com/storybook": "^3.2.4", 37 | "@storybook/addon-essentials": "^8.5.3", 38 | "@storybook/addon-interactions": "^8.5.3", 39 | "@storybook/addon-onboarding": "^8.5.3", 40 | "@storybook/blocks": "^8.5.3", 41 | "@storybook/nextjs": "^8.5.3", 42 | "@storybook/react": "^8.5.3", 43 | "@storybook/test": "^8.5.3", 44 | "@tailwindcss/postcss": "^4.0.0", 45 | "@types/node": "^20", 46 | "@types/react": "^19", 47 | "@types/react-dom": "^19", 48 | "eslint": "^9", 49 | "eslint-config-next": "15.1.6", 50 | "postcss": "^8", 51 | "prettier": "3.4.2", 52 | "storybook": "^8.5.3", 53 | "tailwindcss": "^4.0.6", 54 | "typescript": "^5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const alertVariants = cva( 6 | "relative rounded-lg border px-[17px] py-[17px] [&>svg]:absolute", 7 | { 8 | variants: { 9 | variant: { 10 | default: "bg-[#FFFFFF] text-foreground", 11 | destructive: "text-[#EF4343] border-[#EF4343]", 12 | }, 13 | size: { 14 | default: "w-[556px] h-[78px]", 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: "default", 19 | size: "default", 20 | }, 21 | } 22 | ); 23 | 24 | const Alert = React.forwardRef< 25 | HTMLDivElement, 26 | React.HTMLAttributes & 27 | VariantProps 28 | >(({ className, variant, size, ...props }, ref) => ( 29 |
35 | )); 36 | Alert.displayName = "Alert"; 37 | 38 | const AlertTitle = React.forwardRef< 39 | HTMLParagraphElement, 40 | React.HTMLAttributes 41 | >(({ className, ...props }, ref) => ( 42 |
50 | )); 51 | AlertTitle.displayName = "AlertTitle"; 52 | 53 | const AlertDescription = React.forwardRef< 54 | HTMLParagraphElement, 55 | React.HTMLAttributes 56 | >(({ className, ...props }, ref) => ( 57 |
65 | )); 66 | AlertDescription.displayName = "AlertDescription"; 67 | 68 | export { Alert, AlertTitle, AlertDescription }; 69 | -------------------------------------------------------------------------------- /stories/Carousel/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as CarouselStories from "./Carousel.stories"; 3 | import { CarouselDemo } from "@/components/testcomponents/CarouselDemo"; 4 | 5 | 6 | 7 | # Carousel 8 | 9 | A simple carousel component built on top of ShadCN's carousel component. 10 | 11 | ## Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Props](#props) 16 | - [Variants](#variants) 17 | - [Default](#default) 18 | - [Sizes](#sizes) 19 | - [Default](#default) 20 | - [Best Practices](#best-practices) 21 | 22 | --- 23 | 24 | ## Installation 25 | 26 | To integrate the Carousel component into your project, import it from the components directory: 27 | 28 | ```tsx 29 | import { Carousel } from "@/components/ui/carousel"; 30 | ``` 31 | 32 | --- 33 | 34 | ## Usage 35 | 36 | Using the Carousel component is straightforward. Here’s a basic example: 37 | 38 | ```tsx 39 | import { 40 | CarouselDemo, 41 | } from "@/components/ui/CarouselDemo"; 42 | 43 | ``` 44 | 45 | 46 | 47 | The example above demonstrates a simple carousel with the default styling. 48 | 49 | --- 50 | 51 | ## Props 52 | 53 | The Carousel component accepts a variety of props to control its appearance and behavior. You can explore and modify these properties using the Storybook Controls panel below: 54 | 55 | 56 | 57 | --- 58 | 59 | ## Variants 60 | 61 | The Carousel component currently only includes the default variant. 62 | 63 | ### Default 64 | 65 | The default variant, ideal for showing an carousel. 66 | 67 | 68 | 69 | --- 70 | 71 | ## Sizes 72 | 73 | The Carousel component currently only supports one size. 74 | 75 | ### Default 76 | 77 | The standard size at the moment. 78 | 79 | 80 | 81 | --- 82 | 83 | ## Best Practices 84 | 85 | When using the Carousel component, consider these guidelines to ensure a consistent and user-friendly experience: 86 | 87 | 1. **Concise Text:** Keep Carousel titles and descriptions short and urgent. 88 | 2. **Icon Usage:** Incorporate icons that convey caution. 89 | -------------------------------------------------------------------------------- /stories/DataTable/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from '@storybook/blocks'; 2 | import { DataTable, Payment } from "@/components/ui/data-table"; 3 | import * as DataTableStories from './DataTable.stories'; 4 | 5 | 6 | 7 | # DataTable 8 | 9 | A stylized data-table component built on top of ShadCN's data-table component. 10 | 11 | ## Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Props](#props) 16 | - [Variants](#variants) 17 | - [Default](#default) 18 | - [Sizes](#sizes) 19 | - [Default](#default) 20 | - [Best Practices](#best-practices) 21 | 22 | --- 23 | 24 | ## Installation 25 | 26 | To integrate the DataTable component into your project, import it and the data type it operates on, Payment, from the components directory: 27 | 28 | ```tsx 29 | import { DataTable, Payment } from "@/components/ui/data-table"; 30 | ``` 31 | 32 | --- 33 | 34 | ## Usage 35 | 36 | Using the DataTable component is straightforward. Here’s a basic example: 37 | 38 | ```tsx 39 | const dataTable_data: Payment[] = [ 40 | { 41 | id: "m5gr84i9", 42 | amount: 316, 43 | status: "success", 44 | email: "ken99@example.com", 45 | }, 46 | ] 47 | 48 | ``` 49 | 50 | The example above demonstrates a data table with 1 Payment data entry. 51 | 52 | --- 53 | 54 | ## Props 55 | 56 | The DataTable component accepts a sigle prop, data, which is an aray of Payment entries to render. You can modify the rendered Payment entries using the Storybook Controls panel below: 57 | 58 | 59 | 60 | --- 61 | 62 | ## Variants 63 | 64 | The DataTable component currently only includes the default variant. 65 | 66 | ### Default 67 | 68 | The default variant renders a data table. 69 | 70 | 71 | 72 | --- 73 | 74 | ## Sizes 75 | 76 | The DataTable component currently only supports one size. 77 | 78 | ### Default 79 | 80 | The standard size at the moment. 81 | 82 | 83 | 84 | --- 85 | 86 | ## Best Practices 87 | 88 | When using the DataTable component, consider these guidelines to ensure a consistent and user-friendly experience: 89 | 90 | 1. **Data:** Fetching data, to be rendered, must be incorporated by the user. -------------------------------------------------------------------------------- /stories/assets/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/HoverCard/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as HoverCardStories from "./HoverCard.stories"; 3 | 4 | 5 | 6 | # HoverCard 7 | 8 | A simple Hover Card component built on top of ShadCN's HoverCard component allowing for customization of its trigger, title, description, and a date. 9 | 10 | ## Table of Contents 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Props](#props) 14 | - [Variants](#variants) 15 | - [Default](#default) 16 | - [Sizes](#sizes) 17 | - [Default](#default) 18 | - [Best Practices](#best-practices) 19 | 20 | --- 21 | 22 | ## Installation 23 | 24 | To use the Hover Card component, import it directly from the components directory: 25 | 26 | ```tsx 27 | import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card"; 28 | ``` 29 | 30 | --- 31 | 32 | ## Usage 33 | 34 | Using the HoverCard component is straightforward. Heres a basic example: 35 | 36 | ```tsx 37 | 38 | @codelabdavis 39 | 40 |
Card content lives inside the HoverCardContent tag
41 |
42 |
43 | ``` 44 | 45 | 46 | 47 | The example above demonstrates a simple Hover Card with the default styling and some example content. 48 | 49 | --- 50 | 51 | ## Props 52 | 53 | The HoverCard component accepts variations of size and color to control its appearance. You can explore and modify these properties using the Storybook Controls panel below: 54 | 55 | 56 | 57 | --- 58 | 59 | ## Variants 60 | 61 | The HoverCard component currently only includes the default variant 62 | 63 | ### Default 64 | 65 | The default configuration of a HoverCard has built-in styling for the trigger, padding for the content, and margins. Contains example content. 66 | 67 | 68 | 69 | --- 70 | 71 | ## Sizes 72 | 73 | The HoverCard component currently only supports one size 74 | 75 | ### Default 76 | 77 | The standard size 78 | 79 | 80 | 81 | --- 82 | 83 | ## Best Practices 84 | 85 | When using the HoverCard component, consider these guidelines to ensure a consistent and user-friendly experience: 86 | 87 | 1. **Consise Text:** Keep content compact and neatly laid out in the card for best visual effect. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 CodeLab Open Source 2 | 3 | Welcome to **CodeLab Open Source - Component Library**, a collection of beautifully designed, reusable UI components built with **Next.js, Tailwind CSS, ShadCN, and Storybook**. This project is **licensed under the MIT License**, making it free to use and contribute to. 4 | 5 | ## ✨ Tech Stack 6 | 7 | - **[Next.js](https://nextjs.org/)** – React framework for SSR, API routes, and performance optimization. 8 | - **[Tailwind CSS](https://tailwindcss.com/)** – Utility-first CSS framework for rapid styling. 9 | - **[ShadCN](https://ui.shadcn.com/)** – A customizable component library based on Radix UI. 10 | - **[Storybook](https://storybook.js.org/)** – A UI development environment for building and testing components in isolation. 11 | 12 | ## 🛠 Custom Design Approach 13 | 14 | We use **ShadCN** components as the foundation and apply **our own custom styles** on top of them. This allows us to: 15 | 16 | - Maintain **consistency** across the component library. 17 | - Ensure components **align with our design system**. 18 | - Keep up with **ShadCN updates** while having our own unique look. 19 | 20 | ## 📜 Available Commands 21 | 22 | Run the following **npm scripts** for development and testing: 23 | 24 | | Command | Description | 25 | | ------------------- | ----------------------------------------- | 26 | | `npm install` | Install dependencies | 27 | | `npm run dev` | Start the Next.js dev server | 28 | | `npm run build` | Build the project for production | 29 | | `npm run lint` | Run ESLint to check for code issues | 30 | | `npm run storybook` | Start Storybook for component development | 31 | | `npm run format` | Run prettier | 32 | 33 | ## 🤝 Contributing 34 | 35 | We welcome contributions! Please check out the **[CONTRIBUTING.md](CONTRIBUTING.md)** file for guidelines on how to get started. 36 | 37 | ## 📄 License 38 | 39 | This project is licensed under the **MIT License** – see the [LICENSE](LICENSE) file for details. 40 | 41 | ## 📬 Stay Connected 42 | 43 | - Join the **Discord Community**: [Invite Link](https://discord.gg/Nt6ardbM2X) 44 | - Visit our **Website**: [os.codelabdavis.com](https://os.codelabdavis.com/) 45 | - Follow us on **Insta**: [Instagram link](https://www.instagram.com/codelabdavis) 46 | 47 | --- 48 | 49 | Happy coding! 🎉 50 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
59 | )); 60 | CardDescription.displayName = "CardDescription"; 61 | 62 | const CardContent = React.forwardRef< 63 | HTMLDivElement, 64 | React.HTMLAttributes 65 | >(({ className, ...props }, ref) => ( 66 |
67 | )); 68 | CardContent.displayName = "CardContent"; 69 | 70 | const CardFooter = React.forwardRef< 71 | HTMLDivElement, 72 | React.HTMLAttributes 73 | >(({ className, ...props }, ref) => ( 74 |
79 | )); 80 | CardFooter.displayName = "CardFooter"; 81 | 82 | export { 83 | Card, 84 | CardHeader, 85 | CardFooter, 86 | CardTitle, 87 | CardDescription, 88 | CardContent, 89 | }; 90 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_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", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<"button"> & 44 | VariantProps & { 45 | asChild?: boolean; 46 | }) { 47 | const Comp = asChild ? Slot : "button"; 48 | 49 | return ( 50 | 57 | ); 58 | } 59 | 60 | export { Button, buttonVariants }; 61 | -------------------------------------------------------------------------------- /stories/HoverCard/HoverCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { 3 | HoverCard, 4 | HoverCardTrigger, 5 | HoverCardContent, 6 | } from "@/components/ui/hover-card"; 7 | import { CalendarDays } from "lucide-react"; 8 | 9 | const meta = { 10 | title: "UI/HoverCard", 11 | component: HoverCard, 12 | parameters: { 13 | layout: "centered", 14 | }, 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const Default: Story = { 21 | args: { 22 | children: ( 23 | <> 24 | @codelabdavis 25 | 26 |
27 |
28 | 29 | . 30 | 31 | 32 | / 33 | 34 |
35 |
36 |
37 |

38 | @codelabdavis 39 |

40 | 41 |

42 | Software and Design Agency at UC 43 | Davis 44 |

45 |
46 | 47 |
48 | 53 | 54 | 55 | Joined December 2021 56 | 57 |
58 |
59 |
60 |
61 | 62 | ), 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /stories/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 5 | import { cn } from "@/lib/utils"; 6 | import { cva, type VariantProps } from "class-variance-authority"; 7 | 8 | const HoverCard = HoverCardPrimitive.Root; 9 | 10 | const HoverCardTrigger = React.forwardRef< 11 | React.ComponentRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 19 | )); 20 | HoverCardTrigger.displayName = HoverCardPrimitive.Trigger.displayName; 21 | 22 | const HoverCardContentVarients = cva( 23 | "z-96 rounded-lg border pt-4 pb-4 pr-7 pl-7 mt-2.5 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 24 | { 25 | variants: { 26 | variant: { 27 | default: "bg-popover border-[#E4E4E7]", 28 | }, 29 | size: { 30 | default: "w-[300.58px] h-34", 31 | }, 32 | }, 33 | 34 | defaultVariants: { 35 | variant: "default", 36 | size: "default", 37 | }, 38 | } 39 | ); 40 | 41 | const HoverCardContent = React.forwardRef< 42 | React.ComponentRef, 43 | React.ComponentPropsWithoutRef< 44 | typeof HoverCardPrimitive.Content 45 | > & 46 | VariantProps 47 | >( 48 | ( 49 | { 50 | className, 51 | align = "center", 52 | sideOffset = 4, 53 | variant, 54 | size, 55 | ...props 56 | }, 57 | ref 58 | ) => ( 59 | 69 | ) 70 | ); 71 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 72 | 73 | export { HoverCard, HoverCardTrigger, HoverCardContent }; 74 | -------------------------------------------------------------------------------- /stories/Alert/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as AlertStories from "./Alert.stories"; 3 | import { Alert } from "@/components/ui/alert"; 4 | 5 | 6 | 7 | # Alert 8 | 9 | A simple static alert component built on top of ShadCN's alert component. 10 | 11 | ## Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Props](#props) 16 | - [Variants](#variants) 17 | - [Default](#default) 18 | - [Sizes](#sizes) 19 | - [Default](#default) 20 | - [Best Practices](#best-practices) 21 | 22 | --- 23 | 24 | ## Installation 25 | 26 | To integrate the Alert component into your project, import it from the components directory: 27 | 28 | ```tsx 29 | import { Alert } from "@/components/ui/alert"; 30 | ``` 31 | 32 | --- 33 | 34 | ## Usage 35 | 36 | Using the Alert component is straightforward. Here’s a basic example: 37 | 38 | ```tsx 39 | import { 40 | Alert, 41 | AlertDescription, 42 | AlertTitle, 43 | } from "@/components/ui/alert"; 44 | 45 | import { TriangleAlert } from "lucide-react"; 46 | 47 | 48 | Warning! 49 | 50 | Changes may not be saved if you leave this page. 51 | 52 | ; 53 | ``` 54 | 55 | 56 | 57 | The example above demonstrates a simple alert with the default styling. 58 | 59 | --- 60 | 61 | ## Props 62 | 63 | The Alert component accepts a variety of props to control its appearance and behavior. You can explore and modify these properties using the Storybook Controls panel below: 64 | 65 | 66 | 67 | --- 68 | 69 | ## Variants 70 | 71 | The Alert component includes a default and destructive variant. 72 | 73 | ### Default 74 | 75 | The default variant, ideal for showing an alert. 76 | 77 | 78 | 79 | ### Destructive 80 | 81 | The destructive variant, ideal for a critical alerts that require immediate attention. 82 | 83 | 84 | 85 | --- 86 | 87 | ## Sizes 88 | 89 | The Alert component currently only supports one size. 90 | 91 | ### Default 92 | 93 | The standard size at the moment. 94 | 95 | 96 | 97 | --- 98 | 99 | ## Best Practices 100 | 101 | When using the Alert component, consider these guidelines to ensure a consistent and user-friendly experience: 102 | 103 | 1. **Concise Text:** Keep Alert titles and descriptions short and urgent. 104 | 2. **Icon Usage:** Incorporate icons that convey caution. 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 🚀 2 | 3 | Thank you for considering contributing to CodeLab Open Source! We appreciate your efforts to improve CodeLab and help the community grow. 4 | 5 | ## Getting Started 6 | 7 | 1. **Search for Existing Issues**: Before starting any work, please check if there is already an existing issue related to your contribution. 8 | 2. **Creating a New Issue**: 9 | - If no issue exists, you can create one for: 10 | - Bug fixes 11 | - New features 12 | - Documentation improvements 13 | - Please provide a clear and detailed description. 14 | 3. **Issue Approval**: 15 | - The maintainers will review the issue to determine if it is valid and aligns with the project goals. 16 | - Once approved, you can assign the issue to yourself and start working on it. 17 | 18 | ## Pull Request (PR) Guidelines 19 | 20 | 1. **Issue Reference**: 21 | - All PRs must reference a GitHub issue (e.g., "Fixes #123"). 22 | - PRs without a corresponding issue number will not be reviewed. 23 | 2. **Branching**: 24 | - First, fork the `codelab-ui-components` repository and create a branch there. Once you think you are finished and ready, open a PR from your forked branch to Codelab's main repository. 25 | - All branch names should start with YOUR name in order for Codelab to recognize your contribution. 26 | - Use descriptive branch names (e.g., `Aaryan/add-slider`, `Nandini/typo-in-readme`). 27 | 3. **Commit Messages**: 28 | - Write clear, concise commit messages. 29 | - Follow the format: `(): ` (e.g., `fix(auth): correct password validation`). 30 | 4. **Code Reviews**: 31 | - PRs will be reviewed by maintainers or designated reviewers. 32 | - Address any feedback provided promptly. 33 | 34 | ## Code Quality 35 | 36 | - Follow industry coding standards. 37 | - To properly format code, use `npm run format` to run prettier before committing any code. If there are any file changes after the prettier format, commit those changes as well. 38 | - To check for errors, run `npm run lint` to the code. This should pass without any errors before commits. 39 | - Ensure code is clean and well-documented. 40 | - A lot of people are working on this project, so adding comments will help explain your goal with the code you've written. 41 | - Avoid including unrelated changes in a single PR. If you think there is something that needs to be fixed, open a new issue for it or contact a maintainer on Discord. 42 | 43 | ## Communication 44 | 45 | - Be respectful and considerate in all interactions. 46 | - Use GitHub Discussions or Issues for any questions or clarifications. 47 | 48 | ## License 49 | 50 | By contributing, you agree that your contributions will be licensed under the same license as the project. 51 | 52 | Thank you for your contribution! Happy coding! 53 | -------------------------------------------------------------------------------- /app/test/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | HoverCard, 3 | HoverCardTrigger, 4 | HoverCardContent, 5 | } from "@/components/ui/hover-card"; 6 | import { CalendarDays } from "lucide-react"; 7 | import { DataTable, Payment } from "@/components/ui/data-table"; 8 | 9 | const dataTable_data: Payment[] = [ 10 | { 11 | id: "m5gr84i9", 12 | amount: 316, 13 | status: "success", 14 | email: "ken99@example.com", 15 | }, 16 | { 17 | id: "3u1reuv4", 18 | amount: 242, 19 | status: "success", 20 | email: "Abe45@example.com", 21 | }, 22 | { 23 | id: "derv1ws0", 24 | amount: 837, 25 | status: "processing", 26 | email: "Monserrat44@example.com", 27 | }, 28 | { 29 | id: "5kma53ae", 30 | amount: 874, 31 | status: "success", 32 | email: "Silas22@example.com", 33 | }, 34 | { 35 | id: "bhqecj4p", 36 | amount: 721, 37 | status: "failed", 38 | email: "carmella@example.com", 39 | }, 40 | ]; 41 | 42 | export default function TestPage() { 43 | return ( 44 |
45 |
46 | 47 |
48 | 49 | @codelabdavis 50 | 51 |
52 |
53 | 54 | . 55 | 56 | 57 | / 58 | 59 |
60 |
61 |
62 |

63 | @codelabdavis 64 |

65 | 66 |

67 | Software and Design Agency at UC 68 | Davis 69 |

70 |
71 | 72 |
73 | 78 | 79 | 80 | Joined December 2021 81 | 82 |
83 |
84 |
85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | function Table({ 8 | className, 9 | ...props 10 | }: React.ComponentProps<"table">) { 11 | return ( 12 |
16 | 24 | 25 | ); 26 | } 27 | 28 | function TableHeader({ 29 | className, 30 | ...props 31 | }: React.ComponentProps<"thead">) { 32 | return ( 33 | 38 | ); 39 | } 40 | 41 | function TableBody({ 42 | className, 43 | ...props 44 | }: React.ComponentProps<"tbody">) { 45 | return ( 46 | 51 | ); 52 | } 53 | 54 | function TableFooter({ 55 | className, 56 | ...props 57 | }: React.ComponentProps<"tfoot">) { 58 | return ( 59 | tr]:last:border-b-0", 63 | className 64 | )} 65 | {...props} 66 | /> 67 | ); 68 | } 69 | 70 | function TableRow({ 71 | className, 72 | ...props 73 | }: React.ComponentProps<"tr">) { 74 | return ( 75 | 83 | ); 84 | } 85 | 86 | function TableHead({ 87 | className, 88 | ...props 89 | }: React.ComponentProps<"th">) { 90 | return ( 91 |
[role=checkbox]]:translate-y-[2px]", 95 | className 96 | )} 97 | {...props} 98 | /> 99 | ); 100 | } 101 | 102 | function TableCell({ 103 | className, 104 | ...props 105 | }: React.ComponentProps<"td">) { 106 | return ( 107 | [role=checkbox]]:translate-y-[2px]", 111 | className 112 | )} 113 | {...props} 114 | /> 115 | ); 116 | } 117 | 118 | function TableCaption({ 119 | className, 120 | ...props 121 | }: React.ComponentProps<"caption">) { 122 | return ( 123 |
131 | ); 132 | } 133 | 134 | export { 135 | Table, 136 | TableHeader, 137 | TableBody, 138 | TableFooter, 139 | TableHead, 140 | TableRow, 141 | TableCell, 142 | TableCaption, 143 | }; 144 | -------------------------------------------------------------------------------- /stories/Toast/Toast.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { toast, useToast } from "@/hooks/use-toast"; 3 | import { Toaster } from "@/components/ui/toaster"; 4 | import { Button } from "@/components/ui/button"; 5 | import { ToastAction } from "@/components/ui/toast"; 6 | 7 | const meta = { 8 | title: "UI/Toast", 9 | component: Toaster, 10 | parameters: { 11 | layout: "centered", 12 | }, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | const SimpleToastDemo = () => { 19 | const showToast = () => { 20 | toast({ description: "Your message has been sent." }); 21 | }; 22 | 23 | return ( 24 |
25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | const ToastWithTitleDemo = () => { 32 | const showToastWithTitle = () => { 33 | toast({ 34 | title: "Uh oh! Something went wrong.", 35 | description: "There was a problem with your request.", 36 | }); 37 | }; 38 | 39 | return ( 40 |
41 | 44 | 45 |
46 | ); 47 | }; 48 | 49 | const ToastWithTitleAndActionDemo = () => { 50 | const { dismiss } = useToast(); 51 | 52 | const showToastWithTitleAndAction = () => { 53 | toast({ 54 | title: "Uh oh! Something went wrong.", 55 | description: "There was a problem with your request.", 56 | action: ( 57 | dismiss()} 60 | > 61 | Try again 62 | 63 | ), 64 | }); 65 | }; 66 | 67 | return ( 68 |
69 | 72 | 73 |
74 | ); 75 | }; 76 | 77 | const DestructiveToastDemo = () => { 78 | const { dismiss } = useToast(); 79 | 80 | const showDestructiveToast = () => { 81 | toast({ 82 | title: "Uh oh! Something went wrong.", 83 | description: "There was a problem with your request.", 84 | action: ( 85 | dismiss()} 88 | > 89 | Try again 90 | 91 | ), 92 | variant: "destructive", 93 | }); 94 | }; 95 | 96 | return ( 97 |
98 | 104 | 105 |
106 | ); 107 | }; 108 | 109 | export const Simple: Story = { 110 | render: () => , 111 | }; 112 | 113 | export const WithTitle: Story = { 114 | render: () => , 115 | }; 116 | 117 | export const WithTitleAndAction: Story = { 118 | render: () => , 119 | }; 120 | 121 | export const Destructive: Story = { 122 | render: () => , 123 | }; 124 | -------------------------------------------------------------------------------- /stories/Stepper/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as StepperStories from "./Stepper.stories"; 3 | 4 | 5 | 6 | # Stepper 7 | 8 | A multi-step indictator component designed to guide users through a sequence of steps/stages. It supports a variety of visual styles including labels, icons, and combinations of both, making it ideal for workflows, checkout processes, and onboarding sequences. 9 | 10 | --- 11 | 12 | ## Table of Contents 13 | 14 | - [Installation](#installation) 15 | - [Usage](#usage) 16 | - [Props](#props) 17 | - [Variants](#variants) 18 | - [Sizes](#sizes) 19 | - [Examples](#examples) 20 | - [Best Practices](#best-practices) 21 | - [Accessibility](#accessibility) 22 | 23 | --- 24 | 25 | ## Installation 26 | 27 | To integrate the Stepper component into your project, import it from the components library: 28 | 29 | ```tsx 30 | import { Stepper } from "@/components/ui/stepper"; 31 | ``` 32 | 33 | --- 34 | 35 | ## Usage 36 | 37 | You can use the Stepper component with minimal setup. Here is a basic example: 38 | 39 | ```tsx 40 | 44 | ``` 45 | 46 | 47 | 48 | The example above displays the simplest style of the stepper component that is partially completed. You can further customize it using the available props and variants below. 49 | 50 | ## Props 51 | 52 | The Stepper component accepts the following props to control its appearance: 53 | 54 | 55 | 56 | --- 57 | 58 | ## Variants 59 | 60 | The Stepper component has several variants to accomodate different visiual and functional needs, allowing you to display steps using simple numbers, labels, and/or icons depending on your workflow or UX goals. 61 | 62 | ### Simple 63 | 64 | The bread and butter of the Stepper component—a straightforward version with all the core features you need to get started, no extras required. 65 | 66 | # 67 | 68 | # 69 | 70 | # 71 | 72 | ### With Labels 73 | 74 | This Stepper variant allows you to label each individual step circles for clarity. 75 | 76 | 77 | 78 |   79 | 80 | ### With Icons 81 | 82 | This Stepper variant allows you to customize each step circle with a Lucide icon. Check out the [Lucide icon library](https://lucide.dev/icons/) to browse a wide selection of icons. 83 | 84 | 85 | 86 | Example configuration: 87 | 88 | ```tsx 89 | steps: [ 90 | { label: "1", icon: Lucide.[exampleIcon1] }, 91 | { label: "2", icon: Lucide.[exampleIcon2] }, 92 | { label: "3", icon: Lucide.[exampleIcon3] },] 93 | ``` 94 | 95 | ### With Icons & Labels 96 | 97 | Combine the clarity of text labels with the visual appeal of icons using this variant. It’s ideal for enhancing both usability and design by providing context through both words and visuals. 98 | 99 | 100 | 101 |   102 | 103 | ## Examples 104 | 105 | Here are some practical examples demonstrating various use cases for the Stepper component. 106 | 107 | 108 | 109 |   110 | 111 | ## Best Practices 112 | 113 | When using the Stepper component, keep the following guidelines in mind to ensure clarity, usability, and consistency: 114 | 115 | 1. **Choose the Right Variant:** Select a variant that best matches your use case—use labels for clarity, icons for visual guidance, or both for maximum context. 116 | 2. **Keep Labels Concise:** Step labels should be short, descriptive, and easy to scan. Avoid long phrases that may cause layout issues. 117 | 3. **Use Icons Purposefully:** Add icons only when they enhance understanding or provide meaningful visual cues for each step. 118 | -------------------------------------------------------------------------------- /stories/Stepper/Stepper.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Stepper } from "@/components/ui/stepper"; 3 | import * as Lucide from "lucide-react"; 4 | 5 | const meta: Meta = { 6 | title: "UI/Stepper", 7 | component: Stepper, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | argTypes: { 12 | currentStep: { 13 | control: { type: "number", min: 1, max: 5 }, 14 | }, 15 | variant: { 16 | control: { 17 | type: "select", 18 | options: [ 19 | "simple", 20 | "withLabels", 21 | "withIcons", 22 | "withIconsAndLabels", 23 | "withCustomText", 24 | ], 25 | }, 26 | }, 27 | steps: { 28 | control: "object", 29 | description: 30 | "An array of step objects. Each step can include `content`, `label`, and `icon` (icon must be a Lucide icon component).", 31 | table: { 32 | type: { 33 | summary: 34 | "{ content?: string; label?: string; icon?: React.ComponentType> }[]", 35 | }, 36 | }, 37 | }, 38 | }, 39 | args: { 40 | currentStep: 2, 41 | className: "w-[500px]", 42 | steps: [ 43 | { content: "1", label: "Step 1" }, 44 | { content: "2", label: "Step 2" }, 45 | { content: "3", label: "Step 3" }, 46 | ], 47 | }, 48 | }; 49 | 50 | export default meta; 51 | 52 | type Story = StoryObj; 53 | 54 | export const Default: Story = { 55 | args: { 56 | variant: "simple", 57 | steps: [{ content: "1" }, { content: "2" }, { content: "3" }], 58 | }, 59 | }; 60 | 61 | export const WithLabels: Story = { 62 | args: { 63 | variant: "withLabels", 64 | currentStep: 2, 65 | steps: [ 66 | { content: "A", label: "Step 1" }, 67 | { content: "B", label: "Step 2" }, 68 | { content: "C", label: "Final Step" }, 69 | ], 70 | }, 71 | }; 72 | 73 | export const WithIcons: Story = { 74 | args: { 75 | variant: "withIcons", 76 | currentStep: 2, 77 | steps: [ 78 | { icon: Lucide.MapPinHouse }, 79 | { icon: Lucide.DollarSign }, 80 | { icon: Lucide.CircleCheckBig }, 81 | ], 82 | }, 83 | }; 84 | 85 | export const WithIconsAndLabels: Story = { 86 | args: { 87 | variant: "withIconsAndLabels", 88 | currentStep: 2, 89 | steps: [ 90 | { icon: Lucide.Lock, label: "Label 1" }, 91 | { icon: Lucide.User, label: "Label 2" }, 92 | { icon: Lucide.Check, label: "Label 3" }, 93 | ], 94 | }, 95 | }; 96 | 97 | export const UnfinishedStepper: Story = { 98 | args: { 99 | variant: "simple", 100 | currentStep: 0, 101 | steps: [{ content: "1" }, { content: "2" }, { content: "3" }], 102 | }, 103 | }; 104 | 105 | export const CompleteStepper: Story = { 106 | args: { 107 | variant: "simple", 108 | currentStep: 3, 109 | steps: [{ content: "1" }, { content: "2" }, { content: "3" }], 110 | }, 111 | }; 112 | 113 | export const ExampleUsage: Story = { 114 | args: { 115 | variant: "withIconsAndLabels", 116 | currentStep: 3, 117 | steps: [ 118 | { 119 | icon: Lucide.CircleCheckBigIcon, 120 | label: "Order Placed", 121 | }, 122 | { icon: Lucide.ChefHat, label: "Being Prepped" }, 123 | { icon: Lucide.Microwave, label: "In the Oven" }, 124 | { icon: Lucide.PackageCheck, label: "Boxed" }, 125 | { icon: Lucide.Car, label: "Transporting" }, 126 | { icon: Lucide.MapPinCheckInside, label: "Delivered!" }, 127 | ], 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /components/ui/stepper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | import * as LucideIcons from "lucide-react"; 4 | 5 | export interface StepperProps { 6 | steps: { 7 | label?: string; 8 | icon?: React.ComponentType>; 9 | content?: string; 10 | }[]; 11 | currentStep?: number; 12 | variant?: 13 | | "simple" 14 | | "withLabels" 15 | | "withIcons" 16 | | "withIconsAndLabels"; 17 | className?: string; 18 | } 19 | 20 | export function Stepper({ 21 | steps, 22 | currentStep = 1, 23 | variant = "simple", 24 | className, 25 | }: StepperProps) { 26 | const isCompleted = (stepIndex: number) => 27 | stepIndex + 1 < currentStep; 28 | const isCurrent = (stepIndex: number) => 29 | stepIndex + 1 === currentStep; 30 | 31 | const renderStep = ( 32 | step: StepperProps["steps"][0], 33 | index: number 34 | ) => { 35 | const stepContent = step.content || (index + 1).toString(); 36 | 37 | // Base styles for the step circle 38 | const stepStyles = cn( 39 | "min-w-8 min-h-8 px-3 py-1 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-200 text-center whitespace-nowrap", 40 | isCompleted(index) 41 | ? "bg-gradient-to-r from-[#ff9a3e] to-[#fe353b] text-white" 42 | : isCurrent(index) 43 | ? "bg-gradient-to-r from-[#ff9a3e] to-[#fe353b] text-white" 44 | : "bg-gray-100 text-gray-400" 45 | ); 46 | 47 | return ( 48 |
52 | {/* Step Circle + Label container */} 53 |
54 |
55 | {variant.includes("Icons") && step.icon ? ( 56 | 57 | ) : ( 58 | stepContent 59 | )} 60 |
61 | 62 | {/* Step Label (absolutely positioned below the circle) */} 63 | {(variant === "withLabels" || 64 | variant === "withIconsAndLabels") && ( 65 |
66 | 74 | {step.label || `Step ${index + 1}`} 75 | 76 |
77 | )} 78 |
79 | 80 | {/* Connector Line */} 81 | {index < steps.length - 1 && ( 82 |
83 |
91 |
92 | )} 93 |
94 | ); 95 | }; 96 | 97 | return ( 98 |
99 | {steps.map((step, index) => renderStep(step, index))} 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /stories/Tabs/Tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { 3 | Tabs, 4 | TabsList, 5 | TabsTrigger, 6 | TabsContent, 7 | } from "@/components/ui/tabs"; 8 | 9 | const meta: Meta = { 10 | title: "UI/Tabs", 11 | component: Tabs, 12 | parameters: { 13 | layout: "centered", 14 | }, 15 | argTypes: { 16 | orientation: { 17 | control: "inline-radio", 18 | options: ["horizontal", "vertical"], 19 | }, 20 | }, 21 | args: { 22 | defaultValue: "tab1", 23 | orientation: "horizontal", 24 | }, 25 | }; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | // Base render function for orientation-based layout 31 | const BaseOrientationTabs = (args: any) => { 32 | const isVertical = args.orientation === "vertical"; 33 | return ( 34 | 35 | {isVertical ? ( 36 | // Vertical layout: triggers on left and content on right 37 |
38 | 39 | 40 | Account 41 | 42 | 43 | Password 44 | 45 | Team 46 | Plan 47 | 48 |
49 | 50 | Content for Account Tab 51 | 52 | 53 | Content for Password Tab 54 | 55 | 56 | Content for Team Tab 57 | 58 | 59 | Content for Plan Tab 60 | 61 |
62 |
63 | ) : ( 64 | // Horizontal layout: triggers on top and content below 65 | <> 66 | 67 | 68 | Account 69 | 70 | 71 | Password 72 | 73 | Team 74 | Plan 75 | 76 | 77 | Content for Account Tab 78 | 79 | 80 | Content for Password Tab 81 | 82 | 83 | Content for Team Tab 84 | 85 | 86 | Content for Plan Tab 87 | 88 | 89 | )} 90 |
91 | ); 92 | }; 93 | 94 | /** 95 | * Horizontal variant story. 96 | */ 97 | export const HorizontalTabsVariant: Story = { 98 | args: { 99 | orientation: "horizontal", 100 | defaultValue: "tab1", 101 | }, 102 | render: BaseOrientationTabs, 103 | }; 104 | 105 | /** 106 | * Vertical variant story. 107 | */ 108 | export const VerticalTabsVariant: Story = { 109 | args: { 110 | orientation: "vertical", 111 | defaultValue: "tab1", 112 | }, 113 | render: BaseOrientationTabs, 114 | }; 115 | -------------------------------------------------------------------------------- /stories/Card/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as CardStories from "./Card.stories"; 3 | import { Card } from "@/components/ui/card"; 4 | 5 | 6 | 7 | # Card 8 | 9 | A simple card component built on top of ShadCN's card component. 10 | 11 | ## Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Variants](#variants) 16 | - [Default](#default) 17 | - [Sizes](#sizes) 18 | - [Default](#default) 19 | - [Best Practices](#best-practices) 20 | 21 | --- 22 | 23 | ## Installation 24 | 25 | To integrate the Card component into your project, import it from the components directory: 26 | 27 | ```tsx 28 | import { Card } from "@/components/ui/card"; 29 | ``` 30 | 31 | --- 32 | 33 | ## Usage 34 | 35 | Using the Card component is straightforward. Here’s a basic example: 36 | 37 | ```tsx 38 | import { 39 | Card, 40 | CardHeader, 41 | CardTitle, 42 | CardDescription, 43 | CardContent, 44 | CardFooter, 45 | } from "@/components/ui/card"; 46 | 47 | import { Button } from "@/components/ui/button"; 48 | 49 | 50 | 51 | Create Project 52 | 53 | Adding guiding text tells users what to expect 54 | 55 | 56 | 57 |
58 |
59 |
60 | 63 | 71 |
72 | 73 |
74 | 77 | 85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 |
; 93 | ``` 94 | 95 | 96 | 97 | The example above demonstrates a simple card layout with a title, description, and actions. 98 | 99 | --- 100 | 101 | ## Variants 102 | 103 | The Card component currently only includes the default variant. 104 | 105 | ### Default 106 | 107 | The default variant, ideal for displaying information in a structured format. 108 | 109 |
110 | 111 |
112 | 113 | --- 114 | 115 | ## Sizes 116 | 117 | The Card component sizing will adjust to the number of items inside of it, ensuring it fits the amount of content you want. 118 | 119 | ### Default 120 | 121 | The standard sizing for most use cases are: 122 | 123 |
124 | 125 | 126 | 127 |
128 | 129 | --- 130 | 131 | ## Best Practices 132 | 133 | When using the Card component, consider these guidelines to ensure a consistent and user-friendly experience: 134 | 135 | 1. **Clear Structure:** Use a title, description, and actions to keep content structured. 136 | 2. **Minimalist Design:** Avoid overcrowding cards with too much content. 137 | 3. **Consistent Spacing:** Maintain proper spacing for readability. 138 | 4. **Consistent Sizing:** Maintain uniform card sizes within the same interface context. 139 | -------------------------------------------------------------------------------- /stories/Slider/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as SliderStories from "./Slider.stories"; 3 | 4 | 5 | 6 | # Slider 7 | 8 | A versatile slider component built on top of Radix UI's slider primitives. It provides a smooth and accessible interface for selecting numeric values along a continuous range. With support for labels, tooltips, and multiple size variants, the Slider component can be customized to fit a variety of design needs. 9 | 10 | ## Table of Contents 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Props](#props) 15 | - [Variants](#variants) 16 | - [Default](#default) 17 | - [With Labels](#with-labels) 18 | - [With Tooltip](#with-tooltip) 19 | - [Size Variants](#size-variants) 20 | - [Small](#small) 21 | - [Default Size](#default-size) 22 | - [Large](#large) 23 | - [Custom Range](#custom-range) 24 | - [Examples](#examples) 25 | - [Controlled Slider](#controlled-slider) 26 | - [Uncontrolled Slider](#uncontrolled-slider) 27 | - [Best Practices](#best-practices) 28 | - [Accessibility](#accessibility) 29 | 30 | --- 31 | 32 | ## Installation 33 | 34 | To integrate the Slider component into your project, import it from the components directory: 35 | 36 | ```tsx 37 | import { Slider } from "@/components/ui/slider"; 38 | ``` 39 | 40 | --- 41 | 42 | ## Usage 43 | Using the Slider component is straightforward. Here’s a basic example: 44 | 45 | ```tsx 46 | console.log("Value changed:", value)} 54 | /> 55 | ``` 56 | 57 | 58 | The example above demonstrates a simple slider with default settings. You can further customize its behavior using the available props and variants. 59 | 60 | --- 61 | 62 | ## Props 63 | 64 | The Slider component accepts a variety of props to control its appearance and behavior. You can explore and modify these properties using the Storybook Controls panel below: 65 | 66 | 67 | 68 | --- 69 | 70 | ## Variants 71 | 72 | The Slider component supports several variants to cater to different design and functionality requirements. 73 | 74 | ### Default 75 | A basic slider with the default settings. 76 | 77 | 78 | 79 | ### With Labels 80 | Displays labels at both ends of the slider indicating the minimum and maximum values. 81 | 82 | 83 | 84 | ### With Tooltip 85 | Shows a tooltip that displays the current slider value. 86 | 87 | 88 | 89 | --- 90 | 91 | ## Size Variants 92 | Slider supports multiple sizes to fit various interface contexts. 93 | 94 | ### Small 95 | A compact version of the slider, ideal for tight spaces. 96 | 97 | 98 | 99 | ### Default Size 100 | The standard slider size for most applications. 101 | 102 | 103 | 104 | ### Large 105 | A larger slider for enhanced visibility and interaction. 106 | 107 | 108 | 109 | ## Custom Range 110 | Configure the slider with a custom range, step increments, and enable both labels and tooltip. 111 | 112 | 113 | 114 | --- 115 | 116 | ## Examples 117 | 118 | ### Controlled Slider 119 | A controlled slider example where the value is managed externally: 120 | 121 | ```tsx 122 | import { useState } from "react"; 123 | import { Slider } from "@/components/ui/slider"; 124 | 125 | function ControlledSlider() { 126 | const [value, setValue] = useState([50]); 127 | 128 | return ( 129 | 136 | ); 137 | } 138 | ``` 139 | 140 | ### Uncontrolled Slider 141 | An uncontrolled slider example that manages its own state internally: 142 | 143 | ```tsx 144 | 150 | ``` 151 | 152 | --- 153 | 154 | ## Best Practices 155 | 156 | 1. **Value Management**: Use controlled components when you need to synchronize the slider value with other parts of your application. 157 | 2. **Immediate Feedback**: Provide visual feedback as the slider value changes to improve user experience. 158 | 3. **Customization**: Leverage available props such as hasLabels and showTooltip to enhance usability. 159 | 4. **Responsive Design**: Ensure that the slider scales appropriately across different device sizes. 160 | 161 | --- 162 | 163 | ## Accessibility 164 | The Slider component is designed with accessibility in mind: 165 | 166 | - **Keyboard Navigation**: Fully supports keyboard interactions. 167 | - **Screen Reader Compatibility**: Uses appropriate ARIA attributes to ensure information is accessible. 168 | - **Focus Indicators**: Provides clear focus states to assist users navigating via keyboard. 169 | 170 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); 2 | 3 | /* This imports a ton of tailwind biolerplate which messes with other @import statements 4 | Add imports above this line */ 5 | @import "tailwindcss"; 6 | 7 | @plugin 'tailwindcss-animate'; 8 | 9 | @source './app/**/*.{js,ts,jsx,tsx,mdx}'; 10 | @source './components/**/*.{js,ts,jsx,tsx,mdx}'; 11 | 12 | @custom-variant dark (&:is(.dark *)); 13 | 14 | @theme { 15 | --color-background: hsl(var(--background)); 16 | --color-foreground: hsl(var(--foreground)); 17 | 18 | --color-card: hsl(var(--card)); 19 | --color-card-foreground: hsl(var(--card-foreground)); 20 | 21 | --color-popover: hsl(var(--popover)); 22 | --color-popover-foreground: hsl(var(--popover-foreground)); 23 | 24 | --color-primary: hsl(var(--primary)); 25 | --color-primary-foreground: hsl(var(--primary-foreground)); 26 | 27 | --color-secondary: hsl(var(--secondary)); 28 | --color-secondary-foreground: hsl(var(--secondary-foreground)); 29 | 30 | --color-muted: hsl(var(--muted)); 31 | --color-muted-foreground: hsl(var(--muted-foreground)); 32 | 33 | --color-accent: hsl(var(--accent)); 34 | --color-accent-foreground: hsl(var(--accent-foreground)); 35 | 36 | --color-destructive: hsl(var(--destructive)); 37 | --color-destructive-foreground: hsl( 38 | var(--destructive-foreground) 39 | ); 40 | 41 | --color-border: hsl(var(--border)); 42 | --color-input: hsl(var(--input)); 43 | --color-ring: hsl(var(--ring)); 44 | 45 | --color-chart-1: hsl(var(--chart-1)); 46 | --color-chart-2: hsl(var(--chart-2)); 47 | --color-chart-3: hsl(var(--chart-3)); 48 | --color-chart-4: hsl(var(--chart-4)); 49 | --color-chart-5: hsl(var(--chart-5)); 50 | 51 | --radius-lg: var(--radius); 52 | --radius-md: calc(var(--radius) - 2px); 53 | --radius-sm: calc(var(--radius) - 4px); 54 | 55 | --font-poppins: "Poppins", sans-serif; 56 | } 57 | 58 | /* 59 | The default border color has changed to `currentColor` in Tailwind CSS v4, 60 | so we've added these compatibility styles to make sure everything still 61 | looks the same as it did with Tailwind CSS v3. 62 | 63 | If we ever want to remove these styles, we need to add an explicit border 64 | color utility to any element that depends on these defaults. 65 | */ 66 | @layer base { 67 | *, 68 | ::after, 69 | ::before, 70 | ::backdrop, 71 | ::file-selector-button { 72 | border-color: var(--color-gray-200, currentColor); 73 | } 74 | } 75 | 76 | @layer utilities { 77 | } 78 | 79 | @layer base { 80 | :root { 81 | --background: 0 0% 100%; 82 | --foreground: 240 10% 3.9%; 83 | --card: 0 0% 100%; 84 | --card-foreground: 240 10% 3.9%; 85 | --popover: 0 0% 100%; 86 | --popover-foreground: 240 10% 3.9%; 87 | --primary: 240 5.9% 10%; 88 | --primary-foreground: 0 0% 98%; 89 | --secondary: 240 4.8% 95.9%; 90 | --secondary-foreground: 240 5.9% 10%; 91 | --muted: 240 4.8% 95.9%; 92 | --muted-foreground: 240 3.8% 46.1%; 93 | --accent: 240 4.8% 95.9%; 94 | --accent-foreground: 240 5.9% 10%; 95 | --destructive: 0 72% 51%; 96 | --destructive-foreground: 0 0% 98%; 97 | --border: 240 5.9% 90%; 98 | --input: 240 5.9% 90%; 99 | --ring: 240 10% 3.9%; 100 | --chart-1: 12 76% 61%; 101 | --chart-2: 173 58% 39%; 102 | --chart-3: 197 37% 24%; 103 | --chart-4: 43 74% 66%; 104 | --chart-5: 27 87% 67%; 105 | --radius: 0.5rem; 106 | } 107 | .dark { 108 | --background: 240 10% 3.9%; 109 | --foreground: 0 0% 98%; 110 | --card: 240 10% 3.9%; 111 | --card-foreground: 0 0% 98%; 112 | --popover: 240 10% 3.9%; 113 | --popover-foreground: 0 0% 98%; 114 | --primary: 0 0% 98%; 115 | --primary-foreground: 240 5.9% 10%; 116 | --secondary: 240 3.7% 15.9%; 117 | --secondary-foreground: 0 0% 98%; 118 | --muted: 240 3.7% 15.9%; 119 | --muted-foreground: 240 5% 64.9%; 120 | --accent: 240 3.7% 15.9%; 121 | --accent-foreground: 0 0% 98%; 122 | --destructive: 0 62.8% 30.6%; 123 | --destructive-foreground: 0 0% 98%; 124 | --border: 240 3.7% 15.9%; 125 | --input: 240 3.7% 15.9%; 126 | --ring: 240 4.9% 83.9%; 127 | --chart-1: 220 70% 50%; 128 | --chart-2: 160 60% 45%; 129 | --chart-3: 30 80% 55%; 130 | --chart-4: 280 65% 60%; 131 | --chart-5: 340 75% 55%; 132 | } 133 | } 134 | 135 | @layer base { 136 | * { 137 | @apply border-border; 138 | } 139 | body { 140 | @apply bg-background text-foreground; 141 | } 142 | } 143 | 144 | @layer base { 145 | * { 146 | @apply border-border outline-ring/50; 147 | } 148 | body { 149 | @apply bg-background text-foreground; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | export default function Home() { 3 | return ( 4 |
5 |
6 | Next.js logo 14 |
    15 |
  1. 16 | Get started by editing{" "} 17 | 18 | app/page.tsx 19 | 20 | . 21 |
  2. 22 |
  3. Save and see your changes instantly.
  4. 23 |
24 | 25 | 50 |
51 | 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /stories/Tabs/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from '@storybook/blocks'; 2 | import * as TabsStories from './Tabs.stories'; 3 | 4 | 5 | 6 | # Tabs 7 | 8 | A fully-featured Tabs component built on top of Radix UI's Tabs primitives. It supports both horizontal and vertical layouts and is designed with accessibility and customization in mind. Use it to create intuitive tabbed interfaces for navigating between different sections of your content. 9 | 10 | ## Table of Contents 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Props](#props) 14 | - [Variants](#variants) 15 | - [Horizontal](#horizontal) 16 | - [Vertical](#vertical) 17 | - [Examples](#examples) 18 | - [Basic Tabs](#basic-tabs) 19 | - [Vertical Tabs](#vertical-tabs) 20 | - [Best Practices](#best-practices) 21 | - [Accessibility](#accessibility) 22 | 23 | --- 24 | 25 | ## Installation 26 | 27 | To integrate the Tabs component into your project, simply import it from your components directory: 28 | 29 | ```tsx 30 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; 31 | ``` 32 | 33 | --- 34 | 35 | ## Usage 36 | 37 | Using the Tabs component is straightforward. Below is an example of a basic horizontal tab interface: 38 | 39 | ```tsx 40 | 41 | 42 | Account 43 | Password 44 | Team 45 | Plan 46 | 47 | Content for Account Tab 48 | Content for Password Tab 49 | Content for Team Tab 50 | Content for Plan Tab 51 | 52 | ``` 53 | 54 | 55 | 56 | The code above demonstrates a horizontal tab layout with each tab trigger mapped to corresponding content via the value prop. 57 | 58 | --- 59 | 60 | ## Props 61 | 62 | The Tabs component and its subcomponents—TabsList, TabsTrigger, and TabsContent—support multiple props to control their appearance and behavior. You can experiment with and adjust these properties using the Storybook Controls panel below: 63 | 64 | 65 | 66 | --- 67 | 68 | ## Variants 69 | 70 | The Tabs component can be rendered in two orientations: horizontal (the default) and vertical. Change the layout by setting the orientation prop on the TabsList. 71 | 72 | ### Horizontal 73 | 74 | Horizontal tabs are the default layout, presenting the tab triggers in a single row. 75 | 76 | 77 | 78 | ### Vertical 79 | 80 | To display tabs vertically, pass orientation="vertical" to both the TabsList: 81 | 82 | 83 | 84 | 85 | --- 86 | 87 | ## Examples 88 | 89 | Here are additional examples demonstrating different layouts. 90 | 91 | ### Horizontal Tabs 92 | 93 | A straightforward implementation of horizontal tabs: 94 | 95 | ```tsx 96 | 97 | 98 | Home 99 | Profile 100 | Settings 101 | 102 | Welcome to the home page. 103 | Manage your profile information. 104 | Adjust your settings here. 105 | 106 | ``` 107 | 108 | ### Vertical Tabs 109 | 110 | A vertical layout with a sidebar of tab triggers and corresponding content on the right: 111 | 112 | ```tsx 113 | 114 |
115 | 116 | Dashboard 117 | Reports 118 | Analytics 119 | 120 |
121 | Dashboard content goes here. 122 | Reports content goes here. 123 | Analytics content goes here. 124 |
125 |
126 |
127 | ``` 128 | --- 129 | 130 | ## Best Practices 131 | 132 | When using the Tabs component, consider these guidelines to ensure a consistent and user-friendly experience: 133 | 134 | 1. **Consistent Layout:** Choose a layout (horizontal or vertical) that best fits your design. Stick to one layout style within similar contexts. 135 | 2. **Clear Labels:** Use short, descriptive labels for each tab to help users understand the content behind them. 136 | 137 | --- 138 | 139 | ## Accessibility 140 | 141 | The Button component is built with accessibility in mind: 142 | 143 | - **Keyboard Navigation:** Supports navigation using the arrow keys. 144 | - **Screen Reader Compatibility:** Uses proper ARIA attributes for enhanced accessibility. 145 | - **Focus Indicators:** Provides visible focus states to aid users navigating via keyboard. 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /stories/Toast/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from "@storybook/blocks"; 2 | import * as ToastStories from "./Toast.stories"; 3 | 4 | 5 | 6 | # Toast 7 | 8 | A versatile toast component built on top of Radix UI's Toast primitive. It offers an accessible and flexible solution for user notifications, supporting various styles and actions to meet your design requirements. 9 | 10 | ## Table of Contents 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Props](#props) 15 | - [Variants](#variants) 16 | - [Simple](#simple) 17 | - [With Title](#with-title) 18 | - [With Title and Action](#with-title-and-action) 19 | - [Destructive](#destructive) 20 | - [Examples](#examples) 21 | - [Simple Toast](#simple-toast) 22 | - [Toast with Title](#toast-with-title) 23 | - [Toast with Title and Action](#toast-with-title-and-action) 24 | - [Destructive Toast](#destructive-toast) 25 | - [Best Practices](#best-practices) 26 | - [Accessibility](#accessibility) 27 | 28 | --- 29 | 30 | ## Installation 31 | 32 | To integrate the Toast component into your project, import it from the components directory: 33 | 34 | ```tsx 35 | import { toast, useToast } from "@/hooks/use-toast"; 36 | import { Toaster } from "@/components/ui/toaster"; 37 | import { ToastAction } from "@/components/ui/toast"; 38 | ``` 39 | 40 | --- 41 | 42 | ## Usage 43 | 44 | Using the Toast component is straightforward. Here’s a basic example: 45 | 46 | ```tsx 47 |
48 | 57 |
58 | ``` 59 | 60 | ## Props 61 | 62 | The Toast component accepts a variety of props to control its appearance and behavior. You can explore and modify these properties using the Storybook Controls panel below: 63 | 64 | 65 | 66 | --- 67 | 68 | ## Variants 69 | 70 | The Toast component includes several pre-defined variants to help you match the design context and action priority. Choose the variant that best communicates the intended action. 71 | 72 | ### Simple 73 | 74 | A basic toast with a description. 75 | 76 | 77 | 78 | ### With Title 79 | 80 | A toast with a title and description. 81 | 82 | 83 | 84 | ### With Title and Action 85 | 86 | A toast with a title, description, and an action button. 87 | 88 | 89 | 90 | ### Destructive 91 | 92 | A toast designed for actions that have potentially destructive outcomes, such as deleting data. 93 | 94 | 95 | 96 | --- 97 | 98 | ## Examples 99 | 100 | Here are some practical examples demonstrating various use cases for the Toast component. 101 | 102 | ### Simple Toast 103 | 104 | A basic toast with a description. 105 | 106 | ```tsx 107 | toast({ description: "Your message has been sent." }); 108 | ``` 109 | 110 | ### Toast with Title 111 | 112 | A toast with a title and description. 113 | 114 | ```tsx 115 | toast({ 116 | title: "Uh oh! Something went wrong.", 117 | description: "There was a problem with your request.", 118 | }); 119 | ``` 120 | 121 | ### Toast with Title and Action 122 | 123 | A toast with a title, description, and an action button. 124 | 125 | ```tsx 126 | toast({ 127 | title: "Uh oh! Something went wrong.", 128 | description: "There was a problem with your request.", 129 | action: ( 130 | dismiss()}> 131 | Try again 132 | 133 | ), 134 | }); 135 | ``` 136 | 137 | ### Destructive Toast 138 | 139 | A toast designed for actions that have potentially destructive outcomes. 140 | 141 | ```tsx 142 | toast({ 143 | title: "Uh oh! Something went wrong.", 144 | description: "There was a problem with your request.", 145 | action: ( 146 | dismiss()}> 147 | Try again 148 | 149 | ), 150 | variant: "destructive", 151 | }); 152 | ``` 153 | 154 | --- 155 | 156 | ## Best Practices 157 | 158 | When using the Toast component, consider these guidelines to ensure a consistent and user-friendly experience: 159 | 160 | 1. **Variant Selection:** Choose the appropriate variant to clearly communicate the toast's importance. 161 | 2. **Concise Text:** Keep toast messages short and action-oriented. 162 | 3. **Action Usage:** Incorporate actions only when they add clear context to the notification. 163 | 4. **Consistent Styling:** Maintain uniform toast styles within the same interface context. 164 | 5. **Accessibility:** Ensure that your toasts have sufficient color contrast and clear focus indicators. 165 | 166 | --- 167 | 168 | ## Accessibility 169 | 170 | The Toast component is built with accessibility in mind: 171 | 172 | - **Keyboard Navigation:** Fully supports keyboard interactions. 173 | - **Screen Reader Compatibility:** Uses proper ARIA attributes for enhanced accessibility. 174 | - **Focus Management:** Provides visible focus states to aid users navigating via keyboard. 175 | - **Dismissible:** Allows users to dismiss the toast, ensuring it does not obstruct the interface. 176 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { cn } from "@/lib/utils" 7 | 8 | // Create a custom Tabs component that wraps the Radix UI Tabs 9 | const TabsRoot = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef & { 12 | orientation?: "horizontal" | "vertical" 13 | } 14 | >(({ orientation = "horizontal", ...props }, ref) => { 15 | return 16 | }) 17 | TabsRoot.displayName = "Tabs" 18 | 19 | // Create context to pass orientation from TabsList to its child triggers. 20 | const TabsOrientationContext = React.createContext<"horizontal" | "vertical">("horizontal") 21 | 22 | // Define variants for the TabsList. 23 | const tabsListVariants = cva("rounded-lg p-1 bg-muted text-muted-foreground", { 24 | variants: { 25 | orientation: { 26 | horizontal: "inline-flex", // background wraps tabs only 27 | vertical: "flex flex-col", 28 | }, 29 | }, 30 | defaultVariants: { 31 | orientation: "horizontal", 32 | }, 33 | }) 34 | 35 | interface TabsListProps 36 | extends React.ComponentPropsWithoutRef, 37 | VariantProps { 38 | orientation?: "horizontal" | "vertical" 39 | } 40 | 41 | const TabsList = React.forwardRef< 42 | React.ElementRef, 43 | TabsListProps 44 | >(({ className, orientation = "horizontal", children, ...props }, ref) => { 45 | const content = 46 | orientation === "horizontal" ? ( 47 |
{children}
48 | ) : ( 49 | children 50 | ) 51 | 52 | return ( 53 | 54 | 59 | {content} 60 | 61 | 62 | ) 63 | }) 64 | TabsList.displayName = TabsPrimitive.List.displayName 65 | 66 | // Define variants for the TabsTrigger. 67 | const tabsTriggerVariants = cva( 68 | "w-full text-center inline-flex items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow data-[state=inactive]:hover:bg-accent data-[state=inactive]:hover:text-accent-foreground", 69 | { 70 | variants: { 71 | orientation: { 72 | horizontal: "", 73 | vertical: "mb-1 last:mb-0", 74 | }, 75 | }, 76 | defaultVariants: { 77 | orientation: "horizontal", 78 | }, 79 | } 80 | ) 81 | 82 | interface TabsTriggerProps 83 | extends React.ComponentPropsWithoutRef, 84 | VariantProps { 85 | orientation?: "horizontal" | "vertical" 86 | } 87 | 88 | const TabsTrigger = React.forwardRef< 89 | React.ElementRef, 90 | TabsTriggerProps 91 | >(({ className, orientation, ...props }, ref) => { 92 | // Use the context orientation if not explicitly passed 93 | const contextOrientation = React.useContext(TabsOrientationContext) 94 | const resolvedOrientation = orientation || contextOrientation 95 | 96 | const handleKeyDown = (e: React.KeyboardEvent) => { 97 | // Only apply custom keyboard navigation for vertical orientation 98 | if (resolvedOrientation === "vertical") { 99 | const triggers = e.currentTarget.parentElement?.querySelectorAll('[role="tab"]') 100 | if (!triggers?.length) return 101 | 102 | const triggerArray = Array.from(triggers) 103 | const currentIndex = triggerArray.indexOf(e.currentTarget) 104 | 105 | if (e.key === "ArrowUp") { 106 | e.preventDefault() 107 | const prevIndex = currentIndex > 0 ? currentIndex - 1 : triggerArray.length - 1 108 | ;(triggerArray[prevIndex] as HTMLElement).focus() 109 | } else if (e.key === "ArrowDown") { 110 | e.preventDefault() 111 | const nextIndex = currentIndex < triggerArray.length - 1 ? currentIndex + 1 : 0 112 | ;(triggerArray[nextIndex] as HTMLElement).focus() 113 | } 114 | } 115 | } 116 | 117 | return ( 118 | 124 | ) 125 | }) 126 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 127 | 128 | // Content remains unchanged. 129 | const TabsContent = React.forwardRef< 130 | React.ElementRef, 131 | React.ComponentPropsWithoutRef 132 | >(({ className, ...props }, ref) => ( 133 | 141 | )) 142 | TabsContent.displayName = TabsPrimitive.Content.displayName 143 | 144 | export { TabsRoot as Tabs, TabsList, TabsTrigger, TabsContent } 145 | -------------------------------------------------------------------------------- /stories/Button/Documentation.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Primary, Controls, Story } from '@storybook/blocks'; 2 | import * as ButtonStories from './Button.stories'; 3 | 4 | 5 | 6 | # Button 7 | 8 | A versatile button component built on top of Radix UI's Slot primitive. It offers an accessible and flexible solution for user interactions, supporting various styles, sizes, and states to meet your design requirements. 9 | 10 | ## Table of Contents 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Props](#props) 14 | - [Variants](#variants) 15 | - [Primary](#primary) 16 | - [Secondary](#secondary) 17 | - [Destructive](#destructive) 18 | - [Outline](#outline) 19 | - [Ghost](#ghost) 20 | - [Link](#link) 21 | - [Sizes](#sizes) 22 | - [Default](#default) 23 | - [Small](#small) 24 | - [Large](#large) 25 | - [Examples](#examples) 26 | - [As a Link](#as-a-link) 27 | - [With Icon](#with-icon) 28 | - [Disabled State](#disabled-state) 29 | - [Best Practices](#best-practices) 30 | - [Accessibility](#accessibility) 31 | 32 | --- 33 | 34 | ## Installation 35 | 36 | To integrate the Button component into your project, import it from the components directory: 37 | 38 | ```tsx 39 | import { Button } from "@/components/ui/button"; 40 | ``` 41 | 42 | --- 43 | 44 | ## Usage 45 | 46 | Using the Button component is straightforward. Here’s a basic example: 47 | 48 | ```tsx 49 | 50 | ``` 51 | 52 | 53 | 54 | The example above demonstrates a simple button with the default styling. You can further customize it using the available props and variants. 55 | 56 | --- 57 | 58 | ## Props 59 | 60 | The Button component accepts a variety of props to control its appearance and behavior. You can explore and modify these properties using the Storybook Controls panel below: 61 | 62 | 63 | 64 | --- 65 | 66 | ## Variants 67 | 68 | The Button component includes several pre-defined variants to help you match the design context and action priority. Choose the variant that best communicates the intended action. 69 | 70 | ### Primary 71 | 72 | The default variant, ideal for primary actions. 73 | 74 | 75 | 76 | ### Secondary 77 | 78 | Use this variant for secondary actions that complement the primary call-to-action. 79 | 80 | 81 | 82 | ### Destructive 83 | 84 | Designed for actions that have potentially destructive outcomes, such as deleting data. 85 | 86 | 87 | 88 | ### Outline 89 | 90 | A subtle variant featuring only a border, perfect for less prominent actions or alternative styles. 91 | 92 | 93 | 94 | ### Ghost 95 | 96 | A minimal variant without a background or border, ideal for low-emphasis actions. 97 | 98 | 99 | 100 | ### Link 101 | 102 | Styled to resemble a hyperlink while retaining button functionality, useful for inline actions. 103 | 104 | 105 | 106 | --- 107 | 108 | ## Sizes 109 | 110 | The Button component supports multiple sizes, ensuring it fits well in different interface contexts. 111 | 112 | ### Default 113 | 114 | The standard size for most use cases. 115 | 116 | 117 | 118 | ### Small 119 | 120 | A compact version ideal for use in tight spaces or when a more subtle appearance is needed. 121 | 122 | 123 | 124 | ### Large 125 | 126 | A larger size that emphasizes the action, suitable for prominent calls-to-action. 127 | 128 | 129 | 130 | --- 131 | 132 | ## Examples 133 | 134 | Here are some practical examples demonstrating various use cases for the Button component. 135 | 136 | ### As a Link 137 | 138 | Render the Button as a link by using the `asChild` prop. This approach is great when integrating with Next.js's `Link` component or an anchor tag: 139 | 140 | ```tsx 141 | import Link from "next/link"; 142 | 143 | 146 | ``` 147 | 148 | ### With Icon 149 | 150 | Enhance the button's meaning by combining it with icons. For instance, using an icon from `lucide-react`: 151 | 152 | ```tsx 153 | import { Mail } from "lucide-react"; 154 | 155 | 159 | ``` 160 | 161 | ### Disabled State 162 | 163 | Disable the button when an action is not available, using the `disabled` prop: 164 | 165 | ```tsx 166 | 169 | ``` 170 | 171 | --- 172 | 173 | ## Best Practices 174 | 175 | When using the Button component, consider these guidelines to ensure a consistent and user-friendly experience: 176 | 177 | 1. **Variant Selection:** Choose the appropriate variant to clearly communicate the button's importance. 178 | 2. **Concise Text:** Keep button labels short and action-oriented. 179 | 3. **Icon Usage:** Incorporate icons only when they add clear context to the action. 180 | 4. **Consistent Sizing:** Maintain uniform button sizes within the same interface context. 181 | 5. **Accessibility:** Ensure that your buttons have sufficient color contrast and clear focus indicators. 182 | 183 | --- 184 | 185 | ## Accessibility 186 | 187 | The Button component is built with accessibility in mind: 188 | 189 | - **Keyboard Navigation:** Fully supports keyboard interactions. 190 | - **Screen Reader Compatibility:** Uses proper ARIA attributes for enhanced accessibility. 191 | - **Focus Management:** Provides visible focus states to aid users navigating via keyboard. 192 | - **Disabled State Styling:** Clearly indicates when the button is inactive. 193 | -------------------------------------------------------------------------------- /hooks/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react"; 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast"; 10 | 11 | const TOAST_LIMIT = 1; 12 | const TOAST_REMOVE_DELAY = 1000000; 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string; 16 | title?: React.ReactNode; 17 | description?: React.ReactNode; 18 | action?: ToastActionElement; 19 | }; 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const; 27 | 28 | let count = 0; 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER; 32 | return count.toString(); 33 | } 34 | 35 | type ActionType = typeof actionTypes; 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"]; 40 | toast: ToasterToast; 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"]; 44 | toast: Partial; 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"]; 48 | toastId?: ToasterToast["id"]; 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"]; 52 | toastId?: ToasterToast["id"]; 53 | }; 54 | 55 | interface State { 56 | toasts: ToasterToast[]; 57 | } 58 | 59 | const toastTimeouts = new Map< 60 | string, 61 | ReturnType 62 | >(); 63 | 64 | const addToRemoveQueue = (toastId: string) => { 65 | if (toastTimeouts.has(toastId)) { 66 | return; 67 | } 68 | 69 | const timeout = setTimeout(() => { 70 | toastTimeouts.delete(toastId); 71 | dispatch({ 72 | type: "REMOVE_TOAST", 73 | toastId: toastId, 74 | }); 75 | }, TOAST_REMOVE_DELAY); 76 | 77 | toastTimeouts.set(toastId, timeout); 78 | }; 79 | 80 | export const reducer = (state: State, action: Action): State => { 81 | switch (action.type) { 82 | case "ADD_TOAST": 83 | return { 84 | ...state, 85 | toasts: [action.toast, ...state.toasts].slice( 86 | 0, 87 | TOAST_LIMIT 88 | ), 89 | }; 90 | 91 | case "UPDATE_TOAST": 92 | return { 93 | ...state, 94 | toasts: state.toasts.map((t) => 95 | t.id === action.toast.id 96 | ? { ...t, ...action.toast } 97 | : t 98 | ), 99 | }; 100 | 101 | case "DISMISS_TOAST": { 102 | const { toastId } = action; 103 | 104 | // ! Side effects ! - This could be extracted into a dismissToast() action, 105 | // but I'll keep it here for simplicity 106 | if (toastId) { 107 | addToRemoveQueue(toastId); 108 | } else { 109 | state.toasts.forEach((toast) => { 110 | addToRemoveQueue(toast.id); 111 | }); 112 | } 113 | 114 | return { 115 | ...state, 116 | toasts: state.toasts.map((t) => 117 | t.id === toastId || toastId === undefined 118 | ? { 119 | ...t, 120 | open: false, 121 | } 122 | : t 123 | ), 124 | }; 125 | } 126 | case "REMOVE_TOAST": 127 | if (action.toastId === undefined) { 128 | return { 129 | ...state, 130 | toasts: [], 131 | }; 132 | } 133 | return { 134 | ...state, 135 | toasts: state.toasts.filter( 136 | (t) => t.id !== action.toastId 137 | ), 138 | }; 139 | } 140 | }; 141 | 142 | const listeners: Array<(state: State) => void> = []; 143 | 144 | let memoryState: State = { toasts: [] }; 145 | 146 | function dispatch(action: Action) { 147 | memoryState = reducer(memoryState, action); 148 | listeners.forEach((listener) => { 149 | listener(memoryState); 150 | }); 151 | } 152 | 153 | type Toast = Omit; 154 | 155 | function toast({ ...props }: Toast) { 156 | const id = genId(); 157 | 158 | const update = (props: ToasterToast) => 159 | dispatch({ 160 | type: "UPDATE_TOAST", 161 | toast: { ...props, id }, 162 | }); 163 | const dismiss = () => 164 | dispatch({ type: "DISMISS_TOAST", toastId: id }); 165 | 166 | dispatch({ 167 | type: "ADD_TOAST", 168 | toast: { 169 | ...props, 170 | id, 171 | open: true, 172 | onOpenChange: (open) => { 173 | if (!open) dismiss(); 174 | }, 175 | }, 176 | }); 177 | 178 | return { 179 | id: id, 180 | dismiss, 181 | update, 182 | }; 183 | } 184 | 185 | function useToast() { 186 | const [state, setState] = React.useState(memoryState); 187 | 188 | React.useEffect(() => { 189 | listeners.push(setState); 190 | return () => { 191 | const index = listeners.indexOf(setState); 192 | if (index > -1) { 193 | listeners.splice(index, 1); 194 | } 195 | }; 196 | }, [state]); 197 | 198 | return { 199 | ...state, 200 | toast, 201 | dismiss: (toastId?: string) => 202 | dispatch({ type: "DISMISS_TOAST", toastId }), 203 | }; 204 | } 205 | 206 | export { useToast, toast }; 207 | -------------------------------------------------------------------------------- /components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ToastPrimitives from "@radix-ui/react-toast"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | import { X } from "lucide-react"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | 10 | const ToastProvider = ToastPrimitives.Provider; 11 | 12 | const ToastViewport = React.forwardRef< 13 | React.ComponentRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, ...props }, ref) => ( 16 | 24 | )); 25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 26 | 27 | const toastVariants = cva( 28 | "border border-gray-300 group pointer-events-auto relative flex w-[444px] items-center justify-between space-x-2 overflow-hidden rounded-md p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 29 | { 30 | variants: { 31 | variant: { 32 | default: "border bg-background text-foreground", 33 | destructive: 34 | "destructive group border-destructive bg-red-600 text-destructive-foreground text-white", 35 | }, 36 | }, 37 | defaultVariants: { 38 | variant: "default", 39 | }, 40 | } 41 | ); 42 | 43 | const Toast = React.forwardRef< 44 | React.ComponentRef, 45 | React.ComponentPropsWithoutRef & 46 | VariantProps 47 | >(({ className, variant, ...props }, ref) => { 48 | return ( 49 | 54 | ); 55 | }); 56 | Toast.displayName = ToastPrimitives.Root.displayName; 57 | 58 | const ToastAction = React.forwardRef< 59 | React.ComponentRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 72 | )); 73 | ToastAction.displayName = ToastPrimitives.Action.displayName; 74 | 75 | const ToastClose = React.forwardRef< 76 | React.ComponentRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, ...props }, ref) => ( 79 | 88 | 89 | 90 | )); 91 | ToastClose.displayName = ToastPrimitives.Close.displayName; 92 | 93 | const ToastTitle = React.forwardRef< 94 | React.ComponentRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 105 | )); 106 | ToastTitle.displayName = ToastPrimitives.Title.displayName; 107 | 108 | const ToastDescription = React.forwardRef< 109 | React.ComponentRef, 110 | React.ComponentPropsWithoutRef 111 | >(({ className, ...props }, ref) => ( 112 | 120 | )); 121 | ToastDescription.displayName = 122 | ToastPrimitives.Description.displayName; 123 | 124 | type ToastProps = React.ComponentPropsWithoutRef; 125 | 126 | type ToastActionElement = React.ReactElement; 127 | 128 | export { 129 | type ToastProps, 130 | type ToastActionElement, 131 | ToastProvider, 132 | ToastViewport, 133 | Toast, 134 | ToastTitle, 135 | ToastDescription, 136 | ToastClose, 137 | ToastAction, 138 | }; 139 | -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | 9 | // slider has properties: size, hasLabels, hasTooltip 10 | // size can either be default, sm, or large 11 | // hasLabels can either be true or false 12 | // hasTooltip can either be true or false 13 | const sliderVariants = cva( 14 | "relative flex w-full touch-none select-none items-center", 15 | { 16 | variants: { 17 | size: { 18 | default: 19 | "[&_.slider-track]:h-[6px] [&_.slider-thumb]:h-[20.25px] [&_.slider-thumb]:w-[20.25px] [&_.tool-tip]:min-w-[34.5px] [&_.tool-tip]:text-[24px] [&_.tool-tip-bot]:w-4 [&_.tool-tip-bot]:h-4", 20 | sm: "[&_.slider-track]:h-[4px] [&_.slider-thumb]:h-[13.5px] [&_.slider-thumb]:w-[13.5px] [&_.tool-tip]:min-w-[23px] [&_.tool-tip]:text-[16px] [&_.tool-tip-bot]:w-2 [&_.tool-tip-bot]:h-2", 21 | lg: "[&_.slider-track]:h-[12px] [&_.slider-thumb]:h-[27px] [&_.slider-thumb]:w-[27px] [&_.tool-tip]:min-w-[46px] [&_.tool-tip]:text-[32px] [&_.tool-tip-bot]:w-8 [&_.tool-tip-bot]:h-8", 22 | }, 23 | hasLabels: { 24 | true: "mx-auto ", 25 | false: "mx-0 w-full", 26 | }, 27 | hasTooltip: { 28 | true: "", 29 | false: "", 30 | }, 31 | }, 32 | defaultVariants: { 33 | size: "default", 34 | hasLabels: false, 35 | hasTooltip: false, 36 | }, 37 | } 38 | ); 39 | 40 | interface SliderProps 41 | extends React.ComponentPropsWithoutRef< 42 | typeof SliderPrimitive.Root 43 | >, 44 | VariantProps { 45 | showTooltip?: boolean; 46 | tooltipContent?: (value: number[]) => React.ReactNode; 47 | } 48 | 49 | const Slider = React.forwardRef< 50 | React.ElementRef, 51 | SliderProps 52 | >( 53 | ( 54 | { 55 | className, 56 | size, 57 | hasLabels, 58 | showTooltip, 59 | tooltipContent, 60 | value, 61 | defaultValue = [0], 62 | ...props 63 | }, 64 | ref 65 | ) => { 66 | const isControlled = value !== undefined; 67 | const [internalValue, setInternalValue] = 68 | React.useState(defaultValue); 69 | const sliderValue = isControlled ? value : internalValue; 70 | 71 | const handleValueChange = React.useCallback( 72 | (newValue: number[]) => { 73 | if (!isControlled) { 74 | setInternalValue(newValue); 75 | } 76 | props.onValueChange?.(newValue); 77 | }, 78 | [isControlled, props] 79 | ); 80 | 81 | return ( 82 |
83 |
84 | {hasLabels && ( 85 |
86 | {props.min || 0} 87 |
88 | )} 89 | 104 | 105 | 106 | 107 | 108 | 112 | {showTooltip && ( 113 |
114 |
115 |
121 | {tooltipContent 122 | ? tooltipContent( 123 | sliderValue 124 | ) 125 | : sliderValue[0]} 126 |
127 |
128 |
129 |
130 |
131 |
132 | )} 133 | 134 | 135 | {hasLabels && ( 136 |
137 | {props.max || 100} 138 |
139 | )} 140 |
141 |
142 | ); 143 | } 144 | ); 145 | Slider.displayName = SliderPrimitive.Root.displayName; 146 | 147 | export { Slider, type SliderProps }; 148 | -------------------------------------------------------------------------------- /stories/Card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { 3 | Card, 4 | CardHeader, 5 | CardTitle, 6 | CardDescription, 7 | CardContent, 8 | CardFooter, 9 | } from "@/components/ui/card"; 10 | 11 | import { Button } from "@/components/ui/button"; 12 | 13 | const meta = { 14 | title: "UI/Card", 15 | component: Card, 16 | parameters: { 17 | layout: "centered", 18 | }, 19 | } satisfies Meta; 20 | 21 | export default meta; 22 | type Story = StoryObj; 23 | 24 | export const Default: Story = { 25 | render: () => ( 26 | 27 | 28 | Create Project 29 | 30 | Adding guiding text tells users what to expect 31 | 32 | 33 | 34 |
35 |
36 |
37 | 40 | 48 |
49 | 50 |
51 | 54 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | 69 |
70 | ), 71 | }; 72 | 73 | export const V1: Story = { 74 | render: () => ( 75 | 76 | 77 | Create Project 78 | 79 | Adding guiding text tells users what to expect 80 | 81 | 82 | 83 |
84 |
85 |
86 | 89 | 97 |
98 |
99 |
100 |
101 | 102 | 103 | 104 |
105 | ), 106 | }; 107 | 108 | export const V3: Story = { 109 | render: () => ( 110 | 111 | 112 | Create Project 113 | 114 | Adding guiding text tells users what to expect 115 | 116 | 117 | 118 |
119 |
120 |
121 | 124 | 132 |
133 | 134 |
135 | 138 | 146 |
147 | 148 |
149 | 152 | 160 |
161 |
162 |
163 |
164 | 165 | 166 | 167 |
168 | ), 169 | }; 170 | -------------------------------------------------------------------------------- /FIRST-TICKET.md: -------------------------------------------------------------------------------- 1 | # 🚀 Starting Your First Ticket! 2 | 3 | A go-to guide to kickstart your open source contribution journey. Follow along to get familiar with our recommended process for implementing your first UI component once you claim a ticket. Let's dive in! 🎉 4 | 5 | ## Table of Contents 6 | 7 | - [🚀 Starting Your First Ticket!](#-starting-your-first-ticket) 8 | - [🤔 Claimed Ticket. Now What?](#-claimed-ticket-now-what) 9 | - [🔍 Inspecting the Design](#-inspecting-the-design) 10 | - [🎭 Component States](#-component-states) 11 | - [🎨 Visual Properties](#-visual-properties) 12 | - [🎞️ Interactive Elements](#-interactive-elements) 13 | - [📱 Responsive Design](#-responsive-design) 14 | - [♿ Accessibility](#-accessibility) 15 | - [💻 Implementation](#-implementation) 16 | - [🧪 Testing](#-testing) 17 | - [📚 Adding Your Component to Storybook](#-adding-your-component-to-storybook) 18 | 19 | ## 🤔 Claimed Ticket. Now What? 20 | 21 | After claiming your ticket, carefully read the description. It contains all the details you need for implementing the UI component, including a Figma link to the design. Request for Figma file access so you can use Figma's Dev Mode to inspect developer-specific fields and access boilerplate code. 22 | 23 | ![Figma Dev Mode](assets/images/first-ticket/figma-dev-mode.png) 🔍 24 | 25 | ## 🔍 Inspecting the Design 26 | 27 | When reviewing a design, it’s essential to consider every detail. Here’s what to look for: 28 | 29 | ### 🎭 Component States 30 | 31 | - **Default State:** The base appearance and styling of the component. 🏠 32 | - **Hover State:** Visual changes like color shifts, transitions, or animations when hovered. 🖱️ 33 | - **Active State:** Feedback when the component is clicked or pressed. ⚡ 34 | - **Disabled State:** The look of the component when it’s inactive or unresponsive. 🚫 35 | - **Focus State:** Visual cues for keyboard navigation. 🎯 36 | 37 | ### 🎨 Visual Properties 38 | 39 | #### Typography 📝 40 | 41 | - Font family and weight 42 | - Text size and line height 43 | - Letter spacing 44 | - Text alignment and wrapping 45 | 46 | #### Spacing 📐 47 | 48 | - Padding (inner spacing) 49 | - Margins (outer spacing) 50 | - Gaps between elements 51 | - Content alignment 52 | 53 | #### Colors 🌈 54 | 55 | - Background colors 56 | - Text colors 57 | - Border colors 58 | - Hover state colors 59 | - Any gradients or opacity variations 60 | 61 | ### 🎞️ Interactive Elements 62 | 63 | - **Animations:** 64 | - Transition timing 65 | - Easing functions 66 | - Animation duration 67 | - Behavior during state changes 68 | 69 | ### 📱 Responsive Design 70 | 71 | - **Breakpoints:** How the component adapts at various screen sizes. 72 | - **Scaling:** Adjustments in size, padding, or layout. 73 | - **Mobile Considerations:** Optimized touch targets and spacing for mobile devices. 74 | 75 | ### ♿ Accessibility 76 | 77 | - **Color Contrast:** Ensure compliance with WCAG guidelines. 78 | - **Focus Indicators:** Clear visual cues for interactive elements. 79 | - **ARIA Attributes:** Include necessary accessibility attributes. 80 | - **Keyboard Navigation:** Proper tab order and interactive feedback. 81 | 82 | Documenting these details will help guide your implementation and maintain consistency with the overall design system. 📝 83 | 84 | ## 💻 Implementation 85 | 86 | With the design specifications in hand, it’s time to build your component. All UI components should be added to the `components/ui` directory. 87 | 88 | **🌟 Important!** 89 | 90 | Before you begin, check the [ShadCN Website](https://ui.shadcn.com/) to see if the component already exists. If it does, use it as your starting point. If not, create a new component. 91 | 92 | **Remember:** The core functionality of ShadCN components must remain unchanged—only the design should be updated. 93 | 94 | You can add ShadCN components using the ShadCN CLI. For example, to add a Button component, run: 95 | 96 | ```bash 97 | pnpm dlx shadcn@latest add button 98 | ``` 99 | 100 | This command automatically adds the component file to the `components/ui` directory. Review the implementation and then modify the design to match the Figma specifications. 101 | 102 | ### 🧪 Testing 103 | 104 | Preview your component during development by using the `app/test/page.tsx` file. Start your Next.js development server with: 105 | 106 | ```bash 107 | pnpm run dev 108 | ``` 109 | 110 | When testing, place your code inside `export default function TestPage()`, and then visit `http://local host:3000/test` on your browser to view it. 111 | 112 | Your code should look similar to: 113 | 114 | ``` 115 | export default function TestPage() { 116 | // Your component variants 117 | const variants = [ 118 | // Define your variants here 119 | ] as const; 120 | 121 | // Your component sizes 122 | const sizes = ["default"] as const; 123 | 124 | return ( 125 | // Implement your component here 126 | ); 127 | } 128 | ``` 129 | 130 | Test and iterate until your component looks perfect! 🔧 131 | 132 | ![Button Development Preview](assets/images/first-ticket/button-dev-preview.png) 133 | 134 | ### 📚 Adding Your Component to Storybook 135 | 136 | ### 1. Create a Storybook File 137 | 138 | Create a new folder inside the `stories` which is named after the component. Create a file inside it. The filename should follow the pattern `.stories.tsx`. 139 | 140 | For example, for a `Button` component, create: 141 | 142 | ``` 143 | src/stories/Button/Button.stories.tsx 144 | ``` 145 | 146 | ### 2. Import Necessary Dependencies 147 | 148 | In your Storybook file, import `Meta` and `StoryObj` from `@storybook/react`, your component, and any testing utilities if needed. 149 | 150 | ```tsx 151 | import type { Meta, StoryObj } from "@storybook/react"; 152 | import { fn } from "@storybook/test"; 153 | import { Button } from "@/components/ui/button"; 154 | ``` 155 | 156 | ### 3. Define the Story Metadata 157 | 158 | Use the `Meta` type to define the component’s metadata, including its title, component reference, and parameters. 159 | 160 | ```tsx 161 | const meta = { 162 | title: "UI/Button", 163 | component: Button, 164 | parameters: { 165 | layout: "centered", 166 | }, 167 | args: { onClick: fn() }, 168 | } satisfies Meta; 169 | 170 | export default meta; 171 | type Story = StoryObj; 172 | ``` 173 | 174 | ### 4. Define Story Variants 175 | 176 | Each variant of your component should be defined as a separate export, specifying the `args` property to configure its props. 177 | 178 | ```tsx 179 | export const Default: Story = { 180 | args: { 181 | children: "Button", 182 | variant: "default", 183 | size: "default", 184 | }, 185 | }; 186 | 187 | export const Primary: Story = { 188 | args: { 189 | children: "Button", 190 | variant: "default", 191 | size: "default", 192 | }, 193 | }; 194 | 195 | export const Destructive: Story = { 196 | args: { 197 | children: "Delete", 198 | variant: "destructive", 199 | }, 200 | }; 201 | 202 | export const Outline: Story = { 203 | args: { 204 | children: "Outline", 205 | variant: "outline", 206 | }, 207 | }; 208 | ``` 209 | 210 | ### 5. Run Storybook 211 | 212 | After adding your component, start Storybook to view the new stories. 213 | 214 | ```sh 215 | npm run storybook 216 | ``` 217 | 218 | Storybook will launch in your browser, and you should see your new component under the specified category. 219 | 220 | ### 6. Documentation 221 | 222 | - Add documentation for the component. Create a `Documentation.mdx` file and follow a similar structure as the sample documentation in `stories/Button/Documentation.mdx` 223 | -------------------------------------------------------------------------------- /components/ui/carousel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import useEmblaCarousel, { 5 | type UseEmblaCarouselType, 6 | } from "embla-carousel-react"; 7 | import { ArrowLeft, ArrowRight } from "lucide-react"; 8 | 9 | import { cn } from "@/lib/utils"; 10 | import { Button } from "@/components/ui/button"; 11 | 12 | type CarouselApi = UseEmblaCarouselType[1]; 13 | type UseCarouselParameters = Parameters; 14 | type CarouselOptions = UseCarouselParameters[0]; 15 | type CarouselPlugin = UseCarouselParameters[1]; 16 | 17 | type CarouselProps = { 18 | opts?: CarouselOptions; 19 | plugins?: CarouselPlugin; 20 | orientation?: "horizontal" | "vertical"; 21 | setApi?: (api: CarouselApi) => void; 22 | }; 23 | 24 | type CarouselContextProps = { 25 | carouselRef: ReturnType[0]; 26 | api: ReturnType[1]; 27 | scrollPrev: () => void; 28 | scrollNext: () => void; 29 | canScrollPrev: boolean; 30 | canScrollNext: boolean; 31 | } & CarouselProps; 32 | 33 | const CarouselContext = 34 | React.createContext(null); 35 | 36 | function useCarousel() { 37 | const context = React.useContext(CarouselContext); 38 | 39 | if (!context) { 40 | throw new Error( 41 | "useCarousel must be used within a " 42 | ); 43 | } 44 | 45 | return context; 46 | } 47 | 48 | function Carousel({ 49 | orientation = "horizontal", 50 | opts, 51 | setApi, 52 | plugins, 53 | className, 54 | children, 55 | ...props 56 | }: React.ComponentProps<"div"> & CarouselProps) { 57 | const [carouselRef, api] = useEmblaCarousel( 58 | { 59 | ...opts, 60 | axis: orientation === "horizontal" ? "x" : "y", 61 | }, 62 | plugins 63 | ); 64 | const [canScrollPrev, setCanScrollPrev] = React.useState(false); 65 | const [canScrollNext, setCanScrollNext] = React.useState(false); 66 | 67 | const onSelect = React.useCallback((api: CarouselApi) => { 68 | if (!api) return; 69 | setCanScrollPrev(api.canScrollPrev()); 70 | setCanScrollNext(api.canScrollNext()); 71 | }, []); 72 | 73 | const scrollPrev = React.useCallback(() => { 74 | api?.scrollPrev(); 75 | }, [api]); 76 | 77 | const scrollNext = React.useCallback(() => { 78 | api?.scrollNext(); 79 | }, [api]); 80 | 81 | const handleKeyDown = React.useCallback( 82 | (event: React.KeyboardEvent) => { 83 | if (event.key === "ArrowLeft") { 84 | event.preventDefault(); 85 | scrollPrev(); 86 | } else if (event.key === "ArrowRight") { 87 | event.preventDefault(); 88 | scrollNext(); 89 | } 90 | }, 91 | [scrollPrev, scrollNext] 92 | ); 93 | 94 | React.useEffect(() => { 95 | if (!api || !setApi) return; 96 | setApi(api); 97 | }, [api, setApi]); 98 | 99 | React.useEffect(() => { 100 | if (!api) return; 101 | onSelect(api); 102 | api.on("reInit", onSelect); 103 | api.on("select", onSelect); 104 | 105 | return () => { 106 | api?.off("select", onSelect); 107 | }; 108 | }, [api, onSelect]); 109 | 110 | return ( 111 | 125 |
133 | {children} 134 |
135 |
136 | ); 137 | } 138 | 139 | function CarouselContent({ 140 | className, 141 | ...props 142 | }: React.ComponentProps<"div">) { 143 | const { carouselRef, orientation } = useCarousel(); 144 | 145 | return ( 146 |
151 |
161 |
162 | ); 163 | } 164 | 165 | function CarouselItem({ 166 | className, 167 | ...props 168 | }: React.ComponentProps<"div">) { 169 | const { orientation } = useCarousel(); 170 | 171 | return ( 172 |
183 | ); 184 | } 185 | 186 | function CarouselPrevious({ 187 | className, 188 | variant = "outline", 189 | size = "icon", 190 | ...props 191 | }: React.ComponentProps) { 192 | const { orientation, scrollPrev, canScrollPrev } = useCarousel(); 193 | 194 | return ( 195 | 213 | ); 214 | } 215 | 216 | function CarouselNext({ 217 | className, 218 | variant = "outline", 219 | size = "icon", 220 | ...props 221 | }: React.ComponentProps) { 222 | const { orientation, scrollNext, canScrollNext } = useCarousel(); 223 | 224 | return ( 225 | 243 | ); 244 | } 245 | 246 | export { 247 | type CarouselApi, 248 | Carousel, 249 | CarouselContent, 250 | CarouselItem, 251 | CarouselPrevious, 252 | CarouselNext, 253 | }; 254 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef< 24 | typeof DropdownMenuPrimitive.SubTrigger 25 | > & { 26 | inset?: boolean; 27 | } 28 | >(({ className, inset, children, ...props }, ref) => ( 29 | 38 | {children} 39 | 40 | 41 | )); 42 | DropdownMenuSubTrigger.displayName = 43 | DropdownMenuPrimitive.SubTrigger.displayName; 44 | 45 | const DropdownMenuSubContent = React.forwardRef< 46 | React.ElementRef, 47 | React.ComponentPropsWithoutRef< 48 | typeof DropdownMenuPrimitive.SubContent 49 | > 50 | >(({ className, ...props }, ref) => ( 51 | 59 | )); 60 | DropdownMenuSubContent.displayName = 61 | DropdownMenuPrimitive.SubContent.displayName; 62 | 63 | const DropdownMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef< 66 | typeof DropdownMenuPrimitive.Content 67 | > 68 | >(({ className, sideOffset = 4, ...props }, ref) => ( 69 | 70 | 80 | 81 | )); 82 | DropdownMenuContent.displayName = 83 | DropdownMenuPrimitive.Content.displayName; 84 | 85 | const DropdownMenuItem = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef< 88 | typeof DropdownMenuPrimitive.Item 89 | > & { 90 | inset?: boolean; 91 | } 92 | >(({ className, inset, ...props }, ref) => ( 93 | svg]:size-4 [&>svg]:shrink-0", 97 | inset && "pl-8", 98 | className 99 | )} 100 | {...props} 101 | /> 102 | )); 103 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 104 | 105 | const DropdownMenuCheckboxItem = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef< 108 | typeof DropdownMenuPrimitive.CheckboxItem 109 | > 110 | >(({ className, children, checked, ...props }, ref) => ( 111 | 120 | 121 | 122 | 123 | 124 | 125 | {children} 126 | 127 | )); 128 | DropdownMenuCheckboxItem.displayName = 129 | DropdownMenuPrimitive.CheckboxItem.displayName; 130 | 131 | const DropdownMenuRadioItem = React.forwardRef< 132 | React.ElementRef, 133 | React.ComponentPropsWithoutRef< 134 | typeof DropdownMenuPrimitive.RadioItem 135 | > 136 | >(({ className, children, ...props }, ref) => ( 137 | 145 | 146 | 147 | 148 | 149 | 150 | {children} 151 | 152 | )); 153 | DropdownMenuRadioItem.displayName = 154 | DropdownMenuPrimitive.RadioItem.displayName; 155 | 156 | const DropdownMenuLabel = React.forwardRef< 157 | React.ElementRef, 158 | React.ComponentPropsWithoutRef< 159 | typeof DropdownMenuPrimitive.Label 160 | > & { 161 | inset?: boolean; 162 | } 163 | >(({ className, inset, ...props }, ref) => ( 164 | 173 | )); 174 | DropdownMenuLabel.displayName = 175 | DropdownMenuPrimitive.Label.displayName; 176 | 177 | const DropdownMenuSeparator = React.forwardRef< 178 | React.ElementRef, 179 | React.ComponentPropsWithoutRef< 180 | typeof DropdownMenuPrimitive.Separator 181 | > 182 | >(({ className, ...props }, ref) => ( 183 | 188 | )); 189 | DropdownMenuSeparator.displayName = 190 | DropdownMenuPrimitive.Separator.displayName; 191 | 192 | const DropdownMenuShortcut = ({ 193 | className, 194 | ...props 195 | }: React.HTMLAttributes) => { 196 | return ( 197 | 204 | ); 205 | }; 206 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 207 | 208 | export { 209 | DropdownMenu, 210 | DropdownMenuTrigger, 211 | DropdownMenuContent, 212 | DropdownMenuItem, 213 | DropdownMenuCheckboxItem, 214 | DropdownMenuRadioItem, 215 | DropdownMenuLabel, 216 | DropdownMenuSeparator, 217 | DropdownMenuShortcut, 218 | DropdownMenuGroup, 219 | DropdownMenuPortal, 220 | DropdownMenuSub, 221 | DropdownMenuSubContent, 222 | DropdownMenuSubTrigger, 223 | DropdownMenuRadioGroup, 224 | }; 225 | -------------------------------------------------------------------------------- /stories/Configure.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/blocks"; 2 | import Image from "next/image"; 3 | 4 | import Github from "./assets/github.svg"; 5 | import Discord from "./assets/discord.svg"; 6 | import Youtube from "./assets/youtube.svg"; 7 | import Tutorials from "./assets/tutorials.svg"; 8 | import Styling from "./assets/styling.png"; 9 | import Context from "./assets/context.png"; 10 | import Assets from "./assets/assets.png"; 11 | import Docs from "./assets/docs.png"; 12 | import Share from "./assets/share.png"; 13 | import FigmaPlugin from "./assets/figma-plugin.png"; 14 | import Testing from "./assets/testing.png"; 15 | import Accessibility from "./assets/accessibility.png"; 16 | import Theming from "./assets/theming.png"; 17 | import AddonLibrary from "./assets/addon-library.png"; 18 | 19 | export const RightArrow = () => ( 20 | 33 | 34 | 35 | ); 36 | 37 | 38 | 39 |
40 |
41 | # Configure your project 42 | 43 | Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. 44 | 45 |
46 |
47 |
48 | A wall of logos representing different styling technologies 55 |

Add styling and CSS

56 |

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

57 | Learn more 61 |
62 |
63 | An abstraction representing the composition of data for a component 70 |

Provide context and mocking

71 |

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

72 | Learn more 76 |
77 |
78 | A representation of typography and image assets 85 |
86 |

Load assets and resources

87 |

To link static files (like fonts) to your projects and stories, use the 88 | `staticDirs` configuration option to specify folders to load when 89 | starting Storybook.

90 | Learn more 94 |
95 |
96 |
97 |
98 |
99 |
100 | # Do more with Storybook 101 | 102 | Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. 103 | 104 |
105 | 106 |
107 |
108 |
109 | A screenshot showing the autodocs tag being set, pointing a docs page being generated 116 |

Autodocs

117 |

Auto-generate living, 118 | interactive reference documentation from your components and stories.

119 | Learn more 123 |
124 |
125 | A browser window showing a Storybook being published to a chromatic.com URL 132 |

Publish to Chromatic

133 |

Publish your Storybook to review and collaborate with your entire team.

134 | Learn more 138 |
139 |
140 | Windows showing the Storybook plugin in Figma 147 |

Figma Plugin

148 |

Embed your stories into Figma to cross-reference the design and live 149 | implementation in one place.

150 | Learn more 154 |
155 |
156 | Screenshot of tests passing and failing 163 |

Testing

164 |

Use stories to test a component in all its variations, no matter how 165 | complex.

166 | Learn more 170 |
171 |
172 | Screenshot of accessibility tests passing and failing 179 |

Accessibility

180 |

Automatically test your components for a11y issues as you develop.

181 | Learn more 185 |
186 |
187 | Screenshot of Storybook in light and dark mode 194 |

Theming

195 |

Theme Storybook's UI to personalize it to your project.

196 | Learn more 200 |
201 |
202 |
203 |
204 |
205 |
206 |

Addons

207 |

Integrate your tools with Storybook to connect workflows.

208 | Discover all addons 212 |
213 |
214 | Integrate your tools with Storybook to connect workflows. 220 |
221 |
222 | 223 |
224 |
225 | Github logo 233 | Join our contributors building the future of UI development. 234 | 235 | Star on GitHub 239 |
240 |
241 | Discord logo 249 |
250 | Get support and chat with frontend developers. 251 | 252 | Join Discord server 256 |
257 |
258 |
259 | Youtube logo 267 |
268 | Watch tutorials, feature previews and interviews. 269 | 270 | Watch on YouTube 274 |
275 |
276 |
277 | A book 285 |

Follow guided walkthroughs on for key workflows.

286 | 287 | Discover tutorials 291 |
292 | 293 |
294 | 295 | 452 | -------------------------------------------------------------------------------- /components/ui/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { 5 | ColumnDef, 6 | ColumnFiltersState, 7 | SortingState, 8 | VisibilityState, 9 | flexRender, 10 | getCoreRowModel, 11 | getFilteredRowModel, 12 | getPaginationRowModel, 13 | getSortedRowModel, 14 | useReactTable, 15 | } from "@tanstack/react-table"; 16 | import { ChevronsUpDown, MoreHorizontal } from "lucide-react"; 17 | 18 | import { Button } from "@/components/ui/button"; 19 | import { Checkbox } from "@/components/ui/checkbox"; 20 | import { 21 | DropdownMenu, 22 | DropdownMenuCheckboxItem, 23 | DropdownMenuContent, 24 | DropdownMenuItem, 25 | DropdownMenuLabel, 26 | DropdownMenuSeparator, 27 | DropdownMenuTrigger, 28 | } from "@/components/ui/dropdown-menu"; 29 | import { Input } from "@/components/ui/input"; 30 | import { 31 | Table, 32 | TableBody, 33 | TableCell, 34 | TableHead, 35 | TableHeader, 36 | TableRow, 37 | } from "@/components/ui/table"; 38 | 39 | export type Payment = { 40 | id: string; 41 | amount: number; 42 | status: "pending" | "processing" | "success" | "failed"; 43 | email: string; 44 | }; 45 | 46 | export const columns: ColumnDef[] = [ 47 | { 48 | id: "select", 49 | header: ({ table }) => ( 50 | 57 | table.toggleAllPageRowsSelected(!!value) 58 | } 59 | aria-label="Select all" 60 | className="shadow-none" 61 | /> 62 | ), 63 | cell: ({ row }) => ( 64 | 67 | row.toggleSelected(!!value) 68 | } 69 | aria-label="Select row" 70 | className="shadow-none" 71 | /> 72 | ), 73 | enableSorting: false, 74 | enableHiding: false, 75 | }, 76 | { 77 | accessorKey: "status", 78 | header: () => ( 79 |
Status
80 | ), 81 | cell: ({ row }) => ( 82 |
83 | {row.getValue("status")} 84 |
85 | ), 86 | }, 87 | { 88 | accessorKey: "email", 89 | header: ({ column }) => { 90 | return ( 91 | 108 | ); 109 | }, 110 | cell: ({ row }) => ( 111 |
112 | {row.getValue("email")} 113 |
114 | ), 115 | }, 116 | { 117 | accessorKey: "amount", 118 | header: () => ( 119 |
Amount
120 | ), 121 | cell: ({ row }) => { 122 | const amount = parseFloat(row.getValue("amount")); 123 | 124 | // Format the amount as a dollar amount 125 | const formatted = new Intl.NumberFormat("en-US", { 126 | style: "currency", 127 | currency: "USD", 128 | }).format(amount); 129 | 130 | return ( 131 |
{formatted}
132 | ); 133 | }, 134 | }, 135 | { 136 | id: "actions", 137 | enableHiding: false, 138 | cell: ({ row }) => { 139 | const payment = row.original; 140 | 141 | return ( 142 | 143 | 144 | 151 | 152 | 153 | Actions 154 | 156 | navigator.clipboard.writeText( 157 | payment.id 158 | ) 159 | } 160 | > 161 | Copy payment ID 162 | 163 | 164 | 165 | View customer 166 | 167 | 168 | View payment details 169 | 170 | 171 | 172 | ); 173 | }, 174 | }, 175 | ]; 176 | 177 | type DataTableProps = { 178 | data: Payment[]; 179 | }; 180 | 181 | export function DataTable({ data } : DataTableProps) { 182 | const [sorting, setSorting] = React.useState([]); 183 | const [columnFilters, setColumnFilters] = 184 | React.useState([]); 185 | const [columnVisibility, setColumnVisibility] = 186 | React.useState({}); 187 | const [rowSelection, setRowSelection] = React.useState({}); 188 | const [pagination, setPagination] = React.useState({ 189 | pageIndex: 0, 190 | pageSize: 5, 191 | }); 192 | 193 | const table = useReactTable({ 194 | data, 195 | columns, 196 | onSortingChange: setSorting, 197 | onColumnFiltersChange: setColumnFilters, 198 | getCoreRowModel: getCoreRowModel(), 199 | getPaginationRowModel: getPaginationRowModel(), 200 | getSortedRowModel: getSortedRowModel(), 201 | getFilteredRowModel: getFilteredRowModel(), 202 | onColumnVisibilityChange: setColumnVisibility, 203 | onRowSelectionChange: setRowSelection, 204 | state: { 205 | sorting, 206 | columnFilters, 207 | columnVisibility, 208 | rowSelection, 209 | pagination, 210 | }, 211 | }); 212 | 213 | return ( 214 |
215 |
216 | 224 | table 225 | .getColumn("email") 226 | ?.setFilterValue(event.target.value) 227 | } 228 | className="max-w-sm font-medium pt-2.75 pb-1.75 shadow-none w-75 placeholder-red-600" 229 | /> 230 | 231 | 232 | 245 | 246 | 247 | {table 248 | .getAllColumns() 249 | .filter((column) => column.getCanHide()) 250 | .map((column) => { 251 | return ( 252 | 257 | column.toggleVisibility( 258 | !!value 259 | ) 260 | } 261 | > 262 | {column.id} 263 | 264 | ); 265 | })} 266 | 267 | 268 |
269 |
270 | 271 | 272 | {table 273 | .getHeaderGroups() 274 | .map((headerGroup) => ( 275 | 276 | {headerGroup.headers.map( 277 | (header) => { 278 | return ( 279 | 283 | {header.isPlaceholder 284 | ? null 285 | : flexRender( 286 | header 287 | .column 288 | .columnDef 289 | .header, 290 | header.getContext() 291 | )} 292 | 293 | ); 294 | } 295 | )} 296 | 297 | ))} 298 | 299 | 300 | {table.getRowModel().rows?.length ? ( 301 | table 302 | .getRowModel() 303 | .rows.map((row, index, rows) => ( 304 | 311 | {row 312 | .getVisibleCells() 313 | .map( 314 | (cell, cellIndex) => ( 315 | 328 | {flexRender( 329 | cell 330 | .column 331 | .columnDef 332 | .cell, 333 | cell.getContext() 334 | )} 335 | 336 | ) 337 | )} 338 | 339 | )) 340 | ) : ( 341 | 342 | 346 | No results. 347 | 348 | 349 | )} 350 | 351 |
352 |
353 |
354 |
355 | {table.getFilteredSelectedRowModel().rows.length}{" "} 356 | of {table.getFilteredRowModel().rows.length}{" "} 357 | row(s) selected. 358 |
359 |
360 | 375 | 390 |
391 |
392 |
393 | ); 394 | } 395 | --------------------------------------------------------------------------------