\` element for field labels. |
+| \`Field.Hint\` | Small text for providing helpful hints. |
+| \`Field.Error\` | Error message with optional icon (\`icon.asChild\`, \`icon.children\`). |
+| \`Field.ErrorIcon\` | The default error icon, customizable via the icon prop. |
+| \`Field.Separator\` | Horizontal rule for visually separating sections. |
`,
},
},
},
tags: ['autodocs'],
- decorators: [
- (Story: ComponentType) => (
-
-
-
- ),
- ],
+ decorators: [(Story) => {Story()}
],
};
export default meta;
type Story = StoryObj;
-/**
- * A simple single field example showing label, input, and hint text.
- */
-export const Basic: Story = {
+export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A simple field with label, input, and hint text.',
+ },
+ source: {
+ code: `
+
+ Email
+
+ We'll never share your email with anyone else.
+
+ `,
+ },
+ },
+ },
render: () => (
Email
@@ -98,38 +78,84 @@ export const Basic: Story = {
),
};
-/**
- * A field showing an error state with an error message and icon.
- */
export const WithError: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A field showing an error state with an error message and icon.',
+ },
+ source: {
+ code: `
+
+ Username
+
+ Username must be at least 3 characters long.
+
+ `,
+ },
+ },
+ },
render: () => (
Username
-
+
Username must be at least 3 characters long.
),
};
-/**
- * A field with a horizontal layout, commonly used for checkboxes and switches.
- */
export const HorizontalOrientation: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Use `orientation="horizontal"` for inline layouts, commonly used for checkboxes and switches.',
+ },
+ source: {
+ code: `
+
+
+ Subscribe to newsletter
+
+ `,
+ },
+ },
+ },
render: () => (
-
- Subscribe to newsletter
- Receive weekly updates about new products and offers.
-
+ Subscribe to newsletter
),
};
-/**
- * Multiple related fields grouped together with a legend and description.
- */
export const FieldSet: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Use `Field.Set` with `Field.Legend` and `Field.Description` to group related fields together.',
+ },
+ source: {
+ code: `
+
+ Shipping Address
+ Please provide your shipping address for delivery.
+
+
+ Street Address
+
+
+
+ City
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
Shipping Address
@@ -139,187 +165,67 @@ export const FieldSet: Story = {
Street Address
-
-
- City
-
-
-
- ZIP Code
-
-
-
+
+ City
+
+
),
};
-/**
- * A fieldset with multiple groups separated by visual dividers.
- */
-export const WithSeparators: Story = {
+export const WithSeparator: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use `Field.Separator` to visually divide sections within a fieldset.',
+ },
+ source: {
+ code: `
+
+ Account Settings
+
+
+ Display Name
+
+
+
+
+
+
+
+ Email Notifications
+
+
+
+ Marketing Emails
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
Account Settings
-
- Manage your account preferences and notification settings.
-
Display Name
- This is how your name will appear to others.
-
-
- Bio
-
-
- Email Notifications
- Receive email updates about your account activity.
-
+ Email Notifications
-
- Marketing Emails
- Get the latest news and special offers.
-
-
-
-
- ),
-};
-
-/**
- * A nested field structure showing how groups can be organized within each other.
- */
-export const NestedGroups: Story = {
- render: () => (
-
- Payment Information
- Enter your payment details to complete your purchase.
-
-
- Cardholder Name
-
-
-
- Card Number
-
-
-
-
-
- Expiry Date
-
-
-
- CVV
-
-
-
-
-
-
- Save this card for future purchases
-
-
-
- ),
-};
-
-/**
- * Use the composable primitives to build custom field layouts with full control over structure and styling.
- */
-export const ComposableAnatomy: Story = {
- render: () => (
-
- Contact Form
-
- We'd love to hear from you. Fill out the form below to get in touch.
-
-
-
- Name
-
-
-
- Email
-
- We'll respond within 24 hours.
-
-
- Message
-
+ Marketing Emails
),
- parameters: {
- docs: {
- description: {
- story: `
-Use the composable primitives to build custom field layouts:
-
-\`\`\`tsx
-import * as Field from '@/components/field/primitives';
-import { Input } from '@/components/input';
-
-// Basic vertical field
-
- Label
-
- Optional hint text
-
-
-// Horizontal field (for checkboxes, switches)
-
-
- Label
-
-
-// Field with error
-
- Label
-
- Error message
-
-
-// Multiple fields with fieldset
-
- Section Title
- Section description
-
-
- Field 1
-
-
-
- Field 2
-
-
-
-
-
-// Separated groups
-
-
- {/* First group of fields */}
-
-
-
- {/* Second group of fields */}
-
-
-\`\`\`
- `,
- },
- },
- },
};
diff --git a/src/components/file-input/file-input.tsx b/src/components/file-input/file-input.tsx
index 9455f12..a262462 100644
--- a/src/components/file-input/file-input.tsx
+++ b/src/components/file-input/file-input.tsx
@@ -46,6 +46,26 @@ function formatBytes(bytes: number) {
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${sizes[i]}`;
}
+/**
+ * This component supports various CSS variables for theming. Here's a comprehensive list, along
+ * with their default values:
+ *
+ * ```css
+ * :root {
+ * --file-input-fill: var(--form-fill);
+ * --file-input-fill-hover: var(--form-fill-hover);
+ * --file-input-fill-disabled: var(--form-fill);
+ * --file-input-fill-dragging: var(--success-background);
+ * --file-input-fill-progress: var(--brand);
+ * --file-input-fill-icon: var(--form-fill-icon);
+ * --file-input-text-primary: var(--form-text-primary);
+ * --file-input-text-secondary: var(--form-text-secondary);
+ * --file-input-text-error: var(--error);
+ * --file-input-font-message: var(--font-body);
+ * --file-input-font-header: var(--font-body);
+ * }
+ * ```
+ */
export function FileInput({
id,
className,
diff --git a/src/components/file-input/primitives/file-input-dropzone-error.tsx b/src/components/file-input/primitives/file-input-dropzone-error.tsx
index 75b4302..5304208 100644
--- a/src/components/file-input/primitives/file-input-dropzone-error.tsx
+++ b/src/components/file-input/primitives/file-input-dropzone-error.tsx
@@ -21,7 +21,7 @@ export function FileInputDropzoneError({
return (
+
{children}
);
}
- return (
-
- );
+ return ;
}
function getIconForFile(file: File): ElementType {
diff --git a/src/components/file-input/primitives/file-input-item.tsx b/src/components/file-input/primitives/file-input-item.tsx
index e085a41..33fc15d 100644
--- a/src/components/file-input/primitives/file-input-item.tsx
+++ b/src/components/file-input/primitives/file-input-item.tsx
@@ -58,11 +58,11 @@ export function FileInputItem({ className, children, file, ...props }: FileInput
aria-invalid={status === 'error'}
aria-labelledby={`${generatedId}-name`}
className={cn(
- 'relative flex items-center justify-between gap-2 overflow-hidden rounded-lg border-[1.5px] border-[var(--file-input-item-border,var(--contrast-200))] p-4',
+ 'relative flex items-center justify-between gap-2 overflow-hidden rounded-lg border-[1.5px] border-[--border] p-4',
// Start animation state
'data-[start-animation]:animate-in data-[start-animation]:fade-in-0 data-[start-animation]:slide-in-from-top-2',
// Invalid state
- 'aria-invalid:border-[var(--file-input-item-border-error,var(--error))]',
+ 'aria-invalid:border-[--border-error]',
className,
)}
data-slot="file-input-item"
diff --git a/src/components/file-input/primitives/file-input-message.tsx b/src/components/file-input/primitives/file-input-message.tsx
index 853b2ab..f57514f 100644
--- a/src/components/file-input/primitives/file-input-message.tsx
+++ b/src/components/file-input/primitives/file-input-message.tsx
@@ -13,7 +13,7 @@ export function FileInputMessage({ className, children, ...props }: FileInputMes
return (
;
@@ -16,22 +16,17 @@ export function FileInputRemove({ className, children, ...props }: FileInputRemo
} = useFileInputItem();
return (
-
{children}
-
+
);
}
diff --git a/src/components/file-input/primitives/file-input-status.tsx b/src/components/file-input/primitives/file-input-status.tsx
index c26b3f5..69a2aa4 100644
--- a/src/components/file-input/primitives/file-input-status.tsx
+++ b/src/components/file-input/primitives/file-input-status.tsx
@@ -13,7 +13,7 @@ export function FileInputStatus({ className, children, ...props }: FileInputStat
return (
{
*
* ```css
* :root {
- * --form-status-background-error: var(--error-highlight);
- * --form-status-text-error: var(--error-shadow);
- * --form-status-background-success: var(--success-highlight);
- * --form-status-text-success: var(--success-shadow);
+ * --form-status-fill-error: var(--error-foreground);
+ * --form-status-text-error: var(--error-background);
+ * --form-status-fill-success: var(--success-foreground);
+ * --form-status-text-success: var(--success-background);
* }
* ```
*/
diff --git a/src/components/form-status/primitives/form-status-root.tsx b/src/components/form-status/primitives/form-status-root.tsx
index ea9de09..69dae51 100644
--- a/src/components/form-status/primitives/form-status-root.tsx
+++ b/src/components/form-status/primitives/form-status-root.tsx
@@ -7,9 +7,9 @@ const formStatusVariants = cva('flex items-center gap-3 rounded-xl px-4 py-3 tex
variants: {
type: {
error:
- 'bg-[var(--form-status-background-error,color-mix(in_oklab,_var(--error),_white_75%))] [color:var(--form-status-text-error,color-mix(in_oklab,var(--error),black_75%))]',
+ 'bg-[--form-status-fill-error,var(--error-background)] text-[--form-status-text-error,var(--error-foreground)]',
success:
- 'bg-[var(--form-status-background-success,color-mix(in_oklab,_var(--success),_white_75%))] [color:var(--form-status-text-success,color-mix(in_oklab,var(--success),black_75%))]',
+ 'bg-[--form-status-fill-success,var(--success-background)] text-[--form-status-text-success,var(--success-foreground)]',
},
},
defaultVariants: {
diff --git a/src/components/form-status/storybook/form-status.stories.tsx b/src/components/form-status/storybook/form-status.stories.tsx
index d6e3456..842dcfc 100644
--- a/src/components/form-status/storybook/form-status.stories.tsx
+++ b/src/components/form-status/storybook/form-status.stories.tsx
@@ -18,58 +18,11 @@ A status message component for displaying success or error feedback in forms. Sh
\`\`\`css
:root {
- --form-status-background-error: var(--error-highlight);
- --form-status-text-error: var(--error-shadow);
- --form-status-background-success: var(--success-highlight);
- --form-status-text-success: var(--success-shadow);
+ --form-status-fill-error: var(--error-background);
+ --form-status-text-error: var(--error-foreground);
+ --form-status-fill-success: var(--success-background);
+ --form-status-text-success: var(--success-foreground);
}
-\`\`\`
-
-## Usage
-
-### High-Level Component
-
-The \`FormStatus\` component provides a simple API for displaying form feedback:
-
-\`\`\`tsx
-import { FormStatus } from '@/components/form-status';
-
-Your changes have been saved.
-Something went wrong. Please try again.
-\`\`\`
-
-### Custom Icons
-
-Use the \`successIcon\` or \`errorIcon\` props with \`asChild\` to provide custom icons:
-
-\`\`\`tsx
-import { FormStatus } from '@/components/form-status';
-import { CheckCircle } from 'lucide-react';
-
- }}
->
- Custom icon success message
-
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as FormStatus from '@/components/form-status';
-
-
-
- Your changes have been saved.
-
-
-
-
- Something went wrong.
-
\`\`\`
`,
},
@@ -88,21 +41,33 @@ import * as FormStatus from '@/components/form-status';
},
successIcon: {
control: false,
- description: 'Custom success icon configuration with asChild support',
+ description: 'Custom success icon configuration with `asChild` support',
},
errorIcon: {
control: false,
- description: 'Custom error icon configuration with asChild support',
+ description: 'Custom error icon configuration with `asChild` support',
},
},
+ decorators: [(Story) =>
{Story()}
],
};
export default meta;
type Story = StoryObj;
-// All variants
export const Default: Story = {
- name: 'All Variants',
+ parameters: {
+ docs: {
+ description: {
+ story: 'Success and error status messages with default icons.',
+ },
+ source: {
+ code: `
+Your changes have been saved successfully.
+Something went wrong. Please try again.
+ `,
+ },
+ },
+ },
render: () => (
Your changes have been saved successfully.
@@ -111,8 +76,32 @@ export const Default: Story = {
),
};
-// With custom icons
export const WithCustomIcons: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Use the `successIcon` or `errorIcon` props with `asChild: true` to provide custom icons.',
+ },
+ source: {
+ code: `
+
}}
+>
+ Custom success icon
+
+
+
}}
+>
+ Custom error icon
+
+ `,
+ },
+ },
+ },
render: () => (
+
+ Using primitives for success messages.
+
+
+
+
+ Using primitives for error messages.
+
+ `,
+ },
+ },
+ },
render: () => (
diff --git a/src/components/icon/primitives/icon-skeleton.tsx b/src/components/icon/primitives/icon-skeleton.tsx
index d759471..c901e65 100644
--- a/src/components/icon/primitives/icon-skeleton.tsx
+++ b/src/components/icon/primitives/icon-skeleton.tsx
@@ -10,12 +10,7 @@ export function IconSkeleton({ className, size = 24 }: IconSkeletonProps) {
- }
+ icon={
}
/>
);
}
diff --git a/src/components/icon/storybook/icon.stories.tsx b/src/components/icon/storybook/icon.stories.tsx
index a5240b6..0198795 100644
--- a/src/components/icon/storybook/icon.stories.tsx
+++ b/src/components/icon/storybook/icon.stories.tsx
@@ -14,45 +14,11 @@ const meta: Meta = {
docs: {
description: {
component: `
-A dynamic icon component powered by lucide-react. Supports all Lucide icons with configurable size and stroke width.
+A dynamic icon component powered by lucide-react. Supports all [Lucide icons](https://lucide.dev/icons) with configurable size and stroke width.
-## CSS Variables
+## Styling
-The Icon component inherits color from its parent. Use CSS color or Tailwind text color classes:
-
-\`\`\`css
-.icon-container {
- color: var(--foreground);
-}
-\`\`\`
-
-## Usage
-
-### High-Level Component
-
-The \`Icon\` component dynamically loads icons by name:
-
-\`\`\`tsx
-import { Icon } from '@/components/icon';
-
-
-
-
-\`\`\`
-
-### Available Icons
-
-Browse all available icons at [lucide.dev/icons](https://lucide.dev/icons). Use the kebab-case name (e.g., \`shopping-cart\`, \`arrow-right\`).
-
-### Skeleton Loading State
-
-Use the Skeleton primitive for loading states:
-
-\`\`\`tsx
-import * as Icon from '@/components/icon';
-
-
-\`\`\`
+The Icon inherits color from its parent via \`currentColor\`. Use Tailwind text color classes or CSS color property on a parent element.
`,
},
},
@@ -62,19 +28,19 @@ import * as Icon from '@/components/icon';
name: {
control: 'select',
options: iconNames,
- description: 'The name of the icon from lucide-react',
+ description: 'Icon name from lucide-react (kebab-case)',
},
size: {
control: 'number',
- description: 'The size of the icon in pixels',
+ description: 'Icon size in pixels',
},
strokeWidth: {
control: 'number',
- description: 'The stroke width of the icon',
+ description: 'Stroke width of the icon lines',
},
color: {
control: 'color',
- description: 'The color of the icon',
+ description: 'Icon color (inherits from parent by default)',
},
},
args: {
@@ -87,19 +53,38 @@ import * as Icon from '@/components/icon';
export default meta;
type Story = StoryObj;
-// Default icon
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A dynamically loaded icon by name.',
+ },
+ },
+ },
args: {
name: 'shopping-cart',
},
};
-// All sizes
export const AllSizes: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Icons scale based on the `size` prop.',
+ },
+ source: {
+ code: `
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
-
@@ -107,22 +92,21 @@ export const AllSizes: Story = {
),
};
-// Common e-commerce icons
-export const CommonIcons: Story = {
- render: () => (
-
-
-
-
-
-
-
-
- ),
-};
-
-// Skeleton loading state
export const Skeleton: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use the Skeleton primitive for loading states.',
+ },
+ source: {
+ code: `
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx
index 0201a75..0460798 100644
--- a/src/components/input/input.tsx
+++ b/src/components/input/input.tsx
@@ -1,13 +1,16 @@
+import type { VariantProps } from 'class-variance-authority';
import type { ComponentProps, ReactNode } from 'react';
import * as InputPrimitive from '@/components/input';
+import { inputVariants } from '@/components/input';
-export interface InputProps extends ComponentProps<'input'> {
- prependIcon?: {
- asChild?: boolean;
- children?: ReactNode;
+export type InputProps = Omit
, 'size'> &
+ VariantProps & {
+ prependIcon?: {
+ asChild?: boolean;
+ children?: ReactNode;
+ };
};
-}
/**
* This component supports various CSS variables for theming. Here's a comprehensive list, along
@@ -15,15 +18,23 @@ export interface InputProps extends ComponentProps<'input'> {
*
* ```css
* :root {
- * --input-light-background: var(--background);
- * --input-light-text: var(--foreground);
- * --input-light-placeholder: var(--contrast-500);
+ * --input-fill: var(--form-fill);
+ * --input-fill-icon: var(--form-fill-icon);
+ * --input-fill-disabled: var(--form-fill-disabled);
+ * --input-text: var(--form-text-primary);
+ * --input-text-placeholder: var(--form-text-placeholder);
+ * --input-font: var(--font-body);
* }
* ```
*/
-export function Input({ className, prependIcon, ...props }: InputProps) {
+export function Input({ className, prependIcon, size = 'medium', ...props }: InputProps) {
return (
-
+
{prependIcon?.children != null && (
diff --git a/src/components/input/primitives.ts b/src/components/input/primitives.ts
index 5c15933..cf025c0 100644
--- a/src/components/input/primitives.ts
+++ b/src/components/input/primitives.ts
@@ -1,6 +1,7 @@
export {
InputRoot as Root,
type InputRootProps as RootProps,
+ inputVariants,
} from '@/components/input/primitives/input-root';
export {
InputField as Field,
diff --git a/src/components/input/primitives/input-field.tsx b/src/components/input/primitives/input-field.tsx
index dfbf981..e76553a 100644
--- a/src/components/input/primitives/input-field.tsx
+++ b/src/components/input/primitives/input-field.tsx
@@ -8,13 +8,13 @@ export function InputField({ className, ...props }: InputFieldProps) {
return (
;
export function InputPrepend({ className, children, ...props }: InputPrependProps) {
return (
diff --git a/src/components/input/primitives/input-root.tsx b/src/components/input/primitives/input-root.tsx
index 6368892..7facea2 100644
--- a/src/components/input/primitives/input-root.tsx
+++ b/src/components/input/primitives/input-root.tsx
@@ -1,23 +1,49 @@
+import { cva, type VariantProps } from 'class-variance-authority';
import type { ComponentProps, ReactNode } from 'react';
import { cn } from '@/lib';
-export type InputRootProps = ComponentProps<'div'> & {
- prepend?: ReactNode;
-};
+export const inputVariants = cva(
+ [
+ 'group/input [font-family:var(--input-font,var(--font-body))] relative flex items-center overflow-hidden border border-[--border-subtle] bg-[--input-fill,var(--form-fill)] text-[--input-text,var(--form-text-primary)] transition-colors duration-200',
+ // Focus-within state
+ 'focus-within:border-[--border-focus-secondary]',
+ // Aria-invalid state
+ 'has-[[aria-invalid]]:border-[--border-error]',
+ // Disabled state
+ 'has-[:disabled]:cursor-not-allowed has-[:disabled]:bg-[--input-fill-disabled,var(--form-fill-disabled)] has-[:disabled]:opacity-50',
+ ],
+ {
+ variants: {
+ size: {
+ small: 'h-9 text-sm px-3 py-2 rounded-lg',
+ medium: 'h-11 text-sm px-4 py-3 rounded-lg',
+ large: 'h-14 text-base px-5 py-4 rounded-xl',
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ },
+ },
+);
-export function InputRoot({ className, children, prepend, ...props }: InputRootProps) {
+export type InputRootProps = ComponentProps<'div'> &
+ VariantProps & {
+ prepend?: ReactNode;
+ };
+
+export function InputRoot({
+ className,
+ children,
+ prepend,
+ size = 'medium',
+ ...props
+}: InputRootProps) {
return (
diff --git a/src/components/input/storybook/input.stories.tsx b/src/components/input/storybook/input.stories.tsx
index 8d26d92..af08f7a 100644
--- a/src/components/input/storybook/input.stories.tsx
+++ b/src/components/input/storybook/input.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
-import { DollarSign } from 'lucide-react';
+import { DollarSign, Search } from 'lucide-react';
import { Input, type InputProps } from '@/components/input';
import * as InputPrimitive from '@/components/input/primitives';
@@ -12,67 +12,18 @@ const meta: Meta
= {
docs: {
description: {
component: `
-A text input component with support for prepended icons. Built for forms and user input scenarios.
+A text input with support for prepended icons.
## CSS Variables
\`\`\`css
:root {
- --input-light-background: var(--background);
- --input-light-border: var(--contrast-100);
- --input-light-focus: var(--foreground);
- --input-light-border-error: var(--error);
- --input-light-text: var(--foreground);
- --input-light-placeholder: var(--contrast-500);
- --input-light-icon: var(--contrast-400);
+ --input-fill: var(--form-fill);
+ --input-fill-icon: var(--form-fill-icon);
+ --input-fill-disabled: var(--form-fill-disabled);
+ --input-text: var(--form-text-primary);
+ --input-text-placeholder: var(--form-text-placeholder);
}
-\`\`\`
-
-## Usage
-
-### High-Level Component
-
-The \`Input\` component provides a simple API with an optional \`prependIcon\` prop:
-
-\`\`\`tsx
-import { Input } from '@/components/input';
-import { DollarSign } from 'lucide-react';
-
-// Basic input
-
-
-// With default search icon
-
-
-// With custom icon using asChild
- }} type="number" placeholder="0.00" />
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as Input from '@/components/input';
-import { DollarSign } from 'lucide-react';
-
-// Default search icon
-
-
-
-
-
-
-
-// Custom icon with asChild
-
-
-
-
-
-
-
-
\`\`\`
`,
},
@@ -80,22 +31,28 @@ import { DollarSign } from 'lucide-react';
},
tags: ['autodocs'],
argTypes: {
+ size: {
+ control: 'select',
+ options: ['small', 'medium', 'large'],
+ description: 'The size of the input',
+ },
type: {
control: 'select',
options: ['text', 'email', 'password', 'number', 'tel', 'url', 'search'],
- description: 'The input type',
+ description: 'The HTML input type',
},
placeholder: {
control: 'text',
- description: 'Placeholder text displayed when empty',
+ description: 'Placeholder text when empty',
},
disabled: {
control: 'boolean',
- description: 'Whether the input is disabled',
+ description: 'Disables the input',
},
prependIcon: {
control: false,
- description: 'Icon configuration to prepend before the input',
+ description:
+ 'Icon configuration object with `asChild` and `children` props. Use `asChild: true` to render a custom icon.',
},
},
args: {
@@ -107,18 +64,62 @@ import { DollarSign } from 'lucide-react';
export default meta;
type Story = StoryObj;
-// Default input
export const Default: Story = {
args: {
placeholder: 'Enter text...',
},
};
-// With prepended icons
+export const AllSizes: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Three size options are available: `small`, `medium` (default), and `large`.',
+ },
+ source: {
+ code: `
+
+
+
+ `,
+ },
+ },
+ },
+ render: () => (
+
+
+
+
+
+ ),
+};
+
export const WithPrependIcon: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Add a prepended icon using the `prependIcon` prop. Pass `asChild: true` to use a custom icon component.',
+ },
+ source: {
+ code: `
+ }}
+/>
+
+ }}
+ type="number"
+/>
+ `,
+ },
+ },
+ },
render: () => (
-
+
}} />
}}
@@ -128,18 +129,51 @@ export const WithPrependIcon: Story = {
),
};
-// Disabled state
export const Disabled: Story = {
args: {
disabled: true,
defaultValue: 'Cannot edit this',
},
+ parameters: {
+ docs: {
+ description: {
+ story: 'The `disabled` prop prevents user interaction and applies reduced opacity styling.',
+ },
+ },
+ },
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: `
+Use the primitive components to build custom input layouts.
+
+| Primitive | Description |
+|--------------------------------|-------------------------------------------------------|
+| \`InputPrimitive.Root\` | Container with border, background, and focus states. |
+| \`InputPrimitive.Prepend\` | Positioned wrapper for prepend content. |
+| \`InputPrimitive.PrependIcon\` | Icon with \`asChild\` support; defaults to search. |
+| \`InputPrimitive.Field\` | The actual input element with styling. |
+ `,
+ },
+ source: {
+ code: `
+import * as InputPrimitive from '@/components/input/primitives';
+
+
+
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
-
+
diff --git a/src/components/label/label.tsx b/src/components/label/label.tsx
index d24cd00..82912c3 100644
--- a/src/components/label/label.tsx
+++ b/src/components/label/label.tsx
@@ -5,13 +5,24 @@ import { cn } from '@/lib';
export type LabelProps = ComponentProps;
+/**
+ * This component supports various CSS variables for theming. Here's a comprehensive list, along
+ * with their default values:
+ *
+ * ```css
+ * :root {
+ * --label-text: var(--form-text-primary);
+ * --label-font: var(--font-body);
+ * }
+ * ```
+ */
export function Label({ className, children, ...props }: LabelProps) {
return (
Email Address
-
+\`\`\`css
+:root {
+ --label-text: var(--form-text-primary);
+ --label-font: var(--font-body);
+}
\`\`\`
## Orientation-Aware Styling
-The Label component adapts its styling based on the parent \`Field.Item\`'s orientation:
+The Label component adapts its styling based on the parent \`Field.Item\`'s orientation via the \`data-label-orientation\` attribute:
-| Orientation | Text Size | Font Weight | Color |
-|-------------|-----------|-------------|-------|
-| Vertical (default) | \`text-xs\` | semibold | \`--label-text\` |
-| Horizontal | \`text-sm\` | normal | \`--label-horizontal-text\` |
+| Orientation | Text Size | Font Weight |
+|----------------------|-----------|-------------|
+| Vertical (default) | text-xs | semibold |
+| Horizontal | text-sm | normal |
-## With Field Components
-
-Use \`Field.Label\` inside \`Field.Item\` for automatic orientation handling:
-
-\`\`\`tsx
-import * as Field from '@/components/field';
-import { Input } from '@/components/input';
-import { Checkbox } from '@/components/checkbox';
-
-// Vertical orientation (default)
-
- Name
-
-
-
-// Horizontal orientation for inline controls
-
-
- Accept terms
-
-\`\`\`
+Use \`Field.Item\` with \`orientation="horizontal"\` to get horizontal label styling. This is commonly used for checkboxes and radio buttons.
`,
},
},
@@ -88,21 +45,29 @@ import { Checkbox } from '@/components/checkbox';
description: 'The ID of the form element this label is associated with',
},
},
- decorators: [
- (Story: ComponentType) => (
-
-
-
- ),
- ],
+ decorators: [(Story) => {Story()}
],
};
export default meta;
type Story = StoryObj;
-// Default label with input
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A label paired with an input inside a `Field.Item` container.',
+ },
+ source: {
+ code: `
+
+ Email Address
+
+
+ `,
+ },
+ },
+ },
render: () => (
Email Address
@@ -111,8 +76,29 @@ export const Default: Story = {
),
};
-// Horizontal orientation (for checkboxes)
export const HorizontalOrientation: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'When placed inside a `Field.Item` with `orientation="horizontal"`, the label switches to a lighter weight and larger size, suitable for inline controls like checkboxes.',
+ },
+ source: {
+ code: `
+
+
+
+ Subscribe to newsletter
+
+
+
+ I agree to the terms and conditions
+
+
+ `,
+ },
+ },
+ },
render: () => (
diff --git a/src/components/logo/primitives/logo-link.tsx b/src/components/logo/primitives/logo-link.tsx
index a9f647e..02fbe02 100644
--- a/src/components/logo/primitives/logo-link.tsx
+++ b/src/components/logo/primitives/logo-link.tsx
@@ -11,14 +11,7 @@ export function LogoLink({ asChild = false, className, children, ...props }: Log
const Component = asChild ? Slot : 'a';
return (
-
+
{children}
);
diff --git a/src/components/logo/primitives/logo-text.tsx b/src/components/logo/primitives/logo-text.tsx
index 475afbc..07b2f92 100644
--- a/src/components/logo/primitives/logo-text.tsx
+++ b/src/components/logo/primitives/logo-text.tsx
@@ -10,7 +10,7 @@ export function LogoText({ children, className, ...props }: LogoTextProps) {
return (
- Brand Name
-
-
-// Image logo
-
-
-
-\`\`\`
-
-### Router Integration
-
-Use \`asChild\` for Next.js Link or React Router:
-
-\`\`\`tsx
-import * as Logo from '@/components/logo';
-import Link from 'next/link';
-
-
-
- Brand Name
-
-
-\`\`\`
-
-## Primitives
-
-| Primitive | Description |
-|-----------|-------------|
-| \`Logo.Link\` | Anchor wrapper with focus styles. Supports \`asChild\` for router integration. |
-| \`Logo.Text\` | Text-based logo with responsive sizing. |
-| \`Logo.Image\` | Image-based logo with \`src\`, \`alt\`, \`width\`, and \`height\` props. |
-| \`Logo.Skeleton\` | Loading placeholder for the logo. |
+| Element | Below @xl | @xl and above |
+|---------|-----------|---------------|
+| Text | text-lg | text-2xl |
`,
},
},
@@ -73,8 +38,21 @@ export default meta;
type Story = StoryObj;
-// Text-based logo
export const TextLogo: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A text-based logo wrapped in a link.',
+ },
+ source: {
+ code: `
+
+ Clean Essentials
+
+ `,
+ },
+ },
+ },
render: () => (
Clean Essentials
@@ -82,8 +60,21 @@ export const TextLogo: Story = {
),
};
-// Image-based logo
export const ImageLogo: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'An image-based logo wrapped in a link.',
+ },
+ source: {
+ code: `
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
@@ -91,7 +82,16 @@ export const ImageLogo: Story = {
),
};
-// Skeleton loading state
export const Skeleton: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Loading state placeholder.',
+ },
+ source: {
+ code: ` `,
+ },
+ },
+ },
render: () => ,
};
diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx
index d0c11e2..3ac32e1 100644
--- a/src/components/modal/modal.tsx
+++ b/src/components/modal/modal.tsx
@@ -17,8 +17,8 @@ export interface ModalProps {
*
* ```css
* :root {
- * --modal-background: var(--background);
- * --modal-overlay-background: color-mix(in oklab, var(--foreground) 50%, transparent);
+ * --modal-fill: var(--background);
+ * --modal-fill-overlay: color-mix(in oklab, var(--foreground) 50%, transparent);
* }
* ```
*/
diff --git a/src/components/modal/primitives/modal-content.tsx b/src/components/modal/primitives/modal-content.tsx
index 7d5c682..431bb84 100644
--- a/src/components/modal/primitives/modal-content.tsx
+++ b/src/components/modal/primitives/modal-content.tsx
@@ -9,7 +9,7 @@ export function ModalContent({ children, className, ...props }: ModalContentProp
return (
= {
docs: {
description: {
component: `
-A modal dialog component for displaying content that requires user attention. Built on Radix UI Dialog primitives with overlay and focus management.
+A modal dialog for content requiring user attention. Built on Radix UI Dialog with overlay and focus management.
## CSS Variables
\`\`\`css
:root {
- --modal-background: var(--background);
- --modal-overlay-background: color-mix(in oklab, var(--foreground) 50%, transparent);
+ --modal-fill: var(--background);
+ --modal-fill-overlay: color-mix(in oklab, var(--foreground) 50%, transparent);
}
\`\`\`
-## Usage
+## Container Queries
-### High-Level Component
+The modal content adapts its padding based on overlay width.
-The \`Modal\` component is controlled via \`isOpen\` and \`setOpen\`:
-
-\`\`\`tsx
-import { Modal } from '@/components/modal';
-import { Button } from '@/components/button';
-import { useState } from 'react';
-
-function Example() {
- const [open, setOpen] = useState(false);
-
- return (
- Open Modal}
- >
- Are you sure you want to continue?
- setOpen(false)}>Close
-
- );
-}
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as Modal from '@/components/modal';
-import { Button } from '@/components/button';
-
-
-
- Open
-
-
-
-
- Dialog Title
- Your content here.
-
-
-
-
-\`\`\`
-
-## Primitives
-
-| Primitive | Description |
-|-----------|-------------|
-| \`Modal.Root\` | Container with \`open\` and \`onOpenChange\` props. |
-| \`Modal.Trigger\` | Element that opens the modal. Supports \`asChild\`. |
-| \`Modal.Portal\` | Renders content in a portal. |
-| \`Modal.Overlay\` | Background overlay with animation. |
-| \`Modal.Content\` | Modal content container with focus trap. |
-| \`Modal.Title\` | Accessible title (visually hidden by default). |
+| Element | Below @sm | @sm – @5xl | @5xl and above |
+|---------|-----------|------------|----------------|
+| Content | px-3 py-5 | px-6 py-8 | px-20 py-10 |
`,
},
},
@@ -93,7 +39,7 @@ import { Button } from '@/components/button';
argTypes: {
title: {
control: 'text',
- description: 'Accessible title for the dialog (visually hidden)',
+ description: 'Accessible title (visually hidden)',
},
isOpen: {
control: 'boolean',
@@ -105,7 +51,7 @@ import { Button } from '@/components/button';
},
trigger: {
control: false,
- description: 'Trigger element that opens the modal',
+ description: 'Element that opens the modal',
},
children: {
control: false,
@@ -118,8 +64,32 @@ export default meta;
type Story = StoryObj;
-// Default modal
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A controlled modal with overlay and focus trap.',
+ },
+ source: {
+ code: `
+const [open, setOpen] = useState(false);
+
+Open Modal}
+>
+
+
Welcome
+
Modal content here.
+
setOpen(false)}>Close
+
+
+ `,
+ },
+ },
+ },
render: () => {
const [open, setOpen] = useState(false);
@@ -149,8 +119,33 @@ export const Default: Story = {
},
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitive components for custom layouts.',
+ },
+ source: {
+ code: `
+const [open, setOpen] = useState(false);
+
+
+
+ Open Modal
+
+
+
+
+ Dialog Title
+ Your content here.
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => {
const [open, setOpen] = useState(false);
diff --git a/src/components/navigation-menu/navigation-menu.tsx b/src/components/navigation-menu/navigation-menu.tsx
index 1e73dd3..9885139 100644
--- a/src/components/navigation-menu/navigation-menu.tsx
+++ b/src/components/navigation-menu/navigation-menu.tsx
@@ -37,23 +37,12 @@ export interface NavigationMenuProps {
*
* ```css
* :root {
- * --nav-focus: var(--brand);
- * --nav-text: var(--foreground);
- * --nav-viewport-background: var(--background);
- * --nav-viewport-border: color-mix(in oklab, var(--foreground) 15%, transparent);
- * --nav-link-text: var(--foreground);
- * --nav-link-text-hover: var(--foreground);
- * --nav-link-background: transparent;
- * --nav-link-background-hover: var(--contrast-100);
- * --nav-link-font-family: var(--font-family-body);
- * --nav-grid-label-text: var(--foreground);
- * --nav-grid-label-text-hover: var(--foreground);
- * --nav-grid-label-background: transparent;
- * --nav-grid-label-background-hover: var(--contrast-100);
- * --nav-grid-label-font-family: var(--font-family-body);
- * --nav-grid-link-text: var(--contrast-500);
- * --nav-grid-link-background: transparent;
- * --nav-grid-link-font-family: var(--font-family-body);
+ * --navigation-menu-fill: var(--background);
+ * --navigation-menu-text-primary: var(--text-primary);
+ * --navigation-menu-text-secondary: var(--text-secondary);
+ * --navigation-menu-font: var(--font-body);
+ * --navigation-menu-fill-hover: var(--contrast-100);
+ * --navigation-menu-text-hover: var(--text-primary);
* }
* ```
*/
diff --git a/src/components/navigation-menu/primitives/navigation-menu-grid-column.tsx b/src/components/navigation-menu/primitives/navigation-menu-grid-column.tsx
index 7d1357b..63a4290 100644
--- a/src/components/navigation-menu/primitives/navigation-menu-grid-column.tsx
+++ b/src/components/navigation-menu/primitives/navigation-menu-grid-column.tsx
@@ -10,7 +10,11 @@ export function NavigationMenuGridColumn({
...props
}: NavigationMenuGridColumnProps) {
return (
-
+
{children}
);
diff --git a/src/components/navigation-menu/primitives/navigation-menu-grid-label.tsx b/src/components/navigation-menu/primitives/navigation-menu-grid-label.tsx
index 7eb60d5..5125489 100644
--- a/src/components/navigation-menu/primitives/navigation-menu-grid-label.tsx
+++ b/src/components/navigation-menu/primitives/navigation-menu-grid-label.tsx
@@ -13,11 +13,11 @@ export function NavigationMenuGridLabel({
return (
= {
docs: {
description: {
component: `
-A responsive navigation menu with dropdown support. Built on top of Radix UI Navigation Menu.
+A navigation menu with dropdown support. Built on Radix UI Navigation Menu.
## CSS Variables
\`\`\`css
:root {
- --nav-focus: var(--brand);
- --nav-text: var(--foreground);
- --nav-viewport-background: var(--background);
- --nav-viewport-border: color-mix(in oklab, var(--foreground) 15%, transparent);
- --nav-link-text: var(--foreground);
- --nav-link-text-hover: var(--foreground);
- --nav-link-background: transparent;
- --nav-link-background-hover: var(--contrast-100);
- --nav-link-font-family: var(--font-family-body);
- --nav-grid-label-text: var(--foreground);
- --nav-grid-label-text-hover: var(--foreground);
- --nav-grid-label-background: transparent;
- --nav-grid-label-background-hover: var(--contrast-100);
- --nav-grid-label-font-family: var(--font-family-body);
- --nav-grid-link-text: var(--contrast-500);
- --nav-grid-link-background: transparent;
- --nav-grid-link-font-family: var(--font-family-body);
+ --navigation-menu-fill: var(--background);
+ --navigation-menu-text-primary: var(--text-primary);
+ --navigation-menu-text-secondary: var(--text-secondary);
+ --navigation-menu-font: var(--font-body);
+ --navigation-menu-fill-hover: var(--contrast-100);
+ --navigation-menu-text-hover: var(--text-primary);
}
\`\`\`
-## Usage
+## Item Structure
-### High-Level Component
+Each item can be a simple link or have dropdown content:
-The \`NavigationMenu\` component provides a simple data-driven API:
+- \`{ trigger: 'Home', href: '/' }\` — Simple link
+- \`{ trigger: 'Shop', content: { columns: [...], slot: } }\` — Dropdown with columns and optional slot
-\`\`\`tsx
-import { NavigationMenu } from '@/components/navigation-menu';
-
-
-\`\`\`
-
-### Custom Link Components with asChild
-
-Use the \`asChild\` prop to render your own link component (e.g., Next.js \`Link\`, React Router \`Link\`). This passes all styling and accessibility props to your custom component:
-
-\`\`\`tsx
-import Link from 'next/link';
-import { NavigationMenu } from '@/components/navigation-menu';
-
-Home,
- },
- {
- trigger: 'Shop',
- content: {
- columns: [
- {
- label: {
- label: 'Category',
- href: '/category',
- asChild: true,
- children: Category,
- },
- links: [
- {
- label: 'Item 1',
- href: '/item-1',
- asChild: true,
- children: Item 1,
- },
- ],
- },
- ],
- },
- },
- ]}
-/>
-\`\`\`
-
-### Composable Anatomy
+## Custom Link Components
-For more control, use the primitive components directly:
+Use \`asChild\` to render custom link components (e.g., Next.js \`Link\`):
\`\`\`tsx
-import * as NavigationMenu from '@/components/navigation-menu';
-
-
-
-
- Home
-
-
- Shop
-
-
-
-
- Category
-
-
- Item 1
-
-
-
-
-
-
-
-
-
-
-\`\`\`
-
-With custom link components:
-
-\`\`\`tsx
-import Link from 'next/link';
-import * as NavigationMenu from '@/components/navigation-menu';
-
-
-
-
-
- Home
-
-
-
- Shop
-
-
-
-
- Category
-
-
- Item 1
-
-
-
-
-
-
-
-
-
-
+{ trigger: 'Home', asChild: true, children: Home }
\`\`\`
`,
},
@@ -381,11 +250,11 @@ import * as NavigationMenu from '@/components/navigation-menu';
argTypes: {
items: {
control: 'object',
- description: 'Array of navigation menu items',
+ description: 'Array of navigation items (links or dropdowns)',
},
viewport: {
control: 'boolean',
- description: 'Whether to render the viewport container for dropdown content',
+ description: 'Render viewport container for dropdown content',
},
columns: {
control: 'select',
@@ -405,11 +274,14 @@ import * as NavigationMenu from '@/components/navigation-menu';
export default meta;
type Story = StoryObj;
-/**
- * The default NavigationMenu displays trigger items with dropdown content organized in columns,
- * including an optional slot for featured content like a CategoryCard.
- */
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Dropdown menu with columns and an optional slot for featured content.',
+ },
+ },
+ },
args: {
items: defaultItems,
viewport: true,
@@ -417,20 +289,67 @@ export const Default: Story = {
},
};
-/**
- * Navigation items without dropdown content render as simple links.
- */
export const SimpleLinks: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Items without `content` render as simple links.',
+ },
+ },
+ },
args: {
items: simpleItems,
viewport: true,
},
};
-/**
- * Use the primitive components directly for full customization control.
- */
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitives to build custom navigation menus.',
+ },
+ source: {
+ code: `
+import * as NavigationMenu from '@/components/navigation-menu';
+
+
+
+
+ Home
+
+
+ Shop
+
+
+
+ Featured
+ New arrivals
+ Best sellers
+
+
+ Categories
+ Clothing
+ Accessories
+
+
+
+
+
+
+
+
+ About
+
+
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
@@ -466,11 +385,13 @@ export const ComposableAnatomy: Story = {
= {
docs: {
description: {
component: `
-A pagination component for navigating through pages of content with ellipsis support for large page counts.
+A pagination component for navigating through pages with ellipsis support for large page counts.
## CSS Variables
\`\`\`css
:root {
- --offset-pagination-focus: var(--brand);
- --offset-pagination-font-family: var(--font-family-body);
+ --offset-pagination-font: var(--font-body);
--offset-pagination-ellipsis: var(--foreground);
- --offset-pagination-border: var(--contrast-100);
- --offset-pagination-text: var(--foreground);
- --offset-pagination-background-hover: var(--contrast-100);
- --offset-pagination-current-page-border: var(--foreground);
- --offset-pagination-current-page-background: var(--foreground);
- --offset-pagination-current-page-text: var(--background);
- --offset-pagination-current-page-background-hover: var(--contrast-500);
+ --offset-pagination-text-primary: var(--text-primary);
+ --offset-pagination-fill-hover: var(--contrast-100);
+ --offset-pagination-fill-current: var(--foreground);
+ --offset-pagination-text-current: var(--text-inverse);
+ --offset-pagination-fill-current-hover: var(--contrast-500);
}
\`\`\`
-
-## Usage
-
-### High-Level Component
-
-The \`OffsetPagination\` component takes a \`pages\` array and \`currentPage\`:
-
-\`\`\`tsx
-import { OffsetPagination, type PageItem } from '@/components/offset-pagination';
-
-const pages: Array = [
- { href: '/products?page=1', page: 1 },
- 'ellipsis',
- { href: '/products?page=4', page: 4 },
- { href: '/products?page=5', page: 5 },
- { href: '/products?page=6', page: 6 },
- 'ellipsis',
- { href: '/products?page=10', page: 10 },
-];
-
-
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as OffsetPagination from '@/components/offset-pagination';
-
-
-
-
- 1
-
-
-
-
-
-
- 5
-
-
-
-
-\`\`\`
-
-The \`Link\` component supports \`asChild\` for router integration with Next.js Link or React Router.
`,
},
},
@@ -138,7 +83,7 @@ The \`Link\` component supports \`asChild\` for router integration with Next.js
argTypes: {
pages: {
control: false,
- description: 'Array of page items or ellipsis markers',
+ description: 'Array of `PageItem` objects or `"ellipsis"` strings',
},
currentPage: {
control: 'number',
@@ -154,24 +99,67 @@ The \`Link\` component supports \`asChild\` for router integration with Next.js
export default meta;
type Story = StoryObj;
-// Default pagination (middle of range with ellipsis)
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Pagination in the middle of a range with ellipsis on both sides.',
+ },
+ },
+ },
args: {
pages: generatePages(10, 5),
currentPage: 5,
},
};
-// Few pages (no ellipsis needed)
export const FewPages: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'When there are few pages, no ellipsis is needed.',
+ },
+ },
+ },
args: {
pages: generatePages(4, 2),
currentPage: 2,
},
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitive components for custom layouts.',
+ },
+ source: {
+ code: `
+
+
+
+ 1
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 10
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
diff --git a/src/components/price/price.tsx b/src/components/price/price.tsx
index a9cd734..e260a0d 100644
--- a/src/components/price/price.tsx
+++ b/src/components/price/price.tsx
@@ -29,10 +29,7 @@ export interface PriceProps extends PricePrimitive.RootProps {
*
* ```css
* :root {
- * --price-light-text: var(--foreground);
- * --price-light-sale-text: var(--foreground);
- * --price-dark-text: var(--background);
- * --price-dark-sale-text: var(--background);
+ * --price-text: var(--text-primary);
* }
* ```
*/
diff --git a/src/components/price/primitives/price-root.tsx b/src/components/price/primitives/price-root.tsx
index 7590fe3..e67553a 100644
--- a/src/components/price/primitives/price-root.tsx
+++ b/src/components/price/primitives/price-root.tsx
@@ -8,7 +8,7 @@ export function PriceRoot({ className, children, ...props }: PriceRootProps) {
return (
-
-// Range price (for products with variants)
-
-
-// Sale price (with strikethrough)
-
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as Price from '@/components/price';
-
-
- $18.00 {' '}
- $12.00
-
\`\`\`
`,
},
@@ -63,7 +28,8 @@ import * as Price from '@/components/price';
argTypes: {
price: {
control: 'object',
- description: 'The price object with type and values',
+ description:
+ 'Price object with `type` ("default", "range", or "sale") and corresponding value fields',
},
},
};
@@ -71,8 +37,14 @@ import * as Price from '@/components/price';
export default meta;
type Story = StoryObj;
-// Default single price
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A single price display.',
+ },
+ },
+ },
args: {
price: {
type: 'default',
@@ -81,8 +53,14 @@ export const Default: Story = {
},
};
-// Range price for products with variants
export const Range: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A price range for products with multiple variants.',
+ },
+ },
+ },
args: {
price: {
type: 'range',
@@ -92,8 +70,14 @@ export const Range: Story = {
},
};
-// Sale price with strikethrough
export const Sale: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'A sale price with the original price struck through.',
+ },
+ },
+ },
args: {
price: {
type: 'sale',
@@ -103,8 +87,22 @@ export const Sale: Story = {
},
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitive components for custom layouts.',
+ },
+ source: {
+ code: `
+
+ $18.00 {' '}
+ $12.00
+
+ `,
+ },
+ },
+ },
render: () => (
$18.00 {' '}
diff --git a/src/components/product-card/primitives/product-card-fallback.tsx b/src/components/product-card/primitives/product-card-fallback.tsx
index 496546e..fe86d52 100644
--- a/src/components/product-card/primitives/product-card-fallback.tsx
+++ b/src/components/product-card/primitives/product-card-fallback.tsx
@@ -8,7 +8,7 @@ export function ProductCardFallback({ className, children, ...props }: ProductCa
return (
({
return (
= {
docs: {
description: {
component: `
-A versatile product card component for displaying product information with optional actions. Supports product images, badges, ratings, pricing, and both cart and compare actions.
+A product card for displaying product information with optional actions like add-to-cart and compare.
## CSS Variables
\`\`\`css
:root {
- --product-card-focus: var(--brand);
- --product-card-empty-text: color-mix(in oklab, var(--foreground) 15%, transparent);
- --product-card-light-offset: var(--background);
- --product-card-light-background: var(--contrast-100);
- --product-card-light-title: var(--foreground);
- --product-card-light-subtitle: color-mix(in oklab, var(--foreground) 75%, transparent);
- --product-card-dark-offset: var(--foreground);
- --product-card-dark-background: var(--contrast-500);
- --product-card-dark-title: var(--background);
- --product-card-dark-subtitle: color-mix(in oklab, var(--background) 75%, transparent);
- --product-card-font-family: var(--font-family-body);
- --product-card-border-radius: 1rem;
+ --product-card-text-primary: var(--text-primary);
+ --product-card-text-secondary: var(--text-secondary);
+ --product-card-font-title: var(--font-body);
+ --product-card-font-subtitle: var(--font-body);
+ --product-card-radius: 1rem;
}
\`\`\`
-## Usage
-
-### High-Level Component
-
-The \`ProductCard\` component provides a comprehensive API for displaying products:
-
-\`\`\`tsx
-import { ProductCard } from '@/components/product-card';
-
- console.log('Add to cart:', formData.get('id')),
- label: 'Add to Cart',
- }}
-/>
-\`\`\`
-
-### Price Types
+## Price Types
The \`price\` prop supports three formats:
-- \`{ type: 'default', value: '$19.99' }\` - Standard price
-- \`{ type: 'sale', previousValue: '$29.99', currentValue: '$19.99' }\` - Sale price
-- \`{ type: 'range', minValue: '$19.99', maxValue: '$29.99' }\` - Price range
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as ProductCard from '@/components/product-card';
-
-
-
-
-
- Sale
-
-
-
-
-
- Product Name
- Category
-
-
-
-
-
-
- Add to Cart
-
-
-
-\`\`\`
+- \`{ type: 'default', value: '$19.99' }\` — Standard price
+- \`{ type: 'sale', previousValue: '$29.99', currentValue: '$19.99' }\` — Sale price
+- \`{ type: 'range', minValue: '$19.99', maxValue: '$29.99' }\` — Price range
`,
},
},
@@ -111,19 +43,19 @@ import * as ProductCard from '@/components/product-card';
aspectRatio: {
control: 'select',
options: ['5/6', '3/4', '1/1'],
- description: 'The aspect ratio of the product thumbnail',
+ description: 'Thumbnail aspect ratio',
},
product: {
control: false,
- description: 'Product data object including title, image, price, rating, and link',
+ description: 'Product data (title, image, price, rating, link, badge)',
},
compareAction: {
control: false,
- description: 'Configuration for the compare checkbox action',
+ description: 'Compare checkbox configuration',
},
cartAction: {
control: false,
- description: 'Configuration for the cart action (form submission or link)',
+ description: 'Cart action (form submission or link)',
},
},
decorators: [
@@ -138,31 +70,14 @@ import * as ProductCard from '@/components/product-card';
export default meta;
type Story = StoryObj;
-// Default product card
export const Default: Story = {
- args: {
- product: {
- id: 'natural-fiber-scrub-brush',
- title: 'Natural Fiber Scrub Brush',
- subtitle: 'Kitchen Essentials',
- link: {
- href: '/products/natural-fiber-scrub-brush',
- ariaLabel: 'View Natural Fiber Scrub Brush',
- },
- image: {
- src: 'https://images.unsplash.com/photo-1685052392951-4eb54985d3ae?w=900',
- alt: 'Natural fiber scrub brush with wooden handle',
+ parameters: {
+ docs: {
+ description: {
+ story: 'Product card with badge, rating, and standard price.',
},
- showRating: true,
- rating: 4.5,
- price: { type: 'default', value: '$8.99' },
},
- aspectRatio: '5/6',
},
-};
-
-// With badge overlay
-export const WithBadge: Story = {
args: {
product: {
id: 'eco-cleaning-starter-kit',
@@ -185,8 +100,14 @@ export const WithBadge: Story = {
},
};
-// With sale price
export const WithSalePrice: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use `{ type: "sale" }` price to show original and discounted prices.',
+ },
+ },
+ },
args: {
product: {
id: 'glass-soap-pump-bottle',
@@ -209,8 +130,14 @@ export const WithSalePrice: Story = {
},
};
-// With cart action
export const WithCartAction: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use `cartAction` with `type: "form"` for server action integration.',
+ },
+ },
+ },
args: {
product: {
id: 'minimal-ceramic-soap-dispenser',
@@ -239,8 +166,39 @@ export const WithCartAction: Story = {
},
};
-// With compare action (controlled)
export const WithCompareAction: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use `compareAction` for a controlled compare checkbox.',
+ },
+ source: {
+ code: `
+const [checked, setChecked] = useState(false);
+
+ setChecked(value === true),
+ label: 'Compare',
+ }}
+ product={{
+ id: 'product-id',
+ title: 'Product Name',
+ subtitle: 'Category',
+ link: { href: '/products/id', ariaLabel: 'View Product' },
+ image: { src: '...', alt: '...' },
+ showRating: true,
+ rating: 4.3,
+ price: { type: 'default', value: '$10.50' },
+ }}
+/>
+ `,
+ },
+ },
+ },
render: () => {
const [checked, setChecked] = useState(false);
@@ -274,8 +232,42 @@ export const WithCompareAction: Story = {
},
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitives to build custom product card layouts.',
+ },
+ source: {
+ code: `
+import * as ProductCardPrimitive from '@/components/product-card/primitives';
+
+
+
+
+
+ Popular
+
+
+
+
+
+ Product Name
+ Category
+
+
+
+
+
+
+ Add to Cart
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
@@ -311,8 +303,21 @@ export const ComposableAnatomy: Story = {
),
};
-// Skeleton loading state
export const Skeleton: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Loading state while product data loads.',
+ },
+ source: {
+ code: `
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
diff --git a/src/components/radio-group/primitives/radio-group-indicator.tsx b/src/components/radio-group/primitives/radio-group-indicator.tsx
index ee50827..c15cad0 100644
--- a/src/components/radio-group/primitives/radio-group-indicator.tsx
+++ b/src/components/radio-group/primitives/radio-group-indicator.tsx
@@ -11,7 +11,7 @@ export function RadioGroupIndicator({ className, children, ...props }: RadioGrou
return (
&
*
* ```css
* :root {
- * --radio-group-light-background: var(--background);
- * --radio-group-light-border: var(--contrast-200);
- * --radio-group-light-border-hover: var(--contrast-300);
- * --radio-group-light-border-error: var(--error);
- * --radio-group-light-disabled-border-error: color-mix(in oklab, var(--error) 50%, transparent);
- * --radio-group-light-focus: var(--brand);
- * --radio-group-light-indicator-background: var(--foreground);
+ * --radio-group-fill: var(--form-fill);
+ * --radio-group-fill-checked: var(--form-fill-checked);
* }
* ```
*/
diff --git a/src/components/radio-group/storybook/radio-group.stories.tsx b/src/components/radio-group/storybook/radio-group.stories.tsx
index 05b7d4a..00dc001 100644
--- a/src/components/radio-group/storybook/radio-group.stories.tsx
+++ b/src/components/radio-group/storybook/radio-group.stories.tsx
@@ -32,13 +32,8 @@ A radio group component for selecting a single option from a set of choices. Bui
\`\`\`css
:root {
- --radio-group-light-background: var(--background);
- --radio-group-light-border: var(--contrast-200);
- --radio-group-light-border-hover: var(--contrast-300);
- --radio-group-light-border-error: var(--error);
- --radio-group-light-disabled-border-error: color-mix(in oklab, var(--error) 50%, transparent);
- --radio-group-light-focus: var(--brand);
- --radio-group-light-indicator-background: var(--foreground);
+ --radio-group-fill: var(--form-fill);
+ --radio-group-fill-checked: var(--form-fill-checked);
}
\`\`\`
diff --git a/src/components/range-input/primitives/range-input-icon.tsx b/src/components/range-input/primitives/range-input-icon.tsx
index 94e947d..1000045 100644
--- a/src/components/range-input/primitives/range-input-icon.tsx
+++ b/src/components/range-input/primitives/range-input-icon.tsx
@@ -13,9 +13,11 @@ export interface RangeInputIconProps {
}
export function RangeInputIcon({ asChild = false, className, children }: RangeInputIconProps) {
+ const iconStyles = cn('size-5', className);
+
if (asChild) {
return (
-
+
{children}
);
@@ -24,7 +26,7 @@ export function RangeInputIcon({ asChild = false, className, children }: RangeIn
return (
diff --git a/src/components/rating/primitives/rating-star.tsx b/src/components/rating/primitives/rating-star.tsx
index f2fd529..d0859d1 100644
--- a/src/components/rating/primitives/rating-star.tsx
+++ b/src/components/rating/primitives/rating-star.tsx
@@ -51,7 +51,7 @@ export function RatingStar({ className, ...props }: RatingStarProps) {
return (
= {
docs: {
description: {
component: `
-A star rating component for displaying product reviews and ratings. Shows filled, half-filled, and empty stars based on the rating value.
+A star rating component for displaying product reviews. Shows filled, half-filled, and empty stars based on the rating value.
## CSS Variables
\`\`\`css
:root {
- --rating-icon: var(--foreground);
- --rating-border: var(--contrast-100);
- --rating-text: var(--contrast-400);
+ --rating-fill: var(--foreground);
+ --rating-text: var(--text-secondary);
}
-\`\`\`
-
-## Usage
-
-### High-Level Component
-
-The \`Rating\` component provides a simple API for displaying ratings:
-
-\`\`\`tsx
-import { Rating } from '@/components/rating';
-
-
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as Rating from '@/components/rating';
-
-
-
-
-
-
-
\`\`\`
`,
},
@@ -62,19 +29,19 @@ import * as Rating from '@/components/rating';
argTypes: {
rating: {
control: { type: 'number', min: 0, max: 5, step: 0.5 },
- description: 'The rating value (0-5)',
+ description: 'Rating value from 0 to 5 (supports half stars)',
},
totalReviews: {
control: 'number',
- description: 'Total number of reviews',
+ description: 'Total number of reviews to display',
},
showRating: {
control: 'boolean',
- description: 'Whether to show the numeric rating value',
+ description: 'Show the numeric rating value',
},
showTotalReviews: {
control: 'boolean',
- description: 'Whether to show the total reviews count',
+ description: 'Show the total reviews count',
},
},
};
@@ -82,8 +49,14 @@ import * as Rating from '@/components/rating';
export default meta;
type Story = StoryObj;
-// Default rating
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Rating with stars, numeric value, and review count.',
+ },
+ },
+ },
args: {
rating: 4.5,
totalReviews: 128,
@@ -92,23 +65,14 @@ export const Default: Story = {
},
};
-// Various ratings
-export const AllRatings: Story = {
- render: () => (
-
-
-
-
-
-
-
-
-
- ),
-};
-
-// Stars only
export const StarsOnly: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Stars without numeric value or review count.',
+ },
+ },
+ },
args: {
rating: 4,
showRating: false,
@@ -116,8 +80,24 @@ export const StarsOnly: Story = {
},
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitive components for custom layouts.',
+ },
+ source: {
+ code: `
+
+
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
diff --git a/src/components/reveal/primitives/reveal-viewport.tsx b/src/components/reveal/primitives/reveal-viewport.tsx
index b8e0b72..569470a 100644
--- a/src/components/reveal/primitives/reveal-viewport.tsx
+++ b/src/components/reveal/primitives/reveal-viewport.tsx
@@ -13,7 +13,7 @@ export function RevealViewport({ children, className, ...props }: RevealViewport
return (
= {
docs: {
description: {
component: `
-A component for revealing long content with a show more/less toggle. Useful for product descriptions, FAQ sections, and other collapsible content areas.
+A component for revealing long content with a show more/less toggle. Useful for product descriptions, FAQ sections, and collapsible content.
## CSS Variables
+The underline variant inherits styling from the AnimatedUnderline component:
+
\`\`\`css
:root {
- --reveal-focus: var(--brand);
+ --animated-underline: var(--brand);
}
\`\`\`
-## Usage
-
-### High-Level Component
-
-The \`Reveal\` component provides a simple API for collapsible content:
-
-\`\`\`tsx
-import { Reveal } from '@/components/reveal';
-
-
- Your long content here...
-
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as Reveal from '@/components/reveal';
-
-
-
- Your content here...
-
-
-
-
-
-\`\`\`
+The button variant uses the Button component's theming.
`,
},
},
@@ -95,15 +58,15 @@ import * as Reveal from '@/components/reveal';
variant: {
control: 'select',
options: ['underline', 'button'],
- description: 'The visual variant of the toggle button',
+ description: 'Toggle style: "underline" for text link, "button" for outlined button',
},
showLabel: {
control: 'text',
- description: 'Label for the show more button',
+ description: 'Label when content is collapsed',
},
hideLabel: {
control: 'text',
- description: 'Label for the show less button',
+ description: 'Label when content is expanded',
},
defaultOpen: {
control: 'boolean',
@@ -126,15 +89,38 @@ import * as Reveal from '@/components/reveal';
export default meta;
type Story = StoryObj
;
-// Default with underline variant
export const Default: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Collapsible content with underline toggle.',
+ },
+ },
+ },
args: {
children: longContent,
},
};
-// All variants
export const AllVariants: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Both toggle variants: underline (text link) and button (outlined).',
+ },
+ source: {
+ code: `
+
+ {content}
+
+
+
+ {content}
+
+ `,
+ },
+ },
+ },
render: () => (
@@ -149,8 +135,31 @@ export const AllVariants: Story = {
),
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use primitive components for custom layouts.',
+ },
+ source: {
+ code: `
+
+
+ {content}
+
+
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
{children}
@@ -31,7 +31,7 @@ export function SelectScrollDownIcon({
return (
diff --git a/src/components/select/primitives/select-scroll-up-icon.tsx b/src/components/select/primitives/select-scroll-up-icon.tsx
index a32cdf0..7754825 100644
--- a/src/components/select/primitives/select-scroll-up-icon.tsx
+++ b/src/components/select/primitives/select-scroll-up-icon.tsx
@@ -20,7 +20,7 @@ export function SelectScrollUpIcon({
if (asChild) {
return (
{children}
@@ -31,7 +31,7 @@ export function SelectScrollUpIcon({
return (
diff --git a/src/components/select/primitives/select-trigger-icon.tsx b/src/components/select/primitives/select-trigger-icon.tsx
index 4dcbe93..d6edf4e 100644
--- a/src/components/select/primitives/select-trigger-icon.tsx
+++ b/src/components/select/primitives/select-trigger-icon.tsx
@@ -21,7 +21,7 @@ export function SelectTriggerIcon({
return (
& {
*
* ```css
* :root {
- * --select-light-trigger-background: var(--background);
- * --select-light-trigger-background-hover: var(--contrast-100);
- * --select-light-trigger-border: var(--contrast-100);
- * --select-light-trigger-border-hover: var(--contrast-300);
- * --select-light-trigger-border-error: var(--error);
- * --select-light-trigger-text: var(--foreground);
- * --select-light-trigger-focus: var(--brand);
- * --select-light-icon: var(--foreground);
- * --select-light-content-background: var(--background);
- * --select-light-content-border: color-mix(in oklab, var(--foreground) 10%, transparent);
- * --select-light-item-background-hover: var(--contrast-100);
- * --select-light-item-background-focus: var(--contrast-100);
- * --select-light-item-text: var(--contrast-400);
- * --select-light-item-text-hover: var(--foreground);
- * --select-light-item-text-focus: var(--foreground);
- * --select-light-item-checked-text-focus: var(--foreground);
+ * --select-fill: var(--form-fill);
+ * --select-fill-hover: var(--form-fill-hover);
+ * --select-fill-focus: var(--form-fill-hover);
+ * --select-fill-icon: var(--form-fill-icon);
+ * --select-text-primary: var(--form-text-primary);
+ * --select-text-secondary: var(--form-text-secondary);
+ * --select-text-hover: var(--form-text-hover);
+ * --select-text-focus: var(--form-text-hover);
* }
* ```
*/
diff --git a/src/components/select/storybook/select.stories.tsx b/src/components/select/storybook/select.stories.tsx
index 584e3a7..eedd9ae 100644
--- a/src/components/select/storybook/select.stories.tsx
+++ b/src/components/select/storybook/select.stories.tsx
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
-import type { ComponentType } from 'react';
import { useState } from 'react';
import { Select, type SelectProps } from '@/components/select';
@@ -26,83 +25,25 @@ A customizable select dropdown component built on Radix UI primitives. Supports
\`\`\`css
:root {
- --select-light-trigger-background: var(--background);
- --select-light-trigger-background-hover: var(--contrast-100);
- --select-light-trigger-border: var(--contrast-100);
- --select-light-trigger-border-hover: var(--contrast-300);
- --select-light-trigger-border-error: var(--error);
- --select-light-trigger-text: var(--foreground);
- --select-light-trigger-focus: var(--brand);
- --select-light-icon: var(--foreground);
- --select-light-content-background: var(--background);
- --select-light-content-border: color-mix(in oklab, var(--foreground) 10%, transparent);
- --select-light-item-background-hover: var(--contrast-100);
- --select-light-item-background-focus: var(--contrast-100);
- --select-light-item-text: var(--contrast-400);
- --select-light-item-text-hover: var(--foreground);
- --select-light-item-text-focus: var(--foreground);
- --select-light-item-checked-text-focus: var(--foreground);
+ --select-fill: var(--form-fill);
+ --select-fill-hover: var(--form-fill-hover);
+ --select-fill-focus: var(--form-fill-hover);
+ --select-fill-icon: var(--form-fill-icon);
+ --select-text-primary: var(--form-text-primary);
+ --select-text-secondary: var(--form-text-secondary);
+ --select-text-hover: var(--form-text-hover);
+ --select-text-focus: var(--form-text-hover);
}
\`\`\`
-## Usage
+## Container Queries
-### High-Level Component
+The dropdown content adapts based on container width.
-The \`Select\` component provides a simple API with an \`options\` array:
-
-\`\`\`tsx
-import { Select } from '@/components/select';
-
-const options = [
- { label: 'Small', value: 'sm' },
- { label: 'Medium', value: 'md' },
- { label: 'Large', value: 'lg' },
-];
-
-
-\`\`\`
-
-### Composable Anatomy
-
-For more control, use the primitive components directly:
-
-\`\`\`tsx
-import * as Select from '@/components/select';
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Small
-
-
- Medium
-
-
-
-
-
-
-
-
-\`\`\`
+| Element | Below @4xl | @4xl and above |
+|----------|----------------|----------------|
+| Content | rounded-xl p-2 | rounded-3xl p-4 |
+| Item | text-sm | text-base |
`,
},
},
@@ -132,22 +73,15 @@ import * as Select from '@/components/select';
},
options: {
control: false,
- description: 'Array of options with label and value',
+ description: 'Array of options with `label` and `value`',
},
},
- decorators: [
- (Story: ComponentType) => (
-
-
-
- ),
- ],
+ decorators: [(Story) => {Story()}
],
};
export default meta;
type Story = StoryObj;
-// Default select
export const Default: Story = {
args: {
id: 'select-default',
@@ -157,8 +91,33 @@ export const Default: Story = {
},
};
-// All variants
-export const AllVariants: Story = {
+export const Variants: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: 'Two trigger variants are available: `rectangle` (default) and `round`.',
+ },
+ source: {
+ code: `
+
+
+
+ `,
+ },
+ },
+ },
render: () => (
('');
+
+
+ `,
+ },
+ },
+ },
render: () => {
const [value, setValue] = useState('');
@@ -197,8 +176,65 @@ export const Controlled: Story = {
},
};
-// Composable anatomy example
export const ComposableAnatomy: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story: `
+Use the primitive components for full control over the structure.
+
+| Primitive | Description |
+|-----------------------------------|------------------------------------------|
+| \`SelectPrimitive.Root\` | Provider with variant and pending state. |
+| \`SelectPrimitive.Trigger\` | Button that opens the dropdown. |
+| \`SelectPrimitive.Value\` | Displays current selection or placeholder. |
+| \`SelectPrimitive.Icon\` | Container for the trigger icon. |
+| \`SelectPrimitive.TriggerIcon\` | Chevron icon with \`asChild\` support. |
+| \`SelectPrimitive.Portal\` | Portals content to document body. |
+| \`SelectPrimitive.Content\` | Dropdown container with animations. |
+| \`SelectPrimitive.ScrollUpButton\`| Scroll indicator for overflow. |
+| \`SelectPrimitive.ScrollUpIcon\` | Up chevron with \`asChild\` support. |
+| \`SelectPrimitive.Viewport\` | Scrollable area for items. |
+| \`SelectPrimitive.Item\` | Individual option item. |
+| \`SelectPrimitive.ItemText\` | Text content of an option. |
+| \`SelectPrimitive.ScrollDownButton\`| Scroll indicator for overflow. |
+| \`SelectPrimitive.ScrollDownIcon\`| Down chevron with \`asChild\` support. |
+ `,
+ },
+ source: {
+ code: `
+import * as SelectPrimitive from '@/components/select/primitives';
+
+
+
+