From e4c116c6f3754ca597b0b703d526c4bb55531f72 Mon Sep 17 00:00:00 2001 From: Andres Date: Sat, 23 Aug 2025 20:58:13 +0200 Subject: [PATCH 1/6] [ADD] Chip component --- src/components/atoms/chip/Chip.stories.tsx | 368 +++++++++++++++++++++ src/components/atoms/chip/Chip.tsx | 33 ++ src/components/atoms/chip/index.ts | 3 + src/components/atoms/chip/types.ts | 347 +++++++++++++++++++ src/components/atoms/chip/useChip.ts | 176 ++++++++++ 5 files changed, 927 insertions(+) create mode 100644 src/components/atoms/chip/Chip.stories.tsx create mode 100644 src/components/atoms/chip/Chip.tsx create mode 100644 src/components/atoms/chip/index.ts create mode 100644 src/components/atoms/chip/types.ts create mode 100644 src/components/atoms/chip/useChip.ts diff --git a/src/components/atoms/chip/Chip.stories.tsx b/src/components/atoms/chip/Chip.stories.tsx new file mode 100644 index 00000000..26c24b00 --- /dev/null +++ b/src/components/atoms/chip/Chip.stories.tsx @@ -0,0 +1,368 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Check, Trash2 } from 'lucide-react'; +import React, { useState } from 'react'; +import Avatar from '../avatar/Avatar'; +import IconButton from '../icon-button'; +import Icon from '../icon/Icon'; +import { Chip } from './Chip'; + +/** + * ## DESCRIPTION + * Chip component is a compact element used to display statuses, keywords, or quick actions. + * + * Common use cases include tags, filters, and state indicators in dense interfaces. + * + * - Customizable in color, size, variant, radius and animation. + * - Supports `startContent` / `endContent` (icons or text), optional avatar, and `dot` indicator. + * - Optional interactivity: clickable (`as="button"`), selectable (controlled or uncontrolled), and closable. + * - Accessible via the `ariaLabel` prop when using `variant="dot"` without text. + */ + +const meta: Meta = { + title: 'Atoms/Chip', + component: Chip, + parameters: { + docs: { + autodocs: true + } + }, + tags: ['autodocs'], + argTypes: { + color: { control: 'select', options: ['primary', 'secondary', 'success', 'warning', 'danger'] }, + size: { control: 'select', options: ['sm', 'md', 'lg'] }, + variant: { control: 'select', options: ['solid', 'bordered', 'light', 'flat', 'faded', 'shadow', 'dot'] }, + radius: { control: 'select', options: ['none', 'sm', 'md', 'lg', 'full'] }, + animation: { control: 'select', options: ['default', 'pulse', 'bounce', 'ping'] }, + as: { control: 'select', options: ['div', 'button'] }, + selectable: { control: 'boolean' }, + selected: { control: 'boolean' }, + defaultSelected: { control: 'boolean' }, + closable: { control: 'boolean' }, + onClose: { action: 'onClose' }, + onClick: { action: 'onClick' }, + onSelectedChange: { action: 'onSelectedChange' } + } +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Chip', + color: 'primary', + variant: 'solid', + size: 'md', + animation: 'default', + as: 'div', + selectable: false, + closable: false + } +}; + +/** + * The `size` prop adjusts height, horizontal padding and font-size of the chip. + * + * Available options: + * - `sm` → Small + * - `md` → Medium (default) + * - `lg` → Large + */ +export const Size: Story = { + args: { + variant: 'light' + }, + + render: () => ( +
+ Small + Medium + Large +
+ ) +}; + +/** + * The `color` prop sets background and text color. + * + * Available options: + * - `primary` + * - `secondary` + * - `success` + * - `warning` + * - `danger` + */ +export const Color: Story = { + render: () => ( +
+ Primary + Secondary + Success + Warning + Danger +
+ ) +}; + +/** + * The `variant` prop defines visual style modifications for the chip. + * + * Available options: + * - `solid` → Default filled appearance. + * - `flat` → Lower background opacity, text remains solid. + * - `shadow` → Adds a soft shadow. + * - `bordered` → Outline style. + * - `light`/`faded` → Softer neutral looks. + * - `dot` → Prepends a circular status indicator; combine with text or use `ariaLabel` when text is absent. + */ +export const Variant: Story = { + render: () => ( +
+ Solid + Flat + Shadow + Bordered + Light + Faded + + With dot + +
+ ) +}; + +/** + * The `radius` prop controls the corner roundness. + * + * Available options: + * - `none`, `sm`, `md`, `lg`, `full`(default) + */ +export const Radius: Story = { + render: () => ( +
+ none + sm + md + lg + full +
+ ) +}; + +/** + * `startContent` and `endContent` allow placing icons or text before/after the chip label. + */ +export const StartEndContent: Story = { + args: { + children: 'Status', + color: 'primary', + startContent: , + endContent: ( + + + + ) + } +}; + +/** + * Make chips clickable by setting `as="button"` and providing `onClick`. + */ +export const Clickable: Story = { + args: { + children: 'Clickable', + as: 'button' + } +}; + +/** + * Chips can be added to a list. + */ +/** Chips can be added to a list (with visible label and help text). */ +export const AddableList: Story = { + render: () => { + const [items, setItems] = React.useState([]); + const [val, setVal] = React.useState(''); + const inputId = React.useId(); + const labelId = `${inputId}-label`; + const helpId = `${inputId}-help`; + + const add = (raw: string) => { + const v = raw.trim(); + if (!v) { + return; + } + setItems((prev) => (prev.includes(v) ? prev : [...prev, v])); + setVal(''); + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + const commit = + e.key === 'Enter' || e.key === 'Tab' || e.key === ',' || (e.ctrlKey && (e.key === ' ' || e.code === 'Space')); // Ctrl + Espacio + + if (commit) { + e.preventDefault(); + add(val); + return; + } + + if (e.key === 'Backspace' && val === '' && items.length) { + e.preventDefault(); + setItems((prev) => prev.slice(0, -1)); + } + }; + + return ( +
+ + +
+ {items.map((label, idx) => ( + setItems(items.filter((_, i) => i !== idx))}> + {label} + + ))} + + setVal(e.target.value)} + onKeyDown={onKeyDown} + /> +
+ +

+ Type and press Enter, Tab, or Ctrl+Space to add. When the field is + empty, Backspace removes the last chip. +

+
+ ); + } +}; + +/** + * Closable chips can be removed from a list. + */ +export const ClosableList = () => { + const [items, setItems] = useState(['React', 'NextJS', 'Tailwind']); + + return ( +
+ {items.map((label, idx) => ( + setItems(items.filter((_, i) => i !== idx))}> + {label} + + ))} +
+ ); +}; + +/** + * Chips can be selectable. Use `defaultSelected` for uncontrolled usage, + * or `selected` + `onSelectedChange` for controlled state. + */ +export const SelectableUncontrolled: Story = { + args: { + children: 'Toggle me', + selectable: true, + defaultSelected: false + } +}; + +export const SelectableControlled: Story = { + render: (args) => { + const [sel, setSel] = React.useState(true); + return ( + : null} + > + {sel ? 'Selected' : 'Not selected'} + + ); + } +}; + +/** Avatar al inicio (usa tu componente Avatar) */ +export const WithAvatar: Story = { + render: () => ( +
+ }>EGDEV + + AP
} + > + Andrés + + + ) +}; + +/** + * With text → shows a circular indicator before the label. + */ +export const DotWithText: Story = { + args: { variant: 'dot', color: 'primary', children: 'Pending' } +}; + +/** + * Dot only → provide `ariaLabel` for accessibility. + */ +export const DotOnlyAccessible: Story = { + args: { variant: 'dot', color: 'primary', ariaLabel: 'Online' } +}; + +/** + * You can override slot styles with `classNames`. + */ +export const WithClassNamesOverrides: Story = { + args: { + children: 'Custom Slots', + classNames: { + base: 'bg-blue-700 text-white hover:bg-blue-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-800', + content: 'tracking-wide', + closeButton: 'bg-white/10 hover:bg-white/20' + }, + closable: true, + animation: 'bounce', + as: 'button' + } +}; + +/** + * Stress test for long labels + */ +export const Stress: Story = { + render: () => ( +
+ } + endContent={} + > + Truncated: Very very very long label that should nicely + +
+ ) +}; diff --git a/src/components/atoms/chip/Chip.tsx b/src/components/atoms/chip/Chip.tsx new file mode 100644 index 00000000..d32601bd --- /dev/null +++ b/src/components/atoms/chip/Chip.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import type { ChipProps } from './types'; +import { useChip } from './useChip'; + +export const Chip = React.forwardRef((props, ref) => { + const { Tag, slots, isDot, hasChildren, propsBase, pieces, closable, handleClose } = useChip(props); + const { avatar, startContent, endContent, children } = pieces; + + return ( + + {avatar && {avatar}} + + {startContent && ( + {startContent} + )} + + {isDot && + ); +}); +Chip.displayName = 'Chip'; diff --git a/src/components/atoms/chip/index.ts b/src/components/atoms/chip/index.ts new file mode 100644 index 00000000..f43e952f --- /dev/null +++ b/src/components/atoms/chip/index.ts @@ -0,0 +1,3 @@ +import { Chip } from './Chip'; +export * from './types' +export default Chip; \ No newline at end of file diff --git a/src/components/atoms/chip/types.ts b/src/components/atoms/chip/types.ts new file mode 100644 index 00000000..96e6018a --- /dev/null +++ b/src/components/atoms/chip/types.ts @@ -0,0 +1,347 @@ +import { type VariantProps, cva } from 'class-variance-authority'; +import type * as React from 'react'; + +export const chipVariants = cva( + [ + 'chip relative max-w-full min-w-0', + 'transition-all duration-200 ease-in-out', + 'flex items-center justify-center gap-2', + 'font-secondary-bold whitespace-nowrap leading-[1.2]', + 'disabled:pointer-events-none disabled:opacity-60', + 'focus-visible:outline-none', + 'focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] dark:focus-visible:ring-[var(--color-text-dark)]', + 'focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-bg,white)]' + ], + { + variants: { + color: { primary: '', secondary: '', success: '', warning: '', danger: '' }, + size: { + sm: 'px-sm h-8 fs-small tablet:fs-small-tablet', + md: 'px-md h-10 fs-base tablet:fs-base-tablet', + lg: 'px-lg h-12 fs-h6 tablet:fs-h6-tablet' + }, + radiusSize: { none: '', sm: 'rounded-sm', md: 'rounded-md', lg: 'rounded-lg', full: 'rounded-full' }, + variant: { + solid: 'border border-transparent', + light: 'bg-transparent border border-transparent', + flat: 'border border-transparent', + faded: 'border', + bordered: 'bg-transparent border', + shadow: 'border border-transparent', + dot: 'bg-transparent border' + }, + + startContent: { default: '', icon: 'mr-2', text: 'font-semibold' }, + endContent: { default: '', icon: 'ml-2', text: 'font-semibold' }, + animation: { default: '', pulse: 'animate-pulse', bounce: 'animate-bounce', ping: 'animate-badgePing' } + }, + + compoundVariants: [ + /* ----------------- PRIMARY ----------------- */ + { + color: 'primary', + variant: 'solid', + class: + 'bg-[var(--color-primary)] text-[var(--color-text-dark)] hover:bg-[var(--color-red-600)] dark:hover:bg-[var(--color-red-700)] active:translate-y-[0.5px]' + }, + { + color: 'primary', + variant: 'light', + class: 'text-[var(--color-primary)] hover:bg-[var(--color-white)] dark:hover:bg-[var(--color-red-200)]' + }, + { + color: 'primary', + variant: 'flat', + class: 'bg-[var(--color-red-100)] text-[var(--color-primary)] hover:bg-[var(--color-red-200)]' + }, + { + color: 'primary', + variant: 'faded', + class: [ + 'bg-[var(--color-gray-dark-200)] border-[var(--color-gray-light-300)]', + 'text-[var(--color-primary)] hover:bg-[var(--color-gray-light-200)]', + 'dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)] dark:text-[var(--color-accent)]', + 'dark:hover:bg-[var(--color-gray-dark-600)]' + ].join(' ') + }, + { + color: 'primary', + variant: 'bordered', + class: + 'text-[var(--color-primary)] border-[var(--color-primary)] hover:bg-[var(--color-red-100)] dark:hover:bg-[var(--color-gray-dark-700)]' + }, + { + color: 'primary', + variant: 'shadow', + class: + 'bg-[var(--color-primary)] text-[var(--color-text-dark)] shadow-[var(--shadow-custom-sm)] hover:shadow-[var(--shadow-custom-md)]' + }, + { + color: 'primary', + variant: 'dot', + class: '[--chip-dot:var(--color-primary)]' + }, + + /* ----------------- SECONDARY ----------------- */ + { + color: 'secondary', + variant: 'solid', + class: + 'bg-[var(--color-gray-light-900)] text-[var(--color-text-dark)] hover:bg-[var(--color-gray-light-800)] dark:bg-[var(--color-gray-dark-200)] dark:text-[var(--color-text-light)] dark:hover:bg-[var(--color-gray-dark-300)]' + }, + { + color: 'secondary', + variant: 'light', + class: + 'text-[var(--color-text-light)] hover:bg-[var(--color-gray-light-200)] dark:text-[var(--color-text-dark)] dark:hover:bg-[var(--color-gray-dark-700)]' + }, + { + color: 'secondary', + variant: 'flat', + class: + 'bg-[var(--color-gray-light-200)] text-[var(--color-text-light)] hover:bg-[var(--color-gray-light-300)] dark:bg-[var(--color-gray-dark-800)] dark:text-[var(--color-text-dark)] dark:hover:bg-[var(--color-gray-dark-700)]' + }, + { + color: 'secondary', + variant: 'faded', + class: + 'bg-[var(--color-gray-light-100)] text-[var(--color-text-light)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:text-[var(--color-text-dark)] dark:border-[var(--color-gray-dark-600)]' + }, + { + color: 'secondary', + variant: 'bordered', + class: + 'text-[var(--color-text-light)] border-[var(--color-gray-light-400)] hover:bg-[var(--color-gray-light-200)] dark:text-[var(--color-text-dark)] dark:border-[var(--color-gray-dark-400)] dark:hover:bg-[var(--color-gray-dark-700)]' + }, + { + color: 'secondary', + variant: 'shadow', + class: + 'bg-[var(--color-gray-light-900)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(0,0,0,.35),0_6px_14px_rgba(0,0,0,.25)] hover:shadow-[0_14px_26px_-8px_rgba(0,0,0,.45),0_10px_20px_rgba(0,0,0,.30)]' + }, + { + color: 'secondary', + variant: 'dot', + class: '[--chip-dot:var(--color-gray-light-900)] dark:[--chip-dot:var(--color-gray-dark-200)]' + }, + + /* ----------------- SUCCESS ----------------- */ + { + color: 'success', + variant: 'solid', + class: + 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] hover:bg-[var(--color-green-dark,#16a34a)]' + }, + { + color: 'success', + variant: 'light', + class: 'text-[var(--color-green)] hover:bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)]' + }, + { + color: 'success', + variant: 'flat', + class: + 'bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)] text-[var(--color-green)] hover:bg-[color-mix(in_srgb,var(--color-green)_22%,transparent)]' + }, + { + color: 'success', + variant: 'faded', + class: + 'bg-[var(--color-gray-light-100)] text-[var(--color-green)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' + }, + { + color: 'success', + variant: 'bordered', + class: + 'text-[var(--color-green)] border-[var(--color-green)] hover:bg-[color-mix(in_srgb,var(--color-green)_12%,transparent)]' + }, + { + color: 'success', + variant: 'shadow', + class: + 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] shadow-[0_10px_22px_-6px_rgba(34,197,94,.45),0_6px_14px_rgba(34,197,94,.30)] hover:shadow-[0_14px_26px_-8px_rgba(34,197,94,.55),0_10px_20px_rgba(34,197,94,.35)]' + }, + { color: 'success', variant: 'dot', class: '[--chip-dot:var(--color-green)]' }, + + /* ----------------- WARNING ----------------- */ + { + color: 'warning', + variant: 'solid', + class: 'bg-[var(--color-yellow)] text-[var(--color-text-light)] hover:bg-[var(--color-yellow-dark)]' + }, + { + color: 'warning', + variant: 'light', + class: 'text-[var(--color-yellow)] hover:bg-[color-mix(in_srgb,var(--color-yellow)_18%,transparent)]' + }, + { + color: 'warning', + variant: 'flat', + class: + 'bg-[var(--color-yellow-light)] text-[var(--color-yellow-dark)] hover:bg-[color-mix(in_srgb,var(--color-yellow)_30%,transparent)]' + }, + { + color: 'warning', + variant: 'faded', + class: + 'bg-[var(--color-gray-light-100)] text-[var(--color-yellow-dark)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' + }, + { + color: 'warning', + variant: 'bordered', + class: + 'text-[var(--color-yellow-dark)] border-[var(--color-yellow)] hover:bg-[color-mix(in_srgb,var(--color-yellow)_15%,transparent)]' + }, + { + color: 'warning', + variant: 'shadow', + class: + 'bg-[var(--color-yellow)] text-black shadow-[0_10px_22px_-6px_rgba(234,179,8,.45),0_6px_14px_rgba(234,179,8,.30)] hover:shadow-[0_14px_26px_-8px_rgba(234,179,8,.55),0_10px_20px_rgba(234,179,8,.35)]' + }, + { color: 'warning', variant: 'dot', class: '[--chip-dot:var(--color-yellow)]' }, + + /* ----------------- DANGER ----------------- */ + { + color: 'danger', + variant: 'solid', + class: 'bg-[var(--color-accent)] text-[var(--color-text-dark)] hover:bg-[var(--color-red-700)]' + }, + { color: 'danger', variant: 'light', class: 'text-[var(--color-accent)] hover:bg-[var(--color-red-100)]' }, + { + color: 'danger', + variant: 'flat', + class: 'bg-[var(--color-accent)] text-[var(--color-red-600)] hover:bg-[var(--color-red-200)]' + }, + { + color: 'danger', + variant: 'faded', + class: + 'bg-[var(--color-accent)] text-[var(--color-red-600)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' + }, + { + color: 'danger', + variant: 'bordered', + class: 'text-[var(--color-accent)] border-[var(--color-red-600)] hover:bg-[var(--color-red-100)]' + }, + { + color: 'danger', + variant: 'shadow', + class: + 'bg-[var(--color-accent)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(220,38,38,.45),0_6px_14px_rgba(220,38,38,.30)] hover:shadow-[0_14px_26px_-8px_rgba(220,38,38,.55),0_10px_20px_rgba(220,38,38,.35)]' + }, + { color: 'danger', variant: 'dot', class: '[--chip-dot:var(--color-accent)]' }, + + // ---------- DOT: defaults ---------- + { + variant: 'dot', + class: [ + 'text-[var(--color-text-light)] dark:text-[var(--color-text-dark)]', + 'border-[var(--color-gray-light-600,#9CA3AF)]', + 'dark:border-[var(--color-gray-dark-600,#4B5563)]', + '[--chip-dot:var(--color-primary)]' + ].join(' ') + } + ], + + defaultVariants: { + color: 'primary', + size: 'md', + radiusSize: 'full', + variant: 'solid', + startContent: 'default', + endContent: 'default', + animation: 'default' + } + } +); + +type RadiusSize = 'none' | 'sm' | 'md' | 'lg' | 'full'; +type Animation = 'default' | 'pulse' | 'bounce' | 'ping'; + +type ChipVariant = VariantProps['variant']; +type ChipColorVariants = VariantProps['color']; +type ChipSizeVariants = VariantProps['size']; + +export type ChipProps = { + children?: React.ReactNode; + /** @control text + * @default primary + */ + variant?: ChipVariant; + /** + * @control text + * @default primary + */ + color?: ChipColorVariants; + /** + * @control text + * @default md + */ + size?: ChipSizeVariants; + /** + * @control text + * @default full + */ + radius?: RadiusSize; + /** + * @control text + * @default default + */ + animation?: Animation; + /** @control text + * @default default + */ + avatar?: React.ReactNode; + /** + * @control text + * @default default + */ + startContent?: React.ReactNode; + /** + * @control text + * @default default + */ + endContent?: React.ReactNode; + /** + * @control text + * @default div + */ + as?: 'div' | 'button'; + onClick?: React.MouseEventHandler; + /** + * @control text + * @default false + */ + isDisabled?: boolean; + /** + * @control boolean + * @default false + */ + closable?: boolean; + onClose?: () => void; + /** + * @control text + * @default false + */ + selectable?: boolean; + /** + * @control text + * @default false + */ + selected?: boolean; + /** + * @control boolean + * @default false + */ + defaultSelected?: boolean; + onSelectedChange?: (selected: boolean) => void; + /** + * @control text + */ + className?: string; + /** + * @control text + */ + classNames?: Partial>; + ariaLabel?: string; +}; diff --git a/src/components/atoms/chip/useChip.ts b/src/components/atoms/chip/useChip.ts new file mode 100644 index 00000000..e3f45e9e --- /dev/null +++ b/src/components/atoms/chip/useChip.ts @@ -0,0 +1,176 @@ +import clsx from 'clsx'; +import * as React from 'react'; +import { twMerge } from 'tailwind-merge'; +import { chipVariants } from './types'; +import type { ChipProps } from './types'; + +const cn = (...v: any[]) => twMerge(clsx(v)); +const isText = (n: React.ReactNode) => typeof n === 'string' || typeof n === 'number'; + +export function useChip(props: ChipProps) { + const { + variant = 'solid', + color = 'primary', + size = 'md', + radius, + animation = 'default', + startContent, + endContent, + children, + avatar, + className, + classNames, + isDisabled, + onClick, + as, + selectable, + selected, + defaultSelected, + onSelectedChange, + closable, + onClose, + ariaLabel, + ...rest + } = props; + + const isControlled = typeof selected === 'boolean'; + const [innerSelected, setInnerSelected] = React.useState(!!defaultSelected); + const isSelected = isControlled ? !!selected : innerSelected; + + const setSelected = (next: boolean) => { + if (!isControlled) { + setInnerSelected(next); + } + onSelectedChange?.(next); + }; + + const startKind = startContent == null ? 'default' : isText(startContent) ? 'text' : 'icon'; + const endKind = endContent == null ? 'default' : isText(endContent) ? 'text' : 'icon'; + + const interactive = !!onClick || !!selectable; + const Tag: 'div' | 'button' = as ?? (interactive ? 'button' : 'div'); + + const baseClasses = chipVariants({ + variant, + color, + size, + radiusSize: radius, + startContent: startKind, + endContent: endKind, + animation + }); + + const hasText = (n: React.ReactNode) => !(n === null || n === undefined || (typeof n === 'string' && n.length === 0)); + + const isDot = variant === 'dot'; + const hasChildren = hasText(children); + const hasStart = !!startContent || !!avatar; + const hasEnd = !!endContent; + + const pieceCount = + (hasStart ? 1 : 0) + (hasEnd ? 1 : 0) + (hasChildren ? 1 : 0) + (isDot ? 1 : 0) + (closable ? 1 : 0); + + const iconBySize = + size === 'sm' + ? '[&_svg]:h-3.5 [&_svg]:w-3.5' + : size === 'lg' + ? '[&_svg]:h-4.5 [&_svg]:w-4.5' + : '[&_svg]:h-4 [&_svg]:w-4'; + + const slots = { + base: cn( + baseClasses, + 'min-w-0', + pieceCount > 1 && 'gap-2', + className, + classNames?.base, + interactive ? 'cursor-pointer' : 'cursor-auto', + isSelected && 'ring-2 ring-offset-0 ring-inset ring-accent', + iconBySize + ), + content: cn('truncate', classNames?.content), + dot: cn('inline-block w-2 h-2 rounded-full shrink-0 bg-[var(--chip-dot)]', classNames?.dot), + avatar: cn('shrink-0 ltr:mr-2 rtl:ml-2', classNames?.avatar), + closeButton: cn( + 'relative ml-1 inline-flex h-5 w-5 items-center justify-center rounded-full', + 'shrink-0 aspect-square leading-none', + 'pointer-events-auto cursor-pointer select-none', + 'transition-all duration-150', + 'hover:bg-[currentColor]/12 dark:hover:bg-[currentColor]/22', + 'hover:ring-1 hover:ring-current hover:ring-inset', + 'focus-visible:outline-none', + 'focus-visible:ring-2 focus-visible:ring-accent', + 'focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-bg,white)]', + '[&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:stroke-current', + 'hover:[&_svg]:scale-110 hover:[&_svg]:opacity-90', + 'motion-reduce:transition-none motion-reduce:[&_svg]:transform-none', + classNames?.closeButton + ) + }; + + const handleActivate = (e: React.MouseEvent) => { + if (isDisabled) { + return; + } + if (selectable) { + setSelected(!isSelected); + } + onClick?.(e); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (Tag === 'div' && interactive && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + const fake = new MouseEvent('click', { bubbles: true }); + (e.currentTarget as HTMLElement).dispatchEvent(fake); + } + + if (closable && (e.key === 'Delete' || e.key === 'Backspace')) { + e.preventDefault(); + onClose?.(); + } + }; + + const handleClose = (e: React.MouseEvent) => { + e.stopPropagation(); + if (isDisabled) { + return; + } + onClose?.(); + }; + + const computedAriaLabel = isDot && !hasChildren ? ariaLabel : undefined; + const a11yProps = + Tag === 'button' + ? { + type: 'button' as const, + 'aria-disabled': isDisabled || undefined, + disabled: isDisabled || undefined, + 'aria-pressed': selectable ? isSelected : undefined + } + : { + role: interactive ? 'button' : undefined, + tabIndex: interactive ? 0 : undefined, + 'aria-disabled': isDisabled || undefined, + 'aria-pressed': selectable ? isSelected : undefined, + onKeyDown: handleKeyDown + }; + + return { + Tag, + slots, + isDot, + hasChildren, + isSelected, + interactive, + propsBase: { + ...rest, + ...a11yProps, + 'aria-label': computedAriaLabel, + onClick: interactive ? handleActivate : onClick + }, + pieces: { avatar, startContent, endContent, children }, + closable, + handleClose + }; +} From 0a3264d6fa9b66f48842866bdd55f3d512d74b59 Mon Sep 17 00:00:00 2001 From: Andres Date: Wed, 27 Aug 2025 20:16:51 +0200 Subject: [PATCH 2/6] [FIX] Chip component Shadow Variant --- src/components/atoms/chip/types.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/atoms/chip/types.ts b/src/components/atoms/chip/types.ts index 96e6018a..2de21476 100644 --- a/src/components/atoms/chip/types.ts +++ b/src/components/atoms/chip/types.ts @@ -73,8 +73,14 @@ export const chipVariants = cva( { color: 'primary', variant: 'shadow', - class: - 'bg-[var(--color-primary)] text-[var(--color-text-dark)] shadow-[var(--shadow-custom-sm)] hover:shadow-[var(--shadow-custom-md)]' + class: [ + 'bg-[var(--color-primary)] text-[var(--color-text-dark)] border border-transparent', + 'shadow-none', + 'drop-shadow-[0_16px_16px_color-mix(in_srgb,var(--color-primary)_70%,transparent)]', + 'dark:drop-shadow-[0_16px_16px_color-mix(in_srgb,var(--color-primary)_70%,transparent)]', + 'active:translate-y-[0.5px]', + 'hover:bg-[var(--color-red-600)] dark:hover:bg-[var(--color-red-700)]' + ].join(' ') }, { color: 'primary', From 8b125d1d781df756b018adde2bc5cbb4de0eb490 Mon Sep 17 00:00:00 2001 From: Andres Date: Wed, 27 Aug 2025 21:14:11 +0200 Subject: [PATCH 3/6] [FIX] Deleting Chip adding list component and giving more size to close icon in chip component --- src/components/atoms/chip/Chip.stories.tsx | 86 +--------------------- src/components/atoms/chip/useChip.ts | 30 ++++---- 2 files changed, 18 insertions(+), 98 deletions(-) diff --git a/src/components/atoms/chip/Chip.stories.tsx b/src/components/atoms/chip/Chip.stories.tsx index 26c24b00..dd924b1c 100644 --- a/src/components/atoms/chip/Chip.stories.tsx +++ b/src/components/atoms/chip/Chip.stories.tsx @@ -158,7 +158,7 @@ export const StartEndContent: Story = { color: 'primary', startContent: , endContent: ( - + ) @@ -175,88 +175,6 @@ export const Clickable: Story = { } }; -/** - * Chips can be added to a list. - */ -/** Chips can be added to a list (with visible label and help text). */ -export const AddableList: Story = { - render: () => { - const [items, setItems] = React.useState([]); - const [val, setVal] = React.useState(''); - const inputId = React.useId(); - const labelId = `${inputId}-label`; - const helpId = `${inputId}-help`; - - const add = (raw: string) => { - const v = raw.trim(); - if (!v) { - return; - } - setItems((prev) => (prev.includes(v) ? prev : [...prev, v])); - setVal(''); - }; - - const onKeyDown = (e: React.KeyboardEvent) => { - const commit = - e.key === 'Enter' || e.key === 'Tab' || e.key === ',' || (e.ctrlKey && (e.key === ' ' || e.code === 'Space')); // Ctrl + Espacio - - if (commit) { - e.preventDefault(); - add(val); - return; - } - - if (e.key === 'Backspace' && val === '' && items.length) { - e.preventDefault(); - setItems((prev) => prev.slice(0, -1)); - } - }; - - return ( -
- - -
- {items.map((label, idx) => ( - setItems(items.filter((_, i) => i !== idx))}> - {label} - - ))} - - setVal(e.target.value)} - onKeyDown={onKeyDown} - /> -
- -

- Type and press Enter, Tab, or Ctrl+Space to add. When the field is - empty, Backspace removes the last chip. -

-
- ); - } -}; - /** * Closable chips can be removed from a list. */ @@ -266,7 +184,7 @@ export const ClosableList = () => { return (
{items.map((label, idx) => ( - setItems(items.filter((_, i) => i !== idx))}> + setItems(items.filter((_, i) => i !== idx))}> {label} ))} diff --git a/src/components/atoms/chip/useChip.ts b/src/components/atoms/chip/useChip.ts index e3f45e9e..7142c812 100644 --- a/src/components/atoms/chip/useChip.ts +++ b/src/components/atoms/chip/useChip.ts @@ -75,13 +75,18 @@ export function useChip(props: ChipProps) { ? '[&_svg]:h-3.5 [&_svg]:w-3.5' : size === 'lg' ? '[&_svg]:h-4.5 [&_svg]:w-4.5' - : '[&_svg]:h-4 [&_svg]:w-4'; + : '[&_svg]:h-4 [&_svg]:w-4'; + + const closeBtnBoxBySize = + size === 'sm' ? 'h-[18px] w-[18px]' : size === 'lg' ? 'h-[20px] w-[20px]' : 'h-[18px] w-[18px]'; + + const closeGlyphSizeBySize = size === 'sm' ? 'text-[16px]' : size === 'lg' ? 'text-[20px]' : 'text-[18px]'; const slots = { base: cn( baseClasses, 'min-w-0', - pieceCount > 1 && 'gap-2', + pieceCount > 1 && (closable ? 'gap-1' : 'gap-2'), className, classNames?.base, interactive ? 'cursor-pointer' : 'cursor-auto', @@ -91,19 +96,17 @@ export function useChip(props: ChipProps) { content: cn('truncate', classNames?.content), dot: cn('inline-block w-2 h-2 rounded-full shrink-0 bg-[var(--chip-dot)]', classNames?.dot), avatar: cn('shrink-0 ltr:mr-2 rtl:ml-2', classNames?.avatar), + closeButton: cn( - 'relative ml-1 inline-flex h-5 w-5 items-center justify-center rounded-full', - 'shrink-0 aspect-square leading-none', - 'pointer-events-auto cursor-pointer select-none', - 'transition-all duration-150', - 'hover:bg-[currentColor]/12 dark:hover:bg-[currentColor]/22', - 'hover:ring-1 hover:ring-current hover:ring-inset', - 'focus-visible:outline-none', - 'focus-visible:ring-2 focus-visible:ring-accent', + 'relative inline-flex items-center justify-center overflow-visible', + 'shrink-0 leading-none select-none pointer-events-auto cursor-pointer', + 'p-0 m-0 ltr:-ml-0.5 rtl:-mr-0.5 rounded', + closeBtnBoxBySize, + closeGlyphSizeBySize, + 'text-white dark:text-white font-bold', + 'hover:bg-transparent hover:ring-0', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent', 'focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-bg,white)]', - '[&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:stroke-current', - 'hover:[&_svg]:scale-110 hover:[&_svg]:opacity-90', - 'motion-reduce:transition-none motion-reduce:[&_svg]:transform-none', classNames?.closeButton ) }; @@ -124,7 +127,6 @@ export function useChip(props: ChipProps) { const fake = new MouseEvent('click', { bubbles: true }); (e.currentTarget as HTMLElement).dispatchEvent(fake); } - if (closable && (e.key === 'Delete' || e.key === 'Backspace')) { e.preventDefault(); onClose?.(); From 7c92d8ce1e0435b4777d7e5835ad7e0f4949fb0b Mon Sep 17 00:00:00 2001 From: Andres Date: Fri, 29 Aug 2025 13:25:35 +0200 Subject: [PATCH 4/6] [FIX] Chip Component -Deleting hover effect in every non-interactable elements --- src/components/atoms/chip/Chip.stories.tsx | 61 ++++-------- src/components/atoms/chip/Chip.tsx | 1 + src/components/atoms/chip/types.ts | 107 ++++++++++++--------- src/components/atoms/chip/useChip.ts | 3 +- 4 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/components/atoms/chip/Chip.stories.tsx b/src/components/atoms/chip/Chip.stories.tsx index dd924b1c..906c8c08 100644 --- a/src/components/atoms/chip/Chip.stories.tsx +++ b/src/components/atoms/chip/Chip.stories.tsx @@ -22,9 +22,7 @@ const meta: Meta = { title: 'Atoms/Chip', component: Chip, parameters: { - docs: { - autodocs: true - } + docs: { autodocs: true } }, tags: ['autodocs'], argTypes: { @@ -39,8 +37,10 @@ const meta: Meta = { defaultSelected: { control: 'boolean' }, closable: { control: 'boolean' }, onClose: { action: 'onClose' }, - onClick: { action: 'onClick' }, onSelectedChange: { action: 'onSelectedChange' } + }, + args: { + onClick: undefined } }; export default meta; @@ -56,7 +56,14 @@ export const Default: Story = { animation: 'default', as: 'div', selectable: false, - closable: false + closable: false, + onClick: undefined // 🔧 fuerza no interactivo + }, + parameters: { + actions: { disable: true } // 🔧 evita auto-actions en esta story + }, + argTypes: { + onClick: { table: { disable: true }, control: false } // 🔧 oculta control y acción } }; @@ -69,10 +76,7 @@ export const Default: Story = { * - `lg` → Large */ export const Size: Story = { - args: { - variant: 'light' - }, - + args: { variant: 'light' }, render: () => (
Small @@ -84,13 +88,6 @@ export const Size: Story = { /** * The `color` prop sets background and text color. - * - * Available options: - * - `primary` - * - `secondary` - * - `success` - * - `warning` - * - `danger` */ export const Color: Story = { render: () => ( @@ -106,14 +103,6 @@ export const Color: Story = { /** * The `variant` prop defines visual style modifications for the chip. - * - * Available options: - * - `solid` → Default filled appearance. - * - `flat` → Lower background opacity, text remains solid. - * - `shadow` → Adds a soft shadow. - * - `bordered` → Outline style. - * - `light`/`faded` → Softer neutral looks. - * - `dot` → Prepends a circular status indicator; combine with text or use `ariaLabel` when text is absent. */ export const Variant: Story = { render: () => ( @@ -133,9 +122,6 @@ export const Variant: Story = { /** * The `radius` prop controls the corner roundness. - * - * Available options: - * - `none`, `sm`, `md`, `lg`, `full`(default) */ export const Radius: Story = { render: () => ( @@ -172,6 +158,10 @@ export const Clickable: Story = { args: { children: 'Clickable', as: 'button' + }, + // 👇 Sólo esta story publica un onClick como action + argTypes: { + onClick: { action: 'onClick' } } }; @@ -226,7 +216,6 @@ export const WithAvatar: Story = { render: () => (
}>EGDEV - AP
} @@ -237,23 +226,17 @@ export const WithAvatar: Story = { ) }; -/** - * With text → shows a circular indicator before the label. - */ +/** With text → shows a circular indicator before the label. */ export const DotWithText: Story = { args: { variant: 'dot', color: 'primary', children: 'Pending' } }; -/** - * Dot only → provide `ariaLabel` for accessibility. - */ +/** Dot only → provide `ariaLabel` for accessibility. */ export const DotOnlyAccessible: Story = { args: { variant: 'dot', color: 'primary', ariaLabel: 'Online' } }; -/** - * You can override slot styles with `classNames`. - */ +/** You can override slot styles with `classNames`. */ export const WithClassNamesOverrides: Story = { args: { children: 'Custom Slots', @@ -268,9 +251,7 @@ export const WithClassNamesOverrides: Story = { } }; -/** - * Stress test for long labels - */ +/** Stress test for long labels */ export const Stress: Story = { render: () => (
diff --git a/src/components/atoms/chip/Chip.tsx b/src/components/atoms/chip/Chip.tsx index d32601bd..44093a2e 100644 --- a/src/components/atoms/chip/Chip.tsx +++ b/src/components/atoms/chip/Chip.tsx @@ -30,4 +30,5 @@ export const Chip = React.forwardRef((props, ref) => { ); }); + Chip.displayName = 'Chip'; diff --git a/src/components/atoms/chip/types.ts b/src/components/atoms/chip/types.ts index 2de21476..a7647923 100644 --- a/src/components/atoms/chip/types.ts +++ b/src/components/atoms/chip/types.ts @@ -9,8 +9,9 @@ export const chipVariants = cva( 'font-secondary-bold whitespace-nowrap leading-[1.2]', 'disabled:pointer-events-none disabled:opacity-60', 'focus-visible:outline-none', - 'focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] dark:focus-visible:ring-[var(--color-text-dark)]', - 'focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-bg,white)]' + 'data-[interactive=true]:focus-visible:ring-2 data-[interactive=true]:focus-visible:ring-[var(--color-accent)]', + 'dark:data-[interactive=true]:focus-visible:ring-[var(--color-text-dark)]', + 'data-[interactive=true]:focus-visible:ring-offset-2 data-[interactive=true]:focus-visible:ring-offset-[var(--surface-bg,white)]' ], { variants: { @@ -41,34 +42,39 @@ export const chipVariants = cva( { color: 'primary', variant: 'solid', - class: - 'bg-[var(--color-primary)] text-[var(--color-text-dark)] hover:bg-[var(--color-red-600)] dark:hover:bg-[var(--color-red-700)] active:translate-y-[0.5px]' + class: [ + 'bg-[var(--color-primary)] text-[var(--color-text-dark)]', + 'data-[interactive=true]:hover:bg-[var(--color-red-600)] dark:data-[interactive=true]:hover:bg-[var(--color-red-700)]', + 'data-[interactive=true]:active:translate-y-[0.5px]' + ].join(' ') }, { color: 'primary', variant: 'light', - class: 'text-[var(--color-primary)] hover:bg-[var(--color-white)] dark:hover:bg-[var(--color-red-200)]' + class: + 'text-[var(--color-primary)] data-[interactive=true]:hover:bg-[var(--color-white)] dark:data-[interactive=true]:hover:bg-[var(--color-red-200)]' }, { color: 'primary', variant: 'flat', - class: 'bg-[var(--color-red-100)] text-[var(--color-primary)] hover:bg-[var(--color-red-200)]' + class: + 'bg-[var(--color-red-100)] text-[var(--color-primary)] data-[interactive=true]:hover:bg-[var(--color-red-200)]' }, { color: 'primary', variant: 'faded', class: [ 'bg-[var(--color-gray-dark-200)] border-[var(--color-gray-light-300)]', - 'text-[var(--color-primary)] hover:bg-[var(--color-gray-light-200)]', + 'text-[var(--color-primary)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)]', 'dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)] dark:text-[var(--color-accent)]', - 'dark:hover:bg-[var(--color-gray-dark-600)]' + 'dark:data-[interactive=true]:hover:bg-[var(--color-gray-dark-600)]' ].join(' ') }, { color: 'primary', variant: 'bordered', class: - 'text-[var(--color-primary)] border-[var(--color-primary)] hover:bg-[var(--color-red-100)] dark:hover:bg-[var(--color-gray-dark-700)]' + 'text-[var(--color-primary)] border-[var(--color-primary)] data-[interactive=true]:hover:bg-[var(--color-red-100)] dark:data-[interactive=true]:hover:bg-[var(--color-gray-dark-700)]' }, { color: 'primary', @@ -78,52 +84,51 @@ export const chipVariants = cva( 'shadow-none', 'drop-shadow-[0_16px_16px_color-mix(in_srgb,var(--color-primary)_70%,transparent)]', 'dark:drop-shadow-[0_16px_16px_color-mix(in_srgb,var(--color-primary)_70%,transparent)]', - 'active:translate-y-[0.5px]', - 'hover:bg-[var(--color-red-600)] dark:hover:bg-[var(--color-red-700)]' + 'data-[interactive=true]:active:translate-y-[0.5px]', + 'data-[interactive=true]:hover:bg-[var(--color-red-600)] dark:data-[interactive=true]:hover:bg-[var(--color-red-700)]' ].join(' ') }, - { - color: 'primary', - variant: 'dot', - class: '[--chip-dot:var(--color-primary)]' - }, + { color: 'primary', variant: 'dot', class: '[--chip-dot:var(--color-primary)]' }, /* ----------------- SECONDARY ----------------- */ { color: 'secondary', variant: 'solid', - class: - 'bg-[var(--color-gray-light-900)] text-[var(--color-text-dark)] hover:bg-[var(--color-gray-light-800)] dark:bg-[var(--color-gray-dark-200)] dark:text-[var(--color-text-light)] dark:hover:bg-[var(--color-gray-dark-300)]' + class: [ + 'bg-[var(--color-gray-light-900)] text-[var(--color-text-dark)]', + 'data-[interactive=true]:hover:bg-[var(--color-gray-light-800)]', + 'dark:bg-[var(--color-gray-dark-200)] dark:text-[var(--color-text-light)] dark:data-[interactive=true]:hover:bg-[var(--color-gray-dark-300)]' + ].join(' ') }, { color: 'secondary', variant: 'light', class: - 'text-[var(--color-text-light)] hover:bg-[var(--color-gray-light-200)] dark:text-[var(--color-text-dark)] dark:hover:bg-[var(--color-gray-dark-700)]' + 'text-[var(--color-text-light)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:text-[var(--color-text-dark)] dark:data-[interactive=true]:hover:bg-[var(--color-gray-dark-700)]' }, { color: 'secondary', variant: 'flat', class: - 'bg-[var(--color-gray-light-200)] text-[var(--color-text-light)] hover:bg-[var(--color-gray-light-300)] dark:bg-[var(--color-gray-dark-800)] dark:text-[var(--color-text-dark)] dark:hover:bg-[var(--color-gray-dark-700)]' + 'bg-[var(--color-gray-light-200)] text-[var(--color-text-light)] data-[interactive=true]:hover:bg-[var(--color-gray-light-300)] dark:bg-[var(--color-gray-dark-800)] dark:text-[var(--color-text-dark)] dark:data-[interactive=true]:hover:bg-[var(--color-gray-dark-700)]' }, { color: 'secondary', variant: 'faded', class: - 'bg-[var(--color-gray-light-100)] text-[var(--color-text-light)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:text-[var(--color-text-dark)] dark:border-[var(--color-gray-dark-600)]' + 'bg-[var(--color-gray-light-100)] text-[var(--color-text-light)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:text-[var(--color-text-dark)] dark:border-[var(--color-gray-dark-600)]' }, { color: 'secondary', variant: 'bordered', class: - 'text-[var(--color-text-light)] border-[var(--color-gray-light-400)] hover:bg-[var(--color-gray-light-200)] dark:text-[var(--color-text-dark)] dark:border-[var(--color-gray-dark-400)] dark:hover:bg-[var(--color-gray-dark-700)]' + 'text-[var(--color-text-light)] border-[var(--color-gray-light-400)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:text-[var(--color-text-dark)] dark:border-[var(--color-gray-dark-400)] dark:data-[interactive=true]:hover:bg-[var(--color-gray-dark-700)]' }, { color: 'secondary', variant: 'shadow', class: - 'bg-[var(--color-gray-light-900)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(0,0,0,.35),0_6px_14px_rgba(0,0,0,.25)] hover:shadow-[0_14px_26px_-8px_rgba(0,0,0,.45),0_10px_20px_rgba(0,0,0,.30)]' + 'bg-[var(--color-gray-light-900)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(0,0,0,.35),0_6px_14px_rgba(0,0,0,.25)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(0,0,0,.45),0_10px_20px_rgba(0,0,0,.30)]' }, { color: 'secondary', @@ -136,36 +141,37 @@ export const chipVariants = cva( color: 'success', variant: 'solid', class: - 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] hover:bg-[var(--color-green-dark,#16a34a)]' + 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] data-[interactive=true]:hover:bg-[var(--color-green-dark,#16a34a)]' }, { color: 'success', variant: 'light', - class: 'text-[var(--color-green)] hover:bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)]' + class: + 'text-[var(--color-green)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)]' }, { color: 'success', variant: 'flat', class: - 'bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)] text-[var(--color-green)] hover:bg-[color-mix(in_srgb,var(--color-green)_22%,transparent)]' + 'bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)] text-[var(--color-green)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-green)_22%,transparent)]' }, { color: 'success', variant: 'faded', class: - 'bg-[var(--color-gray-light-100)] text-[var(--color-green)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' + 'bg-[var(--color-gray-light-100)] text-[var(--color-green)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' }, { color: 'success', variant: 'bordered', class: - 'text-[var(--color-green)] border-[var(--color-green)] hover:bg-[color-mix(in_srgb,var(--color-green)_12%,transparent)]' + 'text-[var(--color-green)] border-[var(--color-green)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-green)_12%,transparent)]' }, { color: 'success', variant: 'shadow', class: - 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] shadow-[0_10px_22px_-6px_rgba(34,197,94,.45),0_6px_14px_rgba(34,197,94,.30)] hover:shadow-[0_14px_26px_-8px_rgba(34,197,94,.55),0_10px_20px_rgba(34,197,94,.35)]' + 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] shadow-[0_10px_22px_-6px_rgba(34,197,94,.45),0_6px_14px_rgba(34,197,94,.30)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(34,197,94,.55),0_10px_20px_rgba(34,197,94,.35)]' }, { color: 'success', variant: 'dot', class: '[--chip-dot:var(--color-green)]' }, @@ -173,36 +179,38 @@ export const chipVariants = cva( { color: 'warning', variant: 'solid', - class: 'bg-[var(--color-yellow)] text-[var(--color-text-light)] hover:bg-[var(--color-yellow-dark)]' + class: + 'bg-[var(--color-yellow)] text-[var(--color-text-light)] data-[interactive=true]:hover:bg-[var(--color-yellow-dark)]' }, { color: 'warning', variant: 'light', - class: 'text-[var(--color-yellow)] hover:bg-[color-mix(in_srgb,var(--color-yellow)_18%,transparent)]' + class: + 'text-[var(--color-yellow)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-yellow)_18%,transparent)]' }, { color: 'warning', variant: 'flat', class: - 'bg-[var(--color-yellow-light)] text-[var(--color-yellow-dark)] hover:bg-[color-mix(in_srgb,var(--color-yellow)_30%,transparent)]' + 'bg-[var(--color-yellow-light)] text-[var(--color-yellow-dark)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-yellow)_30%,transparent)]' }, { color: 'warning', variant: 'faded', class: - 'bg-[var(--color-gray-light-100)] text-[var(--color-yellow-dark)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' + 'bg-[var(--color-gray-light-100)] text-[var(--color-yellow-dark)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' }, { color: 'warning', variant: 'bordered', class: - 'text-[var(--color-yellow-dark)] border-[var(--color-yellow)] hover:bg-[color-mix(in_srgb,var(--color-yellow)_15%,transparent)]' + 'text-[var(--color-yellow-dark)] border-[var(--color-yellow)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-yellow)_15%,transparent)]' }, { color: 'warning', variant: 'shadow', class: - 'bg-[var(--color-yellow)] text-black shadow-[0_10px_22px_-6px_rgba(234,179,8,.45),0_6px_14px_rgba(234,179,8,.30)] hover:shadow-[0_14px_26px_-8px_rgba(234,179,8,.55),0_10px_20px_rgba(234,179,8,.35)]' + 'bg-[var(--color-yellow)] text-black shadow-[0_10px_22px_-6px_rgba(234,179,8,.45),0_6px_14px_rgba(234,179,8,.30)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(234,179,8,.55),0_10px_20px_rgba(234,179,8,.35)]' }, { color: 'warning', variant: 'dot', class: '[--chip-dot:var(--color-yellow)]' }, @@ -210,30 +218,37 @@ export const chipVariants = cva( { color: 'danger', variant: 'solid', - class: 'bg-[var(--color-accent)] text-[var(--color-text-dark)] hover:bg-[var(--color-red-700)]' + class: + 'bg-[var(--color-accent)] text-[var(--color-text-dark)] data-[interactive=true]:hover:bg-[var(--color-red-700)]' + }, + { + color: 'danger', + variant: 'light', + class: 'text-[var(--color-accent)] data-[interactive=true]:hover:bg-[var(--color-red-100)]' }, - { color: 'danger', variant: 'light', class: 'text-[var(--color-accent)] hover:bg-[var(--color-red-100)]' }, { color: 'danger', variant: 'flat', - class: 'bg-[var(--color-accent)] text-[var(--color-red-600)] hover:bg-[var(--color-red-200)]' + class: + 'bg-[var(--color-accent)] text-[var(--color-red-600)] data-[interactive=true]:hover:bg-[var(--color-red-200)]' }, { color: 'danger', variant: 'faded', class: - 'bg-[var(--color-accent)] text-[var(--color-red-600)] border-[var(--color-gray-light-300)] hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' + 'bg-[var(--color-accent)] text-[var(--color-red-600)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' }, { color: 'danger', variant: 'bordered', - class: 'text-[var(--color-accent)] border-[var(--color-red-600)] hover:bg-[var(--color-red-100)]' + class: + 'text-[var(--color-accent)] border-[var(--color-red-600)] data-[interactive=true]:hover:bg-[var(--color-red-100)]' }, { color: 'danger', variant: 'shadow', class: - 'bg-[var(--color-accent)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(220,38,38,.45),0_6px_14px_rgba(220,38,38,.30)] hover:shadow-[0_14px_26px_-8px_rgba(220,38,38,.55),0_10px_20px_rgba(220,38,38,.35)]' + 'bg-[var(--color-accent)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(220,38,38,.45),0_6px_14px_rgba(220,38,38,.30)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(220,38,38,.55),0_10px_20px_rgba(220,38,38,.35)]' }, { color: 'danger', variant: 'dot', class: '[--chip-dot:var(--color-accent)]' }, @@ -261,12 +276,12 @@ export const chipVariants = cva( } ); -type RadiusSize = 'none' | 'sm' | 'md' | 'lg' | 'full'; -type Animation = 'default' | 'pulse' | 'bounce' | 'ping'; +export type RadiusSize = 'none' | 'sm' | 'md' | 'lg' | 'full'; +export type Animation = 'default' | 'pulse' | 'bounce' | 'ping'; -type ChipVariant = VariantProps['variant']; -type ChipColorVariants = VariantProps['color']; -type ChipSizeVariants = VariantProps['size']; +export type ChipVariant = VariantProps['variant']; +export type ChipColorVariants = VariantProps['color']; +export type ChipSizeVariants = VariantProps['size']; export type ChipProps = { children?: React.ReactNode; diff --git a/src/components/atoms/chip/useChip.ts b/src/components/atoms/chip/useChip.ts index 7142c812..6895c949 100644 --- a/src/components/atoms/chip/useChip.ts +++ b/src/components/atoms/chip/useChip.ts @@ -47,7 +47,7 @@ export function useChip(props: ChipProps) { const startKind = startContent == null ? 'default' : isText(startContent) ? 'text' : 'icon'; const endKind = endContent == null ? 'default' : isText(endContent) ? 'text' : 'icon'; - const interactive = !!onClick || !!selectable; + const interactive = !!onClick || !!selectable; // ← define interactividad const Tag: 'div' | 'button' = as ?? (interactive ? 'button' : 'div'); const baseClasses = chipVariants({ @@ -169,6 +169,7 @@ export function useChip(props: ChipProps) { ...rest, ...a11yProps, 'aria-label': computedAriaLabel, + 'data-interactive': interactive ? 'true' : 'false', onClick: interactive ? handleActivate : onClick }, pieces: { avatar, startContent, endContent, children }, From 29a7473f48995f36a94fc088ebf4a9b77306ae38 Mon Sep 17 00:00:00 2001 From: Andres Date: Fri, 29 Aug 2025 16:22:40 +0200 Subject: [PATCH 5/6] [FIX] Chip component - fixing accessibility problems --- src/components/atoms/chip/Chip.stories.tsx | 16 +++++++++++----- src/components/atoms/chip/useChip.ts | 11 ++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/atoms/chip/Chip.stories.tsx b/src/components/atoms/chip/Chip.stories.tsx index 906c8c08..a2d62e53 100644 --- a/src/components/atoms/chip/Chip.stories.tsx +++ b/src/components/atoms/chip/Chip.stories.tsx @@ -57,13 +57,13 @@ export const Default: Story = { as: 'div', selectable: false, closable: false, - onClick: undefined // 🔧 fuerza no interactivo + onClick: undefined }, parameters: { - actions: { disable: true } // 🔧 evita auto-actions en esta story + actions: { disable: true } }, argTypes: { - onClick: { table: { disable: true }, control: false } // 🔧 oculta control y acción + onClick: { table: { disable: true }, control: false } } }; @@ -159,7 +159,6 @@ export const Clickable: Story = { children: 'Clickable', as: 'button' }, - // 👇 Sólo esta story publica un onClick como action argTypes: { onClick: { action: 'onClick' } } @@ -247,7 +246,14 @@ export const WithClassNamesOverrides: Story = { }, closable: true, animation: 'bounce', - as: 'button' + as: 'div', + onClick: undefined + }, + parameters: { + actions: { disable: true } + }, + argTypes: { + onClick: { table: { disable: true }, control: false } } }; diff --git a/src/components/atoms/chip/useChip.ts b/src/components/atoms/chip/useChip.ts index 6895c949..e16cae4e 100644 --- a/src/components/atoms/chip/useChip.ts +++ b/src/components/atoms/chip/useChip.ts @@ -47,7 +47,7 @@ export function useChip(props: ChipProps) { const startKind = startContent == null ? 'default' : isText(startContent) ? 'text' : 'icon'; const endKind = endContent == null ? 'default' : isText(endContent) ? 'text' : 'icon'; - const interactive = !!onClick || !!selectable; // ← define interactividad + const interactive = !!onClick || !!selectable; // define interactividad const Tag: 'div' | 'button' = as ?? (interactive ? 'button' : 'div'); const baseClasses = chipVariants({ @@ -141,7 +141,12 @@ export function useChip(props: ChipProps) { onClose?.(); }; - const computedAriaLabel = isDot && !hasChildren ? ariaLabel : undefined; + // A11y: dot-only necesita role válido para usar aria-label en un div no interactivo + const isDotOnly = isDot && !hasChildren; + const computedAriaLabel = isDotOnly ? ariaLabel : undefined; + + const computedRole = Tag === 'button' ? undefined : interactive ? 'button' : isDotOnly ? 'img' : undefined; + const a11yProps = Tag === 'button' ? { @@ -151,7 +156,7 @@ export function useChip(props: ChipProps) { 'aria-pressed': selectable ? isSelected : undefined } : { - role: interactive ? 'button' : undefined, + role: computedRole, tabIndex: interactive ? 0 : undefined, 'aria-disabled': isDisabled || undefined, 'aria-pressed': selectable ? isSelected : undefined, From 6f35ebcec355315142380874f21102a066bc8dba Mon Sep 17 00:00:00 2001 From: Andres Date: Mon, 15 Sep 2025 20:42:18 +0200 Subject: [PATCH 6/6] [FIX]: Chip variants styles --- src/components/atoms/chip/Chip.stories.tsx | 24 ++- src/components/atoms/chip/Chip.tsx | 26 ++- src/components/atoms/chip/types.ts | 226 +++------------------ src/components/atoms/chip/useChip.ts | 10 +- 4 files changed, 77 insertions(+), 209 deletions(-) diff --git a/src/components/atoms/chip/Chip.stories.tsx b/src/components/atoms/chip/Chip.stories.tsx index a2d62e53..bee834d8 100644 --- a/src/components/atoms/chip/Chip.stories.tsx +++ b/src/components/atoms/chip/Chip.stories.tsx @@ -144,8 +144,14 @@ export const StartEndContent: Story = { color: 'primary', startContent: , endContent: ( - - + + ) } @@ -214,10 +220,22 @@ export const SelectableControlled: Story = { export const WithAvatar: Story = { render: () => (
+ {/* Forzamos el slot avatar a 16px y recortamos */} }>EGDEV + + } + > + User + + AP
} + classNames={{ avatar: 'h-4 w-4 overflow-hidden rounded-full' }} + avatar={
A
} > Andrés diff --git a/src/components/atoms/chip/Chip.tsx b/src/components/atoms/chip/Chip.tsx index 44093a2e..7745115d 100644 --- a/src/components/atoms/chip/Chip.tsx +++ b/src/components/atoms/chip/Chip.tsx @@ -8,10 +8,24 @@ export const Chip = React.forwardRef((props, ref) => { return ( - {avatar && {avatar}} + {avatar && ( + *]:origin-center [&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-full [&>svg]:w-full', + '[&>*]:scale-[var(--avatar-scale,1)]' + ].join(' ')} + > + {avatar} + + )} {startContent && ( - {startContent} + + {startContent} + )} {isDot && diff --git a/src/components/atoms/chip/types.ts b/src/components/atoms/chip/types.ts index a7647923..4a320397 100644 --- a/src/components/atoms/chip/types.ts +++ b/src/components/atoms/chip/types.ts @@ -5,7 +5,7 @@ export const chipVariants = cva( [ 'chip relative max-w-full min-w-0', 'transition-all duration-200 ease-in-out', - 'flex items-center justify-center gap-2', + 'flex items-center justify-center', 'font-secondary-bold whitespace-nowrap leading-[1.2]', 'disabled:pointer-events-none disabled:opacity-60', 'focus-visible:outline-none', @@ -16,12 +16,25 @@ export const chipVariants = cva( { variants: { color: { primary: '', secondary: '', success: '', warning: '', danger: '' }, + + // ⬇️ Añadimos la custom prop --chip-h para poder usarla en el wrapper del avatar size: { - sm: 'px-sm h-8 fs-small tablet:fs-small-tablet', - md: 'px-md h-10 fs-base tablet:fs-base-tablet', - lg: 'px-lg h-12 fs-h6 tablet:fs-h6-tablet' + sm: [ + 'h-4 px-1 gap-1 fs-small tablet:fs-small-tablet', + '[--chip-h:theme(spacing.4)]' // 16px + ].join(' '), + md: [ + 'h-6 px-2 gap-1 fs-base tablet:fs-base-tablet', + '[--chip-h:theme(spacing.6)]' // 24px + ].join(' '), + lg: [ + 'h-7 px-3 gap-2 fs-h6 tablet:fs-h6-tablet', + '[--chip-h:theme(spacing.7)]' // 28px + ].join(' ') }, + radiusSize: { none: '', sm: 'rounded-sm', md: 'rounded-md', lg: 'rounded-lg', full: 'rounded-full' }, + variant: { solid: 'border border-transparent', light: 'bg-transparent border border-transparent', @@ -32,8 +45,9 @@ export const chipVariants = cva( dot: 'bg-transparent border' }, - startContent: { default: '', icon: 'mr-2', text: 'font-semibold' }, - endContent: { default: '', icon: 'ml-2', text: 'font-semibold' }, + startContent: { default: '', icon: 'mr-1', text: 'font-semibold' }, + endContent: { default: '', icon: 'ml-1', text: 'font-semibold' }, + animation: { default: '', pulse: 'animate-pulse', bounce: 'animate-bounce', ping: 'animate-badgePing' } }, @@ -82,10 +96,12 @@ export const chipVariants = cva( class: [ 'bg-[var(--color-primary)] text-[var(--color-text-dark)] border border-transparent', 'shadow-none', - 'drop-shadow-[0_16px_16px_color-mix(in_srgb,var(--color-primary)_70%,transparent)]', - 'dark:drop-shadow-[0_16px_16px_color-mix(in_srgb,var(--color-primary)_70%,transparent)]', - 'data-[interactive=true]:active:translate-y-[0.5px]', - 'data-[interactive=true]:hover:bg-[var(--color-red-600)] dark:data-[interactive=true]:hover:bg-[var(--color-red-700)]' + 'drop-shadow-[0_10px_10px_color-mix(in_srgb,var(--color-primary)_60%,transparent)]', + 'dark:drop-shadow-[0_10px_10px_color-mix(in_srgb,var(--color-primary)_80%,transparent)]', + 'shadow-[0_1px_0_rgba(0,0,0,.04),0_4px_10px_color-mix(in_srgb,var(--chip-shadow)_34%,transparent),0_12px_22px_color-mix(in_srgb,var(--chip-shadow)_22%,transparent)]', + 'data-[interactive=true]:hover:shadow-[0_2px_0_rgba(0,0,0,.04),0_6px_14px_color-mix(in_srgb,var(--chip-shadow)_40%,transparent),0_16px_30px_color-mix(in_srgb,var(--chip-shadow)_28%,transparent)]', + 'dark:shadow-[0_1px_0_rgba(255,255,255,.05),0_4px_10px_color-mix(in_srgb,var(--chip-shadow)_26%,transparent),0_12px_24px_color-mix(in_srgb,var(--chip-shadow)_18%,transparent)]', + 'data-[interactive=true]:active:translate-y-[0.5px]' ].join(' ') }, { color: 'primary', variant: 'dot', class: '[--chip-dot:var(--color-primary)]' }, @@ -134,134 +150,10 @@ export const chipVariants = cva( color: 'secondary', variant: 'dot', class: '[--chip-dot:var(--color-gray-light-900)] dark:[--chip-dot:var(--color-gray-dark-200)]' - }, - - /* ----------------- SUCCESS ----------------- */ - { - color: 'success', - variant: 'solid', - class: - 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] data-[interactive=true]:hover:bg-[var(--color-green-dark,#16a34a)]' - }, - { - color: 'success', - variant: 'light', - class: - 'text-[var(--color-green)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)]' - }, - { - color: 'success', - variant: 'flat', - class: - 'bg-[color-mix(in_srgb,var(--color-green)_15%,transparent)] text-[var(--color-green)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-green)_22%,transparent)]' - }, - { - color: 'success', - variant: 'faded', - class: - 'bg-[var(--color-gray-light-100)] text-[var(--color-green)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' - }, - { - color: 'success', - variant: 'bordered', - class: - 'text-[var(--color-green)] border-[var(--color-green)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-green)_12%,transparent)]' - }, - { - color: 'success', - variant: 'shadow', - class: - 'bg-[var(--color-green,#22c55e)] text-[var(--color-text-light)] shadow-[0_10px_22px_-6px_rgba(34,197,94,.45),0_6px_14px_rgba(34,197,94,.30)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(34,197,94,.55),0_10px_20px_rgba(34,197,94,.35)]' - }, - { color: 'success', variant: 'dot', class: '[--chip-dot:var(--color-green)]' }, - - /* ----------------- WARNING ----------------- */ - { - color: 'warning', - variant: 'solid', - class: - 'bg-[var(--color-yellow)] text-[var(--color-text-light)] data-[interactive=true]:hover:bg-[var(--color-yellow-dark)]' - }, - { - color: 'warning', - variant: 'light', - class: - 'text-[var(--color-yellow)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-yellow)_18%,transparent)]' - }, - { - color: 'warning', - variant: 'flat', - class: - 'bg-[var(--color-yellow-light)] text-[var(--color-yellow-dark)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-yellow)_30%,transparent)]' - }, - { - color: 'warning', - variant: 'faded', - class: - 'bg-[var(--color-gray-light-100)] text-[var(--color-yellow-dark)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' - }, - { - color: 'warning', - variant: 'bordered', - class: - 'text-[var(--color-yellow-dark)] border-[var(--color-yellow)] data-[interactive=true]:hover:bg-[color-mix(in_srgb,var(--color-yellow)_15%,transparent)]' - }, - { - color: 'warning', - variant: 'shadow', - class: - 'bg-[var(--color-yellow)] text-black shadow-[0_10px_22px_-6px_rgba(234,179,8,.45),0_6px_14px_rgba(234,179,8,.30)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(234,179,8,.55),0_10px_20px_rgba(234,179,8,.35)]' - }, - { color: 'warning', variant: 'dot', class: '[--chip-dot:var(--color-yellow)]' }, - - /* ----------------- DANGER ----------------- */ - { - color: 'danger', - variant: 'solid', - class: - 'bg-[var(--color-accent)] text-[var(--color-text-dark)] data-[interactive=true]:hover:bg-[var(--color-red-700)]' - }, - { - color: 'danger', - variant: 'light', - class: 'text-[var(--color-accent)] data-[interactive=true]:hover:bg-[var(--color-red-100)]' - }, - { - color: 'danger', - variant: 'flat', - class: - 'bg-[var(--color-accent)] text-[var(--color-red-600)] data-[interactive=true]:hover:bg-[var(--color-red-200)]' - }, - { - color: 'danger', - variant: 'faded', - class: - 'bg-[var(--color-accent)] text-[var(--color-red-600)] border-[var(--color-gray-light-300)] data-[interactive=true]:hover:bg-[var(--color-gray-light-200)] dark:bg-[var(--color-gray-dark-700)] dark:border-[var(--color-gray-dark-600)]' - }, - { - color: 'danger', - variant: 'bordered', - class: - 'text-[var(--color-accent)] border-[var(--color-red-600)] data-[interactive=true]:hover:bg-[var(--color-red-100)]' - }, - { - color: 'danger', - variant: 'shadow', - class: - 'bg-[var(--color-accent)] text-[var(--color-text-dark)] shadow-[0_10px_22px_-6px_rgba(220,38,38,.45),0_6px_14px_rgba(220,38,38,.30)] data-[interactive=true]:hover:shadow-[0_14px_26px_-8px_rgba(220,38,38,.55),0_10px_20px_rgba(220,38,38,.35)]' - }, - { color: 'danger', variant: 'dot', class: '[--chip-dot:var(--color-accent)]' }, - - // ---------- DOT: defaults ---------- - { - variant: 'dot', - class: [ - 'text-[var(--color-text-light)] dark:text-[var(--color-text-dark)]', - 'border-[var(--color-gray-light-600,#9CA3AF)]', - 'dark:border-[var(--color-gray-dark-600,#4B5563)]', - '[--chip-dot:var(--color-primary)]' - ].join(' ') } + + /* ----------------- SUCCESS / WARNING / DANGER ... (igual que tenías) */ + // ... ], defaultVariants: { @@ -285,84 +177,24 @@ export type ChipSizeVariants = VariantProps['size']; export type ChipProps = { children?: React.ReactNode; - /** @control text - * @default primary - */ variant?: ChipVariant; - /** - * @control text - * @default primary - */ color?: ChipColorVariants; - /** - * @control text - * @default md - */ size?: ChipSizeVariants; - /** - * @control text - * @default full - */ radius?: RadiusSize; - /** - * @control text - * @default default - */ animation?: Animation; - /** @control text - * @default default - */ avatar?: React.ReactNode; - /** - * @control text - * @default default - */ startContent?: React.ReactNode; - /** - * @control text - * @default default - */ endContent?: React.ReactNode; - /** - * @control text - * @default div - */ as?: 'div' | 'button'; onClick?: React.MouseEventHandler; - /** - * @control text - * @default false - */ isDisabled?: boolean; - /** - * @control boolean - * @default false - */ closable?: boolean; onClose?: () => void; - /** - * @control text - * @default false - */ selectable?: boolean; - /** - * @control text - * @default false - */ selected?: boolean; - /** - * @control boolean - * @default false - */ defaultSelected?: boolean; onSelectedChange?: (selected: boolean) => void; - /** - * @control text - */ className?: string; - /** - * @control text - */ classNames?: Partial>; ariaLabel?: string; }; diff --git a/src/components/atoms/chip/useChip.ts b/src/components/atoms/chip/useChip.ts index e16cae4e..21fa0ead 100644 --- a/src/components/atoms/chip/useChip.ts +++ b/src/components/atoms/chip/useChip.ts @@ -47,7 +47,7 @@ export function useChip(props: ChipProps) { const startKind = startContent == null ? 'default' : isText(startContent) ? 'text' : 'icon'; const endKind = endContent == null ? 'default' : isText(endContent) ? 'text' : 'icon'; - const interactive = !!onClick || !!selectable; // define interactividad + const interactive = !!onClick || !!selectable; const Tag: 'div' | 'button' = as ?? (interactive ? 'button' : 'div'); const baseClasses = chipVariants({ @@ -78,15 +78,15 @@ export function useChip(props: ChipProps) { : '[&_svg]:h-4 [&_svg]:w-4'; const closeBtnBoxBySize = - size === 'sm' ? 'h-[18px] w-[18px]' : size === 'lg' ? 'h-[20px] w-[20px]' : 'h-[18px] w-[18px]'; + size === 'sm' ? 'h-[10px] w-[10px]' : size === 'lg' ? 'h-[15px] w-[15px]' : 'h-[13px] w-[13px]'; - const closeGlyphSizeBySize = size === 'sm' ? 'text-[16px]' : size === 'lg' ? 'text-[20px]' : 'text-[18px]'; + const closeGlyphSizeBySize = size === 'sm' ? 'text-[16px]' : size === 'lg' ? 'text-[15px]' : 'text-[13px]'; const slots = { base: cn( baseClasses, 'min-w-0', - pieceCount > 1 && (closable ? 'gap-1' : 'gap-2'), + pieceCount > 1 && 'gap-1', className, classNames?.base, interactive ? 'cursor-pointer' : 'cursor-auto', @@ -95,7 +95,7 @@ export function useChip(props: ChipProps) { ), content: cn('truncate', classNames?.content), dot: cn('inline-block w-2 h-2 rounded-full shrink-0 bg-[var(--chip-dot)]', classNames?.dot), - avatar: cn('shrink-0 ltr:mr-2 rtl:ml-2', classNames?.avatar), + avatar: cn('shrink-0 ltr:mr-0.3 rtl:ml-0.3', classNames?.avatar), closeButton: cn( 'relative inline-flex items-center justify-center overflow-visible',