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 | Simple Toast
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 |
42 | Toast with Title
43 |
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 |
70 | Toast with Title and Action
71 |
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 |
102 | Destructive Toast
103 |
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 |
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 |
88 |
89 |
90 | Submit
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 |
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 |
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 |
14 |
15 |
16 | Get started by editing{" "}
17 |
18 | app/page.tsx
19 |
20 | .
21 |
22 | Save and see your changes instantly.
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 | {
50 | toast({
51 | description: "Your message has been sent.",
52 | });
53 | }}
54 | >
55 | Show Toast
56 |
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 | Click me
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 |
144 | About Page
145 |
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 |
156 |
157 | Login with Email
158 |
159 | ```
160 |
161 | ### Disabled State
162 |
163 | Disable the button when an action is not available, using the `disabled` prop:
164 |
165 | ```tsx
166 |
167 | Please wait...
168 |
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 |
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 |
65 |
66 |
67 | Submit
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 |
100 |
101 |
102 | Submit
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 |
163 |
164 |
165 | Submit
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 |  🔍
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 | 
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 |
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 |
210 |
211 | Previous slide
212 |
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 |
240 |
241 | Next slide
242 |
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 |
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 |
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 |
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 |
116 |
Autodocs
117 |
Auto-generate living,
118 | interactive reference documentation from your components and stories.
119 |
Learn more
123 |
124 |
125 |
132 |
Publish to Chromatic
133 |
Publish your Storybook to review and collaborate with your entire team.
134 |
Learn more
138 |
139 |
140 |
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 |
163 |
Testing
164 |
Use stories to test a component in all its variations, no matter how
165 | complex.
166 |
Learn more
170 |
171 |
172 |
179 |
Accessibility
180 |
Automatically test your components for a11y issues as you develop.
181 |
Learn more
185 |
186 |
187 |
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 |
220 |
221 |
222 |
223 |
224 |
225 |
233 | Join our contributors building the future of UI development.
234 |
235 |
Star on GitHub
239 |
240 |
258 |
259 |
267 |
268 | Watch tutorials, feature previews and interviews.
269 |
270 |
Watch on YouTube
274 |
275 |
276 |
277 |
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 |
94 | column.toggleSorting(
95 | column.getIsSorted() === "asc"
96 | )
97 | }
98 | className="!p-1"
99 | >
100 |
101 | Email
102 |
103 |
107 |
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 |
148 | Open menu
149 |
150 |
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 |
236 | Columns{" "}
237 |
244 |
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 | {
364 | table.previousPage();
365 | setPagination({
366 | pageIndex: pagination.pageIndex - 1,
367 | pageSize: 5,
368 | });
369 | }}
370 | disabled={!table.getCanPreviousPage()}
371 | className="!h-auto px-4 py-3 shadow-none"
372 | >
373 | Previous
374 |
375 | {
379 | table.nextPage();
380 | setPagination({
381 | pageIndex: pagination.pageIndex + 1,
382 | pageSize: pagination.pageSize,
383 | });
384 | }}
385 | disabled={!table.getCanNextPage()}
386 | className="!h-auto px-4 py-3 shadow-none bg-gradient-to-r from-[#FF9B3E] to-[#FF343B]"
387 | >
388 | Next
389 |
390 |
391 |
392 |
393 | );
394 | }
395 |
--------------------------------------------------------------------------------