├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .storybook ├── index.css ├── main.ts └── preview.ts ├── .yarn └── releases │ └── yarn-4.7.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── eslint.config.js ├── package.json ├── src ├── avatar │ ├── avatar.stories.tsx │ ├── avatar.tsx │ └── index.ts ├── badge │ ├── badge.stories.tsx │ ├── badge.tsx │ └── index.ts ├── button │ ├── button.stories.tsx │ ├── button.tsx │ └── index.ts ├── callout │ ├── callout.stories.tsx │ ├── callout.tsx │ └── index.ts ├── card │ ├── card.stories.tsx │ ├── card.tsx │ └── index.ts ├── checkbox │ ├── checkbox.stories.tsx │ ├── checkbox.tsx │ └── index.ts ├── collapse │ ├── collapse.stories.tsx │ ├── collapse.tsx │ └── index.ts ├── dialog │ ├── dialog.stories.tsx │ ├── dialog.tsx │ └── index.ts ├── divider │ ├── divider.stories.tsx │ ├── divider.tsx │ └── index.ts ├── dropdown │ ├── dropdown.stories.tsx │ ├── dropdown.tsx │ └── index.ts ├── index.ts ├── input │ ├── index.ts │ ├── input.stories.tsx │ └── input.tsx ├── mask │ ├── index.ts │ ├── mask.stories.tsx │ └── mask.tsx ├── radio │ ├── index.ts │ ├── radio.stories.tsx │ └── radio.tsx ├── select │ ├── index.ts │ ├── select.stories.tsx │ └── select.tsx ├── spinner │ ├── index.ts │ ├── spinner.stories.tsx │ └── spinner.tsx ├── tabs │ ├── index.ts │ ├── tabs.stories.tsx │ └── tabs.tsx ├── textarea │ ├── index.ts │ ├── textarea.stories.tsx │ └── textarea.tsx ├── toast │ ├── index.ts │ ├── toast.stories.tsx │ └── toast.tsx ├── tooltip │ ├── index.ts │ ├── tooltip.stories.tsx │ └── tooltip.tsx ├── vite-env.d.ts └── welcome.mdx ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Yarn 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/plugins 5 | !.yarn/releases 6 | !.yarn/sdks 7 | !.yarn/versions 8 | 9 | # Swap the comments on the following lines if you don't wish to use zero-installs 10 | # Documentation here: https://yarnpkg.com/features/zero-installs 11 | # !.yarn/cache 12 | #.pnp.* 13 | 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | pnpm-debug.log* 21 | lerna-debug.log* 22 | 23 | node_modules 24 | dist 25 | dist-ssr 26 | types 27 | *.local 28 | storybook-static 29 | 30 | # Editor directories and files 31 | .vscode/* 32 | !.vscode/extensions.json 33 | .idea 34 | .DS_Store 35 | *.suo 36 | *.ntvs* 37 | *.njsproj 38 | *.sln 39 | *.sw? -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-docs', 7 | '@storybook/addon-onboarding', 8 | '@storybook/addon-links', 9 | '@storybook/addon-essentials', 10 | '@chromatic-com/storybook', 11 | '@storybook/addon-interactions', 12 | ], 13 | framework: { 14 | name: '@storybook/react-vite', 15 | options: {}, 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | import './index.css'; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i, 11 | }, 12 | }, 13 | }, 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.7.0.cjs 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - 2025 mints-components 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mints UI 2 | 3 | A minimalist React component library based on **Tailwind CSS**. 4 | 5 | ## ✨ Features 6 | 7 | - 🌿 **Minimal & Clean** – No extra dependencies beyond Tailwind. 8 | - ⚡ **SSR Compatible** – Works seamlessly with Next.js and other SSR frameworks. 9 | - 🎨 **Fully Stylable** – Designed to fit into your Tailwind theme easily. 10 | 11 | ## 📦 Installation 12 | 13 | Using **npm**: 14 | 15 | ```bash 16 | npm install @mints/ui 17 | ``` 18 | 19 | Or with **yarn**: 20 | 21 | ```bash 22 | yarn add @mints/ui 23 | ``` 24 | 25 | Or with **pnpm**: 26 | 27 | ```bash 28 | pnpm add @mints/ui 29 | ``` 30 | 31 | ## 🚀 Quick Start 32 | 33 | Make sure Tailwind CSS is properly configured in your project. 34 | 35 | ```tsx 36 | import { Button } from '@mints/ui'; 37 | 38 | export default function App() { 39 | return ; 40 | } 41 | ``` 42 | 43 | --- 44 | 45 | ## 📚 Documentation 46 | 47 | 👉 **[View full Storybook](https://mints-components.github.io/ui/)** 48 | 49 | ## 📄 License 50 | 51 | MIT 52 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import mintsConfig from '@mints/eslint-config'; 2 | import storybook from 'eslint-plugin-storybook'; 3 | 4 | export default [...mintsConfig, ...storybook.configs['flat/recommended']]; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mints/ui", 3 | "packageManager": "yarn@4.7.0", 4 | "version": "1.0.0-alpha.8", 5 | "type": "module", 6 | "files": [ 7 | "dist", 8 | "types" 9 | ], 10 | "types": "types", 11 | "main": "./dist/mints-ui.umd.cjs", 12 | "module": "./dist/mints-ui.js", 13 | "exports": { 14 | ".": { 15 | "import": { 16 | "types": "./types/index.d.ts", 17 | "default": "./dist/mints-ui.js" 18 | }, 19 | "require": { 20 | "types": "./types/index.d.ts", 21 | "default": "./dist/mints-ui.umd.cjs" 22 | } 23 | }, 24 | "./style.css": "./dist/mints-ui.min.css" 25 | }, 26 | "scripts": { 27 | "clean": "rm -rf dist && rm -rf types", 28 | "build": "yarn clean && tsc -p tsconfig.build.json && vite build", 29 | "lint": "eslint . --fix", 30 | "prettier": "prettier . --write", 31 | "storybook": "storybook dev -p 6006", 32 | "build-storybook": "storybook build", 33 | "deploy-storybook": "rm -rf storybook-static && stortstorybook build && gh-pages -d storybook-static", 34 | "prepublish": "yarn build", 35 | "publish": "yarn npm publish --access public", 36 | "postinstall": "husky install", 37 | "prepack": "pinst --disable", 38 | "postpack": "pinst --enable" 39 | }, 40 | "lint-staged": { 41 | "*.{js,jsx,ts,tsx}": [ 42 | "prettier --write", 43 | "eslint --fix" 44 | ], 45 | "*.{html,json,md}": [ 46 | "prettier --write" 47 | ] 48 | }, 49 | "dependencies": { 50 | "clsx": "^2.1.1", 51 | "framer-motion": "^12.15.0", 52 | "tailwindcss": "^4.1.3" 53 | }, 54 | "devDependencies": { 55 | "@chromatic-com/storybook": "^3.2.5", 56 | "@mints/eslint-config": "^2.0.0", 57 | "@storybook/addon-docs": "^8.6.4", 58 | "@storybook/addon-essentials": "^8.6.4", 59 | "@storybook/addon-interactions": "^8.6.4", 60 | "@storybook/addon-links": "^8.6.4", 61 | "@storybook/addon-onboarding": "^8.6.4", 62 | "@storybook/blocks": "^8.6.4", 63 | "@storybook/react": "^8.6.4", 64 | "@storybook/react-vite": "^8.6.4", 65 | "@storybook/test": "^8.6.4", 66 | "@tailwindcss/cli": "^4.1.4", 67 | "@tailwindcss/vite": "^4.1.3", 68 | "@types/gh-pages": "^6", 69 | "@types/node": "^20.4.6", 70 | "@types/react": "^18.2.15", 71 | "@types/react-dom": "^18.2.7", 72 | "@vitejs/plugin-react": "^4.0.3", 73 | "eslint": "^9.22.0", 74 | "eslint-plugin-storybook": "^0.11.4", 75 | "gh-pages": "^6.3.0", 76 | "husky": "^8.0.0", 77 | "lint-staged": "^14.0.1", 78 | "pinst": "^3.0.0", 79 | "prettier": "^3.3.2", 80 | "react": "^19.0.0", 81 | "react-icons": "^5.5.0", 82 | "storybook": "^8.6.4", 83 | "typescript": "^5.0.2", 84 | "vite": "^4.4.5" 85 | }, 86 | "peerDependencies": { 87 | "react": "^19.0.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/avatar/avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Avatar } from './avatar'; 4 | 5 | const meta = { 6 | title: 'Components/Avatar', 7 | component: Avatar, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | docs: { 12 | description: { 13 | component: 14 | 'Avatar is a simple component for displaying user profile pictures or initials. It supports different sizes (`sm`, `md`, `lg`) and allows optional rounding (`rounded=true` by default). If no image is provided, it displays the first letter of the `name` as fallback.', 15 | }, 16 | }, 17 | }, 18 | args: { 19 | name: 'Mints', 20 | size: 'md', 21 | rounded: true, 22 | }, 23 | argTypes: { 24 | rounded: { 25 | control: 'boolean', 26 | }, 27 | }, 28 | } satisfies Meta; 29 | 30 | export default meta; 31 | 32 | type Story = StoryObj; 33 | 34 | export const Default: Story = { 35 | args: {}, 36 | }; 37 | 38 | export const Size: Story = { 39 | render: (args) => ( 40 |
41 | 42 | 43 | 44 |
45 | ), 46 | }; 47 | 48 | export const Rounded: Story = { 49 | render: (args) => ( 50 |
51 | 52 | 53 |
54 | ), 55 | }; 56 | -------------------------------------------------------------------------------- /src/avatar/avatar.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export interface AvatarProps extends React.HTMLAttributes { 4 | src?: string; 5 | alt?: string; 6 | name?: string; 7 | size?: 'sm' | 'md' | 'lg'; 8 | rounded?: boolean; 9 | } 10 | 11 | const sizeMap: Record<'sm' | 'md' | 'lg', { box: string; text: string }> = { 12 | sm: { box: 'w-8 h-8', text: 'text-sm' }, 13 | md: { box: 'w-10 h-10', text: 'text-base' }, 14 | lg: { box: 'w-16 h-16', text: 'text-xl' }, 15 | }; 16 | 17 | export function Avatar({ 18 | src, 19 | alt, 20 | name, 21 | size = 'md', 22 | rounded = true, 23 | className, 24 | ...props 25 | }: AvatarProps) { 26 | const fallback = name?.charAt(0).toUpperCase() ?? '?'; 27 | const isImage = Boolean(src); 28 | const { box, text } = sizeMap[size] ?? sizeMap.md; 29 | 30 | return ( 31 |
41 | {isImage ? ( 42 | {alt 47 | ) : ( 48 | fallback 49 | )} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar'; 2 | -------------------------------------------------------------------------------- /src/badge/badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { AiFillNotification } from 'react-icons/ai'; 3 | 4 | import { Badge } from './badge'; 5 | 6 | const meta: Meta = { 7 | title: 'Components/Badge', 8 | component: Badge, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | docs: { 13 | description: { 14 | component: 15 | 'A Badge component for status and labeling, supporting solid, outline, and soft variants, multiple colors, sizes, and optional icons.', 16 | }, 17 | }, 18 | }, 19 | args: { 20 | size: 'default', 21 | variant: 'solid', 22 | color: 'default', 23 | }, 24 | argTypes: { 25 | variant: { 26 | control: 'radio', 27 | options: ['solid', 'outline', 'soft'], 28 | }, 29 | color: { 30 | control: 'radio', 31 | options: ['default', 'success', 'warning', 'danger'], 32 | }, 33 | size: { 34 | control: 'radio', 35 | options: ['sm', 'default', 'lg'], 36 | }, 37 | icon: { control: false }, 38 | }, 39 | }; 40 | 41 | export default meta; 42 | type Story = StoryObj; 43 | 44 | export const Default: Story = { 45 | args: { 46 | children: 'Badge', 47 | }, 48 | }; 49 | 50 | export const Variants: Story = { 51 | render: (args) => ( 52 |
53 | 54 | Solid 55 | 56 | 57 | Outline 58 | 59 | 60 | Soft 61 | 62 |
63 | ), 64 | }; 65 | 66 | export const Colors: Story = { 67 | render: (args) => ( 68 |
69 | 70 | Default 71 | 72 | 73 | Success 74 | 75 | 76 | Warning 77 | 78 | 79 | Danger 80 | 81 |
82 | ), 83 | }; 84 | 85 | export const Sizes: Story = { 86 | render: (args) => ( 87 |
88 | 89 | Small 90 | 91 | 92 | Default 93 | 94 | 95 | Large 96 | 97 |
98 | ), 99 | }; 100 | 101 | export const WithIcon: Story = { 102 | render: (args) => ( 103 |
104 | } /> 105 | }> 106 | With Text 107 | 108 | } 113 | > 114 | Success 115 | 116 | } 121 | > 122 | Danger 123 | 124 |
125 | ), 126 | }; 127 | -------------------------------------------------------------------------------- /src/badge/badge.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | export type BadgeVariant = 'solid' | 'outline' | 'soft'; 5 | export type BadgeColor = 'default' | 'success' | 'warning' | 'danger'; 6 | export type BadgeSize = 'sm' | 'default' | 'lg'; 7 | 8 | export interface BadgeProps extends React.HTMLAttributes { 9 | variant?: BadgeVariant; 10 | color?: BadgeColor; 11 | size?: BadgeSize; 12 | icon?: React.ReactNode; 13 | } 14 | 15 | const colorMap = { 16 | default: { 17 | solid: 'bg-zinc-900 text-white', 18 | outline: 'border border-zinc-900 text-zinc-900 bg-transparent', 19 | soft: 'bg-zinc-100 text-zinc-900', 20 | }, 21 | success: { 22 | solid: 'bg-green-600 text-white', 23 | outline: 'border border-green-600 text-green-600 bg-transparent', 24 | soft: 'bg-green-100 text-green-800', 25 | }, 26 | warning: { 27 | solid: 'bg-yellow-500 text-white', 28 | outline: 'border border-yellow-500 text-yellow-700 bg-transparent', 29 | soft: 'bg-yellow-100 text-yellow-800', 30 | }, 31 | danger: { 32 | solid: 'bg-red-600 text-white', 33 | outline: 'border border-red-600 text-red-600 bg-transparent', 34 | soft: 'bg-red-100 text-red-800', 35 | }, 36 | }; 37 | 38 | const sizeMap = { 39 | sm: 'text-xs px-2 py-0.5 rounded', 40 | default: 'text-sm px-2.5 py-1 rounded-md', 41 | lg: 'text-base px-3 py-1.5 rounded-lg', 42 | }; 43 | 44 | export function Badge({ 45 | variant = 'solid', 46 | color = 'default', 47 | size = 'default', 48 | icon, 49 | className, 50 | children, 51 | ...props 52 | }: BadgeProps) { 53 | return ( 54 | 63 | {icon && {icon}} 64 | {children} 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/badge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './badge'; 2 | -------------------------------------------------------------------------------- /src/button/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { AiFillNotification } from 'react-icons/ai'; 3 | 4 | import { Button } from './button'; 5 | 6 | const meta: Meta = { 7 | title: 'Components/Button', 8 | component: Button, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | docs: { 13 | description: { 14 | component: 15 | 'A general-purpose Button component supporting primary, outline, and link variants, along with icon, size, and disabled options.', 16 | }, 17 | }, 18 | }, 19 | args: { 20 | size: 'default', 21 | disabled: false, 22 | }, 23 | argTypes: { 24 | disabled: { control: 'boolean' }, 25 | loading: { control: 'boolean' }, 26 | variant: { 27 | control: 'radio', 28 | options: ['primary', 'outline', 'link'], 29 | }, 30 | size: { 31 | control: 'radio', 32 | options: ['sm', 'default', 'lg'], 33 | }, 34 | onClick: { action: 'clicked' }, 35 | }, 36 | }; 37 | 38 | export default meta; 39 | type Story = StoryObj; 40 | 41 | export const Default: Story = { 42 | args: { 43 | children: 'Button', 44 | variant: 'primary', 45 | }, 46 | }; 47 | 48 | export const Variants: Story = { 49 | render: (args) => ( 50 |
51 | 52 | 55 | 58 |
59 | ), 60 | }; 61 | 62 | export const Sizes: Story = { 63 | render: (args) => ( 64 |
65 | 68 | 71 | 74 |
75 | ), 76 | }; 77 | 78 | export const DisabledStates: Story = { 79 | args: { disabled: true }, 80 | render: (args) => ( 81 |
82 | 83 | 86 | 89 |
90 | ), 91 | }; 92 | 93 | export const WithIcon: Story = { 94 | render: (args) => ( 95 |
96 | 102 | 105 | 108 |
109 | ), 110 | }; 111 | 112 | export const LoadingStates: Story = { 113 | args: { loading: true }, 114 | render: (args) => ( 115 |
116 |
117 | 120 | 123 | 126 |
127 | 128 |
129 | 132 | 135 | 138 |
139 | 140 |
141 |
144 | 145 |
146 | 149 | 152 |
153 | 154 |
155 | 158 |
159 |
160 | ), 161 | }; 162 | -------------------------------------------------------------------------------- /src/button/button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | import { Spinner } from '../spinner'; 5 | 6 | export type ButtonVariant = 'primary' | 'outline' | 'link'; 7 | export type ButtonSize = 'sm' | 'default' | 'lg'; 8 | 9 | export interface ButtonProps 10 | extends React.ButtonHTMLAttributes { 11 | variant?: ButtonVariant; 12 | size?: ButtonSize; 13 | icon?: React.ReactNode; 14 | loading?: boolean; 15 | } 16 | 17 | export function Button({ 18 | variant = 'primary', 19 | size = 'default', 20 | disabled, 21 | icon, 22 | loading = false, 23 | className, 24 | children, 25 | ...props 26 | }: ButtonProps) { 27 | const base = 28 | 'inline-flex items-center justify-center font-medium rounded-md transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'; 29 | 30 | const variantClass = { 31 | primary: clsx( 32 | 'text-white bg-zinc-900 hover:bg-zinc-600', 33 | 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-zinc-900', 34 | disabled && 'bg-zinc-400 text-white hover:bg-zinc-400', 35 | ), 36 | outline: clsx( 37 | 'border border-zinc-900 text-zinc-900 hover:bg-zinc-900 hover:text-white', 38 | 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-zinc-900', 39 | disabled && 40 | 'border-zinc-300 text-zinc-300 hover:bg-transparent hover:text-zinc-300', 41 | ), 42 | link: clsx( 43 | 'text-zinc-900 dark:text-zinc-100 underline-offset-4 hover:underline hover:text-zinc-600 dark:hover:text-zinc-300', 44 | disabled && 45 | 'text-zinc-400 hover:text-zinc-400 dark:text-zinc-500 dark:hover:text-zinc-500', 46 | ), 47 | }[variant]; 48 | 49 | const isIconOnly = !children && (icon || loading); 50 | 51 | const sizeClass = isIconOnly 52 | ? { 53 | sm: 'w-8 h-8 text-sm', 54 | default: 'w-10 h-10 text-base', 55 | lg: 'w-12 h-12 text-lg', 56 | }[size] 57 | : { 58 | sm: 'text-sm px-3 py-1.5', 59 | default: 'text-base px-4 py-2', 60 | lg: 'text-lg px-5 py-2.5', 61 | }[size]; 62 | 63 | const iconSizeClass = { sm: 'w-4 h-4', default: 'w-5 h-5', lg: 'w-6 h-6' }[ 64 | size 65 | ]; 66 | 67 | return ( 68 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | -------------------------------------------------------------------------------- /src/callout/callout.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | import { AiOutlineInfoCircle } from 'react-icons/ai'; 4 | 5 | import { Button } from '../button'; 6 | 7 | import { Callout } from './callout'; 8 | 9 | const meta: Meta = { 10 | title: 'Components/Callout', 11 | component: Callout, 12 | tags: ['autodocs'], 13 | argTypes: { 14 | variant: { 15 | control: 'radio', 16 | options: ['primary', 'outline', 'danger', 'warning', 'success'], 17 | }, 18 | size: { 19 | control: 'radio', 20 | options: ['sm', 'default', 'lg'], 21 | }, 22 | closable: { control: 'boolean' }, 23 | onClose: { action: 'closed' }, 24 | }, 25 | args: { 26 | variant: 'primary', 27 | size: 'default', 28 | closable: false, 29 | }, 30 | }; 31 | 32 | export default meta; 33 | type Story = StoryObj; 34 | 35 | export const Default: Story = { 36 | render: (args) => ( 37 | } 40 | > 41 | This is a basic callout. 42 | 43 | ), 44 | }; 45 | 46 | export const Variants: Story = { 47 | render: (args) => ( 48 |
49 | ℹ️}> 50 | Primary variant 51 | 52 | 📦}> 53 | Outline variant 54 | 55 | ❌}> 56 | Danger variant 57 | 58 | ⚠️}> 59 | Warning variant 60 | 61 | ✅}> 62 | Success variant 63 | 64 |
65 | ), 66 | }; 67 | 68 | export const Sizes: Story = { 69 | render: (args) => ( 70 |
71 | 🔹}> 72 | Small size 73 | 74 | 🔸}> 75 | Default size 76 | 77 | 🔷}> 78 | Large size 79 | 80 |
81 | ), 82 | }; 83 | 84 | export const Closable: Story = { 85 | render: (args) => { 86 | const [open, setOpen] = useState(true); 87 | return open ? ( 88 | setOpen(false)} 92 | icon={} 93 | > 94 | This callout can be closed by clicking ×. 95 | 96 | ) : ( 97 | 100 | ); 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /src/callout/callout.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | export type CalloutVariant = 5 | | 'primary' 6 | | 'outline' 7 | | 'danger' 8 | | 'warning' 9 | | 'success'; 10 | export type CalloutSize = 'sm' | 'default' | 'lg'; 11 | 12 | export interface CalloutProps extends React.HTMLAttributes { 13 | variant?: CalloutVariant; 14 | size?: CalloutSize; 15 | closable?: boolean; 16 | onClose?: () => void; 17 | icon?: React.ReactNode; 18 | children: React.ReactNode; 19 | } 20 | 21 | export function Callout({ 22 | variant = 'primary', 23 | size = 'default', 24 | closable = false, 25 | onClose, 26 | icon, 27 | className, 28 | children, 29 | ...props 30 | }: CalloutProps) { 31 | const base = 32 | 'flex justify-between rounded-lg border shadow-sm transition-colors'; 33 | 34 | const variantClass = { 35 | primary: 'bg-zinc-100 text-zinc-900 border-transparent', 36 | outline: 'bg-white text-zinc-900 border border-zinc-300', 37 | danger: 'bg-red-50 text-red-900 border border-red-300', 38 | warning: 'bg-yellow-50 text-yellow-900 border border-yellow-300', 39 | success: 'bg-green-50 text-green-900 border border-green-300', 40 | }[variant]; 41 | 42 | const sizeClass = { 43 | sm: 'text-sm px-3 py-2', 44 | default: 'text-base px-4 py-3', 45 | lg: 'text-lg px-5 py-4', 46 | }[size]; 47 | 48 | return ( 49 |
50 |
51 | {icon && {icon}} 52 | {children} 53 |
54 | {closable && ( 55 | 63 | )} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/callout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './callout'; 2 | -------------------------------------------------------------------------------- /src/card/card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Card } from './card'; 4 | 5 | const meta: Meta = { 6 | title: 'Components/Card', 7 | component: Card, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | docs: { 12 | description: { 13 | component: 14 | 'A Card component that supports different sizes, optional title with divider, and a minimal mode to simplify the layout.', 15 | }, 16 | }, 17 | }, 18 | args: { 19 | size: 'default', 20 | minimal: false, 21 | }, 22 | argTypes: { 23 | size: { 24 | control: 'radio', 25 | options: ['sm', 'default', 'lg'], 26 | }, 27 | minimal: { control: 'boolean' }, 28 | title: { control: 'text' }, 29 | }, 30 | }; 31 | 32 | export default meta; 33 | type Story = StoryObj; 34 | 35 | export const Default: Story = { 36 | args: { 37 | title: 'Card Title', 38 | children:

This is a basic card content.

, 39 | }, 40 | }; 41 | 42 | export const Sizes: Story = { 43 | render: (args) => ( 44 |
45 | 46 |

Content for small card.

47 |
48 | 49 |

Content for default card.

50 |
51 | 52 |

Content for large card.

53 |
54 |
55 | ), 56 | }; 57 | 58 | export const Minimal: Story = { 59 | args: { 60 | title: 'Minimal Card', 61 | minimal: true, 62 | children: ( 63 |

64 | This card does not have a divider between the title and the content. 65 |

66 | ), 67 | }, 68 | }; 69 | 70 | export const WithoutTitle: Story = { 71 | args: { 72 | children:

This card has no title, only content.

, 73 | }, 74 | }; 75 | 76 | export const CustomContent: Story = { 77 | render: (args) => ( 78 | 79 |
    80 |
  • Feature one
  • 81 |
  • Feature two
  • 82 |
  • Feature three
  • 83 |
84 |
85 | ), 86 | }; 87 | -------------------------------------------------------------------------------- /src/card/card.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | export type CardSize = 'sm' | 'default' | 'lg'; 5 | 6 | export interface CardProps 7 | extends Omit, 'title'> { 8 | size?: CardSize; 9 | title?: React.ReactNode; 10 | minimal?: boolean; 11 | children: React.ReactNode; 12 | } 13 | 14 | export function Card({ 15 | size = 'default', 16 | title, 17 | minimal = false, 18 | className, 19 | children, 20 | ...props 21 | }: CardProps) { 22 | const base = 23 | 'rounded-lg border border-zinc-200 shadow-sm bg-white dark:bg-zinc-900 dark:border-zinc-800 transition-shadow hover:shadow-md'; 24 | 25 | const sizeClass = { 26 | sm: 'p-4 text-sm', 27 | default: 'p-6 text-base', 28 | lg: 'p-6 text-base', 29 | }[size]; 30 | 31 | const titleSizeClass = { 32 | sm: 'text-sm', 33 | default: 'text-base', 34 | lg: 'text-lg', 35 | }[size]; 36 | 37 | const titleClass = clsx( 38 | 'font-semibold text-zinc-900 dark:text-zinc-100 mb-3 leading-snug tracking-tight', 39 | titleSizeClass, 40 | ); 41 | 42 | const dividerClass = 'border-t border-zinc-200 dark:border-zinc-800 my-3'; 43 | 44 | return ( 45 |
46 | {title && ( 47 | <> 48 |
{title}
49 | {!minimal &&
} 50 | 51 | )} 52 |
{children}
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card'; 2 | -------------------------------------------------------------------------------- /src/checkbox/checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Checkbox } from './checkbox'; 4 | 5 | const meta = { 6 | title: 'Components/Checkbox', 7 | component: Checkbox, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | docs: { 12 | description: { 13 | component: 14 | "A versatile Checkbox component that supports standard checked and indeterminate states. It is styled to match the Button component's design, with unified focus ring, hover, and disabled styles. Supports labels, accessibility attributes, and server-side rendering (SSR) without relying on client-side effects.", 15 | }, 16 | }, 17 | }, 18 | args: { 19 | label: 'Label', 20 | checked: false, 21 | disabled: false, 22 | indeterminate: false, 23 | onChange: (e) => console.log(e.target.checked), 24 | }, 25 | argTypes: { 26 | checked: { 27 | control: 'boolean', 28 | }, 29 | disabled: { 30 | control: 'boolean', 31 | }, 32 | indeterminate: { 33 | control: 'boolean', 34 | }, 35 | }, 36 | } satisfies Meta; 37 | 38 | export default meta; 39 | type Story = StoryObj; 40 | 41 | export const Default: Story = { 42 | args: {}, 43 | }; 44 | 45 | export const Checked: Story = { 46 | args: { 47 | checked: true, 48 | }, 49 | }; 50 | 51 | export const Indeterminate: Story = { 52 | args: { 53 | indeterminate: true, 54 | }, 55 | }; 56 | 57 | export const Disabled: Story = { 58 | render: (args) => ( 59 |
60 | 61 | 62 | 63 |
64 | ), 65 | }; 66 | -------------------------------------------------------------------------------- /src/checkbox/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export interface CheckboxProps 4 | extends React.InputHTMLAttributes { 5 | label?: string; 6 | indeterminate?: boolean; 7 | } 8 | 9 | export function Checkbox({ 10 | label, 11 | indeterminate = false, 12 | className, 13 | disabled, 14 | ...props 15 | }: CheckboxProps) { 16 | const inputRef = (el: HTMLInputElement | null) => { 17 | if (el) { 18 | el.indeterminate = !!indeterminate && !props.checked; 19 | } 20 | }; 21 | 22 | return ( 23 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkbox'; 2 | -------------------------------------------------------------------------------- /src/collapse/collapse.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import { Collapse } from './collapse'; 5 | 6 | const meta: Meta = { 7 | title: 'Components/Collapse', 8 | component: Collapse, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | docs: { 13 | description: { 14 | component: 15 | 'A general-purpose Collapse component supporting controlled open state, custom header, and disabled options.', 16 | }, 17 | }, 18 | }, 19 | args: { 20 | disabled: false, 21 | header: 'Collapse Header', 22 | }, 23 | argTypes: { 24 | disabled: { control: 'boolean' }, 25 | open: { control: 'boolean' }, 26 | header: { control: 'text' }, 27 | onOpenChange: { action: 'onOpenChange' }, 28 | }, 29 | }; 30 | 31 | export default meta; 32 | type Story = StoryObj; 33 | 34 | export const Default: Story = { 35 | args: { 36 | open: false, 37 | header: 'Click to expand', 38 | children: 'This is the collapsible content.', 39 | }, 40 | render: (args) => { 41 | const [open, setOpen] = useState(args.open); 42 | return ; 43 | }, 44 | }; 45 | 46 | export const Disabled: Story = { 47 | args: { 48 | open: false, 49 | header: 'Disabled state', 50 | disabled: true, 51 | children: 'This collapse is disabled.', 52 | }, 53 | render: (args) => , 54 | }; 55 | 56 | export const Accordion: Story = { 57 | render: (args) => { 58 | const [active, setActive] = useState(null); 59 | return ( 60 |
61 | {[1, 2, 3].map((i) => ( 62 | setActive(active === i ? null : i)} 68 | > 69 | {`This is the content of panel ${i}.`} 70 | 71 | ))} 72 |
73 | ); 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/collapse/collapse.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | export interface CollapseProps extends React.HTMLAttributes { 5 | open: boolean; 6 | onOpenChange: (open: boolean) => void; 7 | disabled?: boolean; 8 | header: React.ReactNode; 9 | children: React.ReactNode; 10 | className?: string; 11 | } 12 | 13 | export function Collapse({ 14 | open, 15 | onOpenChange, 16 | disabled = false, 17 | header, 18 | children, 19 | className, 20 | ...props 21 | }: CollapseProps) { 22 | return ( 23 |
31 |
!disabled && onOpenChange(!open)} 44 | onKeyDown={(e) => { 45 | if (disabled) return; 46 | if (e.key === 'Enter' || e.key === ' ') { 47 | e.preventDefault(); 48 | onOpenChange(!open); 49 | } 50 | }} 51 | > 52 | {header} 53 | 62 | 69 | 70 |
71 |
79 |
{children}
80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/collapse/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collapse'; 2 | -------------------------------------------------------------------------------- /src/dialog/dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import { Button } from '../button'; 5 | 6 | import { Dialog } from './dialog'; 7 | 8 | const meta: Meta = { 9 | title: 'Components/Dialog', 10 | component: Dialog, 11 | tags: ['autodocs'], 12 | parameters: { 13 | layout: 'centered', 14 | docs: { 15 | description: { 16 | component: 17 | 'A general-purpose Dialog/modal component supporting title, content, and footer. The content area supports any custom React nodes, such as forms or text.', 18 | }, 19 | }, 20 | }, 21 | args: { 22 | showClose: true, 23 | }, 24 | argTypes: { 25 | showClose: { control: 'boolean' }, 26 | title: { control: 'text' }, 27 | }, 28 | }; 29 | 30 | export default meta; 31 | type Story = StoryObj; 32 | 33 | export const Default: Story = { 34 | render: (args) => { 35 | const [open, setOpen] = useState(false); 36 | return ( 37 | <> 38 | 39 | setOpen(false)} 43 | title="Default Dialog" 44 | footer={} 45 | > 46 | This is a basic dialog. The content area supports any React nodes. 47 | 48 | 49 | ); 50 | }, 51 | }; 52 | 53 | export const FormDialog: Story = { 54 | render: (args) => { 55 | const [open, setOpen] = useState(false); 56 | return ( 57 | <> 58 | 59 | setOpen(false)} 63 | title="Form Dialog" 64 | footer={ 65 | <> 66 | 69 | 72 | 73 | } 74 | > 75 |
76 |
77 | 80 | 81 |
82 |
83 | 86 | 87 |
88 |
89 |
90 | 91 | ); 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /src/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import { Button } from '../button'; 7 | export interface DialogProps 8 | extends Omit, 'title'> { 9 | open: boolean; 10 | onClose: () => void; 11 | title?: React.ReactNode; 12 | footer?: React.ReactNode; 13 | showClose?: boolean; 14 | children: React.ReactNode; 15 | } 16 | 17 | export function Dialog({ 18 | open, 19 | onClose, 20 | title, 21 | footer, 22 | showClose = true, 23 | className, 24 | children, 25 | ...props 26 | }: DialogProps) { 27 | if (typeof window === 'undefined' || typeof document === 'undefined') { 28 | return null; 29 | } 30 | 31 | return ReactDOM.createPortal( 32 | 33 | {open && ( 34 |
40 | 48 | 62 | {showClose && ( 63 | 73 | )} 74 | {title && ( 75 |
76 |

77 | {title} 78 |

79 |
80 | )} 81 |
{children}
82 | {footer && ( 83 |
{footer}
84 | )} 85 |
86 |
87 | )} 88 |
, 89 | document.body, 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dialog'; 2 | -------------------------------------------------------------------------------- /src/divider/divider.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Divider } from './divider'; 4 | 5 | const meta: Meta = { 6 | title: 'Components/Divider', 7 | component: Divider, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | docs: { 12 | description: { 13 | component: 14 | 'A Divider component used to separate content, supporting optional children (text), horizontal or vertical orientation, and variants (solid, dashed).', 15 | }, 16 | }, 17 | }, 18 | args: { 19 | variant: 'solid', 20 | orientation: 'horizontal', 21 | }, 22 | argTypes: { 23 | variant: { 24 | control: 'radio', 25 | options: ['solid', 'dashed'], 26 | }, 27 | orientation: { 28 | control: 'radio', 29 | options: ['horizontal', 'vertical'], 30 | }, 31 | }, 32 | }; 33 | 34 | export default meta; 35 | type Story = StoryObj; 36 | 37 | const Paragraph = () => ( 38 |

39 | In today' fast-paced digital world, staying connected and informed is 40 | more important than ever. Whether through social media, online news, or 41 | virtual meetings, technology continues to shape the way we communicate and 42 | interact. From remote work to e-learning, the convenience of digital tools 43 | has transformed our daily routines, making it easier to stay productive and 44 | engaged no matter where we are. 45 |

46 | ); 47 | 48 | export const Default: Story = { 49 | args: {}, 50 | render: (args) => ( 51 |
52 | 53 | 54 | 55 |
56 | ), 57 | }; 58 | 59 | export const WithText: Story = { 60 | args: {}, 61 | render: (args) => ( 62 |
63 | 64 | Text 65 | 66 |
67 | ), 68 | }; 69 | 70 | export const Variants: Story = { 71 | render: (args) => ( 72 |
73 | 74 | Solid Divider 75 | 76 | 77 | Dashed Divider 78 | 79 | 80 |
81 | ), 82 | }; 83 | 84 | export const VerticalDivider: Story = { 85 | args: { 86 | orientation: 'vertical', 87 | }, 88 | render: (args) => ( 89 |
90 | Left 91 | 92 | Right 93 |
94 | ), 95 | }; 96 | -------------------------------------------------------------------------------- /src/divider/divider.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ReactNode } from 'react'; 3 | 4 | export interface DividerProps { 5 | children?: ReactNode; 6 | variant?: 'solid' | 'dashed'; 7 | orientation?: 'horizontal' | 'vertical'; 8 | className?: string; 9 | } 10 | 11 | export function Divider({ 12 | children, 13 | variant = 'solid', 14 | orientation = 'horizontal', 15 | className, 16 | }: DividerProps) { 17 | const borderClass = 18 | variant === 'solid' ? 'border-zinc-200' : 'border-dashed border-zinc-200'; 19 | 20 | if (orientation === 'vertical') { 21 | return ( 22 | 31 | ); 32 | } 33 | 34 | return ( 35 |
41 |
42 | {children && ( 43 | 44 | {children} 45 | 46 | )} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/divider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './divider'; 2 | -------------------------------------------------------------------------------- /src/dropdown/dropdown.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Button } from '../button'; 4 | 5 | import { Dropdown } from './dropdown'; 6 | 7 | const meta: Meta = { 8 | title: 'Components/Dropdown', 9 | component: Dropdown, 10 | tags: ['autodocs'], 11 | parameters: { 12 | layout: 'centered', 13 | docs: { 14 | description: { 15 | component: 16 | 'A Dropdown component that displays a menu on hover. Supports alignment and custom trigger elements.', 17 | }, 18 | }, 19 | }, 20 | args: { 21 | menu: [ 22 | { label: 'Profile', href: '/profile' }, 23 | { label: 'Settings', href: '/settings' }, 24 | { label: 'Logout', href: '/logout' }, 25 | ], 26 | }, 27 | argTypes: { 28 | align: { 29 | control: 'radio', 30 | options: ['left', 'right'], 31 | }, 32 | }, 33 | }; 34 | 35 | export default meta; 36 | type Story = StoryObj; 37 | 38 | export const Default: Story = { 39 | args: { 40 | children: , 41 | }, 42 | }; 43 | 44 | export const WithTextTrigger: Story = { 45 | args: { 46 | children: ( 47 | Hover me 48 | ), 49 | }, 50 | }; 51 | 52 | export const LeftAligned: Story = { 53 | args: { 54 | align: 'left', 55 | children: , 56 | }, 57 | }; 58 | 59 | export const RightAligned: Story = { 60 | args: { 61 | align: 'right', 62 | children: , 63 | }, 64 | }; 65 | 66 | export const CustomMenuItems: Story = { 67 | render: (args) => ( 68 | alert('Logging out...'), 76 | }, 77 | ]} 78 | > 79 | 80 | 81 | ), 82 | }; 83 | -------------------------------------------------------------------------------- /src/dropdown/dropdown.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export interface DropdownItem { 4 | label: JSX.Element | string; 5 | href?: string; 6 | onClick?: () => void; 7 | } 8 | 9 | interface DropdownProps { 10 | children: JSX.Element | string; 11 | menu: DropdownItem[]; 12 | align?: 'left' | 'right'; 13 | } 14 | 15 | export function Dropdown({ menu, align = 'right', children }: DropdownProps) { 16 | return ( 17 |
18 |
19 |
{children}
20 |
28 |
    29 | {menu.map((item, i) => 30 | item.href ? ( 31 | 36 | {item.label} 37 | 38 | ) : ( 39 |
  • 44 | {item.label} 45 |
  • 46 | ), 47 | )} 48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dropdown'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar'; 2 | export * from './badge'; 3 | export * from './button'; 4 | export * from './callout'; 5 | export * from './card'; 6 | export * from './checkbox'; 7 | export * from './collapse'; 8 | export * from './dialog'; 9 | export * from './divider'; 10 | export * from './dropdown'; 11 | export * from './input'; 12 | export * from './mask'; 13 | export * from './radio'; 14 | export * from './select'; 15 | export * from './spinner'; 16 | export * from './tabs'; 17 | export * from './textarea'; 18 | export * from './toast'; 19 | export * from './tooltip'; 20 | -------------------------------------------------------------------------------- /src/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input'; 2 | -------------------------------------------------------------------------------- /src/input/input.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Input } from './input'; 4 | 5 | const meta: Meta = { 6 | title: 'Components/Input', 7 | component: Input, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | docs: { 12 | description: { 13 | component: 14 | 'A general-purpose Input component supporting label, size variants, and error message display. Styled using the zinc color palette with full dark mode support. Accepts JSX as label.', 15 | }, 16 | }, 17 | }, 18 | args: { 19 | size: 'default', 20 | }, 21 | argTypes: { 22 | size: { 23 | control: 'radio', 24 | options: ['sm', 'default', 'lg'], 25 | }, 26 | }, 27 | }; 28 | 29 | export default meta; 30 | type Story = StoryObj; 31 | 32 | export const Default: Story = { 33 | args: { 34 | placeholder: 'Enter something...', 35 | }, 36 | }; 37 | 38 | export const Label: Story = { 39 | args: { 40 | label: 'This is a label', 41 | placeholder: 'With label', 42 | }, 43 | }; 44 | 45 | export const WithError: Story = { 46 | args: { 47 | label: 'This is a label', 48 | error: 'This is an error', 49 | placeholder: 'With error', 50 | }, 51 | }; 52 | 53 | export const Sizes: Story = { 54 | render: (args) => ( 55 |
56 | 57 | 58 | 59 |
60 | ), 61 | }; 62 | 63 | export const WithCustomLabel: Story = { 64 | args: { 65 | label: ( 66 | 67 | Email * 68 | 69 | ), 70 | placeholder: 'you@example.com', 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/input/input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | export type InputSize = 'sm' | 'default' | 'lg'; 5 | 6 | type NativeInputProps = Omit< 7 | React.InputHTMLAttributes, 8 | 'size' 9 | >; 10 | 11 | export interface InputProps extends NativeInputProps { 12 | label?: React.ReactNode; 13 | error?: React.ReactNode; 14 | size?: InputSize; 15 | } 16 | 17 | const sizeClass = { 18 | sm: 'text-sm px-3 py-1.5 rounded-md', 19 | default: 'text-base px-4 py-2 rounded-md', 20 | lg: 'text-lg px-5 py-2.5 rounded-md', 21 | }; 22 | 23 | export const Input = React.forwardRef( 24 | ({ label, error, size = 'default', className, ...props }, ref) => { 25 | return ( 26 |
27 | {label && ( 28 | 31 | )} 32 | 43 | {error &&

{error}

} 44 |
45 | ); 46 | }, 47 | ); 48 | 49 | Input.displayName = 'Input'; 50 | -------------------------------------------------------------------------------- /src/mask/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mask'; 2 | -------------------------------------------------------------------------------- /src/mask/mask.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import { Button } from '../button'; 5 | 6 | import { Mask } from './mask'; 7 | 8 | const meta: Meta = { 9 | title: 'Components/Mask', 10 | component: Mask, 11 | tags: ['autodocs'], 12 | parameters: { 13 | layout: 'fullscreen', 14 | docs: { 15 | description: { 16 | component: 17 | 'A full-screen mask overlay component that supports customizable opacity, z-index, and a closable button.', 18 | }, 19 | }, 20 | }, 21 | argTypes: { 22 | opacity: { 23 | control: { type: 'range', min: 0, max: 1, step: 0.1 }, 24 | }, 25 | zIndex: { 26 | control: 'number', 27 | }, 28 | closable: { 29 | control: 'boolean', 30 | }, 31 | }, 32 | }; 33 | 34 | export default meta; 35 | type Story = StoryObj; 36 | 37 | export const Default: Story = { 38 | args: { 39 | open: false, 40 | opacity: 0.6, 41 | zIndex: 1000, 42 | closable: true, 43 | }, 44 | render: (args) => { 45 | const [open, setOpen] = useState(false); 46 | 47 | return ( 48 |
49 | 50 | 51 | setOpen(false)}> 52 |
Loading...
53 |
54 |
55 | ); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/mask/mask.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | 4 | import { Button } from '../button'; 5 | 6 | export interface MaskProps { 7 | open: boolean; 8 | opacity?: number; 9 | zIndex?: number; 10 | children?: React.ReactNode; 11 | closable?: boolean; 12 | onClose?: () => void; 13 | } 14 | 15 | export function Mask({ 16 | open, 17 | opacity = 0.6, 18 | zIndex = 1000, 19 | children, 20 | closable = true, 21 | onClose, 22 | }: MaskProps) { 23 | if (!open) return null; 24 | 25 | const safeOpacity = Math.max(0.05, opacity); 26 | 27 | return ( 28 |
38 | {closable && ( 39 | 47 | )} 48 | {children} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/radio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './radio'; 2 | -------------------------------------------------------------------------------- /src/radio/radio.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Radio } from './radio'; 4 | 5 | const meta: Meta = { 6 | title: 'Components/Radio', 7 | component: Radio, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | docs: { 12 | description: { 13 | component: 14 | 'A Radio component for selecting a single option from a group. Supports different sizes, disabled states, and works with any `children` as the label. Designed for accessible forms with support for controlled and uncontrolled modes.', 15 | }, 16 | }, 17 | }, 18 | args: { 19 | size: 'default', 20 | disabled: false, 21 | }, 22 | argTypes: { 23 | disabled: { control: 'boolean' }, 24 | size: { 25 | control: 'radio', 26 | options: ['sm', 'default', 'lg'], 27 | }, 28 | }, 29 | }; 30 | 31 | export default meta; 32 | type Story = StoryObj; 33 | 34 | export const Default: Story = { 35 | args: { 36 | children: 'Option 1', 37 | }, 38 | }; 39 | 40 | export const Sizes: Story = { 41 | args: { 42 | children: 'Option', 43 | }, 44 | render: (args) => ( 45 |
46 | 47 | Small 48 | 49 | 50 | Default 51 | 52 | 53 | Large 54 | 55 |
56 | ), 57 | }; 58 | 59 | export const DisabledStates: Story = { 60 | args: { 61 | children: 'Option', 62 | disabled: true, 63 | }, 64 | render: (args) => ( 65 |
66 | Disabled 67 | 68 | Disabled & Checked 69 | 70 |
71 | ), 72 | }; 73 | -------------------------------------------------------------------------------- /src/radio/radio.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React, { useId } from 'react'; 3 | 4 | export type RadioSize = 'sm' | 'default' | 'lg'; 5 | 6 | export interface RadioProps 7 | extends Omit< 8 | React.InputHTMLAttributes, 9 | 'type' | 'children' | 'size' 10 | > { 11 | size?: RadioSize; 12 | children?: React.ReactNode; 13 | } 14 | 15 | export function Radio({ 16 | children, 17 | size = 'default', 18 | className, 19 | disabled, 20 | checked, 21 | ...props 22 | }: RadioProps) { 23 | const id = useId(); 24 | 25 | const sizeClass = { 26 | sm: { 27 | wrapper: 'h-3.5 w-3.5', 28 | inner: 'h-1.5 w-1.5', 29 | }, 30 | default: { 31 | wrapper: 'h-4 w-4', 32 | inner: 'h-2 w-2', 33 | }, 34 | lg: { 35 | wrapper: 'h-5 w-5', 36 | inner: 'h-2.5 w-2.5', 37 | }, 38 | }[size]; 39 | 40 | return ( 41 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select'; 2 | -------------------------------------------------------------------------------- /src/select/select.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | import { AiFillApple } from 'react-icons/ai'; 4 | 5 | import { Select } from './select'; 6 | 7 | const meta: Meta = { 8 | title: 'Components/Select', 9 | component: Select, 10 | parameters: { 11 | layout: 'centered', 12 | docs: { 13 | description: { 14 | component: 15 | 'A general-purpose Select component supporting outline style, size, icon, disabled, and placeholder options.', 16 | }, 17 | }, 18 | }, 19 | args: { 20 | disabled: false, 21 | size: 'default', 22 | options: [ 23 | { label: 'Apple', value: 'apple' }, 24 | { label: 'Banana', value: 'banana' }, 25 | { label: 'Orange', value: 'orange' }, 26 | { label: 'Disabled', value: 'disabled', disabled: true }, 27 | ], 28 | placeholder: 'Select a fruit', 29 | }, 30 | argTypes: { 31 | disabled: { control: 'boolean' }, 32 | size: { 33 | control: 'radio', 34 | options: ['sm', 'default', 'lg'], 35 | }, 36 | onChange: { action: 'onChange' }, 37 | }, 38 | }; 39 | 40 | export default meta; 41 | type Story = StoryObj; 42 | 43 | export const Default: Story = { 44 | render: (args) => { 45 | const [value, setValue] = useState(''); 46 | return 58 | 60 |
61 | ); 62 | }, 63 | }; 64 | 65 | export const Disabled: Story = { 66 | render: (args) => ( 67 |
68 | } 80 | value={value} 81 | onChange={setValue} 82 | /> 83 | ); 84 | }, 85 | }; 86 | 87 | export const Controlled: Story = { 88 | render: (args) => { 89 | const [value, setValue] = useState('banana'); 90 | return ( 91 |
92 |