feat: add basic webui

This commit is contained in:
2024-12-30 06:39:09 +08:00
parent 608a7fb9c6
commit a4c549e7c3
462 changed files with 35900 additions and 2491 deletions

45
apps/storybook/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
*storybook.log
# storybook
storybook-static/

View File

@@ -0,0 +1,30 @@
import { dirname, join } from 'node:path';
import type { StorybookConfig } from '@storybook/nextjs';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
const getAbsolutePath = (value: string) =>
dirname(require.resolve(join(value, 'package.json')));
const config: StorybookConfig = {
stories: [
'../stories/**/*.mdx',
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
getAbsolutePath('@storybook/addon-onboarding'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions'),
getAbsolutePath('@storybook/addon-themes'),
],
framework: {
name: getAbsolutePath('@storybook/nextjs'),
options: {},
},
staticDirs: ['../public'],
};
export default config;

View File

@@ -0,0 +1,17 @@
<!-- https://github.com/vercel/geist-font/issues/72 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap" rel="stylesheet">
<style>
:root {
--font-geist-sans: "Geist", sans-serif;
--font-geist-mono: "Geist Mono", monospace;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
touch-action: manipulation;
}
</style>

View File

@@ -0,0 +1,53 @@
import { Toaster } from '@konobangu/design-system/components/ui/sonner';
import { TooltipProvider } from '@konobangu/design-system/components/ui/tooltip';
import { ThemeProvider } from '@konobangu/design-system/providers/theme';
import { withThemeByClassName } from '@storybook/addon-themes';
import type { Preview } from '@storybook/react';
import '@konobangu/design-system/styles/globals.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
chromatic: {
modes: {
light: {
theme: 'light',
className: 'light',
},
dark: {
theme: 'dark',
className: 'dark',
},
},
},
},
decorators: [
withThemeByClassName({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
}),
(Story) => {
return (
<div className="bg-background">
<ThemeProvider>
<TooltipProvider>
<Story />
</TooltipProvider>
<Toaster />
</ThemeProvider>
</div>
);
},
],
};
export default preview;

40
apps/storybook/README.md Normal file
View File

@@ -0,0 +1,40 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;

View File

@@ -0,0 +1,39 @@
{
"name": "storybook",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "storybook dev -p 6006",
"build-storybook": "storybook build",
"chromatic": "chromatic --exit-zero-on-changes",
"clean": "git clean -xdf .cache .turbo dist node_modules",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@konobangu/design-system": "workspace:*",
"lucide-react": "^0.468.0",
"next": "^15.1.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
"@konobangu/typescript-config": "workspace:*",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-onboarding": "^8.4.7",
"@storybook/addon-themes": "^8.4.7",
"@storybook/blocks": "^8.4.7",
"@storybook/nextjs": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/test": "^8.4.7",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"chromatic": "^11.20.1",
"postcss": "^8",
"storybook": "^8.4.7",
"tailwindcss": "^3.4.16",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@konobangu/design-system/components/ui/accordion';
/**
* A vertically stacked set of interactive headings that each reveal a section
* of content.
*/
const meta = {
title: 'ui/Accordion',
component: Accordion,
tags: ['autodocs'],
argTypes: {
type: {
options: ['single', 'multiple'],
control: { type: 'radio' },
},
},
args: {
type: 'single',
collapsible: true,
},
render: (args) => (
<Accordion {...args}>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that matches the other components'
aesthetic.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It's animated by default, but you can disable it if you prefer.
</AccordionContent>
</AccordionItem>
</Accordion>
),
} satisfies Meta<typeof Accordion>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default behavior of the accordion allows only one item to be open.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@konobangu/design-system/components/ui/alert-dialog';
/**
* A modal dialog that interrupts the user with important content and expects
* a response.
*/
const meta = {
title: 'ui/AlertDialog',
component: AlertDialog,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<AlertDialog {...args}>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof AlertDialog>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the alert dialog.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AlertCircle } from 'lucide-react';
import {
Alert,
AlertDescription,
AlertTitle,
} from '@konobangu/design-system/components/ui/alert';
/**
* Displays a callout for user attention.
*/
const meta = {
title: 'ui/Alert',
component: Alert,
tags: ['autodocs'],
argTypes: {
variant: {
options: ['default', 'destructive'],
control: { type: 'radio' },
},
},
args: {
variant: 'default',
},
render: (args) => (
<Alert {...args}>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components to your app using the cli.
</AlertDescription>
</Alert>
),
} satisfies Meta<typeof Alert>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the alert.
*/
export const Default: Story = {};
/**
* Use the `destructive` alert to indicate a destructive action.
*/
export const Destructive: Story = {
render: (args) => (
<Alert {...args}>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired. Please log in again.
</AlertDescription>
</Alert>
),
args: {
variant: 'destructive',
},
};

View File

@@ -0,0 +1,71 @@
import type { Meta, StoryObj } from '@storybook/react';
import Image from 'next/image';
import { AspectRatio } from '@konobangu/design-system/components/ui/aspect-ratio';
/**
* Displays content within a desired ratio.
*/
const meta: Meta<typeof AspectRatio> = {
title: 'ui/AspectRatio',
component: AspectRatio,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<AspectRatio {...args} className="bg-slate-50 dark:bg-slate-800">
<Image
src="https://images.unsplash.com/photo-1576075796033-848c2a5f3696?w=800&dpr=2&q=80"
alt="Photo by Alvaro Pinot"
fill
className="rounded-md object-cover"
/>
</AspectRatio>
),
decorators: [
(Story) => (
<div className="w-1/2">
<Story />
</div>
),
],
} satisfies Meta<typeof AspectRatio>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the aspect ratio.
*/
export const Default: Story = {
args: {
ratio: 16 / 9,
},
};
/**
* Use the `1:1` aspect ratio to display a square image.
*/
export const Square: Story = {
args: {
ratio: 1,
},
};
/**
* Use the `4:3` aspect ratio to display a landscape image.
*/
export const Landscape: Story = {
args: {
ratio: 4 / 3,
},
};
/**
* Use the `2.35:1` aspect ratio to display a cinemascope image.
*/
export const Cinemascope: Story = {
args: {
ratio: 2.35 / 1,
},
};

View File

@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@konobangu/design-system/components/ui/avatar';
/**
* An image element with a fallback for representing the user.
*/
const meta = {
title: 'ui/Avatar',
component: Avatar,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Avatar {...args}>
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Avatar>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the avatar.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Badge } from '@konobangu/design-system/components/ui/badge';
/**
* Displays a badge or a component that looks like a badge.
*/
const meta = {
title: 'ui/Badge',
component: Badge,
tags: ['autodocs'],
argTypes: {
children: {
control: 'text',
},
},
args: {
children: 'Badge',
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the badge.
*/
export const Default: Story = {};
/**
* Use the `secondary` badge to call for less urgent information, blending
* into the interface while still signaling minor updates or statuses.
*/
export const Secondary: Story = {
args: {
variant: 'secondary',
},
};
/**
* Use the `destructive` badge to indicate errors, alerts, or the need for
* immediate attention.
*/
export const Destructive: Story = {
args: {
variant: 'destructive',
},
};
/**
* Use the `outline` badge for overlaying without obscuring interface details,
* emphasizing clarity and subtlety..
*/
export const Outline: Story = {
args: {
variant: 'outline',
},
};

View File

@@ -0,0 +1,78 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ArrowRightSquare } from 'lucide-react';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@konobangu/design-system/components/ui/breadcrumb';
/**
* Displays the path to the current resource using a hierarchy of links.
*/
const meta = {
title: 'ui/Breadcrumb',
component: Breadcrumb,
tags: ['autodocs'],
argTypes: {},
args: {},
render: (args) => (
<Breadcrumb {...args}>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink>Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Breadcrumb>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Displays the path of links to the current resource.
*/
export const Default: Story = {};
/**
* Displays the path with a custom icon for the separator.
*/
export const WithCustomSeparator: Story = {
render: (args) => (
<Breadcrumb {...args}>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink>Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<ArrowRightSquare />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink>Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<ArrowRightSquare />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
),
};

View File

@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Loader2, Mail } from 'lucide-react';
import { Button } from '@konobangu/design-system/components/ui/button';
/**
* Displays a button or a component that looks like a button.
*/
const meta = {
title: 'ui/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
children: {
control: 'text',
},
},
parameters: {
layout: 'centered',
},
args: {
variant: 'default',
size: 'default',
children: 'Button',
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the button, used for primary actions and commands.
*/
export const Default: Story = {};
/**
* Use the `outline` button to reduce emphasis on secondary actions, such as
* canceling or dismissing a dialog.
*/
export const Outline: Story = {
args: {
variant: 'outline',
},
};
/**
* Use the `ghost` button is minimalistic and subtle, for less intrusive
* actions.
*/
export const Ghost: Story = {
args: {
variant: 'ghost',
},
};
/**
* Use the `secondary` button to call for less emphasized actions, styled to
* complement the primary button while being less conspicuous.
*/
export const Secondary: Story = {
args: {
variant: 'secondary',
},
};
/**
* Use the `destructive` button to indicate errors, alerts, or the need for
* immediate attention.
*/
export const Destructive: Story = {
args: {
variant: 'destructive',
},
};
/**
* Use the `link` button to reduce emphasis on tertiary actions, such as
* hyperlink or navigation, providing a text-only interactive element.
*/
export const Link: Story = {
args: {
variant: 'link',
},
};
/**
* Add the `disabled` prop to a button to prevent interactions and add a
* loading indicator, such as a spinner, to signify an in-progress action.
*/
export const Loading: Story = {
render: (args) => (
<Button {...args}>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Button
</Button>
),
args: {
...Outline.args,
disabled: true,
},
};
/**
* Add an icon element to a button to enhance visual communication and
* providing additional context for the action.
*/
export const WithIcon: Story = {
render: (args) => (
<Button {...args}>
<Mail className="mr-2 h-4 w-4" /> Login with Email Button
</Button>
),
args: {
...Secondary.args,
},
};
/**
* Use the `sm` size for a smaller button, suitable for interfaces needing
* compact elements without sacrificing usability.
*/
export const Small: Story = {
args: {
size: 'sm',
},
};
/**
* Use the `lg` size for a larger button, offering better visibility and
* easier interaction for users.
*/
export const Large: Story = {
args: {
size: 'lg',
},
};
/**
* Use the "icon" size for a button with only an icon.
*/
export const Icon: Story = {
args: {
...Secondary.args,
size: 'icon',
children: <Mail />,
},
};
/**
* Add the `disabled` prop to prevent interactions with the button.
*/
export const Disabled: Story = {
args: {
disabled: true,
},
};

View File

@@ -0,0 +1,81 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';
import { addDays } from 'date-fns';
import { Calendar } from '@konobangu/design-system/components/ui/calendar';
/**
* A date field component that allows users to enter and edit date.
*/
const meta = {
title: 'ui/Calendar',
component: Calendar,
tags: ['autodocs'],
argTypes: {},
args: {
mode: 'single',
selected: new Date(),
onSelect: action('onDayClick'),
className: 'rounded-md border w-fit',
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Calendar>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the calendar.
*/
export const Default: Story = {};
/**
* Use the `multiple` mode to select multiple dates.
*/
export const Multiple: Story = {
args: {
min: 1,
selected: [new Date(), addDays(new Date(), 2), addDays(new Date(), 8)],
mode: 'multiple',
},
};
/**
* Use the `range` mode to select a range of dates.
*/
export const Range: Story = {
args: {
selected: {
from: new Date(),
to: addDays(new Date(), 7),
},
mode: 'range',
},
};
/**
* Use the `disabled` prop to disable specific dates.
*/
export const Disabled: Story = {
args: {
disabled: [
addDays(new Date(), 1),
addDays(new Date(), 2),
addDays(new Date(), 3),
addDays(new Date(), 5),
],
},
};
/**
* Use the `numberOfMonths` prop to display multiple months.
*/
export const MultipleMonths: Story = {
args: {
numberOfMonths: 2,
showOutsideDays: false,
},
};

View File

@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from '@storybook/react';
import { BellRing } from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@konobangu/design-system/components/ui/card';
const notifications = [
{
title: 'Your call has been confirmed.',
description: '1 hour ago',
},
{
title: 'You have a new message!',
description: '1 hour ago',
},
{
title: 'Your subscription is expiring soon!',
description: '2 hours ago',
},
];
/**
* Displays a card with header, content, and footer.
*/
const meta = {
title: 'ui/Card',
component: Card,
tags: ['autodocs'],
argTypes: {},
args: {
className: 'w-96',
},
render: (args) => (
<Card {...args}>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>You have 3 unread messages.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
{notifications.map((notification, index) => (
<div key={index} className="flex items-center gap-4">
<BellRing className="size-6" />
<div>
<p>{notification.title}</p>
<p className="text-foreground/50">{notification.description}</p>
</div>
</div>
))}
</CardContent>
<CardFooter>
<button type="button" className="hover:underline">
Close
</button>
</CardFooter>
</Card>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the card.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@konobangu/design-system/components/ui/carousel';
/**
* A carousel with motion and swipe built using Embla.
*/
const meta: Meta<typeof Carousel> = {
title: 'ui/Carousel',
component: Carousel,
tags: ['autodocs'],
argTypes: {},
args: {
className: 'w-full max-w-xs',
},
render: (args) => (
<Carousel {...args}>
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<div className="flex aspect-square items-center justify-center rounded border bg-card p-6">
<span className="font-semibold text-4xl">{index + 1}</span>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Carousel>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the carousel.
*/
export const Default: Story = {};
/**
* Use the `basis` utility class to change the size of the carousel.
*/
export const Size: Story = {
render: (args) => (
<Carousel {...args} className="mx-12 w-full max-w-xs">
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index} className="basis-1/3">
<div className="flex aspect-square items-center justify-center rounded border bg-card p-6">
<span className="font-semibold text-4xl">{index + 1}</span>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
),
args: {
className: 'mx-12 w-full max-w-xs',
},
};

View File

@@ -0,0 +1,271 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useMemo } from 'react';
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Label,
Line,
LineChart,
Pie,
PieChart,
XAxis,
} from 'recharts';
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@konobangu/design-system/components/ui/chart';
const multiSeriesData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
];
const multiSeriesConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig;
const singleSeriesData = [
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
{ browser: 'other', visitors: 190, fill: 'var(--color-other)' },
];
const singleSeriesConfig = {
visitors: {
label: 'Visitors',
},
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-2))',
},
other: {
label: 'Other',
color: 'hsl(var(--chart-5))',
},
} satisfies ChartConfig;
/**
* Beautiful charts. Built using Recharts. Copy and paste into your apps.
*/
const meta = {
title: 'ui/Chart',
component: ChartContainer,
tags: ['autodocs'],
argTypes: {},
args: {
children: <div />,
},
} satisfies Meta<typeof ChartContainer>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Combine multiple Area components to create a stacked area chart.
*/
export const StackedAreaChart: Story = {
args: {
config: multiSeriesConfig,
},
render: (args) => (
<ChartContainer {...args}>
<AreaChart
accessibilityLayer
data={multiSeriesData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.4}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
),
};
/**
* Combine multiple Bar components to create a stacked bar chart.
*/
export const StackedBarChart: Story = {
args: {
config: multiSeriesConfig,
},
render: (args) => (
<ChartContainer {...args}>
<BarChart accessibilityLayer data={multiSeriesData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dashed" />}
/>
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>
),
};
/**
* Combine multiple Line components to create a single line chart.
*/
export const MultiLineChart: Story = {
args: {
config: multiSeriesConfig,
},
render: (args) => (
<ChartContainer {...args}>
<LineChart
accessibilityLayer
data={multiSeriesData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Line
dataKey="desktop"
type="natural"
stroke="var(--color-desktop)"
strokeWidth={2}
dot={false}
/>
<Line
dataKey="mobile"
type="natural"
stroke="var(--color-mobile)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
),
};
/**
* Combine Pie and Label components to create a doughnut chart.
*/
export const DoughnutChart: Story = {
args: {
config: singleSeriesConfig,
},
render: (args) => {
const totalVisitors = useMemo(() => {
return singleSeriesData.reduce((acc, curr) => acc + curr.visitors, 0);
}, []);
return (
<ChartContainer {...args}>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={singleSeriesData}
dataKey="visitors"
nameKey="browser"
innerRadius={48}
strokeWidth={5}
>
<Label
content={({ viewBox }) => {
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground font-bold text-3xl"
>
{totalVisitors.toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
Visitors
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
);
},
};

View File

@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Checkbox } from '@konobangu/design-system/components/ui/checkbox';
/**
* A control that allows the user to toggle between checked and not checked.
*/
const meta: Meta<typeof Checkbox> = {
title: 'ui/Checkbox',
component: Checkbox,
tags: ['autodocs'],
argTypes: {},
args: {
id: 'terms',
disabled: false,
},
render: (args) => (
<div className="flex space-x-2">
<Checkbox {...args} />
<label
htmlFor={args.id}
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
>
Accept terms and conditions
</label>
</div>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Checkbox>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the checkbox.
*/
export const Default: Story = {};
/**
* Use the `disabled` prop to disable the checkbox.
*/
export const Disabled: Story = {
args: {
id: 'disabled-terms',
disabled: true,
},
};

View File

@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Info } from 'lucide-react';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@konobangu/design-system/components/ui/collapsible';
/**
* An interactive component which expands/collapses a panel.
*/
const meta = {
title: 'ui/Collapsible',
component: Collapsible,
tags: ['autodocs'],
argTypes: {},
args: {
className: 'w-96',
disabled: false,
},
render: (args) => (
<Collapsible {...args}>
<CollapsibleTrigger className="flex gap-2">
<h3 className="font-semibold">Can I use this in my project?</h3>
<Info className="size-6" />
</CollapsibleTrigger>
<CollapsibleContent>
Yes. Free to use for personal and commercial projects. No attribution
required.
</CollapsibleContent>
</Collapsible>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Collapsible>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the collapsible.
*/
export const Default: Story = {};
/**
* Use the `disabled` prop to disable the interaction.
*/
export const Disabled: Story = {
args: {
disabled: true,
},
};

View File

@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CommandSeparator } from 'cmdk';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@konobangu/design-system/components/ui/command';
/**
* Fast, composable, unstyled command menu for React.
*/
const meta = {
title: 'ui/Command',
component: Command,
tags: ['autodocs'],
argTypes: {},
args: {
className: 'rounded-lg w-96 border shadow-md',
},
render: (args) => (
<Command {...args}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
<CommandItem>Search Emoji</CommandItem>
<CommandItem>Calculator</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>Profile</CommandItem>
<CommandItem>Billing</CommandItem>
<CommandItem>Settings</CommandItem>
</CommandGroup>
</CommandList>
</Command>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Command>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the command.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,153 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@konobangu/design-system/components/ui/context-menu';
/**
* Displays a menu to the user — such as a set of actions or functions —
* triggered by a button.
*/
const meta = {
title: 'ui/ContextMenu',
component: ContextMenu,
tags: ['autodocs'],
argTypes: {},
args: {},
render: (args) => (
<ContextMenu {...args}>
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="w-32">
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ContextMenu>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the context menu.
*/
export const Default: Story = {};
/**
* A context menu with shortcuts.
*/
export const WithShortcuts: Story = {
render: (args) => (
<ContextMenu {...args}>
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="w-32">
<ContextMenuItem>
Back
<ContextMenuShortcut>[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem disabled>
Forward
<ContextMenuShortcut>]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Reload
<ContextMenuShortcut>R</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
),
};
/**
* A context menu with a submenu.
*/
export const WithSubmenu: Story = {
render: (args) => (
<ContextMenu {...args}>
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="w-32">
<ContextMenuItem>
New Tab
<ContextMenuShortcut>N</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>
Save Page As...
<ContextMenuShortcut>S</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuContent>
</ContextMenu>
),
};
/**
* A context menu with checkboxes.
*/
export const WithCheckboxes: Story = {
render: (args) => (
<ContextMenu {...args}>
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuCheckboxItem checked>
Show Comments
<ContextMenuShortcut>C</ContextMenuShortcut>
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem>Show Preview</ContextMenuCheckboxItem>
</ContextMenuContent>
</ContextMenu>
),
};
/**
* A context menu with a radio group.
*/
export const WithRadioGroup: Story = {
render: (args) => (
<ContextMenu {...args}>
<ContextMenuTrigger className="flex h-48 w-96 items-center justify-center rounded-md border border-dashed bg-accent text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuRadioGroup value="light">
<ContextMenuLabel inset>Theme</ContextMenuLabel>
<ContextMenuRadioItem value="light">Light</ContextMenuRadioItem>
<ContextMenuRadioItem value="dark">Dark</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
),
};

View File

@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@konobangu/design-system/components/ui/dialog';
/**
* A window overlaid on either the primary window or another dialog window,
* rendering the content underneath inert.
*/
const meta = {
title: 'ui/Dialog',
component: Dialog,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Dialog {...args}>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-4">
<button type="button" className="hover:underline">
Cancel
</button>
<DialogClose>
<button
type="button"
className="rounded bg-primary px-4 py-2 text-primary-foreground"
>
Continue
</button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the dialog.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@konobangu/design-system/components/ui/drawer';
/**
* A drawer component for React.
*/
const meta: Meta<typeof Drawer> = {
title: 'ui/Drawer',
component: Drawer,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Drawer {...args}>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you sure absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<button
type="button"
className="rounded bg-primary px-4 py-2 text-primary-foreground"
>
Submit
</button>
<DrawerClose>
<button type="button" className="hover:underline">
Cancel
</button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
),
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the drawer.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,159 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Mail, Plus, PlusCircle, Search, UserPlus } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@konobangu/design-system/components/ui/dropdown-menu';
/**
* Displays a menu to the user — such as a set of actions or functions —
* triggered by a button.
*/
const meta = {
title: 'ui/DropdownMenu',
component: DropdownMenu,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent className="w-44">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof DropdownMenu>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the dropdown menu.
*/
export const Default: Story = {};
/**
* A dropdown menu with shortcuts.
*/
export const WithShortcuts: Story = {
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent className="w-44">
<DropdownMenuLabel>Controls</DropdownMenuLabel>
<DropdownMenuItem>
Back
<DropdownMenuShortcut>[</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem disabled>
Forward
<DropdownMenuShortcut>]</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
};
/**
* A dropdown menu with submenus.
*/
export const WithSubmenus: Story = {
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent className="w-44">
<DropdownMenuItem>
<Search className="mr-2 size-4" />
<span>Search</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Plus className="mr-2 size-4" />
<span>New Team</span>
<DropdownMenuShortcut>+T</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserPlus className="mr-2 size-4" />
<span>Invite users</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<Mail className="mr-2 size-4" />
<span>Email</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<PlusCircle className="mr-2 size-4" />
<span>More...</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
),
};
/**
* A dropdown menu with radio items.
*/
export const WithRadioItems: Story = {
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent className="w-44">
<DropdownMenuLabel inset>Status</DropdownMenuLabel>
<DropdownMenuRadioGroup value="warning">
<DropdownMenuRadioItem value="info">Info</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="warning">Warning</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="error">Error</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
),
};
/**
* A dropdown menu with checkboxes.
*/
export const WithCheckboxes: Story = {
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent className="w-44">
<DropdownMenuCheckboxItem checked>
Autosave
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Show Comments</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
),
};

View File

@@ -0,0 +1,85 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@konobangu/design-system/components/ui/form';
/**
* Building forms with React Hook Form and Zod.
*/
const meta: Meta<typeof Form> = {
title: 'ui/Form',
component: Form,
tags: ['autodocs'],
argTypes: {},
render: (args) => <ProfileForm {...args} />,
} satisfies Meta<typeof Form>;
export default meta;
type Story = StoryObj<typeof meta>;
const formSchema = z.object({
username: z.string().min(2, {
message: 'Username must be at least 2 characters.',
}),
});
const ProfileForm = (args: Story['args']) => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
action('onSubmit')(values);
}
return (
<Form {...args} {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<input
className="w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="username"
{...field}
/>
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<button
className="rounded bg-primary px-4 py-2 text-primary-foreground"
type="submit"
>
Submit
</button>
</form>
</Form>
);
};
/**
* The default form of the form.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@konobangu/design-system/components/ui/hover-card';
/**
* For sighted users to preview content available behind a link.
*/
const meta = {
title: 'ui/HoverCard',
component: HoverCard,
tags: ['autodocs'],
argTypes: {},
args: {},
render: (args) => (
<HoverCard {...args}>
<HoverCardTrigger>Hover</HoverCardTrigger>
<HoverCardContent>
The React Framework - created and maintained by @vercel.
</HoverCardContent>
</HoverCard>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof HoverCard>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the hover card.
*/
export const Default: Story = {};
/**
* Use the `openDelay` and `closeDelay` props to control the delay before the
* hover card opens and closes.
*/
export const Instant: Story = {
args: {
openDelay: 0,
closeDelay: 0,
},
};

View File

@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from '@storybook/react';
import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from '@konobangu/design-system/components/ui/input-otp';
/**
* Accessible one-time password component with copy paste functionality.
*/
const meta = {
title: 'ui/InputOTP',
component: InputOTP,
tags: ['autodocs'],
argTypes: {},
args: {
maxLength: 6,
pattern: REGEXP_ONLY_DIGITS_AND_CHARS,
children: null,
},
render: (args) => (
<InputOTP {...args} render={undefined}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof InputOTP>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the InputOTP field.
*/
export const Default: Story = {};
/**
* Use multiple groups to separate the input slots.
*/
export const SeparatedGroup: Story = {
render: (args) => (
<InputOTP {...args} render={undefined}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
),
};

View File

@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from '@konobangu/design-system/components/ui/input';
/**
* Displays a form input field or a component that looks like an input field.
*/
const meta = {
title: 'ui/Input',
component: Input,
tags: ['autodocs'],
argTypes: {},
args: {
className: 'w-96',
type: 'email',
placeholder: 'Email',
disabled: false,
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the input field.
*/
export const Default: Story = {};
/**
* Use the `disabled` prop to make the input non-interactive and appears faded,
* indicating that input is not currently accepted.
*/
export const Disabled: Story = {
args: { disabled: true },
};
/**
* Use the `Label` component to includes a clear, descriptive label above or
* alongside the input area to guide users.
*/
export const WithLabel: Story = {
render: (args) => (
<div className="grid items-center gap-1.5">
<label htmlFor="email">{args.placeholder}</label>
<Input {...args} id="email" />
</div>
),
};
/**
* Use a text element below the input field to provide additional instructions
* or information to users.
*/
export const WithHelperText: Story = {
render: (args) => (
<div className="grid items-center gap-1.5">
<label htmlFor="email-2">{args.placeholder}</label>
<Input {...args} id="email-2" />
<p className="text-foreground/50 text-sm">Enter your email address.</p>
</div>
),
};
/**
* Use the `Button` component to indicate that the input field can be submitted
* or used to trigger an action.
*/
export const WithButton: Story = {
render: (args) => (
<div className="flex items-center space-x-2">
<Input {...args} />
<button
className="rounded bg-primary px-4 py-2 text-primary-foreground"
type="submit"
>
Subscribe
</button>
</div>
),
};

View File

@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Label } from '@konobangu/design-system/components/ui/label';
/**
* Renders an accessible label associated with controls.
*/
const meta = {
title: 'ui/Label',
component: Label,
tags: ['autodocs'],
argTypes: {
children: {
control: { type: 'text' },
},
},
args: {
children: 'Your email address',
htmlFor: 'email',
},
} satisfies Meta<typeof Label>;
export default meta;
type Story = StoryObj<typeof Label>;
/**
* The default form of the label.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,126 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarLabel,
MenubarMenu,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from '@konobangu/design-system/components/ui/menubar';
/**
* A visually persistent menu common in desktop applications that provides
* quick access to a consistent set of commands.
*/
const meta = {
title: 'ui/Menubar',
component: Menubar,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Menubar {...args}>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab <MenubarShortcut>T</MenubarShortcut>
</MenubarItem>
<MenubarItem>New Window</MenubarItem>
<MenubarSeparator />
<MenubarItem disabled>Share</MenubarItem>
<MenubarSeparator />
<MenubarItem>Print</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Menubar>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the menubar.
*/
export const Default: Story = {};
/**
* A menubar with a submenu.
*/
export const WithSubmenu: Story = {
render: (args) => (
<Menubar {...args}>
<MenubarMenu>
<MenubarTrigger>Actions</MenubarTrigger>
<MenubarContent>
<MenubarItem>Download</MenubarItem>
<MenubarSub>
<MenubarSubTrigger>Share</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Email link</MenubarItem>
<MenubarItem>Messages</MenubarItem>
<MenubarItem>Notes</MenubarItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
</Menubar>
),
};
/**
* A menubar with radio items.
*/
export const WithRadioItems: Story = {
render: (args) => (
<Menubar {...args}>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarLabel inset>Device Size</MenubarLabel>
<MenubarRadioGroup value="md">
<MenubarRadioItem value="sm">Small</MenubarRadioItem>
<MenubarRadioItem value="md">Medium</MenubarRadioItem>
<MenubarRadioItem value="lg">Large</MenubarRadioItem>
</MenubarRadioGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>
),
};
/**
* A menubar with checkbox items.
*/
export const WithCheckboxItems: Story = {
render: (args) => (
<Menubar {...args}>
<MenubarMenu>
<MenubarTrigger>Filters</MenubarTrigger>
<MenubarContent>
<MenubarItem>Show All</MenubarItem>
<MenubarGroup>
<MenubarCheckboxItem checked>Unread</MenubarCheckboxItem>
<MenubarCheckboxItem checked>Important</MenubarCheckboxItem>
<MenubarCheckboxItem>Flagged</MenubarCheckboxItem>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>
),
};

View File

@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from '@konobangu/design-system/components/ui/navigation-menu';
/**
* A collection of links for navigating websites.
*/
const meta = {
title: 'ui/NavigationMenu',
component: NavigationMenu,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<NavigationMenu {...args}>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Overview
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger className={navigationMenuTriggerStyle()}>
Documentation
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-96 p-2">
<li>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
API Reference
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Getting Started
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Guides
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink
className={navigationMenuTriggerStyle()}
href="https:www.google.com"
target="_blank"
>
External
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof NavigationMenu>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the navigation menu.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '@konobangu/design-system/components/ui/pagination';
/**
* Pagination with page navigation, next and previous links.
*/
const meta = {
title: 'ui/Pagination',
component: Pagination,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Pagination {...args}>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Pagination>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the pagination.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,36 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@konobangu/design-system/components/ui/popover';
/**
* Displays rich content in a portal, triggered by a button.
*/
const meta = {
title: 'ui/Popover',
component: Popover,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Popover {...args}>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Place content for the popover here.</PopoverContent>
</Popover>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Popover>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the popover.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Progress } from '@konobangu/design-system/components/ui/progress';
/**
* Displays an indicator showing the completion progress of a task, typically
* displayed as a progress bar.
*/
const meta = {
title: 'ui/Progress',
component: Progress,
tags: ['autodocs'],
argTypes: {},
args: {
value: 30,
max: 100,
},
} satisfies Meta<typeof Progress>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the progress.
*/
export const Default: Story = {};
/**
* When the progress is indeterminate.
*/
export const Indeterminate: Story = {
args: {
value: undefined,
},
};
/**
* When the progress is completed.
*/
export const Completed: Story = {
args: {
value: 100,
},
};

View File

@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
RadioGroup,
RadioGroupItem,
} from '@konobangu/design-system/components/ui/radio-group';
/**
* A set of checkable buttons—known as radio buttons—where no more than one of
* the buttons can be checked at a time.
*/
const meta = {
title: 'ui/RadioGroup',
component: RadioGroup,
tags: ['autodocs'],
argTypes: {},
args: {
defaultValue: 'comfortable',
className: 'grid gap-2 grid-cols-[1rem_1fr] items-center',
},
render: (args) => (
<RadioGroup {...args}>
<RadioGroupItem value="default" id="r1" />
<label htmlFor="r1">Default</label>
<RadioGroupItem value="comfortable" id="r2" />
<label htmlFor="r2">Comfortable</label>
<RadioGroupItem value="compact" id="r3" />
<label htmlFor="r3">Compact</label>
</RadioGroup>
),
} satisfies Meta<typeof RadioGroup>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the radio group.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@konobangu/design-system/components/ui/resizable';
/**
* Accessible resizable panel groups and layouts with keyboard support.
*/
const meta: Meta<typeof ResizablePanelGroup> = {
title: 'ui/ResizablePanelGroup',
component: ResizablePanelGroup,
tags: ['autodocs'],
argTypes: {
onLayout: {
control: false,
},
},
args: {
className: 'max-w-96 rounded-lg border',
direction: 'horizontal',
},
render: (args) => (
<ResizablePanelGroup {...args}>
<ResizablePanel defaultSize={50}>
<div className="flex h-[200px] items-center justify-center p-6">
<span className="font-semibold">One</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={25}>
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Two</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Three</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
),
} satisfies Meta<typeof ResizablePanelGroup>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the resizable panel group.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ScrollArea } from '@konobangu/design-system/components/ui/scroll-area';
/**
* Augments native scroll functionality for custom, cross-browser styling.
*/
const meta = {
title: 'ui/ScrollArea',
component: ScrollArea,
tags: ['autodocs'],
argTypes: {
children: {
control: 'text',
},
},
args: {
className: 'h-32 w-80 rounded-md border p-4',
type: 'auto',
children:
"Jokester began sneaking into the castle in the middle of the night and leaving jokes all over the place: under the king's pillow, in his soup, even in the royal toilet. The king was furious, but he couldn't seem to stop Jokester. And then, one day, the people of the kingdom discovered that the jokes left by Jokester were so funny that they couldn't help but laugh. And once they started laughing, they couldn't stop. The king was so angry that he banished Jokester from the kingdom, but the people still laughed, and they laughed, and they laughed. And they all lived happily ever after.",
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ScrollArea>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the scroll area.
*/
export const Default: Story = {};
/**
* Use the `type` prop with `always` to always show the scroll area.
*/
export const Always: Story = {
args: {
type: 'always',
},
};
/**
* Use the `type` prop with `hover` to show the scroll area on hover.
*/
export const Hover: Story = {
args: {
type: 'hover',
},
};
/**
* Use the `type` prop with `scroll` to show the scroll area when scrolling.
*/
export const Scroll: Story = {
args: {
type: 'scroll',
},
};

View File

@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@konobangu/design-system/components/ui/select';
/**
* Displays a list of options for the user to pick from—triggered by a button.
*/
const meta: Meta<typeof Select> = {
title: 'ui/Select',
component: Select,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Select {...args}>
<SelectTrigger className="w-96">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="blueberry">Blueberry</SelectItem>
<SelectItem value="grapes">Grapes</SelectItem>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Vegetables</SelectLabel>
<SelectItem value="aubergine">Aubergine</SelectItem>
<SelectItem value="broccoli">Broccoli</SelectItem>
<SelectItem value="carrot" disabled>
Carrot
</SelectItem>
<SelectItem value="courgette">Courgette</SelectItem>
<SelectItem value="leek">Leek</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Meat</SelectLabel>
<SelectItem value="beef">Beef</SelectItem>
<SelectItem value="chicken">Chicken</SelectItem>
<SelectItem value="lamb">Lamb</SelectItem>
<SelectItem value="pork">Pork</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Select>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the select.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Separator } from '@konobangu/design-system/components/ui/separator';
/**
* Visually or semantically separates content.
*/
const meta = {
title: 'ui/Separator',
component: Separator,
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof Separator>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the separator.
*/
export const Horizontal: Story = {
render: () => (
<div className="flex gap-2">
<div>Left</div>
<Separator orientation="vertical" className="h-auto" />
<div>Right</div>
</div>
),
};
/**
* A vertical separator.
*/
export const Vertical: Story = {
render: () => (
<div className="grid gap-2">
<div>Top</div>
<Separator orientation="horizontal" />
<div>Bottom</div>
</div>
),
};

View File

@@ -0,0 +1,72 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@konobangu/design-system/components/ui/sheet';
/**
* Extends the Dialog component to display content that complements the main
* content of the screen.
*/
const meta: Meta<typeof SheetContent> = {
title: 'ui/Sheet',
component: Sheet,
tags: ['autodocs'],
argTypes: {
side: {
options: ['top', 'bottom', 'left', 'right'],
control: {
type: 'radio',
},
},
},
args: {
side: 'right',
},
render: (args) => (
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent {...args}>
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose>
<button type="button" className="hover:underline">
Cancel
</button>
</SheetClose>
<button
type="button"
className="rounded bg-primary px-4 py-2 text-primary-foreground"
>
Submit
</button>
</SheetFooter>
</SheetContent>
</Sheet>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof SheetContent>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the sheet.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,494 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
AudioWaveform,
BadgeCheck,
Bell,
BookOpen,
Bot,
ChevronRight,
ChevronsUpDown,
Command,
CreditCard,
Folder,
Forward,
Frame,
GalleryVerticalEnd,
LogOut,
// biome-ignore lint/suspicious/noShadowRestrictedNames: "icon name"
Map,
MoreHorizontal,
PieChart,
Plus,
Settings2,
Sparkles,
SquareTerminal,
Trash2,
} from 'lucide-react';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@konobangu/design-system/components/ui/avatar';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@konobangu/design-system/components/ui/breadcrumb';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@konobangu/design-system/components/ui/collapsible';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@konobangu/design-system/components/ui/dropdown-menu';
import { Separator } from '@konobangu/design-system/components/ui/separator';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '@konobangu/design-system/components/ui/sidebar';
import { useState } from 'react';
const meta: Meta<typeof Sidebar> = {
title: 'ui/Sidebar',
component: Sidebar,
tags: ['autodocs'],
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof Sidebar>;
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise',
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup',
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free',
},
],
navMain: [
{
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{
title: 'History',
url: '#',
},
{
title: 'Starred',
url: '#',
},
{
title: 'Settings',
url: '#',
},
],
},
{
title: 'Models',
url: '#',
icon: Bot,
items: [
{
title: 'Genesis',
url: '#',
},
{
title: 'Explorer',
url: '#',
},
{
title: 'Quantum',
url: '#',
},
],
},
{
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{
title: 'Introduction',
url: '#',
},
{
title: 'Get Started',
url: '#',
},
{
title: 'Tutorials',
url: '#',
},
{
title: 'Changelog',
url: '#',
},
],
},
{
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{
title: 'General',
url: '#',
},
{
title: 'Team',
url: '#',
},
{
title: 'Billing',
url: '#',
},
{
title: 'Limits',
url: '#',
},
],
},
],
projects: [
{
name: 'Design Engineering',
url: '#',
icon: Frame,
},
{
name: 'Sales & Marketing',
url: '#',
icon: PieChart,
},
{
name: 'Travel',
url: '#',
icon: Map,
},
],
};
export const Base: Story = {
render: () => {
const [activeTeam, setActiveTeam] = useState(data.teams[0]);
return (
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam.name}
</span>
<span className="truncate text-xs">
{activeTeam.plan}
</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side="bottom"
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{data.teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>
{index + 1}
</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Add team
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{data.navMain.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{data.projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side="bottom"
align="end"
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage
src={data.user.avatar}
alt={data.user.name}
/>
<AvatarFallback className="rounded-lg">
CN
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{data.user.name}
</span>
<span className="truncate text-xs">
{data.user.email}
</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side="bottom"
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage
src={data.user.avatar}
alt={data.user.name}
/>
<AvatarFallback className="rounded-lg">
CN
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{data.user.name}
</span>
<span className="truncate text-xs">
{data.user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
);
},
args: {},
};

View File

@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Skeleton } from '@konobangu/design-system/components/ui/skeleton';
/**
* Use to show a placeholder while content is loading.
*/
const meta = {
title: 'ui/Skeleton',
component: Skeleton,
tags: ['autodocs'],
argTypes: {},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Skeleton>;
export default meta;
type Story = StoryObj<typeof Skeleton>;
/**
* The default form of the skeleton.
*/
export const Default: Story = {
render: (args) => (
<div className="flex items-center space-x-4">
<Skeleton {...args} className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton {...args} className="h-4 w-[250px]" />
<Skeleton {...args} className="h-4 w-[200px]" />
</div>
</div>
),
};

View File

@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Slider } from '@konobangu/design-system/components/ui/slider';
/**
* An input where the user selects a value from within a given range.
*/
const meta = {
title: 'ui/Slider',
component: Slider,
tags: ['autodocs'],
argTypes: {},
args: {
defaultValue: [33],
max: 100,
step: 1,
},
} satisfies Meta<typeof Slider>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the slider.
*/
export const Default: Story = {};
/**
* Use the `inverted` prop to have the slider fill from right to left.
*/
export const Inverted: Story = {
args: {
inverted: true,
},
};
/**
* Use the `disabled` prop to disable the slider.
*/
export const Disabled: Story = {
args: {
disabled: true,
},
};

View File

@@ -0,0 +1,50 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';
import { toast } from 'sonner';
import { Toaster } from '@konobangu/design-system/components/ui/sonner';
/**
* An opinionated toast component for React.
*/
const meta: Meta<typeof Toaster> = {
title: 'ui/Sonner',
component: Toaster,
tags: ['autodocs'],
argTypes: {},
args: {
position: 'bottom-right',
},
parameters: {
layout: 'fullscreen',
},
} satisfies Meta<typeof Toaster>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the toaster.
*/
export const Default: Story = {
render: (args) => (
<div className="flex min-h-96 items-center justify-center space-x-2">
<button
type="button"
onClick={() =>
toast('Event has been created', {
description: new Date().toLocaleString(),
action: {
label: 'Undo',
onClick: action('Undo clicked'),
},
})
}
>
Show Toast
</button>
<Toaster {...args} />
</div>
),
};

View File

@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Switch } from '@konobangu/design-system/components/ui/switch';
/**
* A control that allows the user to toggle between checked and not checked.
*/
const meta = {
title: 'ui/Switch',
component: Switch,
tags: ['autodocs'],
argTypes: {},
parameters: {
layout: 'centered',
},
render: (args) => (
<div className="flex items-center space-x-2">
<Switch {...args} />
<label htmlFor={args.id} className="peer-disabled:text-foreground/50">
Airplane Mode
</label>
</div>
),
} satisfies Meta<typeof Switch>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the switch.
*/
export const Default: Story = {
args: {
id: 'default-switch',
},
};
/**
* Use the `disabled` prop to disable the switch.
*/
export const Disabled: Story = {
args: {
id: 'disabled-switch',
disabled: true,
},
};

View File

@@ -0,0 +1,80 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@konobangu/design-system/components/ui/table';
const invoices = [
{
invoice: 'INV001',
paymentStatus: 'Paid',
totalAmount: '$250.00',
paymentMethod: 'Credit Card',
},
{
invoice: 'INV002',
paymentStatus: 'Pending',
totalAmount: '$150.00',
paymentMethod: 'PayPal',
},
{
invoice: 'INV003',
paymentStatus: 'Unpaid',
totalAmount: '$350.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV004',
paymentStatus: 'Paid',
totalAmount: '$450.00',
paymentMethod: 'Credit Card',
},
];
/**
* Powerful table and datagrids built using TanStack Table.
*/
const meta = {
title: 'ui/Table',
component: Table,
tags: ['autodocs'],
argTypes: {},
render: (args) => (
<Table {...args}>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell className="text-right">{invoice.totalAmount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
),
} satisfies Meta<typeof Table>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the table.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@konobangu/design-system/components/ui/tabs';
/**
* A set of layered sections of content—known as tab panels—that are displayed
* one at a time.
*/
const meta = {
title: 'ui/Tabs',
component: Tabs,
tags: ['autodocs'],
argTypes: {},
args: {
defaultValue: 'account',
className: 'w-96',
},
render: (args) => (
<Tabs {...args}>
<TabsList className="grid grid-cols-2">
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">
Make changes to your account here.
</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
</Tabs>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Tabs>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the tabs.
*/
export const Default: Story = {};

View File

@@ -0,0 +1,82 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Textarea } from '@konobangu/design-system/components/ui/textarea';
/**
* Displays a form textarea or a component that looks like a textarea.
*/
const meta = {
title: 'ui/Textarea',
component: Textarea,
tags: ['autodocs'],
argTypes: {},
args: {
placeholder: 'Type your message here.',
disabled: false,
},
} satisfies Meta<typeof Textarea>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the textarea.
*/
export const Default: Story = {};
/**
* Use the `disabled` prop to disable the textarea.
*/
export const Disabled: Story = {
args: {
disabled: true,
},
};
/**
* Use the `Label` component to includes a clear, descriptive label above or
* alongside the text area to guide users.
*/
export const WithLabel: Story = {
render: (args) => (
<div className="grid w-full gap-1.5">
<label htmlFor="message">Your message</label>
<Textarea {...args} id="message" />
</div>
),
};
/**
* Use a text element below the text area to provide additional instructions
* or information to users.
*/
export const WithText: Story = {
render: (args) => (
<div className="grid w-full gap-1.5">
<label htmlFor="message-2">Your Message</label>
<Textarea {...args} id="message-2" />
<p className="text-slate-500 text-sm">
Your message will be copied to the support team.
</p>
</div>
),
};
/**
* Use the `Button` component to indicate that the text area can be submitted
* or used to trigger an action.
*/
export const WithButton: Story = {
render: (args) => (
<div className="grid w-full gap-2">
<Textarea {...args} />
<button
className="rounded bg-primary px-4 py-2 text-primary-foreground"
type="submit"
>
Send Message
</button>
</div>
),
};

View File

@@ -0,0 +1,94 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
Toast,
ToastAction,
type ToastActionElement,
type ToastProps,
} from '@konobangu/design-system/components/ui/toast';
import { Toaster } from '@konobangu/design-system/components/ui/toaster';
import { useToast } from '@konobangu/design-system/hooks/use-toast';
/**
* A succinct message that is displayed temporarily.
*/
const meta = {
title: 'ui/Toast',
component: Toast,
tags: ['autodocs'],
argTypes: {},
parameters: {
layout: 'centered',
},
render: (args) => {
const { toast } = useToast();
return (
<div>
<button
type="button"
onClick={() => {
toast(args);
}}
>
Show Toast
</button>
<Toaster />
</div>
);
},
} satisfies Meta<typeof Toast>;
export default meta;
type Story = Omit<StoryObj<typeof meta>, 'args'> & {
args: Omit<ToasterToast, 'id'>;
};
type ToasterToast = ToastProps & {
id: string;
title?: string;
description?: string;
action?: ToastActionElement;
};
/**
* The default form of the toast.
*/
export const Default: Story = {
args: {
description: 'Your message has been sent.',
},
};
/**
* Use the `title` prop to provide a title for the toast.
*/
export const WithTitle: Story = {
args: {
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
},
};
/**
* Use the `action` prop to provide an action for the toast.
*/
export const WithAction: Story = {
args: {
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
action: <ToastAction altText="Try again">Try again</ToastAction>,
},
};
/**
* Use the `destructive` variant to indicate a destructive action.
*/
export const Destructive: Story = {
args: {
variant: 'destructive',
title: 'Uh oh! Something went wrong.',
description: 'There was a problem with your request.',
action: <ToastAction altText="Try again">Try again</ToastAction>,
},
};

View File

@@ -0,0 +1,102 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Bold, Italic, Underline } from 'lucide-react';
import {
ToggleGroup,
ToggleGroupItem,
} from '@konobangu/design-system/components/ui/toggle-group';
/**
* A set of two-state buttons that can be toggled on or off.
*/
const meta = {
title: 'ui/ToggleGroup',
component: ToggleGroup,
tags: ['autodocs'],
argTypes: {
type: {
options: ['multiple', 'single'],
control: { type: 'radio' },
},
},
args: {
variant: 'default',
size: 'default',
type: 'multiple',
disabled: false,
},
render: (args) => (
<ToggleGroup {...args}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<Bold className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<Italic className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Toggle underline">
<Underline className="h-4 w-4" />
</ToggleGroupItem>
</ToggleGroup>
),
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ToggleGroup>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the toggle group.
*/
export const Default: Story = {};
/**
* Use the `outline` variant to emphasizing the individuality of each button
* while keeping them visually cohesive.
*/
export const Outline: Story = {
args: {
variant: 'outline',
},
};
/**
* Use the `single` type to create exclusive selection within the button
* group, allowing only one button to be active at a time.
*/
export const Single: Story = {
args: {
type: 'single',
},
};
/**
* Use the `sm` size for a compact version of the button group, featuring
* smaller buttons for spaces with limited real estate.
*/
export const Small: Story = {
args: {
size: 'sm',
},
};
/**
* Use the `lg` size for a more prominent version of the button group, featuring
* larger buttons for emphasis.
*/
export const Large: Story = {
args: {
size: 'lg',
},
};
/**
* Add the `disabled` prop to a button to prevent interactions.
*/
export const Disabled: Story = {
args: {
disabled: true,
},
};

View File

@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Bold, Italic } from 'lucide-react';
import { Toggle } from '@konobangu/design-system/components/ui/toggle';
/**
* A two-state button that can be either on or off.
*/
const meta: Meta<typeof Toggle> = {
title: 'ui/Toggle',
component: Toggle,
tags: ['autodocs'],
argTypes: {
children: {
control: { disable: true },
},
},
args: {
children: <Bold className="h-4 w-4" />,
'aria-label': 'Toggle bold',
},
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof Toggle>;
/**
* The default form of the toggle.
*/
export const Default: Story = {};
/**
* Use the `outline` variant for a distinct outline, emphasizing the boundary
* of the selection circle for clearer visibility
*/
export const Outline: Story = {
args: {
variant: 'outline',
children: <Italic className="h-4 w-4" />,
'aria-label': 'Toggle italic',
},
};
/**
* Use the text element to add a label to the toggle.
*/
export const WithText: Story = {
render: (args) => (
<Toggle {...args}>
<Italic className="mr-2 h-4 w-4" />
Italic
</Toggle>
),
args: { ...Outline.args },
};
/**
* Use the `sm` size for a smaller toggle, suitable for interfaces needing
* compact elements without sacrificing usability.
*/
export const Small: Story = {
args: {
size: 'sm',
},
};
/**
* Use the `lg` size for a larger toggle, offering better visibility and
* easier interaction for users.
*/
export const Large: Story = {
args: {
size: 'lg',
},
};
/**
* Add the `disabled` prop to prevent interactions with the toggle.
*/
export const Disabled: Story = {
args: {
disabled: true,
},
};

View File

@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Plus } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@konobangu/design-system/components/ui/tooltip';
/**
* A popup that displays information related to an element when the element
* receives keyboard focus or the mouse hovers over it.
*/
const meta: Meta<typeof TooltipContent> = {
title: 'ui/Tooltip',
component: TooltipContent,
tags: ['autodocs'],
argTypes: {
side: {
options: ['top', 'bottom', 'left', 'right'],
control: {
type: 'radio',
},
},
children: {
control: 'text',
},
},
args: {
side: 'top',
children: 'Add to library',
},
parameters: {
layout: 'centered',
},
render: (args) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Plus className="h-4 w-4" />
<span className="sr-only">Add</span>
</TooltipTrigger>
<TooltipContent {...args} />
</Tooltip>
</TooltipProvider>
),
} satisfies Meta<typeof TooltipContent>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* The default form of the tooltip.
*/
export const Default: Story = {};
/**
* Use the `bottom` side to display the tooltip below the element.
*/
export const Bottom: Story = {
args: {
side: 'bottom',
},
};
/**
* Use the `left` side to display the tooltip to the left of the element.
*/
export const Left: Story = {
args: {
side: 'left',
},
};
/**
* Use the `right` side to display the tooltip to the right of the element.
*/
export const Right: Story = {
args: {
side: 'right',
},
};

View File

@@ -0,0 +1 @@
export { config as default } from '@konobangu/tailwind-config/config';

View File

@@ -0,0 +1,13 @@
{
"extends": "@konobangu/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": "."
},
"include": [
"next-env.d.ts",
"next.config.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
]
}