diff --git a/.eslintrc.json b/.eslintrc.json index 94fcc14cc57..426dadaeadb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,8 @@ "next/core-web-vitals", "turbo", "prettier", - "plugin:tailwindcss/recommended" + "plugin:tailwindcss/recommended", + "plugin:storybook/recommended" ], "plugins": ["tailwindcss"], "ignorePatterns": ["**/fixtures/**"], diff --git a/apps/www/.storybook/main.ts b/apps/www/.storybook/main.ts new file mode 100644 index 00000000000..841c85396bf --- /dev/null +++ b/apps/www/.storybook/main.ts @@ -0,0 +1,21 @@ +import type { StorybookConfig } from "@storybook/nextjs" + +const config = { + stories: [ + "../registry/stories/**/*.mdx", + "../registry/stories/**/*.stories.@(js|jsx|ts|tsx)", + "./tokens/**/*.stories.@(js|jsx|ts|tsx)", + ], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/nextjs", + options: {}, + }, + docs: {}, +} satisfies StorybookConfig + +export default config diff --git a/apps/www/.storybook/preview-body.html b/apps/www/.storybook/preview-body.html new file mode 100644 index 00000000000..aa9a55708cd --- /dev/null +++ b/apps/www/.storybook/preview-body.html @@ -0,0 +1,6 @@ + diff --git a/apps/www/.storybook/preview.tsx b/apps/www/.storybook/preview.tsx new file mode 100644 index 00000000000..2e16f251cc2 --- /dev/null +++ b/apps/www/.storybook/preview.tsx @@ -0,0 +1,19 @@ +import type { Preview } from "@storybook/react" + +import "../styles/globals.css" + +const preview = { + parameters: { + nextjs: { + appDirectory: true, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +} satisfies Preview + +export default preview diff --git a/apps/www/.storybook/tokens/color.stories.tsx b/apps/www/.storybook/tokens/color.stories.tsx new file mode 100644 index 00000000000..43a189e73cf --- /dev/null +++ b/apps/www/.storybook/tokens/color.stories.tsx @@ -0,0 +1,125 @@ +import React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import resolveConfig from "tailwindcss/resolveConfig" + +import tailwindConfig from "../../tailwind.config.cjs" +import { hexToHSL, hslToHex } from "./util" + +const meta: Meta<{ + swatch: { + name: string + colors: Record + }[] +}> = { + title: "design/Color", + argTypes: {}, + render: (args) => ( + + + + + + + + + {args.swatch.map(({ name, colors }) => ( + + + + + ))} + +
+ Name + + Swatch +
{name} +
+ {Object.entries(colors).map(([name, value], idx) => { + const isHex = value.startsWith("#") + const style = window.getComputedStyle(document.body) + const variable = value.match(/var\(([^)]+)\)/)?.[1] ?? "" + const [h, s, l] = + style.getPropertyValue(variable).match(/\d+/g) ?? [] + const colorHSL = isHex + ? hexToHSL(value) + : `hsl(${h}, ${s}%, ${l}%)` + const colorHex = isHex + ? value + : hslToHex(Number(h), Number(s), Number(l)) + return ( +
+
+

{name}

+

+ {variable} +

+

{colorHex}

+

{colorHSL}

+
+ ) + })} +
+
+ ), +} + +export default meta + +const fullConfig = resolveConfig(tailwindConfig) +type ColorKey = keyof typeof fullConfig.theme.colors + +type Story = StoryObj + +const functionalSwatch = [ + "foreground", + "background", + "primary", + "secondary", + "card", + "accent", + "muted", + "popover", + "destructive", + "input", + "border", + "ring", +] as unknown as Array + +export const Functional: Story = { + args: { + swatch: Object.entries(fullConfig.theme.colors) + .filter((d) => functionalSwatch.includes(d[0] as ColorKey)) + .sort( + ([a], [b]) => + functionalSwatch.indexOf(a as ColorKey) - + functionalSwatch.indexOf(b as ColorKey) + ) + .map(([name, colors]) => { + return { + name, + colors: typeof colors === "string" ? { [name]: colors } : colors, + } + }), + }, +} +export const Tailwind: Story = { + args: { + swatch: Object.entries(fullConfig.theme.colors) + .filter( + (d) => + ![...functionalSwatch, "inherit", "current", "transparent"].includes( + d[0] as keyof typeof fullConfig.theme.colors + ) + ) + .map(([name, colors]) => { + return { + name, + colors: typeof colors === "string" ? { [name]: colors } : colors, + } + }), + }, +} diff --git a/apps/www/.storybook/tokens/radius.stories.tsx b/apps/www/.storybook/tokens/radius.stories.tsx new file mode 100644 index 00000000000..0c30010093b --- /dev/null +++ b/apps/www/.storybook/tokens/radius.stories.tsx @@ -0,0 +1,74 @@ +import React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import resolveConfig from "tailwindcss/resolveConfig" + +import tailwindConfig from "../../tailwind.config.cjs" + +const fullConfig = resolveConfig(tailwindConfig) + +const meta: Meta<{ + radius: { + name: string + value: string + }[] +}> = { + title: "design/Radius", + argTypes: {}, + args: { + radius: Object.keys(fullConfig.theme.borderRadius).map((name) => { + const value = + fullConfig.theme.borderRadius[ + name as keyof typeof fullConfig.theme.borderRadius + ] + return { + name, + value, + } + }), + }, + render: (args) => ( + + + + + + + + + + {args.radius.map(({ name, value }) => { + const style = window.getComputedStyle(document.body) + const variable = value.match(/var\(([^)]+)\)/)?.[1] ?? "" + const resolved = style.getPropertyValue(variable) + const resolvedValue = value.replace(/var\(--(.*?)\)/, resolved) + return ( + + + + + + ) + })} + +
+ Name + + Size + + Preview +
{name} + {resolvedValue} + +
+
+ ), +} + +export default meta + +type Story = StoryObj + +export const Core: Story = {} diff --git a/apps/www/.storybook/tokens/shadow.stories.tsx b/apps/www/.storybook/tokens/shadow.stories.tsx new file mode 100644 index 00000000000..63805b3638a --- /dev/null +++ b/apps/www/.storybook/tokens/shadow.stories.tsx @@ -0,0 +1,77 @@ +import React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import resolveConfig from "tailwindcss/resolveConfig" + +import tailwindConfig from "../../tailwind.config.cjs" + +const fullConfig = resolveConfig(tailwindConfig) + +const meta: Meta<{ + shadow: { + name: string + value: string + }[] +}> = { + title: "design/Shadow", + argTypes: {}, + args: { + shadow: Object.keys(fullConfig.theme.boxShadow).map((name) => { + const value = + fullConfig.theme.boxShadow[ + name as keyof typeof fullConfig.theme.boxShadow + ] + return { + name, + value, + } + }), + }, + render: (args) => ( + + + + + + + + + + {args.shadow.map(({ name, value }) => { + const style = window.getComputedStyle(document.body) + const variable = value.match(/var\(([^)]+),/)?.[1] ?? "" + const resolved = style.getPropertyValue(variable) + const resolvedValue = value + .replace(/var\(--(.*?)\)/, resolved) + .replace(/,/g, ",\n") + .replace(/\n\n/g, "\n") + return ( + + + + + + ) + })} + +
+ Name + + Size + + Preview +
{name} + {resolvedValue} + +
+
+ ), +} + +export default meta + +type Story = StoryObj + +export const Core: Story = {} diff --git a/apps/www/.storybook/tokens/spacing.stories.tsx b/apps/www/.storybook/tokens/spacing.stories.tsx new file mode 100644 index 00000000000..eba01ca5080 --- /dev/null +++ b/apps/www/.storybook/tokens/spacing.stories.tsx @@ -0,0 +1,79 @@ +import React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import resolveConfig from "tailwindcss/resolveConfig" + +import tailwindConfig from "../../tailwind.config.cjs" + +const fullConfig = resolveConfig(tailwindConfig) + +const meta: Meta<{ + scale: { + name: string + size: string + pixels: number + }[] +}> = { + title: "design/Padding", + argTypes: { + scale: { + control: { + type: "object", + }, + }, + }, + args: { + scale: Object.keys(fullConfig.theme.spacing) + .map((name) => { + const value = + fullConfig.theme.spacing[ + name as keyof typeof fullConfig.theme.spacing + ] + return { + name, + size: value, + pixels: parseFloat(value) * (String(value).endsWith("rem") ? 16 : 1), + } + }) + .sort((a, b) => a.pixels - b.pixels), + }, + render: (args) => ( + + + + + + + + + + + {args.scale.map(({ name, size, pixels }) => ( + + + + + + + ))} + +
+ Name + + Size + + Pixel + + Preview +
{name}{size}{pixels}px +
+
+
+
+ ), +} + +export default meta + +type Story = StoryObj + +export const Core: Story = {} diff --git a/apps/www/.storybook/tokens/typography.stories.tsx b/apps/www/.storybook/tokens/typography.stories.tsx new file mode 100644 index 00000000000..60c785331bf --- /dev/null +++ b/apps/www/.storybook/tokens/typography.stories.tsx @@ -0,0 +1,113 @@ +import React, { CSSProperties } from "react" +import type { Meta, StoryObj } from "@storybook/react" +import resolveConfig from "tailwindcss/resolveConfig" + +import tailwindConfig from "../../tailwind.config.cjs" + +const fullConfig = resolveConfig(tailwindConfig) + +const meta: Meta<{ + children: string + key: keyof CSSProperties + property: { + name: string + value: string + }[] +}> = { + title: "design/Typography", + argTypes: {}, + args: { + children: "Typeface", + }, + render: (args) => ( + + + + + + + + + + {args.property.map(({ name, value }) => { + const style = window.getComputedStyle(document.body) + const variable = value.match(/var\(([^)]+)\)/)?.[1] ?? "" + const resolved = style.getPropertyValue(variable) + const resolvedValue = resolved + ? value.replace(/var\(--(.*?)\)/, resolved) + : value + + return ( + + + + + + ) + })} + +
+ Name + + Property + + Preview +
{name} + {resolvedValue} + +

+ {args.children} +

+
+ ), +} + +export default meta + +type Story = StoryObj + +export const FontFamily: Story = { + args: { + key: "fontFamily", + property: Object.keys(fullConfig.theme.fontFamily).map((name) => { + const value = + fullConfig.theme.fontFamily[ + name as keyof typeof fullConfig.theme.fontFamily + ] + return { + name, + value: Array.isArray(value) ? value.join(", ") : value, + } + }), + }, +} +export const FontSize: Story = { + args: { + key: "fontSize", + property: Object.keys(fullConfig.theme.fontSize).map((name) => { + const value = + fullConfig.theme.fontSize[ + name as keyof typeof fullConfig.theme.fontSize + ] + return { + name, + value: value[0], + } + }), + }, +} +export const FontWeight: Story = { + args: { + key: "fontWeight", + property: Object.keys(fullConfig.theme.fontWeight).map((name) => { + const value = + fullConfig.theme.fontWeight[ + name as keyof typeof fullConfig.theme.fontWeight + ] + return { + name, + value: Array.isArray(value) ? value.join(", ") : value, + } + }), + }, +} diff --git a/apps/www/.storybook/tokens/util.ts b/apps/www/.storybook/tokens/util.ts new file mode 100644 index 00000000000..728e90fc2a7 --- /dev/null +++ b/apps/www/.storybook/tokens/util.ts @@ -0,0 +1,58 @@ +export const hslToHex = (h: number, s: number, l: number) => { + l /= 100 + const a = (s * Math.min(l, 1 - l)) / 100 + const f = (n: number) => { + const k = (n + h / 30) % 12 + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1) + return Math.round(255 * color) + .toString(16) + .padStart(2, "0") // convert to Hex and prefix "0" if needed + } + return `#${f(0)}${f(8)}${f(4)}` +} +export const hexToHSL = (hex: string) => { + if (hex === "#fff" || hex === "#ffffff") { + return "hsl(0, 0%, 100%)" + } + if (hex === "#000" || hex === "#000000") { + return "hsl(0, 0%, 0%)" + } + const r = parseInt(hex.substring(1, 3), 16) / 255 + const g = parseInt(hex.substring(3, 5), 16) / 255 + const b = parseInt(hex.substring(5, 7), 16) / 255 + + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + let h = (max + min) / 2 + let s = (max + min) / 2 + let l = (max + min) / 2 + + if (max === min) { + h = s = 0 + } else { + const d = max - min + s = l > 0.5 ? d / (2 - max - min) : d / (max + min) + + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0) + break + case g: + h = (b - r) / d + 2 + break + case b: + h = (r - g) / d + 4 + break + } + + h /= 6 + } + + s = s * 100 + s = Math.round(s) + l = l * 100 + l = Math.round(l) + h = Math.round(360 * h) + + return `hsl(${h}, ${s}%, ${l}%)` +} diff --git a/apps/www/package.json b/apps/www/package.json index a74b89e038f..ca9e19aba79 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -16,7 +16,9 @@ "preview": "next build && next start -p 3333", "typecheck": "contentlayer2 build && tsc --noEmit", "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache", - "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache" + "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "dependencies": { "@emotion/is-prop-valid": "^1.3.1", @@ -80,6 +82,7 @@ "react-resizable-panels": "^2.0.22", "react-wrap-balancer": "^0.4.1", "recharts": "2.12.7", + "rimraf": "^6.0.1", "sharp": "^0.31.3", "sonner": "^1.2.3", "swr": "2.2.6-beta.3", @@ -90,6 +93,14 @@ }, "devDependencies": { "@shikijs/compat": "^1.1.7", + "@storybook/addon-actions": "^8.2.9", + "@storybook/addon-essentials": "^8.2.9", + "@storybook/addon-interactions": "^8.2.9", + "@storybook/addon-links": "^8.2.9", + "@storybook/blocks": "^8.2.9", + "@storybook/nextjs": "^8.2.9", + "@storybook/react": "^8.2.9", + "@storybook/test": "^8.2.9", "@types/lodash.template": "^4.5.1", "@types/node": "^17.0.45", "@types/react": "^18.2.65", @@ -106,8 +117,8 @@ "remark": "^14.0.3", "remark-code-import": "^1.2.0", "remark-gfm": "^4.0.0", - "rimraf": "^4.1.3", "shiki": "^1.10.1", + "storybook": "^8.2.9", "tailwindcss": "3.4.6", "typescript": "^5.5.3", "unist-builder": "3.0.0", diff --git a/apps/www/registry/registry-ui.ts b/apps/www/registry/registry-ui.ts index 2c33171976d..659b212f3f1 100644 --- a/apps/www/registry/registry-ui.ts +++ b/apps/www/registry/registry-ui.ts @@ -5,7 +5,18 @@ export const ui: Registry = [ name: "accordion", type: "registry:ui", dependencies: ["@radix-ui/react-accordion"], - files: ["ui/accordion.tsx"], + files: [ + "ui/accordion.tsx", + { path: "stories/accordion.stories.tsx", type: "registry:story" }, + ], + }, + { + name: "alert", + type: "registry:ui", + files: [ + "ui/alert.tsx", + { path: "stories/alert.stories.tsx", type: "registry:story" }, + ], tailwind: { config: { theme: { @@ -29,70 +40,95 @@ export const ui: Registry = [ }, }, }, - { - name: "alert", - type: "registry:ui", - files: ["ui/alert.tsx"], - }, { name: "alert-dialog", type: "registry:ui", dependencies: ["@radix-ui/react-alert-dialog"], registryDependencies: ["button"], - files: ["ui/alert-dialog.tsx"], + files: [ + "ui/alert-dialog.tsx", + { path: "stories/alert-dialog.stories.tsx", type: "registry:story" }, + ], }, { name: "aspect-ratio", type: "registry:ui", dependencies: ["@radix-ui/react-aspect-ratio"], - files: ["ui/aspect-ratio.tsx"], + files: [ + "ui/aspect-ratio.tsx", + { path: "stories/aspect-ratio.stories.tsx", type: "registry:story" }, + ], }, { name: "avatar", type: "registry:ui", dependencies: ["@radix-ui/react-avatar"], - files: ["ui/avatar.tsx"], + files: [ + "ui/avatar.tsx", + { path: "stories/avatar.stories.tsx", type: "registry:story" }, + ], }, { name: "badge", type: "registry:ui", - files: ["ui/badge.tsx"], + files: [ + "ui/badge.tsx", + { path: "stories/badge.stories.tsx", type: "registry:story" }, + ], }, { name: "breadcrumb", type: "registry:ui", dependencies: ["@radix-ui/react-slot"], - files: ["ui/breadcrumb.tsx"], + files: [ + "ui/breadcrumb.tsx", + { path: "stories/breadcrumb.stories.tsx", type: "registry:story" }, + ], }, { name: "button", type: "registry:ui", dependencies: ["@radix-ui/react-slot"], - files: ["ui/button.tsx"], + files: [ + "ui/button.tsx", + { path: "stories/button.stories.tsx", type: "registry:story" }, + ], }, { name: "calendar", type: "registry:ui", dependencies: ["react-day-picker@8.10.1", "date-fns"], registryDependencies: ["button"], - files: ["ui/calendar.tsx"], + files: [ + "ui/calendar.tsx", + { path: "stories/calendar.stories.tsx", type: "registry:story" }, + ], }, { name: "card", type: "registry:ui", - files: ["ui/card.tsx"], + files: [ + "ui/card.tsx", + { path: "stories/card.stories.tsx", type: "registry:story" }, + ], }, { name: "carousel", type: "registry:ui", - files: ["ui/carousel.tsx"], + files: [ + "ui/carousel.tsx", + { path: "stories/carousel.stories.tsx", type: "registry:story" }, + ], registryDependencies: ["button"], dependencies: ["embla-carousel-react"], }, { name: "chart", type: "registry:ui", - files: ["ui/chart.tsx"], + files: [ + "ui/chart.tsx", + { path: "stories/chart.stories.tsx", type: "registry:story" }, + ], registryDependencies: ["card"], dependencies: ["recharts", "lucide-react"], }, @@ -100,44 +136,65 @@ export const ui: Registry = [ name: "checkbox", type: "registry:ui", dependencies: ["@radix-ui/react-checkbox"], - files: ["ui/checkbox.tsx"], + files: [ + "ui/checkbox.tsx", + { path: "stories/checkbox.stories.tsx", type: "registry:story" }, + ], }, { name: "collapsible", type: "registry:ui", dependencies: ["@radix-ui/react-collapsible"], - files: ["ui/collapsible.tsx"], + files: [ + "ui/collapsible.tsx", + { path: "stories/collapsible.stories.tsx", type: "registry:story" }, + ], }, { name: "command", type: "registry:ui", dependencies: ["cmdk@1.0.0"], registryDependencies: ["dialog"], - files: ["ui/command.tsx"], + files: [ + "ui/command.tsx", + { path: "stories/command.stories.tsx", type: "registry:story" }, + ], }, { name: "context-menu", type: "registry:ui", dependencies: ["@radix-ui/react-context-menu"], - files: ["ui/context-menu.tsx"], + files: [ + "ui/context-menu.tsx", + { path: "stories/context-menu.stories.tsx", type: "registry:story" }, + ], }, { name: "dialog", type: "registry:ui", dependencies: ["@radix-ui/react-dialog"], - files: ["ui/dialog.tsx"], + files: [ + "ui/dialog.tsx", + { path: "stories/dialog.stories.tsx", type: "registry:story" }, + ], }, { name: "drawer", type: "registry:ui", dependencies: ["vaul", "@radix-ui/react-dialog"], - files: ["ui/drawer.tsx"], + files: [ + "ui/drawer.tsx", + { path: "stories/drawer.stories.tsx", type: "registry:story" }, + ], }, { name: "dropdown-menu", type: "registry:ui", dependencies: ["@radix-ui/react-dropdown-menu"], - files: ["ui/dropdown-menu.tsx"], + files: [ + "ui/dropdown-menu.tsx", + { path: "stories/dropdown-menu.stories.tsx", type: "registry:story" }, + ], }, { name: "form", @@ -150,96 +207,144 @@ export const ui: Registry = [ "react-hook-form", ], registryDependencies: ["button", "label"], - files: ["ui/form.tsx"], + files: [ + "ui/form.tsx", + { path: "stories/form.stories.tsx", type: "registry:story" }, + ], }, { name: "hover-card", type: "registry:ui", dependencies: ["@radix-ui/react-hover-card"], - files: ["ui/hover-card.tsx"], + files: [ + "ui/hover-card.tsx", + { path: "stories/hover-card.stories.tsx", type: "registry:story" }, + ], }, { name: "input", type: "registry:ui", - files: ["ui/input.tsx"], + files: [ + "ui/input.tsx", + { path: "stories/input.stories.tsx", type: "registry:story" }, + ], }, { name: "input-otp", type: "registry:ui", dependencies: ["input-otp"], - files: ["ui/input-otp.tsx"], + files: [ + "ui/input-otp.tsx", + { path: "stories/input-otp.stories.tsx", type: "registry:story" }, + ], }, { name: "label", type: "registry:ui", dependencies: ["@radix-ui/react-label"], - files: ["ui/label.tsx"], + files: [ + "ui/label.tsx", + { path: "stories/label.stories.tsx", type: "registry:story" }, + ], }, { name: "menubar", type: "registry:ui", dependencies: ["@radix-ui/react-menubar"], - files: ["ui/menubar.tsx"], + files: [ + "ui/menubar.tsx", + { path: "stories/menubar.stories.tsx", type: "registry:story" }, + ], }, { name: "navigation-menu", type: "registry:ui", dependencies: ["@radix-ui/react-navigation-menu"], - files: ["ui/navigation-menu.tsx"], + files: [ + "ui/navigation-menu.tsx", + { path: "stories/navigation-menu.stories.tsx", type: "registry:story" }, + ], }, { name: "pagination", type: "registry:ui", registryDependencies: ["button"], - files: ["ui/pagination.tsx"], + files: [ + "ui/pagination.tsx", + { path: "stories/pagination.stories.tsx", type: "registry:story" }, + ], }, { name: "popover", type: "registry:ui", dependencies: ["@radix-ui/react-popover"], - files: ["ui/popover.tsx"], + files: [ + "ui/popover.tsx", + { path: "stories/popover.stories.tsx", type: "registry:story" }, + ], }, { name: "progress", type: "registry:ui", dependencies: ["@radix-ui/react-progress"], - files: ["ui/progress.tsx"], + files: [ + "ui/progress.tsx", + { path: "stories/progress.stories.tsx", type: "registry:story" }, + ], }, { name: "radio-group", type: "registry:ui", dependencies: ["@radix-ui/react-radio-group"], - files: ["ui/radio-group.tsx"], + files: [ + "ui/radio-group.tsx", + { path: "stories/radio-group.stories.tsx", type: "registry:story" }, + ], }, { name: "resizable", type: "registry:ui", dependencies: ["react-resizable-panels"], - files: ["ui/resizable.tsx"], + files: [ + "ui/resizable.tsx", + { path: "stories/resizable.stories.tsx", type: "registry:story" }, + ], }, { name: "scroll-area", type: "registry:ui", dependencies: ["@radix-ui/react-scroll-area"], - files: ["ui/scroll-area.tsx"], + files: [ + "ui/scroll-area.tsx", + { path: "stories/scroll-area.stories.tsx", type: "registry:story" }, + ], }, { name: "select", type: "registry:ui", dependencies: ["@radix-ui/react-select"], - files: ["ui/select.tsx"], + files: [ + "ui/select.tsx", + { path: "stories/select.stories.tsx", type: "registry:story" }, + ], }, { name: "separator", type: "registry:ui", dependencies: ["@radix-ui/react-separator"], - files: ["ui/separator.tsx"], + files: [ + "ui/separator.tsx", + { path: "stories/separator.stories.tsx", type: "registry:story" }, + ], }, { name: "sheet", type: "registry:ui", dependencies: ["@radix-ui/react-dialog"], - files: ["ui/sheet.tsx"], + files: [ + "ui/sheet.tsx", + { path: "stories/sheet.stories.tsx", type: "registry:story" }, + ], }, { name: "sidebar", @@ -305,41 +410,62 @@ export const ui: Registry = [ { name: "skeleton", type: "registry:ui", - files: ["ui/skeleton.tsx"], + files: [ + "ui/skeleton.tsx", + { path: "stories/skeleton.stories.tsx", type: "registry:story" }, + ], }, { name: "slider", type: "registry:ui", dependencies: ["@radix-ui/react-slider"], - files: ["ui/slider.tsx"], + files: [ + "ui/slider.tsx", + { path: "stories/slider.stories.tsx", type: "registry:story" }, + ], }, { name: "sonner", type: "registry:ui", dependencies: ["sonner", "next-themes"], - files: ["ui/sonner.tsx"], + files: [ + "ui/sonner.tsx", + { path: "stories/sonner.stories.tsx", type: "registry:story" }, + ], }, { name: "switch", type: "registry:ui", dependencies: ["@radix-ui/react-switch"], - files: ["ui/switch.tsx"], + files: [ + "ui/switch.tsx", + { path: "stories/switch.stories.tsx", type: "registry:story" }, + ], }, { name: "table", type: "registry:ui", - files: ["ui/table.tsx"], + files: [ + "ui/table.tsx", + { path: "stories/table.stories.tsx", type: "registry:story" }, + ], }, { name: "tabs", type: "registry:ui", dependencies: ["@radix-ui/react-tabs"], - files: ["ui/tabs.tsx"], + files: [ + "ui/tabs.tsx", + { path: "stories/tabs.stories.tsx", type: "registry:story" }, + ], }, { name: "textarea", type: "registry:ui", - files: ["ui/textarea.tsx"], + files: [ + "ui/textarea.tsx", + { path: "stories/textarea.stories.tsx", type: "registry:story" }, + ], }, { name: "toast", @@ -358,25 +484,44 @@ export const ui: Registry = [ path: "ui/toaster.tsx", type: "registry:ui", }, + { + path: "stories/toast.stories.tsx", + type: "registry:story", + }, ], }, { name: "toggle", type: "registry:ui", dependencies: ["@radix-ui/react-toggle"], - files: ["ui/toggle.tsx"], + files: [ + "ui/toggle.tsx", + { + path: "stories/toggle.stories.tsx", + type: "registry:story", + }, + ], }, { name: "toggle-group", type: "registry:ui", dependencies: ["@radix-ui/react-toggle-group"], registryDependencies: ["toggle"], - files: ["ui/toggle-group.tsx"], + files: [ + "ui/toggle-group.tsx", + { + path: "stories/toggle-group.stories.tsx", + type: "registry:story", + }, + ], }, { name: "tooltip", type: "registry:ui", dependencies: ["@radix-ui/react-tooltip"], - files: ["ui/tooltip.tsx"], + files: [ + "ui/tooltip.tsx", + { path: "stories/tooltip.stories.tsx", type: "registry:story" }, + ], }, ] diff --git a/apps/www/registry/schema.ts b/apps/www/registry/schema.ts index ebd45304351..81ba921733c 100644 --- a/apps/www/registry/schema.ts +++ b/apps/www/registry/schema.ts @@ -23,6 +23,7 @@ export const registryItemTypeSchema = z.enum([ "registry:hook", "registry:theme", "registry:page", + "registry:story", ]) export const registryItemFileSchema = z.union([ diff --git a/apps/www/registry/stories/accordion.stories.tsx b/apps/www/registry/stories/accordion.stories.tsx new file mode 100644 index 00000000000..a8d1866c461 --- /dev/null +++ b/apps/www/registry/stories/accordion.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/registry/default/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) => ( + + + Is it accessible? + + Yes. It adheres to the WAI-ARIA design pattern. + + + + Is it styled? + + Yes. It comes with default styles that matches the other components' + aesthetic. + + + + Is it animated? + + Yes. It's animated by default, but you can disable it if you prefer. + + + + ), +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default behavior of the accordion allows only one item to be open. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/alert-dialog.stories.tsx b/apps/www/registry/stories/alert-dialog.stories.tsx new file mode 100644 index 00000000000..1eabc2acbee --- /dev/null +++ b/apps/www/registry/stories/alert-dialog.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/registry/default/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) => ( + + Open + + + Are you sure absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + Cancel + Continue + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the alert dialog. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/alert.stories.tsx b/apps/www/registry/stories/alert.stories.tsx new file mode 100644 index 00000000000..cb8d2358632 --- /dev/null +++ b/apps/www/registry/stories/alert.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { AlertCircle } from "lucide-react" + +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/registry/default/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) => ( + + Heads up! + + You can add components to your app using the cli. + + + ), +} satisfies Meta + +export default meta + +type Story = StoryObj +/** + * 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) => ( + + + Error + + Your session has expired. Please log in again. + + + ), + args: { + variant: "destructive", + }, +} diff --git a/apps/www/registry/stories/aspect-ratio.stories.tsx b/apps/www/registry/stories/aspect-ratio.stories.tsx new file mode 100644 index 00000000000..d4d50d65a5b --- /dev/null +++ b/apps/www/registry/stories/aspect-ratio.stories.tsx @@ -0,0 +1,71 @@ +import Image from "next/image" +import type { Meta, StoryObj } from "@storybook/react" + +import { AspectRatio } from "@/registry/default/ui/aspect-ratio" + +/** + * Displays content within a desired ratio. + */ +const meta: Meta = { + title: "ui/AspectRatio", + component: AspectRatio, + tags: ["autodocs"], + argTypes: {}, + render: (args) => ( + + Photo by Alvaro Pinot + + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/avatar.stories.tsx b/apps/www/registry/stories/avatar.stories.tsx new file mode 100644 index 00000000000..1ebfa81a26b --- /dev/null +++ b/apps/www/registry/stories/avatar.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/registry/default/ui/avatar" + +/** + * An image element with a fallback for representing the user. + */ +const meta = { + title: "ui/Avatar", + component: Avatar, + tags: ["autodocs"], + argTypes: {}, + render: (args) => ( + + + CN + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the avatar. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/badge.stories.tsx b/apps/www/registry/stories/badge.stories.tsx new file mode 100644 index 00000000000..0c09b9cf9c1 --- /dev/null +++ b/apps/www/registry/stories/badge.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Badge } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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", + }, +} diff --git a/apps/www/registry/stories/breadcrumb.stories.tsx b/apps/www/registry/stories/breadcrumb.stories.tsx new file mode 100644 index 00000000000..ce65dbfccbd --- /dev/null +++ b/apps/www/registry/stories/breadcrumb.stories.tsx @@ -0,0 +1,78 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { ArrowRightSquare } from "lucide-react" + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/registry/default/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) => ( + + + + Home + + + + Components + + + + Breadcrumb + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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) => ( + + + + Home + + + + + + Components + + + + + + Breadcrumb + + + + ), +} diff --git a/apps/www/registry/stories/button.stories.tsx b/apps/www/registry/stories/button.stories.tsx new file mode 100644 index 00000000000..c226835d6fa --- /dev/null +++ b/apps/www/registry/stories/button.stories.tsx @@ -0,0 +1,157 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { Loader2, Mail } from "lucide-react" + +import { Button } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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) => ( + + ), + 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) => ( + + ), + 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: , + }, +} + +/** + * Add the `disabled` prop to prevent interactions with the button. + */ +export const Disabled: Story = { + args: { + disabled: true, + }, +} diff --git a/apps/www/registry/stories/calendar.stories.tsx b/apps/www/registry/stories/calendar.stories.tsx new file mode 100644 index 00000000000..c84f7e28c58 --- /dev/null +++ b/apps/www/registry/stories/calendar.stories.tsx @@ -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 "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/card.stories.tsx b/apps/www/registry/stories/card.stories.tsx new file mode 100644 index 00000000000..e40227dccd7 --- /dev/null +++ b/apps/www/registry/stories/card.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { BellRing } from "lucide-react" + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/registry/default/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) => ( + + + Notifications + You have 3 unread messages. + + + {notifications.map((notification, index) => ( +
+ +
+

{notification.title}

+

{notification.description}

+
+
+ ))} +
+ + + +
+ ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the card. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/carousel.stories.tsx b/apps/www/registry/stories/carousel.stories.tsx new file mode 100644 index 00000000000..acf211be9d2 --- /dev/null +++ b/apps/www/registry/stories/carousel.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/registry/default/ui/carousel" + +/** + * A carousel with motion and swipe built using Embla. + */ +const meta: Meta = { + title: "ui/Carousel", + component: Carousel, + tags: ["autodocs"], + argTypes: {}, + args: { + className: "w-full max-w-xs", + }, + render: (args) => ( + + + {Array.from({ length: 5 }).map((_, index) => ( + +
+ {index + 1} +
+
+ ))} +
+ + +
+ ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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) => ( + + + {Array.from({ length: 5 }).map((_, index) => ( + +
+ {index + 1} +
+
+ ))} +
+ + +
+ ), + args: { + className: "mx-12 w-full max-w-xs", + }, +} diff --git a/apps/www/registry/stories/chart.stories.tsx b/apps/www/registry/stories/chart.stories.tsx new file mode 100644 index 00000000000..1ab780f1552 --- /dev/null +++ b/apps/www/registry/stories/chart.stories.tsx @@ -0,0 +1,271 @@ +import { useMemo } from "react" +import type { Meta, StoryObj } from "@storybook/react" +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Label, + Line, + LineChart, + Pie, + PieChart, + XAxis, +} from "recharts" + +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/registry/default/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:
, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * Combine multiple Area components to create a stacked area chart. + */ +export const StackedAreaChart: Story = { + args: { + config: multiSeriesConfig, + }, + render: (args) => ( + + + + value.slice(0, 3)} + /> + } + /> + + + + + ), +} + +/** + * Combine multiple Bar components to create a stacked bar chart. + */ +export const StackedBarChart: Story = { + args: { + config: multiSeriesConfig, + }, + render: (args) => ( + + + + value.slice(0, 3)} + /> + } + /> + + + + + ), +} + +/** + * Combine multiple Line components to create a single line chart. + */ +export const MultiLineChart: Story = { + args: { + config: multiSeriesConfig, + }, + render: (args) => ( + + + + value.slice(0, 3)} + /> + } + /> + + + + + ), +} + +/** + * 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 ( + + + } + /> + + + + + ) + }, +} diff --git a/apps/www/registry/stories/checkbox.stories.tsx b/apps/www/registry/stories/checkbox.stories.tsx new file mode 100644 index 00000000000..f3c7c648bd6 --- /dev/null +++ b/apps/www/registry/stories/checkbox.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Checkbox } from "@/registry/default/ui/checkbox" + +/** + * A control that allows the user to toggle between checked and not checked. + */ +const meta: Meta = { + title: "ui/Checkbox", + component: Checkbox, + tags: ["autodocs"], + argTypes: {}, + args: { + id: "terms", + disabled: false, + }, + render: (args) => ( +
+ + +
+ ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/collapsible.stories.tsx b/apps/www/registry/stories/collapsible.stories.tsx new file mode 100644 index 00000000000..1fd11a9af00 --- /dev/null +++ b/apps/www/registry/stories/collapsible.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { Info } from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/registry/default/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) => ( + + +

Can I use this in my project?

+ +
+ + Yes. Free to use for personal and commercial projects. No attribution + required. + +
+ ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/command.stories.tsx b/apps/www/registry/stories/command.stories.tsx new file mode 100644 index 00000000000..b11f11f4bae --- /dev/null +++ b/apps/www/registry/stories/command.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { CommandSeparator } from "cmdk" + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/registry/default/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) => ( + + + + No results found. + + Calendar + Search Emoji + Calculator + + + + Profile + Billing + Settings + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the command. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/context-menu.stories.tsx b/apps/www/registry/stories/context-menu.stories.tsx new file mode 100644 index 00000000000..f4986edfa16 --- /dev/null +++ b/apps/www/registry/stories/context-menu.stories.tsx @@ -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 "@/registry/default/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) => ( + + + Right click here + + + Profile + Billing + Team + Subscription + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the context menu. + */ +export const Default: Story = {} + +/** + * A context menu with shortcuts. + */ +export const WithShortcuts: Story = { + render: (args) => ( + + + Right click here + + + + Back + ⌘[ + + + Forward + ⌘] + + + Reload + ⌘R + + + + ), +} + +/** + * A context menu with a submenu. + */ +export const WithSubmenu: Story = { + render: (args) => ( + + + Right click here + + + + New Tab + ⌘N + + + More Tools + + + Save Page As... + ⇧⌘S + + Create Shortcut... + Name Window... + + Developer Tools + + + + + ), +} + +/** + * A context menu with checkboxes. + */ +export const WithCheckboxes: Story = { + render: (args) => ( + + + Right click here + + + + Show Comments + ⌘⇧C + + Show Preview + + + ), +} + +/** + * A context menu with a radio group. + */ +export const WithRadioGroup: Story = { + render: (args) => ( + + + Right click here + + + + Theme + Light + Dark + + + + ), +} diff --git a/apps/www/registry/stories/dialog.stories.tsx b/apps/www/registry/stories/dialog.stories.tsx new file mode 100644 index 00000000000..74da016dc9d --- /dev/null +++ b/apps/www/registry/stories/dialog.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/registry/default/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) => ( + + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + + + + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the dialog. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/drawer.stories.tsx b/apps/www/registry/stories/drawer.stories.tsx new file mode 100644 index 00000000000..3a50e077980 --- /dev/null +++ b/apps/www/registry/stories/drawer.stories.tsx @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/registry/default/ui/drawer" + +/** + * A drawer component for React. + */ +const meta: Meta = { + title: "ui/Drawer", + component: Drawer, + tags: ["autodocs"], + argTypes: {}, + render: (args) => ( + + Open + + + Are you sure absolutely sure? + This action cannot be undone. + + + + + + + + + + ), + parameters: { + layout: "centered", + }, +} + +export default meta + +type Story = StoryObj + +/** + * The default form of the drawer. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/dropdown-menu.stories.tsx b/apps/www/registry/stories/dropdown-menu.stories.tsx new file mode 100644 index 00000000000..d4d1dbb54fb --- /dev/null +++ b/apps/www/registry/stories/dropdown-menu.stories.tsx @@ -0,0 +1,165 @@ +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 "@/registry/default/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) => ( + + Open + + My Account + + Profile + Billing + Team + Subscription + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the dropdown menu. + */ +export const Default: Story = {} + +/** + * A dropdown menu with shortcuts. + */ +export const WithShortcuts: Story = { + render: (args) => ( + + Open + + Controls + + Back + ⌘[ + + + Forward + ⌘] + + + + ), +} + +/** + * A dropdown menu with submenus. + */ +export const WithSubmenus: Story = { + render: (args) => ( + + Open + + + + Search + + + + + + New Team + ⌘+T + + + + + Invite users + + + + + + Email + + + + + More... + + + + + + + + ), +} + +/** + * A dropdown menu with radio items. + */ +export const WithRadioItems: Story = { + render: (args) => ( + + Open + + Status + + Info + Warning + Error + + + + ), +} + +/** + * A dropdown menu with checkboxes. + */ +export const WithCheckboxes: Story = { + render: (args) => ( + + Open + + + Autosave + ⌘S + + Show Comments + + + ), +} diff --git a/apps/www/registry/stories/form.stories.tsx b/apps/www/registry/stories/form.stories.tsx new file mode 100644 index 00000000000..f4d547467e1 --- /dev/null +++ b/apps/www/registry/stories/form.stories.tsx @@ -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 "@/registry/default/ui/form" + +/** + * Building forms with React Hook Form and Zod. + */ +const meta: Meta = { + title: "ui/Form", + component: Form, + tags: ["autodocs"], + argTypes: {}, + render: (args) => , +} satisfies Meta + +export default meta + +type Story = StoryObj + +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>({ + resolver: zodResolver(formSchema), + defaultValues: { + username: "", + }, + }) + function onSubmit(values: z.infer) { + action("onSubmit")(values) + } + return ( +
+ + ( + + Username + + + + + This is your public display name. + + + + )} + /> + + + + ) +} + +/** + * The default form of the form. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/hover-card.stories.tsx b/apps/www/registry/stories/hover-card.stories.tsx new file mode 100644 index 00000000000..3811a39c2e8 --- /dev/null +++ b/apps/www/registry/stories/hover-card.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/registry/default/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) => ( + + Hover + + The React Framework - created and maintained by @vercel. + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/input-otp.stories.tsx b/apps/www/registry/stories/input-otp.stories.tsx new file mode 100644 index 00000000000..0a6c11d9732 --- /dev/null +++ b/apps/www/registry/stories/input-otp.stories.tsx @@ -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 "@/registry/default/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) => ( + + + + + + + + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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) => ( + + + + + + + + + + + + + + ), +} diff --git a/apps/www/registry/stories/input.stories.tsx b/apps/www/registry/stories/input.stories.tsx new file mode 100644 index 00000000000..57903ffa185 --- /dev/null +++ b/apps/www/registry/stories/input.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Input } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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) => ( +
+ + +
+ ), +} + +/** + * Use a text element below the input field to provide additional instructions + * or information to users. + */ +export const WithHelperText: Story = { + render: (args) => ( +
+ + +

Enter your email address.

+
+ ), +} + +/** + * 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) => ( +
+ + +
+ ), +} diff --git a/apps/www/registry/stories/label.stories.tsx b/apps/www/registry/stories/label.stories.tsx new file mode 100644 index 00000000000..c750ed07e51 --- /dev/null +++ b/apps/www/registry/stories/label.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Label } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * The default form of the label. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/menubar.stories.tsx b/apps/www/registry/stories/menubar.stories.tsx new file mode 100644 index 00000000000..d69367ca5a9 --- /dev/null +++ b/apps/www/registry/stories/menubar.stories.tsx @@ -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 "@/registry/default/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) => ( + + + File + + + New Tab ⌘T + + New Window + + Share + + Print + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the menubar. + */ +export const Default: Story = {} + +/** + * A menubar with a submenu. + */ +export const WithSubmenu: Story = { + render: (args) => ( + + + Actions + + Download + + Share + + Email link + Messages + Notes + + + + + + ), +} + +/** + * A menubar with radio items. + */ +export const WithRadioItems: Story = { + render: (args) => ( + + + View + + Device Size + + Small + Medium + Large + + + + + ), +} + +/** + * A menubar with checkbox items. + */ +export const WithCheckboxItems: Story = { + render: (args) => ( + + + Filters + + Show All + + Unread + Important + Flagged + + + + + ), +} diff --git a/apps/www/registry/stories/navigation-menu.stories.tsx b/apps/www/registry/stories/navigation-menu.stories.tsx new file mode 100644 index 00000000000..dc8c9b3787f --- /dev/null +++ b/apps/www/registry/stories/navigation-menu.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from "@/registry/default/ui/navigation-menu" + +/** + * A collection of links for navigating websites. + */ +const meta = { + title: "ui/NavigationMenu", + component: NavigationMenu, + tags: ["autodocs"], + argTypes: {}, + render: (args) => ( + + + + + Overview + + + + + + Documentation + + +
    +
  • + + API Reference + +
  • +
  • + + Getting Started + +
  • +
  • + + Guides + +
  • +
+
+
+
+ + + External + + +
+
+ ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the navigation menu. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/pagination.stories.tsx b/apps/www/registry/stories/pagination.stories.tsx new file mode 100644 index 00000000000..4eca19c83d0 --- /dev/null +++ b/apps/www/registry/stories/pagination.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/registry/default/ui/pagination" + +/** + * Pagination with page navigation, next and previous links. + */ +const meta = { + title: "ui/Pagination", + component: Pagination, + tags: ["autodocs"], + argTypes: {}, + render: (args) => ( + + + + + + + 1 + + + 2 + + + 3 + + + + + + + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the pagination. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/popover.stories.tsx b/apps/www/registry/stories/popover.stories.tsx new file mode 100644 index 00000000000..0e4dd69b99d --- /dev/null +++ b/apps/www/registry/stories/popover.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/registry/default/ui/popover" + +/** + * Displays rich content in a portal, triggered by a button. + */ +const meta = { + title: "ui/Popover", + component: Popover, + tags: ["autodocs"], + argTypes: {}, + + render: (args) => ( + + Open + Place content for the popover here. + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the popover. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/progress.stories.tsx b/apps/www/registry/stories/progress.stories.tsx new file mode 100644 index 00000000000..73f793959d9 --- /dev/null +++ b/apps/www/registry/stories/progress.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Progress } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/radio-group.stories.tsx b/apps/www/registry/stories/radio-group.stories.tsx new file mode 100644 index 00000000000..33afca76d47 --- /dev/null +++ b/apps/www/registry/stories/radio-group.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { RadioGroup, RadioGroupItem } from "@/registry/default/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) => ( + + + + + + + + + ), +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the radio group. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/resizable.stories.tsx b/apps/www/registry/stories/resizable.stories.tsx new file mode 100644 index 00000000000..5dd8c48bf39 --- /dev/null +++ b/apps/www/registry/stories/resizable.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/registry/default/ui/resizable" + +/** + * Accessible resizable panel groups and layouts with keyboard support. + */ +const meta: Meta = { + title: "ui/ResizablePanelGroup", + component: ResizablePanelGroup, + tags: ["autodocs"], + argTypes: { + onLayout: { + control: false, + }, + }, + args: { + className: "max-w-96 rounded-lg border", + direction: "horizontal", + }, + render: (args) => ( + + +
+ One +
+
+ + + + +
+ Two +
+
+ + +
+ Three +
+
+
+
+
+ ), +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the resizable panel group. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/scroll-area.stories.tsx b/apps/www/registry/stories/scroll-area.stories.tsx new file mode 100644 index 00000000000..5e177df0d24 --- /dev/null +++ b/apps/www/registry/stories/scroll-area.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { ScrollArea } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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", + }, +} diff --git a/apps/www/registry/stories/select.stories.tsx b/apps/www/registry/stories/select.stories.tsx new file mode 100644 index 00000000000..828ed658e34 --- /dev/null +++ b/apps/www/registry/stories/select.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@/registry/default/ui/select" + +/** + * Displays a list of options for the user to pick from—triggered by a button. + */ +const meta: Meta = { + title: "ui/Select", + component: Select, + tags: ["autodocs"], + argTypes: {}, + render: (args) => ( + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the select. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/separator.stories.tsx b/apps/www/registry/stories/separator.stories.tsx new file mode 100644 index 00000000000..c0cbeec023f --- /dev/null +++ b/apps/www/registry/stories/separator.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Separator } from "@/registry/default/ui/separator" + +/** + * Visually or semantically separates content. + */ +const meta = { + title: "ui/Separator", + component: Separator, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the separator. + */ +export const Horizontal: Story = { + render: () => ( +
+
Left
+ +
Right
+
+ ), +} + +/** + * A vertical separator. + */ +export const Vertical: Story = { + render: () => ( +
+
Top
+ +
Bottom
+
+ ), +} diff --git a/apps/www/registry/stories/sheet.stories.tsx b/apps/www/registry/stories/sheet.stories.tsx new file mode 100644 index 00000000000..2ad721313eb --- /dev/null +++ b/apps/www/registry/stories/sheet.stories.tsx @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/registry/default/ui/sheet" + +/** + * Extends the Dialog component to display content that complements the main + * content of the screen. + */ +const meta: Meta = { + title: "ui/Sheet", + component: Sheet, + tags: ["autodocs"], + argTypes: { + side: { + options: ["top", "bottom", "left", "right"], + control: { + type: "radio", + }, + }, + }, + args: { + side: "right", + }, + render: (args) => ( + + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + + + + + + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the sheet. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/skeleton.stories.tsx b/apps/www/registry/stories/skeleton.stories.tsx new file mode 100644 index 00000000000..3efc7a93fdc --- /dev/null +++ b/apps/www/registry/stories/skeleton.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Skeleton } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * The default form of the skeleton. + */ +export const Default: Story = { + render: (args) => ( +
+ +
+ + +
+
+ ), +} diff --git a/apps/www/registry/stories/slider.stories.tsx b/apps/www/registry/stories/slider.stories.tsx new file mode 100644 index 00000000000..78183a735a5 --- /dev/null +++ b/apps/www/registry/stories/slider.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Slider } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/sonner.stories.tsx b/apps/www/registry/stories/sonner.stories.tsx new file mode 100644 index 00000000000..6bc3c2606f7 --- /dev/null +++ b/apps/www/registry/stories/sonner.stories.tsx @@ -0,0 +1,49 @@ +import { action } from "@storybook/addon-actions" +import type { Meta, StoryObj } from "@storybook/react" +import { toast } from "sonner" + +import { Toaster } from "@/registry/default/ui/sonner" + +/** + * An opinionated toast component for React. + */ +const meta: Meta = { + title: "ui/Sonner", + component: Toaster, + tags: ["autodocs"], + argTypes: {}, + args: { + position: "bottom-right", + }, + parameters: { + layout: "fullscreen", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the toaster. + */ +export const Default: Story = { + render: (args) => ( +
+ + +
+ ), +} diff --git a/apps/www/registry/stories/switch.stories.tsx b/apps/www/registry/stories/switch.stories.tsx new file mode 100644 index 00000000000..00f9642d827 --- /dev/null +++ b/apps/www/registry/stories/switch.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Switch } from "@/registry/default/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) => ( +
+ + +
+ ), +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * 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, + }, +} diff --git a/apps/www/registry/stories/table.stories.tsx b/apps/www/registry/stories/table.stories.tsx new file mode 100644 index 00000000000..7e2d7c5ac75 --- /dev/null +++ b/apps/www/registry/stories/table.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/registry/default/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) => ( + + A list of your recent invoices. + + + Invoice + Status + Method + Amount + + + + {invoices.map((invoice) => ( + + {invoice.invoice} + {invoice.paymentStatus} + {invoice.paymentMethod} + {invoice.totalAmount} + + ))} + +
+ ), +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the table. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/tabs.stories.tsx b/apps/www/registry/stories/tabs.stories.tsx new file mode 100644 index 00000000000..6739dd67bf0 --- /dev/null +++ b/apps/www/registry/stories/tabs.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/registry/default/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) => ( + + + Account + Password + + + Make changes to your account here. + + Change your password here. + + ), + parameters: { + layout: "centered", + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +/** + * The default form of the tabs. + */ +export const Default: Story = {} diff --git a/apps/www/registry/stories/textarea.stories.tsx b/apps/www/registry/stories/textarea.stories.tsx new file mode 100644 index 00000000000..f4839dfbf27 --- /dev/null +++ b/apps/www/registry/stories/textarea.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Textarea } from "@/registry/default/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 + +export default meta + +type Story = StoryObj + +/** + * 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) => ( +
+ +