diff --git a/packages/frosted-ui/.storybook/stories/components/filter-chip.stories.tsx b/packages/frosted-ui/.storybook/stories/components/filter-chip.stories.tsx new file mode 100644 index 00000000..f6ebbd0f --- /dev/null +++ b/packages/frosted-ui/.storybook/stories/components/filter-chip.stories.tsx @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import React from 'react'; +import { FilterChip, Flex } from '../../../src/components'; +import { filterChipPropDefs } from '../../../src/components/filter-chip.props'; + +const ExampleIcon = ({ size }: { size: number }) => ( + + + + +); + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: 'Controls/FilterChip', + component: FilterChip, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout + layout: 'centered', + }, + args: { + children: null, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = { + args: { + size: filterChipPropDefs.size.default, + color: filterChipPropDefs.color.default, + }, + render: (args) => ( + + + + + Disabled checked + + + + Disabled unchecked + + + ), +}; + +export const Size: Story = { + render: (args) => ( + + + Size 1 + + + Size 2 + + + Size 3 + + + ), +}; + +export const Color: Story = { + render: (args) => ( + + + + Indigo + + + + Cyan + + + + Orange + + + + Crimson + + + ), +}; diff --git a/packages/frosted-ui/src/components/filter-chip.css b/packages/frosted-ui/src/components/filter-chip.css new file mode 100644 index 00000000..c08c29d6 --- /dev/null +++ b/packages/frosted-ui/src/components/filter-chip.css @@ -0,0 +1,117 @@ +.fui-BaseChip { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + flex-shrink: 0; + user-select: none; + vertical-align: top; + color: var(--base-chip-color); +} + +/*************************************************************************************************** + * * + * SIZES * + * * + ***************************************************************************************************/ + +.fui-BaseChip { + height: var(--base-chip-height); + border-radius: var(--radius-thumb); +} + +@breakpoints { + .fui-BaseChip { + &:where(.fui-r-size-1) { + --base-chip-height: var(--space-5); + padding-left: var(--space-2); + padding-right: var(--space-2); + + gap: var(--space-1); + font-size: var(--font-size-1); + line-height: var(--line-height-1); + letter-spacing: var(--letter-spacing-1); + font-weight: var(--font-weight-medium); + } + &:where(.fui-r-size-2) { + --base-chip-height: var(--space-6); + padding-left: var(--space-3); + padding-right: var(--space-3); + + gap: calc(1.5 * var(--space-1)); + font-size: var(--font-size-2); + line-height: var(--line-height-2); + letter-spacing: var(--letter-spacing-2); + font-weight: var(--font-weight-medium); + } + &:where(.fui-r-size-3) { + --base-chip-height: var(--space-7); + padding-left: var(--space-4); + padding-right: var(--space-4); + + gap: var(--space-2); + font-size: var(--font-size-3); + line-height: var(--line-height-3); + letter-spacing: var(--letter-spacing-3); + font-weight: var(--font-weight-medium); + } + } +} + +.fui-BaseChip:where([data-state='unchecked']) { + box-shadow: inset 0 0 0 1px var(--gray-a5); + --base-chip-color: var(--gray-a12); + + &:where(:hover) { + background-color: var(--gray-a2); + } + &:where(:active) { + background-color: var(--gray-a3); + } + + &:where(:focus-visible) { + outline: 2px solid var(--color-focus-root); + outline-offset: -1px; + } + + &:where([data-disabled]) { + cursor: var(--cursor-disabled); + --base-chip-color: var(--gray-a8); + box-shadow: inset 0 0 0 1px var(--gray-a4); + background-color: transparent; + } + &:where(:not([data-disabled])) > svg { + color: var(--gray-a11); + } +} + +.fui-BaseChip:where([data-state='checked']) { + --base-chip-color: var(--accent-11); + background-color: var(--accent-a3); + box-shadow: inset 0 0 0 1px var(--accent-a6); + + &:where(:focus-visible) { + outline: 2px solid var(--accent-8); + outline-offset: -1px; + } + @media (hover: hover) { + &:where(:hover) { + background-color: var(--accent-a4); + } + } + + &:where(:active) { + background-color: var(--accent-a5); + } + + &:where([data-disabled]) { + cursor: var(--cursor-disabled); + --base-chip-color: var(--gray-8); + background-color: var(--gray-a3); + box-shadow: inset 0 0 0 1px var(--gray-a5); + } + + &:where(:not([data-disabled])) > svg { + color: var(--accent-10); + } +} diff --git a/packages/frosted-ui/src/components/filter-chip.props.ts b/packages/frosted-ui/src/components/filter-chip.props.ts new file mode 100644 index 00000000..dff6517b --- /dev/null +++ b/packages/frosted-ui/src/components/filter-chip.props.ts @@ -0,0 +1,14 @@ +import type { PropDef } from '../helpers'; +import { colorProp } from '../helpers'; + +const sizes = ['1', '2', '3'] as const; + +const filterChipPropDefs = { + size: { type: 'enum', values: sizes, default: '2', responsive: true }, + color: colorProp, +} satisfies { + size: PropDef<(typeof sizes)[number]>; + color: typeof colorProp; +}; + +export { filterChipPropDefs }; diff --git a/packages/frosted-ui/src/components/filter-chip.tsx b/packages/frosted-ui/src/components/filter-chip.tsx new file mode 100644 index 00000000..e7565428 --- /dev/null +++ b/packages/frosted-ui/src/components/filter-chip.tsx @@ -0,0 +1,62 @@ +'use client'; + +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import classNames from 'classnames'; +import * as React from 'react'; +import { + extractMarginProps, + withBreakpoints, + withMarginProps, +} from '../helpers'; +import { filterChipPropDefs } from './filter-chip.props'; + +import type { + GetPropDefTypes, + MarginProps, + PropsWithoutRefOrColor, +} from '../helpers'; + +type FilterChipElement = React.ElementRef; +type FilterChipOwnProps = GetPropDefTypes; +interface FilterChipProps + extends PropsWithoutRefOrColor, + MarginProps, + FilterChipOwnProps { + children: React.ReactNode; +} + +const FilterChip = React.forwardRef( + (props, forwardedRef) => { + const { rest: marginRest, ...marginProps } = extractMarginProps(props); + const { + children, + className, + style, + size = filterChipPropDefs.size.default, + color = filterChipPropDefs.color.default, + ...checkboxProps + } = marginRest; + + return ( + + {children} + + ); + }, +); +FilterChip.displayName = 'FilterChip'; + +export { FilterChip }; +export type { FilterChipProps }; diff --git a/packages/frosted-ui/src/components/index.ts b/packages/frosted-ui/src/components/index.ts index 996585f6..0c3b1c5c 100644 --- a/packages/frosted-ui/src/components/index.ts +++ b/packages/frosted-ui/src/components/index.ts @@ -46,6 +46,8 @@ export { DatePicker } from './date-picker'; export * from './date-picker.props'; export { DateRangePicker } from './date-range-picker'; export * from './date-range-picker.props'; +export { FilterChip } from './filter-chip'; +export * from './filter-chip.props'; export { Shine } from './lab/shine'; export { Select, diff --git a/packages/frosted-ui/src/styles/index.css b/packages/frosted-ui/src/styles/index.css index 9d1e253e..5cc6ee36 100644 --- a/packages/frosted-ui/src/styles/index.css +++ b/packages/frosted-ui/src/styles/index.css @@ -30,6 +30,7 @@ @import '../components/drawer.css'; @import '../components/dropdown-menu.css'; @import '../components/em.css'; +@import '../components/filter-chip.css'; @import '../components/flex.css'; @import '../components/grid.css'; @import '../components/heading.css';