diff --git a/CONTEXT.md b/CONTEXT.md index 331c8e8..d33a032 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,4 +1,4 @@ -# Project Context: @creativecodeco/ui (v2.6.0) +# Project Context: @creativecodeco/ui (v2.7.0) ## Purpose diff --git a/Changelog.md b/Changelog.md index 70a112f..99ac56f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,28 @@ # Changelog +## v2.7.0 - Full DaisyUI v5 Specification Alignment, Combinations, Stories & Unit Tests + +### 👾 Feature & DaisyUI v5 Alignment + +- **Global Types (`SizeType` & `ColorType`)**: Add `'xl'` size (`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) and `'neutral'` semantic color across base types (`src/types/ui/base/constants.types.ts`) and custom theme CSS files (`avatar.css`, `badge.css`, `button.css`, `checkbox.css`, `radio.css`, `textbox.css`). +- **Display Components**: + - `Button`: Add DaisyUI v5 `variant` (`'outline' | 'dash' | 'soft' | 'ghost' | 'link'`), `shape` (`'square' | 'circle'`), `wide`, `block`, `active`, `size='xl'`, and automatic `` rendering when `withBadge` and `badge` are provided. + - `Badge`: Add `variant` (`'outline' | 'dash' | 'soft' | 'ghost'`), `ColorButtonType` (`neutral`, `ghost`), and `size='xl'`. + - `Card`: Migrate legacy v4 classes (`card-bordered` -> `card-border`, `card-compact` -> `card-sm`) and add `size` (`xs`–`xl`), `dash` (`card-dash`), `side` (`card-side`), and `imageFull` (`image-full`). + - `Table`: Migrate to DaisyUI v5 `table-xs` and `row-hover` on ``, and add `size` (`xs`–`xl`), `pinRows` (`table-pin-rows`), and `pinCols` (`table-pin-cols`). + - `Carousel`, `Stat`, `Swap`, `Timeline`, `AvatarGroup`: Add `snap='start'` and `carousel-horizontal` in `Carousel`, `actions` (`.stat-actions`) in `Stat`, `indeterminateContent` (`.swap-indeterminate`) in `Swap`, `snapIcon` (`timeline-snap-icon`) in `Timeline`, and `xl` size in `AvatarGroup`. +- **Feedback & Layout Components**: + - `Alert`: Add DaisyUI v5 style variants (`'soft' | 'outline' | 'dash'`) and `vertical` (`alert-vertical` / `alert-horizontal`) layout. + - `Modal`, `Toast`, `Tooltip`, `Skeleton`, `Spinner`, `Collapse`: Add `position` (`'top' | 'middle' | 'bottom' | 'start' | 'end'`) and `size='xl'` in `Modal`, `middle-*` (`toast-middle`) positions in `Toast`, `'start' | 'center' | 'end'` positions and `.tooltip-content` rich JSX support in `Tooltip`, `skeleton-text` in `Skeleton`, `loading-xl` in `Spinner`, and `close` (`collapse-close`) in `Collapse`. +- **Navigation Components**: + - `Tabs`: Migrate to DaisyUI v5 `tabs-border`, `tabs-lift`, `tabs-box` (preserving backward compatibility with `bordered`, `lifted`, `boxed`), add `position` (`'top' | 'bottom'`), and support `tabs-xl`. + - `Menu`, `ActionDropdown`, `Steps`, `Fab`, `Drawer`, `Pagination`: Migrate to `menu-active`, `menu-disabled`, and `menu-focus`, wrap step icons in ``, add `mainAction` (`.fab-main-action`) in `Fab`, add `.drawer-content` in `Drawer`, and support `btn-xl` in `Pagination`. +- **Form Controls**: + - `TextBox`, `TextArea`, `FileInput`, `Range`, `Toggle`: Add `ghost` modifier (`input-ghost`, `textarea-ghost`, `file-input-ghost`), `vertical` (`range-vertical`) in `Range`, and full `xl` size / `neutral` color support. +- **Tailwind CSS v4 Safelisting**: Expand `@source inline(...)` in `src/theme/main.css` to guarantee all dynamic DaisyUI v5 modifiers, variants, sizes, and component classes are emitted in `lib/theme/main.css`. +- **Storybook & Unit Tests**: Update 34 Storybook files (`*.stories.tsx`) with new `argTypes` and interactive stories, and expand unit tests (`*.test.tsx`) to 245 passing tests across 70 test suites. + ## v2.6.0 - Component Standardization, A11y, Security Hardening & Dependency Upgrades ### 👾 Component Standardization & Design Polish diff --git a/README.md b/README.md index 43bd0b5..1a72b9d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Configuration > [!NOTE] -> **Current Version: `v2.6.0`** — This library is designed for **React 19** (`v19.3.1`), **Tailwind CSS v4** (`v4.3.3`), and **DaisyUI v5** (`v5.7.50`), and requires **Node.js 24 LTS** (`>=24.22.1`) and **npm 12** (`>=12.2.1`). The configuration is primarily CSS-first. +> **Current Version: `v2.7.0`** — This library is designed for **React 19** (`v19.3.1`), **Tailwind CSS v4** (`v4.3.3`), and **DaisyUI v5** (`v5.7.50`), and requires **Node.js 24 LTS** (`>=24.22.1`) and **npm 12** (`>=12.2.1`). The configuration is primarily CSS-first. ### Install diff --git a/package-lock.json b/package-lock.json index e7213fd..4d179b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@creativecodeco/ui", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@creativecodeco/ui", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "dependencies": { "@tailwindcss/postcss": "4.3.3", diff --git a/package.json b/package.json index d5eb3a6..34d13c5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "react-hook-form", "storybook" ], - "version": "2.6.0", + "version": "2.7.0", "homepage": "https://github.com/creativecodeco/ui", "repository": { "type": "git", diff --git a/src/theme/avatar.css b/src/theme/avatar.css index c92fa9a..00c6883 100644 --- a/src/theme/avatar.css +++ b/src/theme/avatar.css @@ -30,6 +30,14 @@ } } +.avatar-size-xl { + @apply w-32; + + > span { + @apply text-4xl; + } +} + .avatar-ring { @apply ring ring-primary ring-offset-base-100 ring-offset-2; } diff --git a/src/theme/badge.css b/src/theme/badge.css index d50b574..746fd47 100644 --- a/src/theme/badge.css +++ b/src/theme/badge.css @@ -1,7 +1,15 @@ +.badge-size-xl { + @apply badge-xl; +} + .badge-size-lg { @apply badge-lg; } +.badge-size-md { + @apply badge-md; +} + .badge-size-sm { @apply badge-sm; } @@ -22,6 +30,14 @@ @apply badge-accent; } +.badge-color-neutral { + @apply badge-neutral; +} + +.badge-color-ghost { + @apply badge-ghost; +} + .badge-color-success { @apply badge-success; } diff --git a/src/theme/button.css b/src/theme/button.css index 837f7b7..3e8dfba 100644 --- a/src/theme/button.css +++ b/src/theme/button.css @@ -76,10 +76,18 @@ @apply btn-ghost; } +.button-size-xl { + @apply btn-xl; +} + .button-size-lg { @apply btn-lg; } +.button-size-md { + @apply btn-md; +} + .button-size-sm { @apply btn-sm; } diff --git a/src/theme/checkbox.css b/src/theme/checkbox.css index bd27cc6..1a8b31a 100644 --- a/src/theme/checkbox.css +++ b/src/theme/checkbox.css @@ -10,6 +10,10 @@ @apply checkbox-accent; } +.checkbox-color-neutral { + @apply checkbox-neutral; +} + .checkbox-color-success { @apply checkbox-success; } @@ -41,3 +45,7 @@ .checkbox-size-lg { @apply checkbox-lg; } + +.checkbox-size-xl { + @apply checkbox-xl; +} diff --git a/src/theme/main.css b/src/theme/main.css index a5def8b..deb3472 100644 --- a/src/theme/main.css +++ b/src/theme/main.css @@ -11,7 +11,27 @@ @plugin "daisyui"; @source inline("{btn,badge,alert,toggle,range,select,textarea,file-input,progress,status,chat-bubble,step,tooltip,checkbox,radio,input,otp,divider,link,text,bg,border}-{primary,secondary,accent,info,success,warning,error,neutral,ghost}"); -@source inline("{btn,badge,toggle,range,select,textarea,file-input,checkbox,radio,input,otp,tab,table,menu,dock,kbd,loading,status}-{xs,sm,md,lg,xl}"); +@source inline("{btn,badge,toggle,range,select,textarea,file-input,checkbox,radio,input,otp,tabs,table,menu,dock,kbd,loading,status,aura,rating,megamenu,card}-{xs,sm,md,lg,xl}"); +@source inline("aura-{rainbow,holo,dual,silver,gold,glow}"); +@source inline("mask-{squircle,heart,hexagon,hexagon-2,decagon,pentagon,diamond,square,circle,star,star-2,triangle,triangle-2,triangle-3,triangle-4}"); +@source inline("stack-{top,bottom,start,end}"); +@source inline("dropdown-{top,bottom,left,right,start,center,end}"); +@source inline("collapse-{arrow,plus,open,close}"); +@source inline("swap-{rotate,flip,active,indeterminate}"); +@source inline("tabs-{border,lift,box,top,bottom}"); +@source inline("card-{border,dash,side}"); +@source inline("alert-{soft,outline,dash,vertical,horizontal}"); +@source inline("badge-{outline,dash,soft,ghost}"); +@source inline("btn-{outline,dash,soft,ghost,link,wide,block,square,circle,active,disabled}"); +@source inline("modal-{top,middle,bottom,start,end}"); +@source inline("toast-{start,center,end,top,middle,bottom}"); +@source inline("tooltip-{top,bottom,left,right,start,center,end,open,content}"); +@source inline("table-{pin-rows,pin-cols,zebra}"); +@source inline("timeline-{vertical,horizontal,compact,snap-icon}"); +@source inline("{input,textarea,file-input,select}-ghost"); +@source inline("range-vertical"); +@source inline("skeleton-text"); +@source inline("fab-{flower,close,main-action}"); :root { --ease-out: cubic-bezier(0.23, 1, 0.32, 1); diff --git a/src/theme/radio.css b/src/theme/radio.css index ecf0762..37ced99 100644 --- a/src/theme/radio.css +++ b/src/theme/radio.css @@ -10,6 +10,10 @@ @apply radio-accent; } +.radio-color-neutral { + @apply radio-neutral; +} + .radio-color-success { @apply radio-success; } @@ -41,3 +45,7 @@ .radio-size-lg { @apply radio-lg; } + +.radio-size-xl { + @apply radio-xl; +} diff --git a/src/theme/textbox.css b/src/theme/textbox.css index 891d29e..349f1e5 100644 --- a/src/theme/textbox.css +++ b/src/theme/textbox.css @@ -46,6 +46,18 @@ } } +.textbox-size-xl { + @apply input-xl; + + &.textbox-with-left-icon { + @apply pl-10; + } + + &.textbox-with-right-icon { + @apply pr-10; + } +} + .textbox-left-icon { @apply absolute z-10; } @@ -66,6 +78,10 @@ @apply top-6 left-3; } +.textbox-left-icon-size-xl { + @apply top-7 left-3; +} + .textbox-right-icon { @apply absolute z-10; } @@ -86,6 +102,10 @@ @apply top-6 right-3; } +.textbox-right-icon-size-xl { + @apply top-7 right-3; +} + .textbox-color-primary { @apply input-primary; } @@ -98,6 +118,10 @@ @apply input-accent; } +.textbox-color-neutral { + @apply input-neutral; +} + .textbox-color-success { @apply input-success; } diff --git a/src/types/ui/base/constants.types.ts b/src/types/ui/base/constants.types.ts index d15a983..ad93623 100644 --- a/src/types/ui/base/constants.types.ts +++ b/src/types/ui/base/constants.types.ts @@ -1,6 +1,13 @@ -export type SizeType = 'xs' | 'sm' | 'md' | 'lg'; +export type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export type ColorType = - 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'info' | 'error'; + | 'primary' + | 'secondary' + | 'accent' + | 'neutral' + | 'success' + | 'warning' + | 'info' + | 'error'; export type ColorButtonType = | 'primary' | 'secondary' diff --git a/src/ui/components/display/aura/aura.stories.tsx b/src/ui/components/display/aura/aura.stories.tsx index 84d2c40..0fe47c1 100644 --- a/src/ui/components/display/aura/aura.stories.tsx +++ b/src/ui/components/display/aura/aura.stories.tsx @@ -10,7 +10,7 @@ const meta: Meta = { control: 'select', options: ['rainbow', 'holo', 'dual', 'silver', 'gold'] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, glow: { control: 'boolean' } } }; @@ -48,3 +48,25 @@ export const Variants: Story = { ) }; + +export const Sizes: Story = { + render: () => ( +
+ + + + + + + + + + + + + + + +
+ ) +}; diff --git a/src/ui/components/display/avatar-group/avatar-group.component.tsx b/src/ui/components/display/avatar-group/avatar-group.component.tsx index 80aff4e..f6f854c 100644 --- a/src/ui/components/display/avatar-group/avatar-group.component.tsx +++ b/src/ui/components/display/avatar-group/avatar-group.component.tsx @@ -7,7 +7,8 @@ const sizeClasses: Record = { xs: 'w-6 h-6', sm: 'w-8 h-8', md: 'w-12 h-12', - lg: 'w-16 h-16' + lg: 'w-16 h-16', + xl: 'w-20 h-20' }; const AvatarGroup = ({ diff --git a/src/ui/components/display/avatar-group/avatar-group.stories.tsx b/src/ui/components/display/avatar-group/avatar-group.stories.tsx index 5a45f1f..943a9b2 100644 --- a/src/ui/components/display/avatar-group/avatar-group.stories.tsx +++ b/src/ui/components/display/avatar-group/avatar-group.stories.tsx @@ -6,7 +6,7 @@ const meta: Meta = { component: AvatarGroup, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } } }; diff --git a/src/ui/components/display/avatar-group/avatar-group.test.tsx b/src/ui/components/display/avatar-group/avatar-group.test.tsx index 29bb954..2e8671c 100644 --- a/src/ui/components/display/avatar-group/avatar-group.test.tsx +++ b/src/ui/components/display/avatar-group/avatar-group.test.tsx @@ -33,4 +33,9 @@ describe('', () => { expect(screen.queryByAltText('User 3')).not.toBeInTheDocument(); expect(screen.getByText('+2')).toBeInTheDocument(); }); + + it('applies xl size class', () => { + const { container } = render(); + expect(container.querySelector('.avatar > div')).toHaveClass('w-20'); + }); }); diff --git a/src/ui/components/display/avatar/avatar.stories.tsx b/src/ui/components/display/avatar/avatar.stories.tsx index ec920dc..deabea8 100644 --- a/src/ui/components/display/avatar/avatar.stories.tsx +++ b/src/ui/components/display/avatar/avatar.stories.tsx @@ -53,7 +53,7 @@ const meta: Meta = { size: { description: 'Size', type: 'string', - options: ['xs', 'sm', 'md', 'lg'], + options: ['xs', 'sm', 'md', 'lg', 'xl'], control: { type: 'select' }, table: { type: { @@ -101,6 +101,12 @@ export const SizeLg: Story = { } }; +export const SizeXl: Story = { + args: { + size: 'xl' + } +}; + export const Image: Story = { args: { src: imageAvatar diff --git a/src/ui/components/display/badge/badge.component.tsx b/src/ui/components/display/badge/badge.component.tsx index 76eec4d..b5a2646 100644 --- a/src/ui/components/display/badge/badge.component.tsx +++ b/src/ui/components/display/badge/badge.component.tsx @@ -6,6 +6,7 @@ import type { BadgeType } from '@/types'; const Badge = ({ children, color, + variant, icon: Icon, iconPosition = 'left', outline, @@ -20,12 +21,17 @@ const Badge = ({ return ; }, [Icon]); + const isStyledVariant = Boolean(outline || variant); + return (
= { tags: ['autodocs'], argTypes: { color: { - description: 'Color', - type: 'string', + description: 'Badge color token', + control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', 'success', 'warning', 'info', - 'error' - ], - control: { type: 'select' }, - table: { type: { summary: 'string' } } + 'error', + 'ghost' + ] + }, + variant: { + description: 'Badge style variant', + control: 'select', + options: ['outline', 'dash', 'soft', 'ghost'] }, outline: { - description: 'Is Outline', - type: 'boolean', - table: { type: { summary: 'boolean' } } + description: 'Outline style', + control: 'boolean' }, size: { - description: 'Size', - type: 'string', - options: ['xs', 'sm', 'md', 'lg'], - control: { type: 'select' }, - table: { type: { summary: 'string' }, defaultValue: { summary: 'md' } } + description: 'Badge size', + control: 'select', + options: ['xs', 'sm', 'md', 'lg', 'xl'] }, icon: { - description: 'Left Icon', - type: 'function', + description: 'Badge icon component', options: Object.keys(Icons), mapping: Icons, - control: { type: 'select' } + control: 'select' }, iconPosition: { - description: 'Icon Position', - type: 'string', - options: ['left', 'right'], - control: { type: 'radio' }, - table: { type: { summary: 'string' }, defaultValue: { summary: 'left' } } + description: 'Badge icon placement', + control: 'inline-radio', + options: ['left', 'right'] } }, args: { @@ -76,7 +75,19 @@ export const SizeXs: Story = createBadgeStory({ size: 'xs' }); export const SizeSm: Story = createBadgeStory({ size: 'sm' }); export const SizeMd: Story = createBadgeStory({ size: 'md' }); export const SizeLg: Story = createBadgeStory({ size: 'lg' }); +export const SizeXl: Story = createBadgeStory({ size: 'xl' }); + +export const Soft: Story = createBadgeStory({ + color: 'primary', + variant: 'soft' +}); +export const Dash: Story = createBadgeStory({ + color: 'primary', + variant: 'dash' +}); +export const Ghost: Story = createBadgeStory({ variant: 'ghost' }); +export const ColorNeutral: Story = createBadgeStory({ color: 'neutral' }); export const ColorPrimary: Story = createBadgeStory({ color: 'primary' }); export const ColorSecondary: Story = createBadgeStory({ color: 'secondary' }); export const ColorAccent: Story = createBadgeStory({ color: 'accent' }); diff --git a/src/ui/components/display/badge/badge.test.tsx b/src/ui/components/display/badge/badge.test.tsx index 0c05a27..348f5cc 100644 --- a/src/ui/components/display/badge/badge.test.tsx +++ b/src/ui/components/display/badge/badge.test.tsx @@ -76,4 +76,31 @@ describe('', () => { 'badge badge-primary badge-outline' ); }); + + it('DaisyUI v5 variants (soft, dash, ghost) and size xl', () => { + const { container: softContainer } = render( + + Soft + + ); + expect(softContainer.querySelector('div')?.className).toEqual( + 'badge badge-primary badge-soft badge-size-xl' + ); + + const { container: dashContainer } = render( + + Dash + + ); + expect(dashContainer.querySelector('div')?.className).toEqual( + 'badge badge-neutral badge-dash' + ); + + const { container: ghostContainer } = render( + Ghost + ); + expect(ghostContainer.querySelector('div')?.className).toEqual( + 'badge badge-ghost' + ); + }); }); diff --git a/src/ui/components/display/badge/badge.types.ts b/src/ui/components/display/badge/badge.types.ts index 1379516..b1ef036 100644 --- a/src/ui/components/display/badge/badge.types.ts +++ b/src/ui/components/display/badge/badge.types.ts @@ -1,10 +1,13 @@ -import type { ColorType, PositionType, SizeType } from '@/types'; +import type { ColorButtonType, PositionType, SizeType } from '@/types'; import type { IconType } from 'react-icons'; +export type BadgeVariantType = 'outline' | 'dash' | 'soft' | 'ghost'; + export interface BadgeType { children: React.ReactNode; - color?: ColorType; + color?: ColorButtonType; + variant?: BadgeVariantType; outline?: boolean; size?: SizeType; icon?: IconType; diff --git a/src/ui/components/display/button/button.component.tsx b/src/ui/components/display/button/button.component.tsx index 0f61500..07606bd 100644 --- a/src/ui/components/display/button/button.component.tsx +++ b/src/ui/components/display/button/button.component.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, useMemo } from 'react'; import cls from 'classnames'; import { useSafeButtonProps } from '@/hooks'; +import { Badge } from '../badge'; import { ButtonRef, type ButtonType } from '@/types'; @@ -11,12 +12,20 @@ const Button = forwardRef( children, isLink, color, + variant, outline, + shape, + wide, + block, + active, size = 'md', icon: Icon, iconPosition = 'left', loading, loadingLabel, + withBadge, + badge, + badgeProps, type = 'button', className, ...otherProps @@ -47,20 +56,45 @@ const Button = forwardRef( <> {iconPosition === 'left' && newIcon} {children} + {withBadge && badge !== undefined && ( + + {badge} + + )} {iconPosition === 'right' && newIcon} ); - }, [loading, loadingLabel, iconPosition, children, newIcon]); + }, [ + loading, + loadingLabel, + iconPosition, + children, + newIcon, + withBadge, + badge, + size, + badgeProps + ]); + + const isStyledVariant = Boolean(outline || variant); return ( + ); + const btn = container.querySelector('button'); + expect(btn?.className).toContain('btn-soft'); + expect(btn?.className).toContain('btn-primary'); + expect(btn?.className).toContain('button-size-xl'); + expect(btn?.className).toContain('btn-circle'); + expect(btn?.className).toContain('btn-wide'); + expect(btn?.className).toContain('btn-block'); + expect(btn?.className).toContain('btn-active'); + expect(container.querySelector('.badge')).toHaveTextContent('+9'); + }); }); diff --git a/src/ui/components/display/button/button.types.ts b/src/ui/components/display/button/button.types.ts index 8c70566..6af4849 100644 --- a/src/ui/components/display/button/button.types.ts +++ b/src/ui/components/display/button/button.types.ts @@ -6,10 +6,18 @@ import type { } from '@/types'; import type { IconType } from 'react-icons'; +export type ButtonVariantType = 'outline' | 'dash' | 'soft' | 'ghost' | 'link'; +export type ButtonShapeType = 'square' | 'circle'; + export interface ButtonType extends React.ButtonHTMLAttributes { isLink?: boolean; color?: ColorButtonType; + variant?: ButtonVariantType; outline?: boolean; + shape?: ButtonShapeType; + wide?: boolean; + block?: boolean; + active?: boolean; size?: SizeType; icon?: IconType; diff --git a/src/ui/components/display/card/card.component.tsx b/src/ui/components/display/card/card.component.tsx index ce49156..22ed9e1 100644 --- a/src/ui/components/display/card/card.component.tsx +++ b/src/ui/components/display/card/card.component.tsx @@ -10,8 +10,12 @@ const Card = ({ image, imageAlt = 'Card image', actions, + size, compact, bordered = true, + dash = false, + side = false, + imageFull = false, glass, className }: CardType) => { @@ -20,8 +24,12 @@ const Card = ({ return (
diff --git a/src/ui/components/display/card/card.stories.tsx b/src/ui/components/display/card/card.stories.tsx index 578431d..f007fd7 100644 --- a/src/ui/components/display/card/card.stories.tsx +++ b/src/ui/components/display/card/card.stories.tsx @@ -6,8 +6,12 @@ const meta: Meta = { component: Card, tags: ['autodocs'], argTypes: { + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, compact: { control: 'boolean' }, bordered: { control: 'boolean' }, + dash: { control: 'boolean' }, + side: { control: 'boolean' }, + imageFull: { control: 'boolean' }, glass: { control: 'boolean' } } }; @@ -35,3 +39,37 @@ export const WithImage: Story = { actions: } }; + +export const Dash: Story = { + args: { + title: 'Dashed Card', + dash: true, + children: 'Card using DaisyUI v5 card-dash modifier.', + actions: + } +}; + +export const Side: Story = { + args: { + title: 'Side Layout Card', + side: true, + bordered: true, + image: + 'https://img.daisyui.com/images/stock/photo-1606107557195-0e29a4b5b4aa.webp', + imageAlt: 'Shoes', + children: 'Horizontal card layout using card-side.', + actions: + } +}; + +export const ImageFull: Story = { + args: { + title: 'Overlay Card', + imageFull: true, + image: + 'https://img.daisyui.com/images/stock/photo-1606107557195-0e29a4b5b4aa.webp', + imageAlt: 'Shoes', + children: 'Background image overlay card using image-full.', + actions: + } +}; diff --git a/src/ui/components/display/card/card.test.tsx b/src/ui/components/display/card/card.test.tsx index e769e27..0d9ef2a 100644 --- a/src/ui/components/display/card/card.test.tsx +++ b/src/ui/components/display/card/card.test.tsx @@ -20,12 +20,31 @@ describe('', () => { }); it('renders subtitle and actions', () => { - render( - Action}> + const { container } = render( + Action}> Body ); expect(screen.getByText('Sub header')).toBeInTheDocument(); expect(screen.getByText('Action')).toBeInTheDocument(); + expect(container.querySelector('.card')).toHaveClass( + 'card-border', + 'card-sm' + ); + }); + + it('applies DaisyUI v5 size, dash, side, and imageFull modifiers', () => { + const { container } = render( + + Content + + ); + expect(container.querySelector('.card')).toHaveClass( + 'card-lg', + 'card-dash', + 'card-side', + 'image-full', + 'glass' + ); }); }); diff --git a/src/ui/components/display/card/card.types.ts b/src/ui/components/display/card/card.types.ts index ca6dfea..2922128 100644 --- a/src/ui/components/display/card/card.types.ts +++ b/src/ui/components/display/card/card.types.ts @@ -1,3 +1,5 @@ +import type { SizeType } from '@/types'; + export interface CardType { children?: React.ReactNode; title?: React.ReactNode; @@ -5,8 +7,12 @@ export interface CardType { image?: string; imageAlt?: string; actions?: React.ReactNode; + size?: SizeType; compact?: boolean; bordered?: boolean; + dash?: boolean; + side?: boolean; + imageFull?: boolean; glass?: boolean; className?: string; } diff --git a/src/ui/components/display/carousel/carousel.component.tsx b/src/ui/components/display/carousel/carousel.component.tsx index 988cb78..3b9f9c2 100644 --- a/src/ui/components/display/carousel/carousel.component.tsx +++ b/src/ui/components/display/carousel/carousel.component.tsx @@ -16,9 +16,11 @@ const Carousel = ({ className={cls( 'carousel', { + 'carousel-start': snap === 'start', 'carousel-center': snap === 'center', 'carousel-end': snap === 'end', - 'carousel-vertical': vertical + 'carousel-vertical': vertical, + 'carousel-horizontal': !vertical }, className )} diff --git a/src/ui/components/display/carousel/carousel.stories.tsx b/src/ui/components/display/carousel/carousel.stories.tsx index feb4ce0..9b35554 100644 --- a/src/ui/components/display/carousel/carousel.stories.tsx +++ b/src/ui/components/display/carousel/carousel.stories.tsx @@ -4,7 +4,12 @@ import Carousel from './carousel.component'; const meta: Meta = { title: '@creativecodeco-ui/Display/Carousel', component: Carousel, - tags: ['autodocs'] + tags: ['autodocs'], + argTypes: { + snap: { control: 'select', options: ['start', 'center', 'end'] }, + vertical: { control: 'boolean' }, + fullWidth: { control: 'boolean' } + } }; export default meta; diff --git a/src/ui/components/display/carousel/carousel.test.tsx b/src/ui/components/display/carousel/carousel.test.tsx index 5d967b6..59fd9df 100644 --- a/src/ui/components/display/carousel/carousel.test.tsx +++ b/src/ui/components/display/carousel/carousel.test.tsx @@ -29,4 +29,13 @@ describe('', () => { const carousel = container.querySelector('.carousel'); expect(carousel).toHaveClass('carousel-center', 'carousel-vertical'); }); + + it('applies horizontal and start snap classes', () => { + const { container } = render( + + ); + const carousel = container.querySelector('.carousel'); + expect(carousel).toHaveClass('carousel-start', 'carousel-horizontal'); + expect(container.querySelector('.carousel-item')).toHaveClass('w-full'); + }); }); diff --git a/src/ui/components/display/carousel/carousel.types.ts b/src/ui/components/display/carousel/carousel.types.ts index 5596f19..1a15de3 100644 --- a/src/ui/components/display/carousel/carousel.types.ts +++ b/src/ui/components/display/carousel/carousel.types.ts @@ -10,8 +10,8 @@ export interface CarouselType { items?: CarouselItemType[]; /** Custom children elements */ children?: React.ReactNode; - /** Snap position mode (center or end) */ - snap?: 'center' | 'end'; + /** Snap position mode (start, center, or end) */ + snap?: 'start' | 'center' | 'end'; /** Vertical scroll orientation */ vertical?: boolean; /** Full width items */ diff --git a/src/ui/components/display/chat-bubble/chat-bubble.stories.tsx b/src/ui/components/display/chat-bubble/chat-bubble.stories.tsx index 0cfdc78..bdae109 100644 --- a/src/ui/components/display/chat-bubble/chat-bubble.stories.tsx +++ b/src/ui/components/display/chat-bubble/chat-bubble.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', diff --git a/src/ui/components/display/indicator/indicator.stories.tsx b/src/ui/components/display/indicator/indicator.stories.tsx index 7d9510e..e289758 100644 --- a/src/ui/components/display/indicator/indicator.stories.tsx +++ b/src/ui/components/display/indicator/indicator.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', diff --git a/src/ui/components/display/kbd/kbd.stories.tsx b/src/ui/components/display/kbd/kbd.stories.tsx index 1f2b3ea..3b091c5 100644 --- a/src/ui/components/display/kbd/kbd.stories.tsx +++ b/src/ui/components/display/kbd/kbd.stories.tsx @@ -6,7 +6,7 @@ const meta: Meta = { component: Kbd, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } } }; @@ -39,6 +39,7 @@ export const Sizes: Story = { sm md lg + xl
) }; diff --git a/src/ui/components/display/mask/mask.stories.tsx b/src/ui/components/display/mask/mask.stories.tsx index e284179..ae283d8 100644 --- a/src/ui/components/display/mask/mask.stories.tsx +++ b/src/ui/components/display/mask/mask.stories.tsx @@ -20,7 +20,12 @@ const meta: Meta = { 'circle', 'star', 'star-2', - 'triangle' + 'triangle', + 'triangle-2', + 'triangle-3', + 'triangle-4', + 'half-1', + 'half-2' ] } } diff --git a/src/ui/components/display/rating/rating.stories.tsx b/src/ui/components/display/rating/rating.stories.tsx index 7e38662..aff2e98 100644 --- a/src/ui/components/display/rating/rating.stories.tsx +++ b/src/ui/components/display/rating/rating.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -18,7 +19,7 @@ const meta: Meta = { 'error' ] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, shape: { control: 'select', options: ['star', 'star-2', 'heart'] } } }; diff --git a/src/ui/components/display/stat/stat.component.tsx b/src/ui/components/display/stat/stat.component.tsx index 0a1af11..1a2bef2 100644 --- a/src/ui/components/display/stat/stat.component.tsx +++ b/src/ui/components/display/stat/stat.component.tsx @@ -6,6 +6,7 @@ const Stat = ({ title, value, description, + actions, icon: Icon, color, className @@ -32,6 +33,7 @@ const Stat = ({ {description && (
{description}
)} + {actions &&
{actions}
}
); }; diff --git a/src/ui/components/display/stat/stat.stories.tsx b/src/ui/components/display/stat/stat.stories.tsx index 61c10a1..0feb5c1 100644 --- a/src/ui/components/display/stat/stat.stories.tsx +++ b/src/ui/components/display/stat/stat.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -32,3 +33,17 @@ export const Default: Story = { color: 'success' } }; + +export const WithActions: Story = { + args: { + title: 'Account Balance', + value: '$89,400', + description: 'Available for withdrawal', + actions: ( +
+ + +
+ ) + } +}; diff --git a/src/ui/components/display/stat/stat.test.tsx b/src/ui/components/display/stat/stat.test.tsx index 4a2850a..a0c8b7b 100644 --- a/src/ui/components/display/stat/stat.test.tsx +++ b/src/ui/components/display/stat/stat.test.tsx @@ -19,4 +19,17 @@ describe('', () => { expect(screen.getByText('31K')).toBeInTheDocument(); expect(screen.getByText('Jan 1st - Feb 1st')).toBeInTheDocument(); }); + + it('renders stat actions when provided', () => { + const { container } = render( + Withdraw} + /> + ); + expect(container.querySelector('.stat-actions')).toBeInTheDocument(); + expect(screen.getByText('Withdraw')).toBeInTheDocument(); + }); }); diff --git a/src/ui/components/display/stat/stat.types.ts b/src/ui/components/display/stat/stat.types.ts index 58ab8dd..cd6d7c4 100644 --- a/src/ui/components/display/stat/stat.types.ts +++ b/src/ui/components/display/stat/stat.types.ts @@ -5,6 +5,7 @@ export interface StatType { title?: React.ReactNode; value: React.ReactNode; description?: React.ReactNode; + actions?: React.ReactNode; icon?: IconType; color?: ColorType; className?: string; diff --git a/src/ui/components/display/status/status.stories.tsx b/src/ui/components/display/status/status.stories.tsx index ba3f85b..78c49ff 100644 --- a/src/ui/components/display/status/status.stories.tsx +++ b/src/ui/components/display/status/status.stories.tsx @@ -19,7 +19,7 @@ const meta: Meta = { 'error' ] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, animate: { control: 'select', options: ['none', 'ping', 'bounce'] } } }; @@ -43,6 +43,26 @@ export const PingAnimation: Story = { } }; +export const BounceAnimation: Story = { + args: { + color: 'info', + animate: 'bounce', + label: 'New messages' + } +}; + +export const Sizes: Story = { + render: () => ( +
+ + + + + +
+ ) +}; + export const Colors: Story = { render: () => (
diff --git a/src/ui/components/display/swap/swap.component.tsx b/src/ui/components/display/swap/swap.component.tsx index 3205fef..087aff5 100644 --- a/src/ui/components/display/swap/swap.component.tsx +++ b/src/ui/components/display/swap/swap.component.tsx @@ -5,6 +5,7 @@ import type { SwapType } from '@/types'; const Swap = ({ onContent, offContent, + indeterminateContent, active, onChange, effect, @@ -32,6 +33,9 @@ const Swap = ({ />
{onContent}
{offContent}
+ {indeterminateContent && ( +
{indeterminateContent}
+ )} ); }; diff --git a/src/ui/components/display/swap/swap.stories.tsx b/src/ui/components/display/swap/swap.stories.tsx index 77cde91..8c3a74f 100644 --- a/src/ui/components/display/swap/swap.stories.tsx +++ b/src/ui/components/display/swap/swap.stories.tsx @@ -28,3 +28,11 @@ export const FlipEffect: Story = { effect: 'flip' } }; + +export const Indeterminate: Story = { + args: { + onContent: '🌞 ON', + offContent: '🌙 OFF', + indeterminateContent: '🌤️ AUTO' + } +}; diff --git a/src/ui/components/display/swap/swap.test.tsx b/src/ui/components/display/swap/swap.test.tsx index 7e35a16..2f76dfa 100644 --- a/src/ui/components/display/swap/swap.test.tsx +++ b/src/ui/components/display/swap/swap.test.tsx @@ -16,4 +16,19 @@ describe('', () => { fireEvent.click(checkbox); expect(handleChange).toHaveBeenCalledTimes(1); }); + + it('renders indeterminate content when provided', () => { + const { container } = render( + + ); + expect(container.querySelector('.swap-indeterminate')).toHaveTextContent( + 'AUTO' + ); + expect(container.querySelector('.swap')).toHaveClass('swap-flip'); + }); }); diff --git a/src/ui/components/display/swap/swap.types.ts b/src/ui/components/display/swap/swap.types.ts index 68c1008..b961002 100644 --- a/src/ui/components/display/swap/swap.types.ts +++ b/src/ui/components/display/swap/swap.types.ts @@ -3,6 +3,8 @@ export interface SwapType { onContent: React.ReactNode; /** Content shown when swap state is OFF (unchecked) */ offContent: React.ReactNode; + /** Optional content shown when swap state is indeterminate */ + indeterminateContent?: React.ReactNode; /** Active / checked boolean state */ active?: boolean; /** Callback fired when toggle changes */ diff --git a/src/ui/components/display/table/table.component.tsx b/src/ui/components/display/table/table.component.tsx index 8dac9b2..b0dee93 100644 --- a/src/ui/components/display/table/table.component.tsx +++ b/src/ui/components/display/table/table.component.tsx @@ -5,18 +5,23 @@ import type { TableType } from '@/types'; const Table = >({ columns, data, + size, zebra = true, compact, hover = true, + pinRows = false, + pinCols = false, className }: TableType) => { return (
@@ -33,7 +38,7 @@ const Table = >({ (row.key as string | number) ?? `row-${rowIndex}`; return ( - + {columns.map((col) => { const cellValue = row[col.key]; return ( diff --git a/src/ui/components/display/table/table.stories.tsx b/src/ui/components/display/table/table.stories.tsx index f793258..90687ac 100644 --- a/src/ui/components/display/table/table.stories.tsx +++ b/src/ui/components/display/table/table.stories.tsx @@ -6,8 +6,12 @@ const meta: Meta = { component: Table, tags: ['autodocs'], argTypes: { + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, zebra: { control: 'boolean' }, - compact: { control: 'boolean' } + compact: { control: 'boolean' }, + hover: { control: 'boolean' }, + pinRows: { control: 'boolean' }, + pinCols: { control: 'boolean' } } }; diff --git a/src/ui/components/display/table/table.test.tsx b/src/ui/components/display/table/table.test.tsx index db6cd0f..4014d12 100644 --- a/src/ui/components/display/table/table.test.tsx +++ b/src/ui/components/display/table/table.test.tsx @@ -20,11 +20,37 @@ describe('
', () => { ]; it('renders table headers and rows correctly', () => { - render(
); + const { container } = render( +
+ ); expect(screen.getByText('Name')).toBeInTheDocument(); expect(screen.getByText('Role')).toBeInTheDocument(); expect(screen.getByText('Alice')).toBeInTheDocument(); expect(screen.getByText('Developer')).toBeInTheDocument(); expect(screen.getByText('Edit Alice')).toBeInTheDocument(); + expect(container.querySelector('table')).toHaveClass( + 'table-zebra', + 'table-xs' + ); + expect(container.querySelector('tbody tr')).toHaveClass('row-hover'); + }); + + it('applies size, pinRows and pinCols classes', () => { + const { container } = render( +
+ ); + expect(container.querySelector('table')).toHaveClass( + 'table-lg', + 'table-pin-rows', + 'table-pin-cols' + ); + expect(container.querySelector('tbody tr')).not.toHaveClass('row-hover'); }); }); diff --git a/src/ui/components/display/table/table.types.ts b/src/ui/components/display/table/table.types.ts index 1e5b461..f45da46 100644 --- a/src/ui/components/display/table/table.types.ts +++ b/src/ui/components/display/table/table.types.ts @@ -1,3 +1,5 @@ +import type { SizeType } from '@/types'; + export interface TableColumn { key: string; header: React.ReactNode; @@ -9,8 +11,11 @@ export interface TableType< > { columns: TableColumn[]; data: T[]; + size?: SizeType; zebra?: boolean; compact?: boolean; hover?: boolean; + pinRows?: boolean; + pinCols?: boolean; className?: string; } diff --git a/src/ui/components/display/timeline/timeline.component.tsx b/src/ui/components/display/timeline/timeline.component.tsx index 6b7492b..bd242ca 100644 --- a/src/ui/components/display/timeline/timeline.component.tsx +++ b/src/ui/components/display/timeline/timeline.component.tsx @@ -21,6 +21,7 @@ const Timeline = ({ items = [], vertical = true, compact = false, + snapIcon = false, className }: TimelineType) => { return ( @@ -30,7 +31,8 @@ const Timeline = ({ { 'timeline-vertical': vertical, 'timeline-horizontal': !vertical, - 'timeline-compact': compact + 'timeline-compact': compact, + 'timeline-snap-icon': snapIcon }, className )} diff --git a/src/ui/components/display/timeline/timeline.stories.tsx b/src/ui/components/display/timeline/timeline.stories.tsx index 6d360e7..803c660 100644 --- a/src/ui/components/display/timeline/timeline.stories.tsx +++ b/src/ui/components/display/timeline/timeline.stories.tsx @@ -4,7 +4,12 @@ import Timeline from './timeline.component'; const meta: Meta = { title: '@creativecodeco-ui/Display/Timeline', component: Timeline, - tags: ['autodocs'] + tags: ['autodocs'], + argTypes: { + vertical: { control: 'boolean' }, + compact: { control: 'boolean' }, + snapIcon: { control: 'boolean' } + } }; export default meta; diff --git a/src/ui/components/display/timeline/timeline.test.tsx b/src/ui/components/display/timeline/timeline.test.tsx index 8d2742a..05b675e 100644 --- a/src/ui/components/display/timeline/timeline.test.tsx +++ b/src/ui/components/display/timeline/timeline.test.tsx @@ -16,7 +16,13 @@ describe('', () => { }); it('applies vertical/horizontal classes', () => { - const { container } = render(); - expect(container.querySelector('ul')).toHaveClass('timeline-horizontal'); + const { container } = render( + + ); + expect(container.querySelector('ul')).toHaveClass( + 'timeline-horizontal', + 'timeline-compact', + 'timeline-snap-icon' + ); }); }); diff --git a/src/ui/components/display/timeline/timeline.types.ts b/src/ui/components/display/timeline/timeline.types.ts index 49f96f9..bdd6fd1 100644 --- a/src/ui/components/display/timeline/timeline.types.ts +++ b/src/ui/components/display/timeline/timeline.types.ts @@ -18,6 +18,8 @@ export interface TimelineType { vertical?: boolean; /** Compact density layout */ compact?: boolean; + /** Snap icon to the start of the timeline item */ + snapIcon?: boolean; /** Custom CSS classes */ className?: string; } diff --git a/src/ui/components/feedback/alert/alert.component.tsx b/src/ui/components/feedback/alert/alert.component.tsx index 044933d..dbbea6d 100644 --- a/src/ui/components/feedback/alert/alert.component.tsx +++ b/src/ui/components/feedback/alert/alert.component.tsx @@ -6,6 +6,8 @@ import type { AlertType } from '@/types'; const Alert = ({ children, status = 'info', + variant, + vertical, title, icon: Icon, onClose, @@ -23,7 +25,12 @@ const Alert = ({ 'alert-info': status === 'info', 'alert-success': status === 'success', 'alert-warning': status === 'warning', - 'alert-error': status === 'error' + 'alert-error': status === 'error', + 'alert-soft': variant === 'soft', + 'alert-outline': variant === 'outline', + 'alert-dash': variant === 'dash', + 'alert-vertical': vertical === true, + 'alert-horizontal': vertical === false })} > {renderedIcon} diff --git a/src/ui/components/feedback/alert/alert.stories.tsx b/src/ui/components/feedback/alert/alert.stories.tsx index 972dfab..fddac96 100644 --- a/src/ui/components/feedback/alert/alert.stories.tsx +++ b/src/ui/components/feedback/alert/alert.stories.tsx @@ -9,6 +9,13 @@ const meta: Meta = { status: { control: 'select', options: ['info', 'success', 'warning', 'error'] + }, + variant: { + control: 'select', + options: ['soft', 'outline', 'dash'] + }, + vertical: { + control: 'boolean' } } }; @@ -47,3 +54,39 @@ export const ErrorAlert: Story = { children: 'Failed to process request. Please try again later.' } }; + +export const Soft: Story = { + args: { + status: 'info', + variant: 'soft', + title: 'Soft Alert', + children: 'DaisyUI v5 alert-soft style variant.' + } +}; + +export const Outline: Story = { + args: { + status: 'warning', + variant: 'outline', + title: 'Outline Alert', + children: 'DaisyUI v5 alert-outline style variant.' + } +}; + +export const Dash: Story = { + args: { + status: 'error', + variant: 'dash', + title: 'Dashed Alert', + children: 'DaisyUI v5 alert-dash style variant.' + } +}; + +export const Vertical: Story = { + args: { + status: 'info', + vertical: true, + title: 'Vertical Layout', + children: 'Alert content stacked vertically.' + } +}; diff --git a/src/ui/components/feedback/alert/alert.test.tsx b/src/ui/components/feedback/alert/alert.test.tsx index ed305e1..8f4e9db 100644 --- a/src/ui/components/feedback/alert/alert.test.tsx +++ b/src/ui/components/feedback/alert/alert.test.tsx @@ -34,4 +34,17 @@ describe('', () => { fireEvent.click(screen.getByLabelText('Dismiss alert')); expect(handleClose).toHaveBeenCalledTimes(1); }); + + it('applies DaisyUI v5 variant and vertical classes', () => { + render( + + Soft vertical alert + + ); + expect(screen.getByRole('alert')).toHaveClass( + 'alert-warning', + 'alert-soft', + 'alert-vertical' + ); + }); }); diff --git a/src/ui/components/feedback/alert/alert.types.ts b/src/ui/components/feedback/alert/alert.types.ts index 5f89d81..7e8abb5 100644 --- a/src/ui/components/feedback/alert/alert.types.ts +++ b/src/ui/components/feedback/alert/alert.types.ts @@ -1,9 +1,13 @@ import type { ColorType } from '@/types'; import type { IconType } from 'react-icons'; +export type AlertVariantType = 'soft' | 'outline' | 'dash'; + export interface AlertType { children: React.ReactNode; status?: Extract; + variant?: AlertVariantType; + vertical?: boolean; title?: React.ReactNode; icon?: IconType; onClose?: () => void; diff --git a/src/ui/components/feedback/modal/modal.component.tsx b/src/ui/components/feedback/modal/modal.component.tsx index 0af951d..d77843c 100644 --- a/src/ui/components/feedback/modal/modal.component.tsx +++ b/src/ui/components/feedback/modal/modal.component.tsx @@ -10,6 +10,7 @@ const Modal = ({ children, actions, size = 'md', + position, closeOnBackdropClick = true, className }: ModalType) => { @@ -30,7 +31,9 @@ const Modal = ({ return ( {onClose && ( diff --git a/src/ui/components/feedback/modal/modal.stories.tsx b/src/ui/components/feedback/modal/modal.stories.tsx index 8e3d6b2..ae3086b 100644 --- a/src/ui/components/feedback/modal/modal.stories.tsx +++ b/src/ui/components/feedback/modal/modal.stories.tsx @@ -7,7 +7,11 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { isOpen: { control: 'boolean' }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, + position: { + control: 'select', + options: ['top', 'middle', 'bottom', 'start', 'end'] + }, closeOnBackdropClick: { control: 'boolean' } } }; @@ -33,3 +37,12 @@ export const Large: Story = { 'This is a large modal window suitable for detailed content or forms.' } }; + +export const BottomPosition: Story = { + args: { + isOpen: true, + position: 'bottom', + title: 'Bottom Sheet Modal', + children: 'Modal aligned to the bottom using modal-bottom.' + } +}; diff --git a/src/ui/components/feedback/modal/modal.test.tsx b/src/ui/components/feedback/modal/modal.test.tsx index 4e251b6..0bc00e3 100644 --- a/src/ui/components/feedback/modal/modal.test.tsx +++ b/src/ui/components/feedback/modal/modal.test.tsx @@ -60,4 +60,14 @@ describe('', () => { fireEvent.keyDown(document, { key: 'Escape' }); expect(handleClose).toHaveBeenCalledTimes(1); }); + + it('applies position and xl size classes', () => { + const { container } = render( + + Bottom XL Modal + + ); + expect(container.querySelector('.modal')).toHaveClass('modal-bottom'); + expect(container.querySelector('.modal-box')).toHaveClass('max-w-5xl'); + }); }); diff --git a/src/ui/components/feedback/modal/modal.types.ts b/src/ui/components/feedback/modal/modal.types.ts index c3e63b0..11f0fab 100644 --- a/src/ui/components/feedback/modal/modal.types.ts +++ b/src/ui/components/feedback/modal/modal.types.ts @@ -1,5 +1,7 @@ import type { SizeType } from '@/types'; +export type ModalPositionType = 'top' | 'middle' | 'bottom' | 'start' | 'end'; + export interface ModalType { isOpen: boolean; onClose?: () => void; @@ -7,6 +9,7 @@ export interface ModalType { children: React.ReactNode; actions?: React.ReactNode; size?: SizeType; + position?: ModalPositionType; closeOnBackdropClick?: boolean; className?: string; } diff --git a/src/ui/components/feedback/progress/progress.stories.tsx b/src/ui/components/feedback/progress/progress.stories.tsx index ae36d57..c95b20d 100644 --- a/src/ui/components/feedback/progress/progress.stories.tsx +++ b/src/ui/components/feedback/progress/progress.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -18,7 +19,7 @@ const meta: Meta = { 'error' ] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } } }; diff --git a/src/ui/components/feedback/radial-progress/radial-progress.stories.tsx b/src/ui/components/feedback/radial-progress/radial-progress.stories.tsx index 37747df..6447b22 100644 --- a/src/ui/components/feedback/radial-progress/radial-progress.stories.tsx +++ b/src/ui/components/feedback/radial-progress/radial-progress.stories.tsx @@ -10,6 +10,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', diff --git a/src/ui/components/feedback/skeleton/skeleton.component.tsx b/src/ui/components/feedback/skeleton/skeleton.component.tsx index 733abd3..d2bac55 100644 --- a/src/ui/components/feedback/skeleton/skeleton.component.tsx +++ b/src/ui/components/feedback/skeleton/skeleton.component.tsx @@ -19,6 +19,7 @@ const Skeleton = ({ aria-hidden='true' style={style} className={cls('skeleton', className, { + 'skeleton-text': variant === 'text', 'rounded-full': variant === 'circular', 'h-4 w-full': variant === 'text' && !height, 'h-32 w-full': variant === 'rectangular' && !height diff --git a/src/ui/components/feedback/skeleton/skeleton.test.tsx b/src/ui/components/feedback/skeleton/skeleton.test.tsx index 228d306..3c0f9e9 100644 --- a/src/ui/components/feedback/skeleton/skeleton.test.tsx +++ b/src/ui/components/feedback/skeleton/skeleton.test.tsx @@ -18,4 +18,10 @@ describe('', () => { expect(skeleton).toHaveClass('rounded-full'); expect(skeleton).toHaveStyle({ width: '40px', height: '40px' }); }); + + it('applies skeleton-text class for text variant', () => { + const { container } = render(); + const skeleton = container.firstChild as HTMLElement; + expect(skeleton).toHaveClass('skeleton', 'skeleton-text'); + }); }); diff --git a/src/ui/components/feedback/spinner/spinner.component.tsx b/src/ui/components/feedback/spinner/spinner.component.tsx index 2699e1f..3f78f36 100644 --- a/src/ui/components/feedback/spinner/spinner.component.tsx +++ b/src/ui/components/feedback/spinner/spinner.component.tsx @@ -22,6 +22,7 @@ const Spinner = ({ 'loading-sm': size === 'sm', 'loading-md': size === 'md', 'loading-lg': size === 'lg', + 'loading-xl': size === 'xl', [`text-${color}`]: color })} /> diff --git a/src/ui/components/feedback/spinner/spinner.stories.tsx b/src/ui/components/feedback/spinner/spinner.stories.tsx index 26c5b9e..f449f41 100644 --- a/src/ui/components/feedback/spinner/spinner.stories.tsx +++ b/src/ui/components/feedback/spinner/spinner.stories.tsx @@ -10,10 +10,11 @@ const meta: Meta = { control: 'select', options: ['spinner', 'dots', 'ring', 'ball', 'bars', 'infinity'] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -44,3 +45,28 @@ export const Dots: Story = { color: 'secondary' } }; + +export const Variants: Story = { + render: () => ( +
+ + + + + + +
+ ) +}; + +export const Sizes: Story = { + render: () => ( +
+ + + + + +
+ ) +}; diff --git a/src/ui/components/feedback/spinner/spinner.test.tsx b/src/ui/components/feedback/spinner/spinner.test.tsx index 419dfa8..9284a32 100644 --- a/src/ui/components/feedback/spinner/spinner.test.tsx +++ b/src/ui/components/feedback/spinner/spinner.test.tsx @@ -13,10 +13,10 @@ describe('', () => { }); it('applies variant, size and color classes', () => { - render(); + render(); const spinnerElement = screen.getByRole('status'); expect(spinnerElement).toHaveClass('loading-dots'); - expect(spinnerElement).toHaveClass('loading-lg'); - expect(spinnerElement).toHaveClass('text-primary'); + expect(spinnerElement).toHaveClass('loading-xl'); + expect(spinnerElement).toHaveClass('text-neutral'); }); }); diff --git a/src/ui/components/feedback/toast/toast.component.tsx b/src/ui/components/feedback/toast/toast.component.tsx index 9a2970b..ef02638 100644 --- a/src/ui/components/feedback/toast/toast.component.tsx +++ b/src/ui/components/feedback/toast/toast.component.tsx @@ -17,6 +17,7 @@ const Toast = ({ 'toast-center': position.endsWith('-center'), 'toast-end': position.endsWith('-end'), 'toast-top': position.startsWith('top'), + 'toast-middle': position.startsWith('middle'), 'toast-bottom': position.startsWith('bottom') })} > diff --git a/src/ui/components/feedback/toast/toast.stories.tsx b/src/ui/components/feedback/toast/toast.stories.tsx index 3cb8d18..729e171 100644 --- a/src/ui/components/feedback/toast/toast.stories.tsx +++ b/src/ui/components/feedback/toast/toast.stories.tsx @@ -12,6 +12,9 @@ const meta: Meta = { 'top-start', 'top-center', 'top-end', + 'middle-start', + 'middle-center', + 'middle-end', 'bottom-start', 'bottom-center', 'bottom-end' @@ -20,6 +23,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', diff --git a/src/ui/components/feedback/toast/toast.test.tsx b/src/ui/components/feedback/toast/toast.test.tsx index b53cf46..0d9beac 100644 --- a/src/ui/components/feedback/toast/toast.test.tsx +++ b/src/ui/components/feedback/toast/toast.test.tsx @@ -19,8 +19,14 @@ describe('', () => { }); it('applies alert color class when color is provided', () => { - const { container } = render(Success Toast); + const { container } = render( + + Neutral Middle Toast + + ); + const toastElement = container.querySelector('.toast'); + expect(toastElement).toHaveClass('toast-middle', 'toast-center'); const alertElement = container.querySelector('.alert'); - expect(alertElement).toHaveClass('alert-success'); + expect(alertElement).toHaveClass('alert-neutral'); }); }); diff --git a/src/ui/components/feedback/toast/toast.types.ts b/src/ui/components/feedback/toast/toast.types.ts index 54ef143..8c5d3a9 100644 --- a/src/ui/components/feedback/toast/toast.types.ts +++ b/src/ui/components/feedback/toast/toast.types.ts @@ -4,6 +4,9 @@ export type ToastPositionType = | 'top-start' | 'top-center' | 'top-end' + | 'middle-start' + | 'middle-center' + | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'; diff --git a/src/ui/components/feedback/tooltip/tooltip.component.tsx b/src/ui/components/feedback/tooltip/tooltip.component.tsx index da3da99..12d946b 100644 --- a/src/ui/components/feedback/tooltip/tooltip.component.tsx +++ b/src/ui/components/feedback/tooltip/tooltip.component.tsx @@ -14,14 +14,14 @@ const Tooltip = ({
+ {typeof content !== 'string' && content && ( +
{content}
+ )} {children}
); diff --git a/src/ui/components/feedback/tooltip/tooltip.stories.tsx b/src/ui/components/feedback/tooltip/tooltip.stories.tsx index 59d08bc..b1e6611 100644 --- a/src/ui/components/feedback/tooltip/tooltip.stories.tsx +++ b/src/ui/components/feedback/tooltip/tooltip.stories.tsx @@ -8,11 +8,12 @@ const meta: Meta = { argTypes: { position: { control: 'select', - options: ['top', 'bottom', 'left', 'right'] + options: ['top', 'bottom', 'left', 'right', 'start', 'center', 'end'] }, color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -21,7 +22,8 @@ const meta: Meta = { 'warning', 'error' ] - } + }, + open: { control: 'boolean' } } }; @@ -35,3 +37,17 @@ export const Default: Story = { children: } }; + +export const RichContent: Story = { + args: { + content: ( +
+ ✨ + Rich JSX Tooltip +
+ ), + open: true, + color: 'primary', + children: + } +}; diff --git a/src/ui/components/feedback/tooltip/tooltip.test.tsx b/src/ui/components/feedback/tooltip/tooltip.test.tsx index e1b20bd..cbb498d 100644 --- a/src/ui/components/feedback/tooltip/tooltip.test.tsx +++ b/src/ui/components/feedback/tooltip/tooltip.test.tsx @@ -26,4 +26,27 @@ describe('', () => { expect(tooltipElement).toHaveClass('tooltip-bottom'); expect(tooltipElement).toHaveClass('tooltip-primary'); }); + + it('renders rich JSX tooltip-content when content is ReactNode', () => { + const { container } = render( + Rich Content} + position='start' + color='neutral' + open + > + Target + + ); + const tooltipElement = container.querySelector('.tooltip'); + expect(tooltipElement).toHaveClass( + 'tooltip-start', + 'tooltip-neutral', + 'tooltip-open' + ); + expect(tooltipElement).not.toHaveAttribute('data-tip'); + expect(container.querySelector('.tooltip-content')).toHaveTextContent( + 'Rich Content' + ); + }); }); diff --git a/src/ui/components/feedback/tooltip/tooltip.types.ts b/src/ui/components/feedback/tooltip/tooltip.types.ts index 1b0e71f..bf4907a 100644 --- a/src/ui/components/feedback/tooltip/tooltip.types.ts +++ b/src/ui/components/feedback/tooltip/tooltip.types.ts @@ -1,9 +1,12 @@ import type { ColorType } from '@/types'; +export type TooltipPositionType = + 'top' | 'bottom' | 'left' | 'right' | 'start' | 'center' | 'end'; + export interface TooltipType { children: React.ReactNode; content: React.ReactNode; - position?: 'top' | 'bottom' | 'left' | 'right'; + position?: TooltipPositionType; color?: ColorType; open?: boolean; className?: string; diff --git a/src/ui/components/layout/collapse/collapse.component.tsx b/src/ui/components/layout/collapse/collapse.component.tsx index 4c7966b..84ce5d1 100644 --- a/src/ui/components/layout/collapse/collapse.component.tsx +++ b/src/ui/components/layout/collapse/collapse.component.tsx @@ -6,6 +6,7 @@ const Collapse = ({ title, children, open, + close, onChange, icon = 'arrow', className @@ -15,7 +16,8 @@ const Collapse = ({ className={cls( 'collapse border border-base-300 rounded-box', { - 'collapse-open': open === true, + 'collapse-open': open === true && !close, + 'collapse-close': close === true || open === false, [`collapse-${icon}`]: Boolean(icon) }, className diff --git a/src/ui/components/layout/collapse/collapse.stories.tsx b/src/ui/components/layout/collapse/collapse.stories.tsx index 94163a1..05707a3 100644 --- a/src/ui/components/layout/collapse/collapse.stories.tsx +++ b/src/ui/components/layout/collapse/collapse.stories.tsx @@ -6,7 +6,9 @@ const meta: Meta = { component: Collapse, tags: ['autodocs'], argTypes: { - icon: { control: 'select', options: ['arrow', 'plus'] } + icon: { control: 'select', options: ['arrow', 'plus'] }, + open: { control: 'boolean' }, + close: { control: 'boolean' } } }; @@ -29,3 +31,21 @@ export const PlusIcon: Story = { icon: 'plus' } }; + +export const ForceOpen: Story = { + args: { + title: 'Always Open Collapse', + children: 'Forced open using collapse-open modifier.', + open: true, + icon: 'arrow' + } +}; + +export const ForceClose: Story = { + args: { + title: 'Always Closed Collapse', + children: 'Forced closed using collapse-close modifier.', + close: true, + icon: 'arrow' + } +}; diff --git a/src/ui/components/layout/collapse/collapse.test.tsx b/src/ui/components/layout/collapse/collapse.test.tsx index 7730dec..1be9d00 100644 --- a/src/ui/components/layout/collapse/collapse.test.tsx +++ b/src/ui/components/layout/collapse/collapse.test.tsx @@ -12,4 +12,25 @@ describe('', () => { expect(screen.getByText('Click to expand')).toBeInTheDocument(); expect(screen.getByText('Panel content details')).toBeInTheDocument(); }); + + it('applies collapse-open and collapse-close modifiers', () => { + const { container: openContainer } = render( + +
Open Content
+
+ ); + expect(openContainer.querySelector('.collapse')).toHaveClass( + 'collapse-open', + 'collapse-plus' + ); + + const { container: closeContainer } = render( + +
Closed Content
+
+ ); + expect(closeContainer.querySelector('.collapse')).toHaveClass( + 'collapse-close' + ); + }); }); diff --git a/src/ui/components/layout/collapse/collapse.types.ts b/src/ui/components/layout/collapse/collapse.types.ts index 84f3cff..96d31af 100644 --- a/src/ui/components/layout/collapse/collapse.types.ts +++ b/src/ui/components/layout/collapse/collapse.types.ts @@ -5,6 +5,8 @@ export interface CollapseType { children: React.ReactNode; /** Open / expanded boolean state */ open?: boolean; + /** Force closed boolean state */ + close?: boolean; /** Callback fired on toggle change */ onChange?: (event: React.ChangeEvent) => void; /** Indicator icon style (arrow or plus) */ diff --git a/src/ui/components/layout/divider/divider.stories.tsx b/src/ui/components/layout/divider/divider.stories.tsx index b4c65b0..14dfe39 100644 --- a/src/ui/components/layout/divider/divider.stories.tsx +++ b/src/ui/components/layout/divider/divider.stories.tsx @@ -7,7 +7,20 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { vertical: { control: 'boolean' }, - position: { control: 'select', options: ['start', 'center', 'end'] } + position: { control: 'select', options: ['start', 'center', 'end'] }, + color: { + control: 'select', + options: [ + 'neutral', + 'primary', + 'secondary', + 'accent', + 'info', + 'success', + 'warning', + 'error' + ] + } } }; diff --git a/src/ui/components/navigation/action-dropdown/action-dropdown.component.tsx b/src/ui/components/navigation/action-dropdown/action-dropdown.component.tsx index 032e544..f9c231a 100644 --- a/src/ui/components/navigation/action-dropdown/action-dropdown.component.tsx +++ b/src/ui/components/navigation/action-dropdown/action-dropdown.component.tsx @@ -54,7 +54,7 @@ const ActionDropdown = ({ {items.map((item, index) => (
  • + + )} {closeIcon && (
    {closeIcon} diff --git a/src/ui/components/navigation/fab/fab.stories.tsx b/src/ui/components/navigation/fab/fab.stories.tsx index 99c8fd1..2a4fed4 100644 --- a/src/ui/components/navigation/fab/fab.stories.tsx +++ b/src/ui/components/navigation/fab/fab.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -46,3 +47,26 @@ export const Default: Story = { flower: false } }; + +export const Flower: Story = { + render: (args) => ( +
    + +
    + ), + args: { + icon: '+', + color: 'primary', + flower: true + } +}; diff --git a/src/ui/components/navigation/fab/fab.test.tsx b/src/ui/components/navigation/fab/fab.test.tsx index 5e316c8..7ed2b20 100644 --- a/src/ui/components/navigation/fab/fab.test.tsx +++ b/src/ui/components/navigation/fab/fab.test.tsx @@ -18,4 +18,24 @@ describe('', () => { fireEvent.click(screen.getByText('✎')); expect(onEdit).toHaveBeenCalledTimes(1); }); + + it('renders fab-main-action when mainAction is provided', () => { + const onMain = jest.fn(); + const { container } = render( + + ); + const mainActionEl = container.querySelector('.fab-main-action'); + expect(mainActionEl).toBeInTheDocument(); + expect(screen.getByText('Primary Action')).toBeInTheDocument(); + fireEvent.click(screen.getByText('★')); + expect(onMain).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/ui/components/navigation/fab/fab.types.ts b/src/ui/components/navigation/fab/fab.types.ts index b9ff638..80e73a0 100644 --- a/src/ui/components/navigation/fab/fab.types.ts +++ b/src/ui/components/navigation/fab/fab.types.ts @@ -20,6 +20,8 @@ export interface FabType { color?: ColorType; /** Speed dial action items */ actions?: FabActionType[]; + /** Optional main action shown when FAB opens */ + mainAction?: FabActionType; /** Enable flower (quarter-circle) radial layout */ flower?: boolean; /** Optional close button label/icon when open */ diff --git a/src/ui/components/navigation/mega-menu/mega-menu.stories.tsx b/src/ui/components/navigation/mega-menu/mega-menu.stories.tsx index 0810276..e5b5d5a 100644 --- a/src/ui/components/navigation/mega-menu/mega-menu.stories.tsx +++ b/src/ui/components/navigation/mega-menu/mega-menu.stories.tsx @@ -6,7 +6,7 @@ const meta: Meta = { component: MegaMenu, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, variant: { control: 'select', options: ['default', 'wide', 'full'] }, vertical: { control: 'boolean' } } diff --git a/src/ui/components/navigation/menu/menu.component.tsx b/src/ui/components/navigation/menu/menu.component.tsx index c542675..b2215fe 100644 --- a/src/ui/components/navigation/menu/menu.component.tsx +++ b/src/ui/components/navigation/menu/menu.component.tsx @@ -15,17 +15,17 @@ const renderMenuItem = (item: MenuItemType, index: number) => { } const hasSubmenu = Boolean(item.children && item.children.length > 0); + const itemClasses = cls({ + 'menu-active': item.active, + 'menu-focus': item.focus, + 'menu-disabled': item.disabled + }); let content; if (hasSubmenu) { content = (
    - + {item.icon} {item.label} {item.badge && {item.badge}} @@ -42,10 +42,7 @@ const renderMenuItem = (item: MenuItemType, index: number) => { {item.icon} {item.label} @@ -58,10 +55,7 @@ const renderMenuItem = (item: MenuItemType, index: number) => { type='button' onClick={item.onClick} disabled={item.disabled} - className={cls({ - active: item.active, - disabled: item.disabled - })} + className={itemClasses} > {item.icon} {item.label} @@ -70,7 +64,11 @@ const renderMenuItem = (item: MenuItemType, index: number) => { ); } - return
  • {content}
  • ; + return ( +
  • + {content} +
  • + ); }; const Menu = ({ diff --git a/src/ui/components/navigation/menu/menu.stories.tsx b/src/ui/components/navigation/menu/menu.stories.tsx index 8d2a477..e3d39bc 100644 --- a/src/ui/components/navigation/menu/menu.stories.tsx +++ b/src/ui/components/navigation/menu/menu.stories.tsx @@ -6,7 +6,7 @@ const meta: Meta = { component: Menu, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } } }; diff --git a/src/ui/components/navigation/menu/menu.test.tsx b/src/ui/components/navigation/menu/menu.test.tsx index 8cdece1..b19c95a 100644 --- a/src/ui/components/navigation/menu/menu.test.tsx +++ b/src/ui/components/navigation/menu/menu.test.tsx @@ -7,14 +7,23 @@ describe('', () => { const items = [ { id: 1, label: 'Header Title', isTitle: true }, { id: 2, label: 'Item 1', active: true, onClick: handleClick }, - { id: 3, label: 'Item 2', disabled: true } + { id: 3, label: 'Item 2', disabled: true }, + { id: 4, label: 'Item 3', focus: true } ]; it('renders menu title and items correctly', () => { - render(); + const { container } = render(); expect(screen.getByText('Header Title')).toBeInTheDocument(); - expect(screen.getByText('Item 1')).toBeInTheDocument(); - expect(screen.getByText('Item 2')).toBeInTheDocument(); + expect(screen.getByText('Item 1').closest('button')).toHaveClass( + 'menu-active' + ); + expect(screen.getByText('Item 2').closest('li')).toHaveClass( + 'menu-disabled' + ); + expect(screen.getByText('Item 3').closest('button')).toHaveClass( + 'menu-focus' + ); + expect(container.querySelector('.menu')).toBeInTheDocument(); }); it('handles item clicks', () => { diff --git a/src/ui/components/navigation/menu/menu.types.ts b/src/ui/components/navigation/menu/menu.types.ts index ffcb3ec..9d04457 100644 --- a/src/ui/components/navigation/menu/menu.types.ts +++ b/src/ui/components/navigation/menu/menu.types.ts @@ -6,6 +6,7 @@ export interface MenuItemType { icon?: React.ReactNode; badge?: React.ReactNode; active?: boolean; + focus?: boolean; disabled?: boolean; href?: string; onClick?: (event: React.MouseEvent) => void; diff --git a/src/ui/components/navigation/pagination/pagination.component.tsx b/src/ui/components/navigation/pagination/pagination.component.tsx index c36c99e..982675f 100644 --- a/src/ui/components/navigation/pagination/pagination.component.tsx +++ b/src/ui/components/navigation/pagination/pagination.component.tsx @@ -16,10 +16,7 @@ const Pagination = ({
  • ); diff --git a/src/ui/components/navigation/steps/steps.stories.tsx b/src/ui/components/navigation/steps/steps.stories.tsx index 19fe2ab..0ebca53 100644 --- a/src/ui/components/navigation/steps/steps.stories.tsx +++ b/src/ui/components/navigation/steps/steps.stories.tsx @@ -23,3 +23,14 @@ export const Default: Story = { ] } }; + +export const WithIcons: Story = { + args: { + activeStep: 2, + items: [ + { title: 'Step 1', icon: '★', color: 'primary' }, + { title: 'Step 2', icon: '✓', color: 'primary' }, + { title: 'Step 3', icon: '●', color: 'neutral' } + ] + } +}; diff --git a/src/ui/components/navigation/steps/steps.test.tsx b/src/ui/components/navigation/steps/steps.test.tsx index e436cfa..93be8eb 100644 --- a/src/ui/components/navigation/steps/steps.test.tsx +++ b/src/ui/components/navigation/steps/steps.test.tsx @@ -17,10 +17,20 @@ describe('', () => { expect(screen.getByText('Payment')).toBeInTheDocument(); }); - it('applies step-primary to completed steps', () => { - const { container } = render(); + it('applies step-primary to completed steps and renders step-icon when icon is provided', () => { + const { container } = render( + ✓ }, + { title: 'Choose Plan' }, + { title: 'Payment' } + ]} + activeStep={1} + /> + ); const stepElements = container.querySelectorAll('.step'); expect(stepElements[0]).toHaveClass('step-primary'); + expect(stepElements[0].querySelector('.step-icon')).toHaveTextContent('✓'); expect(stepElements[1]).toHaveClass('step-primary'); expect(stepElements[2]).not.toHaveClass('step-primary'); }); diff --git a/src/ui/components/navigation/tabs/tabs.component.tsx b/src/ui/components/navigation/tabs/tabs.component.tsx index 344d309..0d981a2 100644 --- a/src/ui/components/navigation/tabs/tabs.component.tsx +++ b/src/ui/components/navigation/tabs/tabs.component.tsx @@ -8,6 +8,7 @@ const Tabs = ({ activeId: controlledActiveId, onChange, variant = 'bordered', + position, size = 'md', className }: TabsType) => { @@ -64,13 +65,12 @@ const Tabs = ({
    {items.map((item, index) => { diff --git a/src/ui/components/navigation/tabs/tabs.stories.tsx b/src/ui/components/navigation/tabs/tabs.stories.tsx index e379f6f..98519d7 100644 --- a/src/ui/components/navigation/tabs/tabs.stories.tsx +++ b/src/ui/components/navigation/tabs/tabs.stories.tsx @@ -6,8 +6,15 @@ const meta: Meta = { component: Tabs, tags: ['autodocs'], argTypes: { - variant: { control: 'select', options: ['bordered', 'lifted', 'boxed'] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + variant: { + control: 'select', + options: ['border', 'lift', 'box', 'bordered', 'lifted', 'boxed'] + }, + position: { + control: 'select', + options: ['top', 'bottom'] + }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } } }; @@ -16,7 +23,7 @@ type Story = StoryObj; export const Default: Story = { args: { - variant: 'bordered', + variant: 'border', items: [ { id: '1', label: 'Overview', content: 'Overview panel content' }, { id: '2', label: 'Settings', content: 'Settings panel content' }, @@ -30,9 +37,20 @@ export const Default: Story = { } }; +export const Lifted: Story = { + args: { + variant: 'lift', + items: [ + { id: 'tab-1', label: 'Tab 1', content: 'Tab 1 lifted content' }, + { id: 'tab-2', label: 'Tab 2', content: 'Tab 2 lifted content' }, + { id: 'tab-3', label: 'Tab 3', content: 'Tab 3 lifted content' } + ] + } +}; + export const Boxed: Story = { args: { - variant: 'boxed', + variant: 'box', items: [ { id: 'tab-1', label: 'HTML', content: '

    HTML code

    ' }, { id: 'tab-2', label: 'JSX', content: '' }, @@ -40,3 +58,14 @@ export const Boxed: Story = { ] } }; + +export const BottomPosition: Story = { + args: { + variant: 'lift', + position: 'bottom', + items: [ + { id: 'tab-1', label: 'First', content: 'Content above bottom tabs' }, + { id: 'tab-2', label: 'Second', content: 'Second panel content' } + ] + } +}; diff --git a/src/ui/components/navigation/tabs/tabs.test.tsx b/src/ui/components/navigation/tabs/tabs.test.tsx index f6ada27..6ec6946 100644 --- a/src/ui/components/navigation/tabs/tabs.test.tsx +++ b/src/ui/components/navigation/tabs/tabs.test.tsx @@ -16,7 +16,12 @@ describe('', () => { ]; it('renders all tabs and first tab content by default', () => { - render(); + render(); + expect(screen.getByRole('tablist')).toHaveClass( + 'tabs-lift', + 'tabs-xl', + 'tabs-bottom' + ); expect(screen.getByText('Tab One')).toBeInTheDocument(); expect(screen.getByText('Tab Two')).toBeInTheDocument(); expect(screen.getByText('Content 1')).toBeInTheDocument(); diff --git a/src/ui/components/navigation/tabs/tabs.types.ts b/src/ui/components/navigation/tabs/tabs.types.ts index 4ddedba..977ba0e 100644 --- a/src/ui/components/navigation/tabs/tabs.types.ts +++ b/src/ui/components/navigation/tabs/tabs.types.ts @@ -13,7 +13,8 @@ export interface TabsType { items: TabItem[]; activeId?: string; onChange?: (id: string) => void; - variant?: 'bordered' | 'lifted' | 'boxed'; + variant?: 'bordered' | 'lifted' | 'boxed' | 'border' | 'lift' | 'box'; + position?: 'top' | 'bottom'; size?: SizeType; className?: string; } diff --git a/src/ui/forms/checkbox/checkbox.stories.tsx b/src/ui/forms/checkbox/checkbox.stories.tsx index d8bd253..d807918 100644 --- a/src/ui/forms/checkbox/checkbox.stories.tsx +++ b/src/ui/forms/checkbox/checkbox.stories.tsx @@ -7,48 +7,17 @@ const meta: Meta = { component: Checkbox, tags: ['autodocs'], argTypes: { - name: { - description: 'Unique Name', - type: { name: 'string', required: true } - }, - label: { - description: 'Label', - type: 'string' - }, - isError: { - description: 'Checkbox has error', - type: 'boolean', - table: { - defaultValue: { summary: 'false' } - } - }, - error: { - description: 'Error message', - type: 'string' - }, - disabled: { - description: 'Disabled', - type: 'boolean', - table: { - defaultValue: { summary: 'false' } - } - }, - position: { - description: 'Position', - type: 'string', - options: ['left', 'right'], - control: { type: 'radio' }, - table: { - type: { - summary: 'string' - }, - defaultValue: { summary: 'left' } - } - }, + name: { control: 'text', description: 'Checkbox input name' }, + label: { control: 'text', description: 'Checkbox label text' }, + checked: { control: 'boolean' }, + isError: { control: 'boolean' }, + error: { control: 'text' }, + disabled: { control: 'boolean' }, + position: { control: 'radio', options: ['left', 'right'] }, color: { - description: 'Color', - type: 'string', + control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -56,21 +25,9 @@ const meta: Meta = { 'warning', 'info', 'error' - ], - control: { type: 'select' } + ] }, - size: { - description: 'Size', - type: 'string', - options: ['xs', 'sm', 'md', 'lg'], - control: { type: 'select' }, - table: { - type: { - summary: 'string' - }, - defaultValue: { summary: 'md' } - } - } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } }, args: { name: 'checkbox', @@ -86,124 +43,70 @@ Checkbox.displayName = 'Checkbox'; type Story = StoryObj; -export const Primary: Story = { - args: {} -}; - -export const Checked: Story = { - args: { - checked: true - } -}; - -export const Label: Story = { - args: { - label: 'Eres humano?', - checked: true - } -}; - -export const Left: Story = { - args: { - label: 'Eres humano?', - position: 'left' - } -}; - -export const Right: Story = { - args: { - label: 'Eres humano?', - position: 'right' - } -}; - -export const Disabled: Story = { - args: { - label: 'Eres humano?', - disabled: true - } -}; - -export const ColorPrimary: Story = { - args: { - label: 'Eres humano?', - color: 'primary', - checked: true - } -}; - -export const ColorSecondary: Story = { - args: { - label: 'Eres humano?', - color: 'secondary', - checked: true - } -}; - -export const ColorAccent: Story = { - args: { - label: 'Eres humano?', - color: 'accent', - checked: true - } -}; - -export const ColorSuccess: Story = { - args: { - label: 'Eres humano?', - color: 'success', - checked: true - } -}; - -export const ColorWarning: Story = { - args: { - label: 'Eres humano?', - color: 'warning', - checked: true - } -}; - -export const ColorInfo: Story = { - args: { - label: 'Eres humano?', - color: 'info', - checked: true - } -}; - -export const ColorError: Story = { - args: { - label: 'Eres humano?', - color: 'error', - checked: true - } -}; - -export const SizeXs: Story = { - args: { - size: 'xs', - checked: true - } -}; - -export const SizeSm: Story = { - args: { - size: 'sm', - checked: true - } -}; - -export const SizeMd: Story = { - args: { - size: 'md', - checked: true - } -}; - -export const SizeLg: Story = { - args: { - size: 'lg', - checked: true - } -}; +const createCheckboxStory = (args: Story['args'] = {}): Story => ({ args }); + +export const Primary: Story = createCheckboxStory(); +export const Checked: Story = createCheckboxStory({ checked: true }); +export const Label: Story = createCheckboxStory({ + label: 'Eres humano?', + checked: true +}); +export const Left: Story = createCheckboxStory({ + label: 'Eres humano?', + position: 'left' +}); +export const Right: Story = createCheckboxStory({ + label: 'Eres humano?', + position: 'right' +}); +export const Disabled: Story = createCheckboxStory({ + label: 'Eres humano?', + disabled: true +}); + +export const ColorNeutral: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'neutral', + checked: true +}); +export const ColorPrimary: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'primary', + checked: true +}); +export const ColorSecondary: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'secondary', + checked: true +}); +export const ColorAccent: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'accent', + checked: true +}); +export const ColorSuccess: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'success', + checked: true +}); +export const ColorWarning: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'warning', + checked: true +}); +export const ColorInfo: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'info', + checked: true +}); +export const ColorError: Story = createCheckboxStory({ + label: 'Eres humano?', + color: 'error', + checked: true +}); + +export const SizeXs: Story = createCheckboxStory({ size: 'xs', checked: true }); +export const SizeSm: Story = createCheckboxStory({ size: 'sm', checked: true }); +export const SizeMd: Story = createCheckboxStory({ size: 'md', checked: true }); +export const SizeLg: Story = createCheckboxStory({ size: 'lg', checked: true }); +export const SizeXl: Story = createCheckboxStory({ size: 'xl', checked: true }); diff --git a/src/ui/forms/file-input/file-input.component.tsx b/src/ui/forms/file-input/file-input.component.tsx index 39ca71d..7457a04 100644 --- a/src/ui/forms/file-input/file-input.component.tsx +++ b/src/ui/forms/file-input/file-input.component.tsx @@ -12,6 +12,7 @@ const FileInput = forwardRef( color, size = 'md', bordered = true, + ghost = false, className, id, ...restProps @@ -20,7 +21,8 @@ const FileInput = forwardRef( ) => { const hasError = Boolean(isError || error); const inputClasses = cls('file-input w-full', className, { - 'file-input-bordered': bordered, + 'file-input-bordered': bordered && !ghost, + 'file-input-ghost': ghost, [`file-input-${color}`]: Boolean(color), [`file-input-${size}`]: Boolean(size), 'file-input-error': hasError diff --git a/src/ui/forms/file-input/file-input.stories.tsx b/src/ui/forms/file-input/file-input.stories.tsx index f4aa857..d000a77 100644 --- a/src/ui/forms/file-input/file-input.stories.tsx +++ b/src/ui/forms/file-input/file-input.stories.tsx @@ -6,10 +6,11 @@ const meta: Meta = { component: FileInput, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -18,7 +19,8 @@ const meta: Meta = { 'warning', 'error' ] - } + }, + ghost: { control: 'boolean' } } }; @@ -31,3 +33,10 @@ export const Default: Story = { color: 'primary' } }; + +export const Ghost: Story = { + args: { + label: 'Upload attachment (ghost)', + ghost: true + } +}; diff --git a/src/ui/forms/file-input/file-input.test.tsx b/src/ui/forms/file-input/file-input.test.tsx index f3ddffa..a7d38f2 100644 --- a/src/ui/forms/file-input/file-input.test.tsx +++ b/src/ui/forms/file-input/file-input.test.tsx @@ -26,4 +26,14 @@ describe('', () => { render(); expect(screen.getByRole('alert')).toHaveTextContent('File is too large'); }); + + it('applies ghost, size xl and color neutral classes', () => { + const { container } = render(); + const input = container.querySelector('input[type="file"]'); + expect(input).toHaveClass( + 'file-input-ghost', + 'file-input-xl', + 'file-input-neutral' + ); + }); }); diff --git a/src/ui/forms/file-input/file-input.types.ts b/src/ui/forms/file-input/file-input.types.ts index 5abe7e5..90bef1e 100644 --- a/src/ui/forms/file-input/file-input.types.ts +++ b/src/ui/forms/file-input/file-input.types.ts @@ -6,4 +6,5 @@ export interface FileInputType color?: ColorType; size?: SizeType; bordered?: boolean; + ghost?: boolean; } diff --git a/src/ui/forms/filter/filter.stories.tsx b/src/ui/forms/filter/filter.stories.tsx index 51b5123..d909ea2 100644 --- a/src/ui/forms/filter/filter.stories.tsx +++ b/src/ui/forms/filter/filter.stories.tsx @@ -7,7 +7,7 @@ const meta: Meta = { component: Filter, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] } } }; diff --git a/src/ui/forms/otp/otp.stories.tsx b/src/ui/forms/otp/otp.stories.tsx index 082d5a2..def25ef 100644 --- a/src/ui/forms/otp/otp.stories.tsx +++ b/src/ui/forms/otp/otp.stories.tsx @@ -19,7 +19,7 @@ const meta: Meta = { 'error' ] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, joined: { control: 'boolean' }, disabled: { control: 'boolean' } } diff --git a/src/ui/forms/radio/radio.stories.tsx b/src/ui/forms/radio/radio.stories.tsx index 7186e19..367319b 100644 --- a/src/ui/forms/radio/radio.stories.tsx +++ b/src/ui/forms/radio/radio.stories.tsx @@ -7,77 +7,27 @@ const meta: Meta = { component: Radio, tags: ['autodocs'], argTypes: { - name: { - description: 'Unique Name', - type: { name: 'string', required: true } - }, - label: { - description: 'Label', - type: 'string' - }, - isError: { - description: 'Radio has error', - type: 'boolean', - table: { - defaultValue: { summary: 'false' } - } - }, - error: { - description: 'Error message', - type: 'string' - }, - checked: { - description: 'Checked', - type: 'boolean', - table: { - defaultValue: { summary: 'false' } - } - }, - disabled: { - description: 'Disabled', - type: 'boolean', - table: { - defaultValue: { summary: 'false' } - } - }, - position: { - description: 'Position', - type: 'string', - options: ['left', 'right'], - control: { type: 'radio' }, - table: { - type: { - summary: 'string' - }, - defaultValue: { summary: 'left' } - } - }, + name: { control: 'text', description: 'Radio group name' }, + label: { control: 'text', description: 'Radio option label' }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, color: { - description: 'Color', - type: 'string', + control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', + 'info', 'success', 'warning', - 'info', 'error' - ], - control: { type: 'select' } + ] }, - size: { - description: 'Size', - type: 'string', - options: ['xs', 'sm', 'md', 'lg'], - control: { type: 'select' }, - table: { - type: { - summary: 'string' - }, - defaultValue: { summary: 'md' } - } - } + position: { control: 'inline-radio', options: ['left', 'right'] }, + checked: { control: 'boolean' }, + disabled: { control: 'boolean' }, + isError: { control: 'boolean' }, + error: { control: 'text' } }, args: { name: 'radio', @@ -92,142 +42,92 @@ Radio.displayName = 'Radio'; type Story = StoryObj; -export const Primary: Story = { - args: { - name: 'radio-primary' - } -}; - -export const Checked: Story = { - args: { - name: 'radio-checked', - checked: true - } -}; - -export const Label: Story = { - args: { - name: 'radio-label', - label: 'Eres humano?', - checked: true - } -}; - -export const Left: Story = { - args: { - name: 'radio-left', - label: 'Eres humano?', - position: 'left' - } -}; - -export const Right: Story = { - args: { - name: 'radio-right', - label: 'Eres humano?', - position: 'right' - } -}; - -export const Disabled: Story = { - args: { - name: 'radio-disabled', - label: 'Eres humano?', - disabled: true - } -}; - -export const ColorPrimary: Story = { - args: { - name: 'radio-color-primary', - label: 'Eres humano?', - color: 'primary', - checked: true - } -}; - -export const ColorSecondary: Story = { - args: { - name: 'radio-color-secondary', - label: 'Eres humano?', - color: 'secondary', - checked: true - } -}; - -export const ColorAccent: Story = { - args: { - name: 'radio-color-accent', - label: 'Eres humano?', - color: 'accent', - checked: true - } -}; - -export const ColorSuccess: Story = { - args: { - name: 'radio-color-success', - label: 'Eres humano?', - color: 'success', - checked: true - } -}; - -export const ColorWarning: Story = { - args: { - name: 'radio-color-warning', - label: 'Eres humano?', - color: 'warning', - checked: true - } -}; - -export const ColorInfo: Story = { - args: { - name: 'radio-color-info', - label: 'Eres humano?', - color: 'info', - checked: true - } -}; - -export const ColorError: Story = { - args: { - name: 'radio-color-error', - label: 'Eres humano?', - color: 'error', - checked: true - } -}; - -export const SizeXs: Story = { - args: { - name: 'radio-size-xs', - size: 'xs', - checked: true - } -}; - -export const SizeSm: Story = { - args: { - name: 'radio-size-sm', - size: 'sm', - checked: true - } -}; - -export const SizeMd: Story = { - args: { - name: 'radio-size-md', - size: 'md', - checked: true - } -}; - -export const SizeLg: Story = { - args: { - name: 'radio-size-lg', - size: 'lg', - checked: true - } -}; +const createRadioStory = ( + name: string, + args: Partial = {} +): Story => ({ + args: { name, ...args } +}); + +export const Primary: Story = createRadioStory('radio-primary'); +export const Checked: Story = createRadioStory('radio-checked', { + checked: true +}); +export const Label: Story = createRadioStory('radio-label', { + label: 'Eres humano?', + checked: true +}); +export const Left: Story = createRadioStory('radio-left', { + label: 'Eres humano?', + position: 'left' +}); +export const Right: Story = createRadioStory('radio-right', { + label: 'Eres humano?', + position: 'right' +}); +export const Disabled: Story = createRadioStory('radio-disabled', { + label: 'Eres humano?', + disabled: true +}); + +export const ColorNeutral: Story = createRadioStory('radio-color-neutral', { + label: 'Eres humano?', + color: 'neutral', + checked: true +}); +export const ColorPrimary: Story = createRadioStory('radio-color-primary', { + label: 'Eres humano?', + color: 'primary', + checked: true +}); +export const ColorSecondary: Story = createRadioStory('radio-color-secondary', { + label: 'Eres humano?', + color: 'secondary', + checked: true +}); +export const ColorAccent: Story = createRadioStory('radio-color-accent', { + label: 'Eres humano?', + color: 'accent', + checked: true +}); +export const ColorSuccess: Story = createRadioStory('radio-color-success', { + label: 'Eres humano?', + color: 'success', + checked: true +}); +export const ColorWarning: Story = createRadioStory('radio-color-warning', { + label: 'Eres humano?', + color: 'warning', + checked: true +}); +export const ColorInfo: Story = createRadioStory('radio-color-info', { + label: 'Eres humano?', + color: 'info', + checked: true +}); +export const ColorError: Story = createRadioStory('radio-color-error', { + label: 'Eres humano?', + color: 'error', + checked: true +}); + +export const SizeXs: Story = createRadioStory('radio-size-xs', { + size: 'xs', + checked: true +}); +export const SizeSm: Story = createRadioStory('radio-size-sm', { + size: 'sm', + checked: true +}); +export const SizeMd: Story = createRadioStory('radio-size-md', { + size: 'md', + checked: true +}); +export const SizeLg: Story = createRadioStory('radio-size-lg', { + size: 'lg', + checked: true +}); +export const SizeXl: Story = createRadioStory('radio-size-xl', { + size: 'xl', + checked: true +}); diff --git a/src/ui/forms/range/range.component.tsx b/src/ui/forms/range/range.component.tsx index af53b7f..8093ea5 100644 --- a/src/ui/forms/range/range.component.tsx +++ b/src/ui/forms/range/range.component.tsx @@ -14,6 +14,7 @@ const Range = forwardRef( label, color, size = 'md', + vertical = false, disabled, isError, error, @@ -46,6 +47,7 @@ const Range = forwardRef( className={cls('range', { [`range-${color}`]: Boolean(color), [`range-${size}`]: Boolean(size), + 'range-vertical': vertical, 'range-error': Boolean(isError || error) })} /> diff --git a/src/ui/forms/range/range.stories.tsx b/src/ui/forms/range/range.stories.tsx index fd8ea52..ead656a 100644 --- a/src/ui/forms/range/range.stories.tsx +++ b/src/ui/forms/range/range.stories.tsx @@ -9,6 +9,7 @@ const meta: Meta = { color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -18,7 +19,8 @@ const meta: Meta = { 'error' ] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] } + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, + vertical: { control: 'boolean' } } }; @@ -48,10 +50,20 @@ export const WithError: Story = { export const Sizes: Story = { render: () => (
    - - - - + + + + +
    ) }; + +export const Vertical: Story = { + args: { + label: 'Vertical Range', + value: 60, + vertical: true, + color: 'primary' + } +}; diff --git a/src/ui/forms/range/range.test.tsx b/src/ui/forms/range/range.test.tsx index 86406d8..dd57d0f 100644 --- a/src/ui/forms/range/range.test.tsx +++ b/src/ui/forms/range/range.test.tsx @@ -29,7 +29,11 @@ describe('', () => { }); it('applies color and size classes', () => { - render(); - expect(screen.getByRole('slider')).toHaveClass('range-primary', 'range-lg'); + render(); + expect(screen.getByRole('slider')).toHaveClass( + 'range-neutral', + 'range-xl', + 'range-vertical' + ); }); }); diff --git a/src/ui/forms/range/range.types.ts b/src/ui/forms/range/range.types.ts index 7af5d47..bca21b0 100644 --- a/src/ui/forms/range/range.types.ts +++ b/src/ui/forms/range/range.types.ts @@ -17,6 +17,8 @@ export interface RangeType extends ErrorType { color?: ColorType; /** Size variant */ size?: SizeType; + /** Vertical orientation */ + vertical?: boolean; /** Disabled state */ disabled?: boolean; /** Input name attribute */ diff --git a/src/ui/forms/select/select.stories.tsx b/src/ui/forms/select/select.stories.tsx index ba9764e..972a42b 100644 --- a/src/ui/forms/select/select.stories.tsx +++ b/src/ui/forms/select/select.stories.tsx @@ -19,7 +19,7 @@ const meta: Meta = { 'error' ] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, ghost: { control: 'boolean' }, disabled: { control: 'boolean' } } @@ -43,6 +43,18 @@ export const Default: Story = { } }; +export const Ghost: Story = { + args: { + id: 'ghost-select', + label: 'Ghost Select', + ghost: true, + options: [ + { value: 'react', label: 'React 19' }, + { value: 'nextjs', label: 'Next.js' } + ] + } +}; + export const WithError: Story = { args: { id: 'error-select', diff --git a/src/ui/forms/textarea/textarea.component.tsx b/src/ui/forms/textarea/textarea.component.tsx index ff99210..46771a4 100644 --- a/src/ui/forms/textarea/textarea.component.tsx +++ b/src/ui/forms/textarea/textarea.component.tsx @@ -12,6 +12,7 @@ const TextArea = forwardRef( color, size = 'md', bordered = true, + ghost = false, className, rows = 4, id, @@ -32,7 +33,8 @@ const TextArea = forwardRef( rows={rows} aria-invalid={Boolean(isError || error) || undefined} className={cls('textarea w-full', className, { - 'textarea-bordered': bordered, + 'textarea-bordered': bordered && !ghost, + 'textarea-ghost': ghost, [`textarea-${color}`]: Boolean(color), [`textarea-${size}`]: Boolean(size), 'textarea-error': Boolean(isError || error) diff --git a/src/ui/forms/textarea/textarea.stories.tsx b/src/ui/forms/textarea/textarea.stories.tsx index 469aa21..6d92770 100644 --- a/src/ui/forms/textarea/textarea.stories.tsx +++ b/src/ui/forms/textarea/textarea.stories.tsx @@ -6,10 +6,11 @@ const meta: Meta = { component: TextArea, tags: ['autodocs'], argTypes: { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, + size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, color: { control: 'select', options: [ + 'neutral', 'primary', 'secondary', 'accent', @@ -18,7 +19,8 @@ const meta: Meta = { 'warning', 'error' ] - } + }, + ghost: { control: 'boolean' } } }; @@ -33,6 +35,14 @@ export const Default: Story = { } }; +export const Ghost: Story = { + args: { + label: 'Ghost TextArea', + placeholder: 'Borderless textarea...', + ghost: true + } +}; + export const WithError: Story = { args: { label: 'Summary', diff --git a/src/ui/forms/textarea/textarea.test.tsx b/src/ui/forms/textarea/textarea.test.tsx index 9d4d6ad..0be1129 100644 --- a/src/ui/forms/textarea/textarea.test.tsx +++ b/src/ui/forms/textarea/textarea.test.tsx @@ -24,4 +24,13 @@ describe('