+10
-26
@@ -7,31 +7,15 @@ import chalk from "chalk"; | ||
| const __dirname = path.dirname(__filename); | ||
| function toPascalCase(str) { | ||
| return str.replace(/(^\w|-\w)/g, (m) => m.replace("-", "").toUpperCase()); | ||
| } | ||
| function toKebabCase(str) { | ||
| return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| } | ||
| const pascalCase = (str) => str.replace(/(^\w|-\w)/g, (match) => match.replace("-", "").toUpperCase()); | ||
| export async function addComponent(name) { | ||
| // Support both kebab-case and PascalCase input | ||
| const kebabName = toKebabCase(name); // e.g. animated-tabs | ||
| const pascalName = toPascalCase(kebabName); // e.g. AnimatedTabs | ||
| const fileName = `${pascalName}.tsx`; // e.g. AnimatedTabs.tsx | ||
| // Look for the component in the CLI's components directory | ||
| const sourceFile = path.resolve(__dirname, "../../components/ui", pascalName, fileName); | ||
| const targetDir = path.resolve(process.cwd(), "components/ui", pascalName); | ||
| const targetFile = path.join(targetDir, fileName); | ||
| const exists = await fs.pathExists(sourceFile); | ||
| if (!exists) { | ||
| console.log(chalk.red(`❌ Component '${name}' not found.\nExpected at: ${sourceFile}\nMake sure the component exists in the CLI package.`)); | ||
| const componentName = pascalCase(name); // animated-tabs -> AnimatedTabs | ||
| const source = path.resolve(__dirname, "../../src/components", componentName); | ||
| const target = path.resolve(process.cwd(), "components/ui", componentName); | ||
| if (!(await fs.pathExists(source))) { | ||
| console.log(chalk.red(`❌ Component "${componentName}" not found.`)); | ||
| return; | ||
| } | ||
| try { | ||
| await fs.ensureDir(targetDir); | ||
| await fs.copyFile(sourceFile, targetFile); | ||
| console.log(chalk.green(`✅ ${fileName} copied to components/ui/${pascalName}/`)); | ||
| console.log(chalk.yellow(`👉 Import using: import { ${pascalName} } from "@/components/ui/${pascalName}/${pascalName}";`)); | ||
| } | ||
| catch (error) { | ||
| console.error(chalk.red("❌ Error copying component:"), error); | ||
| } | ||
| await fs.ensureDir(path.dirname(target)); | ||
| await fs.copy(source, target); | ||
| console.log(chalk.green(`✅ Copied ${componentName} to components/ui/${componentName}`)); | ||
| console.log(chalk.yellow(`👉 Make sure Tailwind is configured if this component uses classes or animations.`)); | ||
| } |
+3
-13
| #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import { addComponent } from "./commands/add.js"; | ||
| import { initProject } from "./commands/init.js"; | ||
| const program = new Command(); | ||
| program.name("zedblock").description("ZedBlock UI CLI").version("0.0.1"); | ||
| program | ||
| .name("zedblock") | ||
| .description("ZedBlock UI CLI - Add beautiful UI components to your project") | ||
| .version("0.0.8"); | ||
| program | ||
| .command("add") | ||
| .description("Add a component to your project") | ||
| .argument("<component>", "Component name (e.g., animated-tabs)") | ||
| .argument("<component>", "Component name") | ||
| .action(async (component) => { | ||
| await addComponent(component); | ||
| }); | ||
| program | ||
| .command("init") | ||
| .description("Initialize ZedBlock UI in your project") | ||
| .action(async () => { | ||
| await initProject(); | ||
| }); | ||
| program.parse(); | ||
| program.parse(process.argv); |
+4
-24
| { | ||
| "name": "zedblock", | ||
| "version": "0.0.9", | ||
| "version": "0.0.11", | ||
| "type": "module", | ||
@@ -9,9 +9,6 @@ "bin": { | ||
| "scripts": { | ||
| "build": "node build.js && tsc", | ||
| "dev": "tsc --watch", | ||
| "prepublishOnly": "npm run build" | ||
| "build": "tsc" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "components" | ||
| "dist" | ||
| ], | ||
@@ -25,21 +22,4 @@ "dependencies": { | ||
| "@types/fs-extra": "^11.0.4", | ||
| "@types/node": "^20.0.0", | ||
| "typescript": "^5.2.2" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "keywords": [ | ||
| "ui", | ||
| "components", | ||
| "react", | ||
| "tailwind", | ||
| "cli" | ||
| ], | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "your-repo-url" | ||
| }, | ||
| "author": "Your Name", | ||
| "license": "MIT" | ||
| } | ||
| } |
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Button } from "."; | ||
| const meta: Meta<typeof Button> = { | ||
| title: "Components/Button", | ||
| component: Button, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| tags: ["autodocs"], | ||
| }; | ||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
| export const Solid: Story = { | ||
| args: { | ||
| variant: "solid", | ||
| children: "Button", | ||
| }, | ||
| }; | ||
| export const Outline: Story = { | ||
| args: { | ||
| variant: "outline", | ||
| children: "Button", | ||
| }, | ||
| }; | ||
| export const Ghost: Story = { | ||
| args: { | ||
| variant: "ghost", | ||
| children: "Button", | ||
| }, | ||
| }; |
| import { cn } from "@/utils"; | ||
| import { cva, VariantProps } from "class-variance-authority"; | ||
| import { ComponentProps, forwardRef } from "react"; | ||
| const buttonStyles = cva( | ||
| [ | ||
| "w-full", | ||
| "rounded-md", | ||
| "font-semibold", | ||
| "focus:outline-none", | ||
| "disabled:cursor-not-allowed", | ||
| ], | ||
| { | ||
| variants: { | ||
| variant: { | ||
| solid: "", | ||
| outline: "border-2", | ||
| ghost: "transition-colors duration-300", | ||
| }, | ||
| size: { | ||
| sm: "px-4 py-2 text-sm", | ||
| md: "px-4 py-2 text-base", | ||
| lg: "px-6 py-3 text-lg", | ||
| }, | ||
| colorscheme: { | ||
| primary: "text-white", | ||
| }, | ||
| }, | ||
| compoundVariants: [ | ||
| { | ||
| variant: "solid", | ||
| colorscheme: "primary", | ||
| className: "bg-primary-500 hover:bg-primary-600", | ||
| }, | ||
| { | ||
| variant: "outline", | ||
| colorscheme: "primary", | ||
| className: | ||
| "text-primary-600 border-primary-500 bg-transparent hover:bg-primary-100", | ||
| }, | ||
| { | ||
| variant: "ghost", | ||
| colorscheme: "primary", | ||
| className: "text-primary-600 bg-transparent hover:bg-primary-100", | ||
| }, | ||
| ], | ||
| defaultVariants: { | ||
| variant: "solid", | ||
| size: "md", | ||
| colorscheme: "primary", | ||
| }, | ||
| } | ||
| ); | ||
| type ButtonProps = ComponentProps<"button"> & VariantProps<typeof buttonStyles>; | ||
| export const Button = forwardRef<HTMLButtonElement, ButtonProps>( | ||
| ({ variant, size, colorscheme, className, ...props }, ref) => { | ||
| return ( | ||
| <button | ||
| ref={ref} | ||
| className={cn(buttonStyles({ variant, size, colorscheme, className }))} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
| ); |
| export { Animatedtabs } from "./ui/Animatedtabs/animatedtabs"; |
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Input } from "."; | ||
| const meta: Meta<typeof Input> = { | ||
| title: "Components/Input", | ||
| component: Input, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| tags: ["autodocs"], | ||
| }; | ||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
| export const Text: Story = { | ||
| args: { | ||
| type: "text", | ||
| placeholder: "Insert text here", | ||
| }, | ||
| }; | ||
| export const Password: Story = { | ||
| args: { | ||
| type: "password", | ||
| placeholder: "Password", | ||
| }, | ||
| }; | ||
| export const Number: Story = { | ||
| args: { | ||
| type: "number", | ||
| placeholder: "Number", | ||
| }, | ||
| }; | ||
| export const Date: Story = { | ||
| args: { | ||
| type: "date", | ||
| placeholder: "Date", | ||
| }, | ||
| }; |
| import { cn } from "@/utils"; | ||
| import { cva, VariantProps } from "class-variance-authority"; | ||
| import { ComponentProps, forwardRef } from "react"; | ||
| const inputStyles = cva([ | ||
| "w-full", | ||
| "border", | ||
| "border-gray-200", | ||
| "p-2", | ||
| "rounded-lg", | ||
| "transition-all", | ||
| "duration-100", | ||
| "outline-none", | ||
| "focus:outline-primary-500 ", | ||
| "focus:border-transparent", | ||
| "placeholder:text-gray-400", | ||
| "placeholder:text-sm", | ||
| ]); | ||
| type InputProps = ComponentProps<"input"> & VariantProps<typeof inputStyles>; | ||
| export const Input = forwardRef<HTMLInputElement, InputProps>( | ||
| ({ className, ...props }, ref) => { | ||
| return ( | ||
| <input | ||
| ref={ref} | ||
| type="text" | ||
| autoComplete="off" | ||
| className={cn(inputStyles({ className }))} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
| ); |
| import { ComponentPropsWithRef, forwardRef } from "react"; | ||
| export type BoxProps = ComponentPropsWithRef<"div">; | ||
| export const Box = forwardRef<HTMLDivElement, BoxProps>(({ ...props }, ref) => { | ||
| return <div ref={ref} {...props} />; | ||
| }); |
| export * from "./Box"; | ||
| export * from "./Stack"; |
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Stack } from "."; | ||
| import { Box } from ".."; | ||
| const meta: Meta<typeof Stack> = { | ||
| title: "Layout/Stack", | ||
| component: Stack, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| }; | ||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
| export const Default: Story = { | ||
| render: (args) => ( | ||
| <Stack className="gap-4 p-4 bg-gray-300" {...args}> | ||
| <Box className="w-[100px] h-[100px] bg-blue-500" /> | ||
| <Box className="w-[100px] h-[100px] bg-red-500" /> | ||
| <Box className="w-[100px] h-[100px] bg-green-500" /> | ||
| </Stack> | ||
| ), | ||
| }; |
| import { Box, BoxProps } from "@/components"; | ||
| import { cn } from "@/utils"; | ||
| type StackProps = BoxProps; | ||
| export const Stack = ({ className, ...props }: StackProps) => { | ||
| return ( | ||
| <Box className={cn("flex flex-col items-start", className)} {...props} /> | ||
| ); | ||
| }; |
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Text } from "."; | ||
| const meta: Meta<typeof Text> = { | ||
| title: "Components/Text", | ||
| component: Text, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| tags: ["autodocs"], | ||
| }; | ||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
| export const Default: Story = { | ||
| args: { | ||
| as: "h1", | ||
| children: | ||
| "be parts correct potatoes sides donkey extra climate happily freedom relationship tape unit tall hung call cat window steady world front graph particular pick", | ||
| }, | ||
| }; |
| import { cn } from "@/utils"; | ||
| import { | ||
| PolymorphicComponentPropsWithRef, | ||
| PolymorphicRef, | ||
| } from "@/utils/types"; | ||
| import { VariantProps, cva } from "class-variance-authority"; | ||
| import { forwardRef } from "react"; | ||
| const textStyles = cva("w-full", { | ||
| variants: { | ||
| emphasis: { | ||
| low: "text-gray-600 font-light", | ||
| }, | ||
| size: { | ||
| sm: "text-sm", | ||
| base: "text-base", | ||
| lg: "text-lg", | ||
| xl: "text-xl", | ||
| "2xl": "text-2xl", | ||
| "3xl": "text-3xl", | ||
| }, | ||
| weight: { | ||
| thin: "font-thin", | ||
| normal: "font-normal", | ||
| medium: "font-medium", | ||
| semibold: "font-semibold", | ||
| bold: "font-bold", | ||
| black: "font-black", | ||
| }, | ||
| align: { | ||
| left: "text-left", | ||
| center: "text-center", | ||
| right: "text-right", | ||
| }, | ||
| italic: { | ||
| true: "italic", | ||
| }, | ||
| underline: { | ||
| true: "underline underline-offset-2", | ||
| }, | ||
| }, | ||
| defaultVariants: { | ||
| size: "base", | ||
| align: "left", | ||
| }, | ||
| }); | ||
| type TextProps<C extends React.ElementType> = PolymorphicComponentPropsWithRef< | ||
| C, | ||
| VariantProps<typeof textStyles> | ||
| >; | ||
| type TextComponent = <C extends React.ElementType = "span">( | ||
| props: TextProps<C> | ||
| ) => React.ReactElement | null; | ||
| // @ts-expect-error - unexpected typing errors | ||
| export const Text: TextComponent = forwardRef( | ||
| <C extends React.ElementType = "span">( | ||
| { | ||
| as, | ||
| align, | ||
| size, | ||
| emphasis, | ||
| italic, | ||
| underline, | ||
| weight, | ||
| className, | ||
| ...props | ||
| }: TextProps<C>, | ||
| ref?: PolymorphicRef<C> | ||
| ) => { | ||
| const Component = as || "span"; | ||
| return ( | ||
| <Component | ||
| ref={ref} | ||
| className={cn( | ||
| textStyles({ | ||
| size, | ||
| weight, | ||
| emphasis, | ||
| italic, | ||
| underline, | ||
| align, | ||
| className, | ||
| }) | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
| ); |
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Animatedtabs, type Tab } from "./animatedtabs"; | ||
| const meta: Meta<typeof Animatedtabs> = { | ||
| title: "Components/AnimatedTabs", | ||
| component: Animatedtabs, | ||
| parameters: { | ||
| layout: "padded", | ||
| docs: { | ||
| description: { | ||
| component: | ||
| "A reusable animated tab component with multiple variants and sizes. Built with Framer Motion for smooth animations.", | ||
| }, | ||
| }, | ||
| }, | ||
| argTypes: { | ||
| variant: { | ||
| control: "select", | ||
| options: ["default", "pills", "underline", "cards"], | ||
| description: "Visual style variant of the tabs", | ||
| }, | ||
| size: { | ||
| control: "select", | ||
| options: ["sm", "md", "lg"], | ||
| description: "Size of the tabs", | ||
| }, | ||
| orientation: { | ||
| control: "select", | ||
| options: ["horizontal", "vertical"], | ||
| description: "Orientation of the tab layout", | ||
| }, | ||
| animated: { | ||
| control: "boolean", | ||
| description: "Enable/disable animations", | ||
| }, | ||
| onTabChange: { | ||
| action: "tab-changed", | ||
| description: "Callback fired when tab changes", | ||
| }, | ||
| }, | ||
| tags: ["autodocs"], | ||
| }; | ||
| export default meta; | ||
| type Story = StoryObj<typeof Animatedtabs>; | ||
| const sampleTabs: Tab[] = [ | ||
| { | ||
| id: "research", | ||
| label: "Research", | ||
| content: ( | ||
| <div className="p-4 rounded-lg bg-muted/50"> | ||
| <h3 className="mb-2 text-lg font-semibold">Research Phase</h3> | ||
| <p className="text-muted-foreground"> | ||
| Gather information, analyze market trends, and understand user needs. | ||
| This phase is crucial for making informed decisions. | ||
| </p> | ||
| </div> | ||
| ), | ||
| }, | ||
| { | ||
| id: "outline", | ||
| label: "Outline", | ||
| content: ( | ||
| <div className="p-4 rounded-lg bg-muted/50"> | ||
| <h3 className="mb-2 text-lg font-semibold">Project Outline</h3> | ||
| <ul className="space-y-1 list-disc list-inside text-muted-foreground"> | ||
| <li>Define project scope and objectives</li> | ||
| <li>Create timeline and milestones</li> | ||
| <li>Identify resources and constraints</li> | ||
| <li>Plan deliverables and success metrics</li> | ||
| </ul> | ||
| </div> | ||
| ), | ||
| }, | ||
| { | ||
| id: "script", | ||
| label: "Script", | ||
| content: ( | ||
| <div className="p-4 rounded-lg bg-muted/50"> | ||
| <h3 className="mb-2 text-lg font-semibold">Script Development</h3> | ||
| <p className="text-muted-foreground"> | ||
| Write detailed scripts, create user flows, and document technical | ||
| specifications. This ensures smooth execution. | ||
| </p> | ||
| </div> | ||
| ), | ||
| }, | ||
| ]; | ||
| const longTabList: Tab[] = [ | ||
| { | ||
| id: "overview", | ||
| label: "Overview", | ||
| content: <div className="p-4">Overview content</div>, | ||
| }, | ||
| { | ||
| id: "features", | ||
| label: "Features", | ||
| content: <div className="p-4">Features content</div>, | ||
| }, | ||
| { | ||
| id: "pricing", | ||
| label: "Pricing", | ||
| content: <div className="p-4">Pricing content</div>, | ||
| }, | ||
| { | ||
| id: "documentation", | ||
| label: "Documentation", | ||
| content: <div className="p-4">Documentation content</div>, | ||
| }, | ||
| { | ||
| id: "support", | ||
| label: "Support", | ||
| content: <div className="p-4">Support content</div>, | ||
| }, | ||
| { | ||
| id: "changelog", | ||
| label: "Changelog", | ||
| content: <div className="p-4">Changelog content</div>, | ||
| }, | ||
| ]; | ||
| const tabsWithDisabled: Tab[] = [ | ||
| { | ||
| id: "active1", | ||
| label: "Active Tab", | ||
| content: <div className="p-4">This tab is active</div>, | ||
| }, | ||
| { | ||
| id: "active2", | ||
| label: "Another Active", | ||
| content: <div className="p-4">This tab is also active</div>, | ||
| }, | ||
| { | ||
| id: "disabled", | ||
| label: "Disabled Tab", | ||
| content: <div className="p-4">This content won't show</div>, | ||
| disabled: true, | ||
| }, | ||
| { | ||
| id: "active3", | ||
| label: "Last Active", | ||
| content: <div className="p-4">This is the last active tab</div>, | ||
| }, | ||
| ]; | ||
| // Default story | ||
| export const Default: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "default", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| // Variant stories | ||
| export const Pills: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "pills", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| export const Underline: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "underline", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| export const Cards: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "cards", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| // Size stories | ||
| export const Small: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "pills", | ||
| size: "sm", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| export const Large: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "pills", | ||
| size: "lg", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| // Orientation stories | ||
| export const Vertical: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "default", | ||
| size: "md", | ||
| orientation: "vertical", | ||
| animated: true, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: | ||
| "Vertical orientation places tabs in a column layout, useful for sidebar navigation.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| export const VerticalPills: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "pills", | ||
| size: "md", | ||
| orientation: "vertical", | ||
| animated: true, | ||
| }, | ||
| }; | ||
| // Special cases | ||
| export const WithoutAnimation: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "default", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: false, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: | ||
| "Tabs without animation for better performance or accessibility needs.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| export const ManyTabs: Story = { | ||
| args: { | ||
| tabs: longTabList, | ||
| defaultTab: "overview", | ||
| variant: "underline", | ||
| size: "sm", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: "Example with many tabs to test overflow behavior.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| export const WithDisabledTabs: Story = { | ||
| args: { | ||
| tabs: tabsWithDisabled, | ||
| defaultTab: "active1", | ||
| variant: "pills", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: "Tabs with some disabled options that cannot be selected.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| export const TabsOnly: Story = { | ||
| args: { | ||
| tabs: [ | ||
| { id: "tab1", label: "Tab 1" }, | ||
| { id: "tab2", label: "Tab 2" }, | ||
| { id: "tab3", label: "Tab 3" }, | ||
| ], | ||
| defaultTab: "tab1", | ||
| variant: "underline", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: | ||
| "Tabs without content panels, useful for navigation-only scenarios.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| // Custom styling story | ||
| export const CustomStyling: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "pills", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| className: "bg-slate-100 p-4 rounded-xl", | ||
| tabClassName: "font-bold", | ||
| contentClassName: "mt-6 p-6 bg-white rounded-lg shadow-sm", | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: "Example with custom styling applied via className props.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| // Interactive playground | ||
| export const Playground: Story = { | ||
| args: { | ||
| tabs: sampleTabs, | ||
| defaultTab: "research", | ||
| variant: "default", | ||
| size: "md", | ||
| orientation: "horizontal", | ||
| animated: true, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: | ||
| "Interactive playground to test different combinations of props.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; |
| "use client"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { motion } from "framer-motion"; | ||
| import * as React from "react"; | ||
| export interface Tab { | ||
| id: string; | ||
| label: string; | ||
| content?: React.ReactNode; | ||
| disabled?: boolean; | ||
| } | ||
| export interface AnimatedTabsProps { | ||
| tabs: Tab[]; | ||
| defaultTab?: string; | ||
| onTabChange?: (tabId: string) => void; | ||
| variant?: "default" | "pills" | "underline" | "cards"; | ||
| size?: "sm" | "md" | "lg"; | ||
| orientation?: "horizontal" | "vertical"; | ||
| className?: string; | ||
| tabClassName?: string; | ||
| contentClassName?: string; | ||
| animated?: boolean; | ||
| } | ||
| const AnimatedTabs = React.forwardRef<HTMLDivElement, AnimatedTabsProps>( | ||
| ( | ||
| { | ||
| tabs, | ||
| defaultTab, | ||
| onTabChange, | ||
| variant = "default", | ||
| size = "md", | ||
| orientation = "horizontal", | ||
| className, | ||
| tabClassName, | ||
| contentClassName, | ||
| animated = true, | ||
| ...props | ||
| }, | ||
| ref | ||
| ) => { | ||
| const [activeTab, setActiveTab] = React.useState(defaultTab || tabs[0]?.id); | ||
| const handleTabChange = (tabId: string) => { | ||
| const tab = tabs.find((t) => t.id === tabId); | ||
| if (tab && !tab.disabled) { | ||
| setActiveTab(tabId); | ||
| onTabChange?.(tabId); | ||
| } | ||
| }; | ||
| const activeTabContent = tabs.find((tab) => tab.id === activeTab)?.content; | ||
| const tabListVariants = { | ||
| default: "border-b border-muted", | ||
| pills: "bg-muted p-1 rounded-lg", | ||
| underline: "border-b border-border", | ||
| cards: "gap-2", | ||
| }; | ||
| const tabVariants = { | ||
| default: { | ||
| base: "relative px-4 py-2 text-sm font-medium transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50", | ||
| active: "text-foreground", | ||
| inactive: "text-muted-foreground", | ||
| }, | ||
| pills: { | ||
| base: "relative px-3 py-1.5 text-sm font-medium transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50 rounded-md", | ||
| active: "text-foreground", | ||
| inactive: "text-muted-foreground", | ||
| }, | ||
| underline: { | ||
| base: "relative px-4 py-2 text-sm font-medium transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50", | ||
| active: "text-foreground", | ||
| inactive: "text-muted-foreground", | ||
| }, | ||
| cards: { | ||
| base: "relative px-4 py-2 text-sm font-medium transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50 rounded-lg border", | ||
| active: "text-foreground bg-background border-border shadow-sm", | ||
| inactive: | ||
| "text-muted-foreground border-transparent hover:border-border", | ||
| }, | ||
| }; | ||
| const sizeVariants = { | ||
| sm: "text-xs px-2 py-1", | ||
| md: "text-sm px-4 py-2", | ||
| lg: "text-base px-6 py-3", | ||
| }; | ||
| const getIndicatorProps = () => { | ||
| switch (variant) { | ||
| case "pills": | ||
| return { | ||
| className: "absolute inset-0 bg-background rounded-md shadow-sm", | ||
| layoutId: "pill-indicator", | ||
| }; | ||
| case "underline": | ||
| return { | ||
| className: "absolute bottom-0 left-0 right-0 h-0.5 bg-primary", | ||
| layoutId: "underline-indicator", | ||
| }; | ||
| default: | ||
| return { | ||
| className: "absolute bottom-0 left-0 right-0 h-0.5 bg-primary", | ||
| layoutId: "default-indicator", | ||
| }; | ||
| } | ||
| }; | ||
| return ( | ||
| <div | ||
| ref={ref} | ||
| className={cn( | ||
| "w-full", | ||
| orientation === "vertical" && "flex gap-4", | ||
| className | ||
| )} | ||
| {...props} | ||
| > | ||
| <div | ||
| className={cn( | ||
| "flex", | ||
| orientation === "horizontal" ? "flex-row" : "flex-col", | ||
| orientation === "vertical" && "min-w-[200px]", | ||
| tabListVariants[variant] | ||
| )} | ||
| role="tablist" | ||
| > | ||
| {tabs.map((tab) => ( | ||
| <button | ||
| key={tab.id} | ||
| role="tab" | ||
| aria-selected={activeTab === tab.id} | ||
| aria-controls={`panel-${tab.id}`} | ||
| disabled={tab.disabled} | ||
| className={cn( | ||
| tabVariants[variant].base, | ||
| sizeVariants[size], | ||
| activeTab === tab.id | ||
| ? tabVariants[variant].active | ||
| : tabVariants[variant].inactive, | ||
| tabClassName | ||
| )} | ||
| onClick={() => handleTabChange(tab.id)} | ||
| > | ||
| {animated && activeTab === tab.id && variant !== "cards" && ( | ||
| <motion.div | ||
| {...getIndicatorProps()} | ||
| transition={{ type: "spring", bounce: 0.2, duration: 0.6 }} | ||
| /> | ||
| )} | ||
| <span className="relative z-10">{tab.label}</span> | ||
| </button> | ||
| ))} | ||
| </div> | ||
| {activeTabContent && ( | ||
| <div | ||
| className={cn( | ||
| "mt-4", | ||
| orientation === "vertical" && "mt-0 flex-1", | ||
| contentClassName | ||
| )} | ||
| > | ||
| {animated ? ( | ||
| <motion.div | ||
| key={activeTab} | ||
| initial={{ opacity: 0, y: 10 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| exit={{ opacity: 0, y: -10 }} | ||
| transition={{ duration: 0.2 }} | ||
| role="tabpanel" | ||
| id={`panel-${activeTab}`} | ||
| > | ||
| {activeTabContent} | ||
| </motion.div> | ||
| ) : ( | ||
| <div role="tabpanel" id={`panel-${activeTab}`}> | ||
| {activeTabContent} | ||
| </div> | ||
| )} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| ); | ||
| AnimatedTabs.displayName = "AnimatedTabs"; | ||
| export { AnimatedTabs }; |
| import * as React from "react" | ||
| import { Slot } from "@radix-ui/react-slot" | ||
| import { cva, type VariantProps } from "class-variance-authority" | ||
| import { cn } from "@/lib/utils" | ||
| const buttonVariants = cva( | ||
| "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: | ||
| "bg-primary text-primary-foreground shadow hover:bg-primary/90", | ||
| destructive: | ||
| "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", | ||
| outline: | ||
| "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", | ||
| secondary: | ||
| "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", | ||
| ghost: "hover:bg-accent hover:text-accent-foreground", | ||
| link: "text-primary underline-offset-4 hover:underline", | ||
| }, | ||
| size: { | ||
| default: "h-9 px-4 py-2", | ||
| sm: "h-8 rounded-md px-3 text-xs", | ||
| lg: "h-10 rounded-md px-8", | ||
| icon: "h-9 w-9", | ||
| }, | ||
| }, | ||
| defaultVariants: { | ||
| variant: "default", | ||
| size: "default", | ||
| }, | ||
| } | ||
| ) | ||
| export interface ButtonProps | ||
| extends React.ButtonHTMLAttributes<HTMLButtonElement>, | ||
| VariantProps<typeof buttonVariants> { | ||
| asChild?: boolean | ||
| } | ||
| const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( | ||
| ({ className, variant, size, asChild = false, ...props }, ref) => { | ||
| const Comp = asChild ? Slot : "button" | ||
| return ( | ||
| <Comp | ||
| className={cn(buttonVariants({ variant, size, className }))} | ||
| ref={ref} | ||
| {...props} | ||
| /> | ||
| ) | ||
| } | ||
| ) | ||
| Button.displayName = "Button" | ||
| export { Button, buttonVariants } |
| export declare function addComponent(name: string): Promise<void>; |
| export declare function initProject(): Promise<void>; |
| #!/usr/bin/env node | ||
| export {}; |
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
No License Found
LicenseLicense information could not be found.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
2
-33.33%2301
-91.37%4
-80.95%1
Infinity%46
-95.22%2
100%1
Infinity%