diff --git a/.changeset/fuzzy-berries-do.md b/.changeset/fuzzy-berries-do.md new file mode 100644 index 0000000..a286108 --- /dev/null +++ b/.changeset/fuzzy-berries-do.md @@ -0,0 +1,5 @@ +--- +'commerce-toolkit': minor +--- + +Feat: simplified CSS variables available for customizing components diff --git a/.cursor/commands/create-story.md b/.cursor/commands/create-story.md index 19f81c8..9d73eb8 100644 --- a/.cursor/commands/create-story.md +++ b/.cursor/commands/create-story.md @@ -16,6 +16,94 @@ Our composable components allow for custom CSS classes to be passed in to overri Do not provide too many examples. Focus on examples that highlight the monolith component and the interface it exposes. A composable example that simply highlights how to use them is fine, but try and limit the amount of customization. +## Code preview source + +Stories using `render` functions (especially with hooks like `useState`) show the raw story object in Storybook's "Show code" feature. To display clean React code instead, add a `source` parameter: + +```tsx +export const Default: Story = { + parameters: { + docs: { + description: { + story: 'A controlled component example.', + }, + source: { + code: ` +const [value, setValue] = useState(false); + + + `, + }, + }, + }, + render: () => { + const [value, setValue] = useState(false); + + return ; + }, +}; +``` + +Always include `source.code` for stories that use `render` functions to ensure users see clean, copyable React code. + +## Container queries + +Components that use container queries (identified by `@container` class on the root and `@` prefixed breakpoints like `@lg:text-xl`) should document their responsive behavior. Include: + +1. **Documentation table** - Add a table in the component description showing what changes at each breakpoint: + +```markdown +## Container Queries + +The component uses container queries to adapt based on container width. + +| Element | Below @lg | @lg and above | +|---------|-----------|---------------| +| Title | text-base | text-xl | +| Content | text-sm | text-base | +``` + +2. **ContainerQueries story** - Create a story showing the component at different container sizes side by side: + +```tsx +export const ContainerQueries: Story = { + render: () => ( +
+
+

Small container (below @lg breakpoint)

+
+ +
+
+
+

Large container (at @lg breakpoint)

+
+ +
+
+
+ ), +}; +``` + +### Tailwind container query breakpoints + +Use these default breakpoint widths when sizing containers: + +| Breakpoint | Width | +|------------|-------| +| `@xs` | 20rem (320px) | +| `@sm` | 24rem (384px) | +| `@md` | 28rem (448px) | +| `@lg` | 32rem (512px) | +| `@xl` | 36rem (576px) | +| `@2xl` | 42rem (672px) | +| `@3xl` | 48rem (768px) | +| `@4xl` | 56rem (896px) | +| `@5xl` | 64rem (1024px) | +| `@6xl` | 72rem (1152px) | +| `@7xl` | 80rem (1280px) | + ## Product images Below are some product names and images you can use for examples: diff --git a/.cursor/commands/organize-tailwind-classes.md b/.cursor/commands/organize-tailwind-classes.md index 5c9461e..c61a006 100644 --- a/.cursor/commands/organize-tailwind-classes.md +++ b/.cursor/commands/organize-tailwind-classes.md @@ -1,6 +1,23 @@ # Organize Tailwind Classes -You are helping to organize long Tailwind CSS class strings into readable, grouped lines. Take a single long className string and break it into multiple lines within a `cn()` function call, with comments indicating the purpose of each group. +You are helping to organize Tailwind CSS class strings into readable, maintainable code. The level of organization depends on the complexity of the component. + +## When to Use Which Approach + +### Simple Format (1-2 lines) +Use when the className has: +- **Fewer than ~15 total classes**, OR +- **Minimal state variants** (0-1 state variants with just 1-2 classes each) + +Keep all classes on one line, or split into two lines (base + states) if it improves readability. + +### Detailed Grouping (multiple commented sections) +Use when the className has: +- **3+ state variants**, OR +- **Any state variant with 3+ classes**, OR +- **Complex modifiers** (e.g., `group-data-[*]`, multiple breakpoints per variant) + +Break into multiple commented sections for better maintainability. ## Grouping Strategy @@ -25,7 +42,31 @@ Organize classes into logical groups in this order (skip groups that don't apply 10. **Responsive** - breakpoint prefixes (`sm:*`, `md:*`, `lg:*`, `xl:*`, `2xl:*`) 11. **Container queries** - `@*:` classes -## Output Format +## Simple Format Examples + +For simple components, keep classes concise: + +### Single line (very simple) + +```tsx +className={cn('truncate text-sm font-semibold text-foreground', className)} +``` + +### Two lines (simple with minimal states) + +```tsx +className={cn( + 'flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors', + 'hover:bg-accent disabled:opacity-50', + className, +)} +``` + +## Detailed Format Examples + +For complex components with many interactive states, use commented groupings: + +### Standard detailed format Use the `cn()` function with base styles on an uncommented first line, then state variants with comments: @@ -44,9 +85,23 @@ className={cn( )} ``` -## Example Reference +## Real-World Examples + +### Simple component (FileInputHeader) + +```tsx +
+ {children} +
+``` -Here's a well-organized component to use as a reference: +### Complex component (NavigationMenuTrigger) ```tsx + + + diff --git a/.storybook/preview.css b/.storybook/preview.css new file mode 100644 index 0000000..f1a35c1 --- /dev/null +++ b/.storybook/preview.css @@ -0,0 +1,33 @@ +/* Fonts for Storybook examples only - not exported with the library */ + +@font-face { + font-family: 'Inter'; + src: url('/fonts/InterVariable.woff2') format('woff2-variations'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Inter'; + src: url('/fonts/InterVariable-Italic.woff2') format('woff2-variations'); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'General Sans'; + src: url('/fonts/GeneralSans-Variable.woff2') format('woff2-variations'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'General Sans'; + src: url('/fonts/GeneralSans-VariableItalic.woff2') format('woff2-variations'); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} diff --git a/.storybook/preview.js b/.storybook/preview.js index c36b637..c34c785 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,5 @@ import '../src/globals.css'; +import './preview.css'; const preview = { parameters: { diff --git a/package.json b/package.json index 2066df2..6fe7928 100644 --- a/package.json +++ b/package.json @@ -67,14 +67,14 @@ "default": "./dist/button-radio-group.cjs" } }, - "./blog-post-card": { + "./blog-card": { "import": { - "types": "./dist/components/blog-post-card/primitives.d.ts", - "default": "./dist/blog-post-card.js" + "types": "./dist/components/blog-card/primitives.d.ts", + "default": "./dist/blog-card.js" }, "require": { - "types": "./dist/components/blog-post-card/primitives.d.ts", - "default": "./dist/blog-post-card.cjs" + "types": "./dist/components/blog-card/primitives.d.ts", + "default": "./dist/blog-card.cjs" } }, "./button": { @@ -481,7 +481,7 @@ "scripts": { "dev": "vite", "build": "vite build", - "build:lib": "vite build --config vite.config.lib.js && tsc --project tsconfig.lib.json && resolve-tspaths -p tsconfig.lib.json && cp src/styles.css dist/styles.css", + "build:lib": "vite build --config vite.config.lib.js && tsc --project tsconfig.lib.json && resolve-tspaths -p tsconfig.lib.json && cp src/globals.css dist/styles.css", "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/public/fonts/GeneralSans-Variable.woff2 b/public/fonts/GeneralSans-Variable.woff2 new file mode 100644 index 0000000..55e906b Binary files /dev/null and b/public/fonts/GeneralSans-Variable.woff2 differ diff --git a/public/fonts/GeneralSans-VariableItalic.woff2 b/public/fonts/GeneralSans-VariableItalic.woff2 new file mode 100644 index 0000000..5c1e1ca Binary files /dev/null and b/public/fonts/GeneralSans-VariableItalic.woff2 differ diff --git a/public/fonts/InterVariable-Italic.woff2 b/public/fonts/InterVariable-Italic.woff2 new file mode 100644 index 0000000..b3530f3 Binary files /dev/null and b/public/fonts/InterVariable-Italic.woff2 differ diff --git a/public/fonts/InterVariable.woff2 b/public/fonts/InterVariable.woff2 new file mode 100644 index 0000000..5a8d3e7 Binary files /dev/null and b/public/fonts/InterVariable.woff2 differ diff --git a/src/components/accordion/accordion.tsx b/src/components/accordion/accordion.tsx index 31fac50..c3b85e3 100644 --- a/src/components/accordion/accordion.tsx +++ b/src/components/accordion/accordion.tsx @@ -30,20 +30,11 @@ export type AccordionProps = AccordionSingleProps | AccordionMultipleProps; * * ```css * :root { - * --accordion-focus: var(--brand); - * --accordion-offset: var(--background); - * --accordion-light-title-text: var(--contrast-400); - * --accordion-light-title-text-hover: var(--foreground); - * --accordion-light-title-icon: var(--contrast-500); - * --accordion-light-title-icon-hover: var(--foreground); - * --accordion-light-content-text: var(--foreground); - * --accordion-dark-title-text: var(--contrast-200); - * --accordion-dark-title-text-hover: var(--background); - * --accordion-dark-title-icon: var(--contrast-200); - * --accordion-dark-title-icon-hover: var(--background); - * --accordion-dark-content-text: var(--background); - * --accordion-title-font-family: var(--font-family-body); - * --accordion-content-font-family: var(--font-family-body); + * --accordion-text-primary: var(--text-primary); + * --accordion-text-secondary: var(--text-secondary); + * --accordion-fill-icon: var(--contrast-400); + * --accordion-font-title: var(--font-heading); + * --accordion-font-body: var(--font-body); * } * ``` */ @@ -59,7 +50,7 @@ export function Accordion({ className, items, ...props }: AccordionProps) { - {content} + {content} ))} diff --git a/src/components/accordion/primitives.ts b/src/components/accordion/primitives.ts index 70b0d3b..08bab2b 100644 --- a/src/components/accordion/primitives.ts +++ b/src/components/accordion/primitives.ts @@ -11,9 +11,9 @@ export { type AccordionContentProps as ContentProps, } from '@/components/accordion/primitives/accordion-content'; export { - AccordionContentArea as ContentArea, - type AccordionContentAreaProps as ContentAreaProps, -} from '@/components/accordion/primitives/accordion-content-area'; + AccordionBody as Body, + type AccordionBodyProps as BodyProps, +} from '@/components/accordion/primitives/accordion-body'; export { AccordionHeader as Header, type AccordionHeaderProps as HeaderProps, diff --git a/src/components/accordion/primitives/accordion-body.tsx b/src/components/accordion/primitives/accordion-body.tsx new file mode 100644 index 0000000..f7b59d8 --- /dev/null +++ b/src/components/accordion/primitives/accordion-body.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export type AccordionBodyProps = ComponentProps<'div'>; + +export function AccordionBody({ children, className, ...props }: AccordionBodyProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/accordion/primitives/accordion-chevron.tsx b/src/components/accordion/primitives/accordion-chevron.tsx index 265a1c8..7b7b9c0 100644 --- a/src/components/accordion/primitives/accordion-chevron.tsx +++ b/src/components/accordion/primitives/accordion-chevron.tsx @@ -8,9 +8,7 @@ export function AccordionChevron({ className, ...props }: AccordionChevronProps) return ( line]:origin-center [&>line]:transition [&>line]:duration-300 [&>line]:ease-out', - // Hover state - 'group-hover/accordion:stroke-[var(--accordion-light-title-icon-hover,var(--foreground))]', + 'mt-1 shrink-0 stroke-[--accordion-fill-icon,var(--contrast-400)] [&>line]:origin-center [&>line]:transition [&>line]:duration-300 [&>line]:ease-out', className, )} data-slot="accordion-chevron" diff --git a/src/components/accordion/primitives/accordion-content-area.tsx b/src/components/accordion/primitives/accordion-content-area.tsx deleted file mode 100644 index ca00507..0000000 --- a/src/components/accordion/primitives/accordion-content-area.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export type AccordionContentAreaProps = ComponentProps<'div'>; - -export function AccordionContentArea({ children, className, ...props }: AccordionContentAreaProps) { - return ( -
- {children} -
- ); -} diff --git a/src/components/accordion/primitives/accordion-content.tsx b/src/components/accordion/primitives/accordion-content.tsx index 54a4705..9aed7fb 100644 --- a/src/components/accordion/primitives/accordion-content.tsx +++ b/src/components/accordion/primitives/accordion-content.tsx @@ -19,7 +19,6 @@ export function AccordionContent({ className, children, ...props }: AccordionCon ; export function AccordionItem({ children, className, ...props }: AccordionItemProps) { return ( - + {children} ); diff --git a/src/components/accordion/primitives/accordion-title.tsx b/src/components/accordion/primitives/accordion-title.tsx index ef205fa..2351280 100644 --- a/src/components/accordion/primitives/accordion-title.tsx +++ b/src/components/accordion/primitives/accordion-title.tsx @@ -8,9 +8,7 @@ export function AccordionTitle({ children, className, ...props }: AccordionTitle return (
= { title: 'Components/Accordion', component: Accordion, parameters: { - layout: 'fullscreen', + layout: 'centered', docs: { description: { component: ` -A collapsible content component that allows users to toggle sections of content open and closed. +A collapsible content component for organizing and revealing information in expandable sections. ## CSS Variables \`\`\`css :root { - --accordion-focus: var(--brand); - --accordion-light-title-text: var(--contrast-400); - --accordion-light-title-text-hover: var(--foreground); - --accordion-light-title-icon: var(--contrast-500); - --accordion-light-title-icon-hover: var(--foreground); - --accordion-light-content-text: var(--foreground); - --accordion-dark-title-text: var(--contrast-200); - --accordion-dark-title-text-hover: var(--background); - --accordion-dark-title-icon: var(--contrast-200); - --accordion-dark-title-icon-hover: var(--background); - --accordion-dark-content-text: var(--background); - --accordion-title-font-family: var(--font-family-body); - --accordion-content-font-family: var(--font-family-body); + --accordion-text-primary: var(--text-primary); + --accordion-text-secondary: var(--text-secondary); + --accordion-fill-icon: var(--contrast-400); + --accordion-font-title: var(--font-heading); + --accordion-font-body: var(--font-body); } \`\`\` `, @@ -42,7 +33,7 @@ A collapsible content component that allows users to toggle sections of content type: { control: 'select', options: ['single', 'multiple'], - description: 'Whether one or multiple items can be open at a time', + description: 'Whether one or multiple items can be expanded at once', }, collapsible: { control: 'boolean', @@ -58,8 +49,8 @@ A collapsible content component that allows users to toggle sections of content }, }, decorators: [ - (Story: ComponentType) => ( -
+ (Story) => ( +
), @@ -70,79 +61,82 @@ export default meta; type Story = StoryObj; -const faqItems: AccordionProps['items'] = [ +const sampleItems = [ { - value: 'shipping', - title: 'Shipping Information', - content: ( -

- We offer free standard shipping on orders over $50. Standard shipping takes 5-7 business - days, while expedited shipping arrives in 2-3 business days for an additional fee. All - orders include tracking information sent via email. -

- ), + title: 'What materials are used?', + content: + 'Our products are crafted from sustainable materials including bamboo, natural fibers, and recycled glass. Each item is designed with environmental impact in mind.', + value: 'materials', }, { - value: 'returns', - title: 'Returns & Exchanges', - content: ( -

- Items can be returned within 30 days of purchase for a full refund. Products must be unused - and in original packaging. Exchanges are processed within 5-7 business days of receiving - your return. -

- ), + title: 'How do I care for these products?', + content: + 'Most items can be cleaned with mild soap and water. Wooden items should be dried thoroughly after cleaning. Glass containers are dishwasher safe.', + value: 'care', }, { - value: 'materials', - title: 'Materials & Care', - content: ( -

- Our products are crafted from sustainable, eco-friendly materials including bamboo, recycled - glass, and organic cotton. Hand wash with mild soap and water. Avoid harsh chemicals to - extend product life. -

- ), + title: 'What is your return policy?', + content: + 'We offer a 30-day return policy on all unused items in original packaging. Contact our support team to initiate a return.', + value: 'returns', }, ]; -export const Default: Story = { +export const Single: Story = { + args: { + type: 'single', + items: sampleItems, + }, + parameters: { + docs: { + description: { + story: + 'Only one item can be expanded at a time. Expanding a new item closes the previous one.', + }, + }, + }, +}; + +export const SingleCollapsible: Story = { args: { type: 'single', collapsible: true, - defaultValue: 'shipping', - items: faqItems, + items: sampleItems, + }, + parameters: { + docs: { + description: { + story: 'With `collapsible` enabled, clicking an expanded item will close it.', + }, + }, }, }; -export const MultipleOpen: Story = { +export const Multiple: Story = { args: { type: 'multiple', - defaultValue: ['shipping', 'materials'], - items: faqItems, + items: sampleItems, }, parameters: { docs: { description: { - story: - 'When `type` is set to `"multiple"`, users can expand multiple accordion items at once.', + story: 'Multiple items can be expanded simultaneously.', }, }, }, }; -export const NonCollapsible: Story = { +export const WithDefaultValue: Story = { args: { type: 'single', - collapsible: false, - defaultValue: 'shipping', - items: faqItems, + collapsible: true, + defaultValue: 'care', + items: sampleItems, }, parameters: { docs: { description: { - story: - 'When `collapsible` is `false` and `type` is `"single"`, one item must always remain open.', + story: 'Use `defaultValue` to specify which item(s) should be expanded on initial render.', }, }, }, @@ -154,56 +148,31 @@ export const NonCollapsible: Story = { */ export const ComposableAnatomy: Story = { render: () => ( - - - - - Natural Fiber Scrub Brush - - - - - -

- Hand-crafted from sustainable plant fibers, this scrub brush is perfect for dishes, - vegetables, and general cleaning. The ergonomic wooden handle provides a comfortable - grip while the natural bristles are tough on grime but gentle on surfaces. -

-
-
-
- + + - Minimal Ceramic Soap Dispenser + Shipping Information - -

- Elevate your bathroom or kitchen with this sleek ceramic soap dispenser. Features a - smooth pump mechanism and a weighted base for stability. Refillable design helps - reduce plastic waste. -

-
+ + Free shipping on orders over $50. Standard delivery takes 3-5 business days. +
- + - Linen Hand Towel + Warranty Details - -

- Made from 100% European linen, these hand towels are highly absorbent and quick - drying. They become softer with each wash while maintaining their durability. A - timeless addition to any home. -

-
+ + All products come with a 1-year warranty against manufacturing defects. +
@@ -226,9 +195,7 @@ import * as AccordionPrimitive from '@/components/accordion/primitives'; - - Content goes here - + Content goes here
diff --git a/src/components/alert/alert.tsx b/src/components/alert/alert.tsx index e75bae7..b8f088a 100644 --- a/src/components/alert/alert.tsx +++ b/src/components/alert/alert.tsx @@ -23,28 +23,29 @@ export interface AlertProps { * * ```css * :root { - * --alert-success-background: color-mix(in oklab, var(--success), white 75%); - * --alert-warning-background: color-mix(in oklab, var(--warning), white 75%); - * --alert-error-background: color-mix(in oklab, var(--error), white 75%); - * --alert-info-background: var(--background); - * --alert-font-family: var(--font-family-body); - * --alert-border: color-mix(in oklab, var(--foreground) 10%, transparent); - * --alert-message-text: var(--foreground); - * --alert-description-text: color-mix(in oklab, var(--foreground) 50%, transparent); + * --alert-text: var(--text-primary); + * --alert-fill-info: var(--background); + * --alert-fill-success: var(--success-background); + * --alert-fill-warning: var(--warning-background); + * --alert-fill-error: var(--error-background); + * --alert-font-title: var(--font-body); + * --alert-font-description: var(--font-body); * } * ``` */ export function Alert({ className, variant, message, description, action, dismiss }: AlertProps) { return ( - + {message} {description} - - + {action && ( + {action.label} + )} + ); } diff --git a/src/components/alert/primitives.ts b/src/components/alert/primitives.ts index 6664622..a6504e0 100644 --- a/src/components/alert/primitives.ts +++ b/src/components/alert/primitives.ts @@ -1,7 +1,6 @@ export { AlertRoot as Root, type AlertRootProps as RootProps, - useAlert, } from '@/components/alert/primitives/alert-root'; export { AlertHeader as Header, diff --git a/src/components/alert/primitives/alert-action.tsx b/src/components/alert/primitives/alert-action.tsx index 5a28d51..7f25a63 100644 --- a/src/components/alert/primitives/alert-action.tsx +++ b/src/components/alert/primitives/alert-action.tsx @@ -2,21 +2,21 @@ import type { ComponentProps } from 'react'; -import { useAlert } from '@/components/alert'; import { Button } from '@/components/button'; +import { cn } from '@/lib'; export type AlertActionProps = ComponentProps; -export function AlertAction({ children, ...props }: AlertActionProps) { - const { action } = useAlert(); - - if (!action) return null; - - const { label, onClick } = action; - +export function AlertAction({ children, className, ...props }: AlertActionProps) { return ( - ); } diff --git a/src/components/alert/primitives/alert-actions.tsx b/src/components/alert/primitives/alert-actions.tsx index 5769a82..f8ae4b2 100644 --- a/src/components/alert/primitives/alert-actions.tsx +++ b/src/components/alert/primitives/alert-actions.tsx @@ -6,7 +6,11 @@ export type AlertActionsProps = ComponentProps<'div'>; export function AlertActions({ children, className, ...props }: AlertActionsProps) { return ( -
+
{children}
); diff --git a/src/components/alert/primitives/alert-description.tsx b/src/components/alert/primitives/alert-description.tsx index b8421e8..26d37bc 100644 --- a/src/components/alert/primitives/alert-description.tsx +++ b/src/components/alert/primitives/alert-description.tsx @@ -4,11 +4,11 @@ import { cn } from '@/lib'; export type AlertDescriptionProps = ComponentProps<'p'>; -export function AlertDescription({ className, children, ...props }: AlertDescriptionProps) { +export function AlertDescription({ children, className, ...props }: AlertDescriptionProps) { return (

; -export function AlertDismiss({ ...props }: AlertDismissProps) { - const { dismiss } = useAlert(); - - const { label, onClick } = dismiss; - +export function AlertDismiss({ children, className, ...props }: AlertDismissProps) { return ( ); } diff --git a/src/components/alert/primitives/alert-header.tsx b/src/components/alert/primitives/alert-header.tsx index a5f4dfa..0cc6492 100644 --- a/src/components/alert/primitives/alert-header.tsx +++ b/src/components/alert/primitives/alert-header.tsx @@ -7,7 +7,7 @@ export type AlertHeaderProps = ComponentProps<'div'>; export function AlertHeader({ children, className, ...props }: AlertHeaderProps) { return (

diff --git a/src/components/alert/primitives/alert-root.tsx b/src/components/alert/primitives/alert-root.tsx index b4db156..a19c21c 100644 --- a/src/components/alert/primitives/alert-root.tsx +++ b/src/components/alert/primitives/alert-root.tsx @@ -1,33 +1,19 @@ 'use client'; import { cva, type VariantProps } from 'class-variance-authority'; -import { createContext, use, useMemo } from 'react'; -import type { ComponentProps, MouseEventHandler } from 'react'; +import type { ComponentProps } from 'react'; import { cn } from '@/lib'; -interface AlertContext { - action?: { - label: string; - onClick: MouseEventHandler | undefined; - }; - dismiss: { - label: string; - onClick: MouseEventHandler | undefined; - }; -} - -export const AlertContext = createContext(undefined); - const alertVariants = cva( 'group/alert flex max-w-[356px] items-center justify-between gap-2 rounded-xl border border-black/10 py-3 pe-3 ps-4 shadow', { variants: { variant: { - success: 'bg-[var(--alert-success-background,var(--success-background))]', - warning: 'bg-[var(--alert-warning-background,var(--warning-background))]', - error: 'bg-[var(--alert-error-background,var(--error-background))]', - info: 'bg-[var(--alert-info-background,var(--background))]', + success: 'bg-[--alert-fill-success,var(--success-background)]', + warning: 'bg-[--alert-fill-warning,var(--warning-background)]', + error: 'bg-[--alert-fill-error,var(--error-background)]', + info: 'bg-[--alert-fill-info,var(--background)]', }, }, defaultVariants: { @@ -36,55 +22,18 @@ const alertVariants = cva( }, ); -export type AlertRootProps = ComponentProps<'div'> & - VariantProps & { - action?: { - label: string; - onClick: MouseEventHandler | undefined; - }; - dismiss: { - label: string; - onClick: MouseEventHandler | undefined; - }; - }; - -export function AlertRoot({ - className, - children, - action, - dismiss, - variant, - ...props -}: AlertRootProps) { - const contextValues = useMemo( - () => ({ - action, - dismiss, - }), - [action, dismiss], - ); +export type AlertRootProps = ComponentProps<'div'> & VariantProps; +export function AlertRoot({ children, className, variant, ...props }: AlertRootProps) { return ( - -
- {children} -
-
+
+ {children} +
); } - -export function useAlert() { - const context = use(AlertContext); - - if (context === undefined) { - throw new Error('useAlert must be used within an AlertRoot'); - } - - return context; -} diff --git a/src/components/alert/primitives/alert-title.tsx b/src/components/alert/primitives/alert-title.tsx index f429843..5401c47 100644 --- a/src/components/alert/primitives/alert-title.tsx +++ b/src/components/alert/primitives/alert-title.tsx @@ -7,10 +7,7 @@ export type AlertTitleProps = ComponentProps<'h5'>; export function AlertTitle({ children, className, ...props }: AlertTitleProps) { return (
diff --git a/src/components/alert/storybook/alert.stories.tsx b/src/components/alert/storybook/alert.stories.tsx index 7781f64..f81e105 100644 --- a/src/components/alert/storybook/alert.stories.tsx +++ b/src/components/alert/storybook/alert.stories.tsx @@ -17,14 +17,13 @@ A notification component for displaying important messages to users with support \`\`\`css :root { - --alert-success-background: color-mix(in oklab, var(--success), white 75%); - --alert-warning-background: color-mix(in oklab, var(--warning), white 75%); - --alert-error-background: color-mix(in oklab, var(--error), white 75%); - --alert-info-background: var(--background); - --alert-font-family: var(--font-family-body); - --alert-border: color-mix(in oklab, var(--foreground) 10%, transparent); - --alert-message-text: var(--foreground); - --alert-description-text: color-mix(in oklab, var(--foreground) 50%, transparent); + --alert-text: var(--text-primary); + --alert-fill-info: var(--background); + --alert-fill-success: var(--success-background); + --alert-fill-warning: var(--warning-background); + --alert-fill-error: var(--error-background); + --alert-font-title: var(--font-body); + --alert-font-description: var(--font-body); } \`\`\` `, @@ -46,6 +45,13 @@ A notification component for displaying important messages to users with support control: 'text', description: 'Optional description text', }, + action: { + description: 'Optional action button configuration with label and onClick handler', + }, + dismiss: { + description: + 'Dismiss button configuration with label (for accessibility) and onClick handler', + }, }, }; @@ -53,50 +59,53 @@ export default meta; type Story = StoryObj; -export const Success: Story = { +export const Default: Story = { args: { - variant: 'success', - message: 'Payment successful', - description: 'Your payment has been processed successfully.', - dismiss: { - label: 'Dismiss payment alert', - onClick: () => console.log('Dismissed'), - }, - }, -}; - -export const Warning: Story = { - args: { - variant: 'warning', - message: 'Low inventory', - description: 'Only 3 items left in stock.', - dismiss: { - label: 'Dismiss inventory alert', - onClick: () => console.log('Dismissed'), - }, - }, -}; - -export const Error: Story = { - args: { - variant: 'error', - message: 'Payment failed', - description: 'There was an error processing your payment. Please try again.', + variant: 'info', + message: 'New feature available', + description: 'Check out our new shopping experience.', dismiss: { - label: 'Dismiss payment failure alert', + label: 'Dismiss alert', onClick: () => console.log('Dismissed'), }, }, }; -export const Info: Story = { - args: { - variant: 'info', - message: 'New feature available', - description: 'Check out our new shopping experience.', - dismiss: { - label: 'Dismiss new feature alert', - onClick: () => console.log('Dismissed'), +export const AllVariants: Story = { + render: () => ( +
+ console.log('Dismissed') }} + message="Order confirmed" + variant="success" + /> + console.log('Dismissed') }} + message="Low inventory" + variant="warning" + /> + console.log('Dismissed') }} + message="Payment failed" + variant="error" + /> + console.log('Dismissed') }} + message="New feature available" + variant="info" + /> +
+ ), + parameters: { + docs: { + description: { + story: + 'The alert supports four semantic variants: `success`, `warning`, `error`, and `info`.', + }, }, }, }; @@ -130,10 +139,7 @@ export const WithAction: Story = { */ export const ComposableAnatomy: Story = { render: () => ( - console.log('Dismissed') }} - variant="success" - > + Order confirmed @@ -141,7 +147,12 @@ export const ComposableAnatomy: Story = { - + console.log('View order')}> + View Order + + console.log('Dismissed')}> + Dismiss alert + ), @@ -154,17 +165,14 @@ Use the composable primitives to build custom alert layouts: \`\`\`tsx import * as AlertPrimitive from '@/components/alert/primitives'; - + Title Description text - - + Action + Dismiss \`\`\` diff --git a/src/components/animated-underline/animated-underline.tsx b/src/components/animated-underline/animated-underline.tsx index 72f917b..0b17b0e 100644 --- a/src/components/animated-underline/animated-underline.tsx +++ b/src/components/animated-underline/animated-underline.tsx @@ -12,9 +12,7 @@ export type AnimatedUnderlineProps = ComponentProps<'span'> & { * * ```css * :root { - * --animated-underline-hover: var(--brand); - * --animated-underline-text: var(--foreground); - * --animated-underline-font-family: var(--font-family-body); + * --animated-underline: var(--brand); * } * ``` */ @@ -22,7 +20,7 @@ export function AnimatedUnderline({ className, children, ...props }: AnimatedUnd return ( & * * ```css * :root { - * --badge-brand-background: color-mix(in oklab, var(--brand), white 75%); - * --badge-success-background: color-mix(in oklab, var(--success), white 75%); - * --badge-warning-background: color-mix(in oklab, var(--warning), white 75%); - * --badge-error-background: color-mix(in oklab, var(--error), white 75%); - * --badge-info-background: color-mix(in oklab, var(--info), white 75%); + * --badge-fill-brand: color-mix(in oklab, var(--brand), white 75%); + * --badge-fill-success: color-mix(in oklab, var(--success), white 75%); + * --badge-fill-warning: color-mix(in oklab, var(--warning), white 75%); + * --badge-fill-error: color-mix(in oklab, var(--error), white 75%); + * --badge-fill-info: color-mix(in oklab, var(--info), white 75%); * --badge-text: var(--foreground); - * --badge-font-family: var(--font-family-body); + * --badge-font: var(--font-body); * } * ``` */ diff --git a/src/components/badge/storybook/badge.stories.tsx b/src/components/badge/storybook/badge.stories.tsx index eff3970..54372d5 100644 --- a/src/components/badge/storybook/badge.stories.tsx +++ b/src/components/badge/storybook/badge.stories.tsx @@ -16,13 +16,13 @@ A small status indicator component for displaying labels, tags, and statuses. \`\`\`css :root { - --badge-brand-background: var(--brand-background); - --badge-success-background: var(--success-background); - --badge-warning-background: var(--warning-background); - --badge-error-background: var(--error-background); - --badge-info-background: var(--background); + --badge-fill-brand: var(--brand-background); + --badge-fill-success: var(--success-background); + --badge-fill-warning: var(--warning-background); + --badge-fill-error: var(--error-background); + --badge-fill-info: var(--background); --badge-text: var(--text-brand); - --badge-font-family: var(--font-family-body); + --badge-font: var(--font-body); } \`\`\` `, @@ -37,7 +37,7 @@ A small status indicator component for displaying labels, tags, and statuses. }, variant: { control: 'select', - options: ['primary', 'success', 'warning', 'error', 'info'], + options: ['brand', 'success', 'warning', 'error', 'info'], description: 'The semantic variant of the badge', }, shape: { @@ -54,14 +54,14 @@ type Story = StoryObj; export const Default: Story = { args: { children: 'New', - variant: 'primary', + variant: 'brand', }, }; export const AllVariants: Story = { render: () => (
- Primary + Brand Success Warning Error @@ -80,10 +80,10 @@ export const AllVariants: Story = { export const Shapes: Story = { render: () => (
- + Rounded - + Pill
diff --git a/src/components/banner/banner.tsx b/src/components/banner/banner.tsx index f622304..45c5129 100644 --- a/src/components/banner/banner.tsx +++ b/src/components/banner/banner.tsx @@ -8,6 +8,10 @@ export interface BannerProps { hideDismiss?: boolean; children: ReactNode; onDismiss?: () => void; + dismissIcon?: { + asChild?: boolean; + children?: ReactNode; + }; } /** @@ -16,18 +20,21 @@ export interface BannerProps { * * ```css * :root { - * --banner-focus: var(--foreground); - * --banner-background: var(--brand); - * --banner-text: var(--foreground); - * --banner-close-icon: color-mix(in oklab, var(--foreground) 50%, transparent); - * --banner-close-icon-hover: var(--foreground); - * --banner-close-background: transparent; - * --banner-close-background-hover: color-mix(in oklab, var(--background) 40%, transparent); - * --banner-font-family: var(--font-family-body); + * --banner-text: var(--text-primary); + * --banner-fill: var(--brand); + * --banner-fill-icon: var(--contrast-400); + * --banner-font: var(--font-body); * } * ``` */ -export function Banner({ id, children, hideDismiss = false, className, onDismiss }: BannerProps) { +export function Banner({ + id, + children, + hideDismiss = false, + className, + onDismiss, + dismissIcon, +}: BannerProps) { return ( {children} - + + + {dismissIcon?.children} + + ); diff --git a/src/components/banner/primitives.ts b/src/components/banner/primitives.ts index 43ad458..366321a 100644 --- a/src/components/banner/primitives.ts +++ b/src/components/banner/primitives.ts @@ -15,3 +15,7 @@ export { BannerDismiss as Dismiss, type BannerDismissProps as DismissProps, } from '@/components/banner/primitives/banner-dismiss'; +export { + BannerDismissIcon as DismissIcon, + type BannerDismissIconProps as DismissIconProps, +} from '@/components/banner/primitives/banner-dismiss-icon'; diff --git a/src/components/banner/primitives/banner-dismiss-icon.tsx b/src/components/banner/primitives/banner-dismiss-icon.tsx new file mode 100644 index 0000000..f36badc --- /dev/null +++ b/src/components/banner/primitives/banner-dismiss-icon.tsx @@ -0,0 +1,36 @@ +import { Slot } from '@radix-ui/react-slot'; +import { X } from 'lucide-react'; +import type { ReactNode } from 'react'; + +import { cn } from '@/lib'; + +export interface BannerDismissIconProps { + asChild?: boolean; + className?: string; + children?: ReactNode; +} + +export function BannerDismissIcon({ + asChild = false, + className, + children, +}: BannerDismissIconProps) { + const iconStyles = cn('size-5 shrink-0 text-[--banner-fill-icon,var(--foreground)]', className); + + if (asChild) { + return ( + + {children} + + ); + } + + return ( + + ); +} diff --git a/src/components/banner/primitives/banner-dismiss.tsx b/src/components/banner/primitives/banner-dismiss.tsx index a3b27fa..d34f8cb 100644 --- a/src/components/banner/primitives/banner-dismiss.tsx +++ b/src/components/banner/primitives/banner-dismiss.tsx @@ -1,37 +1,33 @@ 'use client'; -import { X } from 'lucide-react'; import type { ComponentProps } from 'react'; import { useBanner } from '@/components/banner'; +import { Button } from '@/components/button'; import { cn } from '@/lib'; export type BannerDismissProps = ComponentProps<'button'>; -export function BannerDismiss({ className, ...props }: BannerDismissProps) { +export function BannerDismiss({ children, className, ...props }: BannerDismissProps) { const { hideDismiss, handleDismiss } = useBanner(); if (hideDismiss) return null; return ( - + {children} + ); } diff --git a/src/components/banner/primitives/banner-root.tsx b/src/components/banner/primitives/banner-root.tsx index 2afb2e9..8836bd7 100644 --- a/src/components/banner/primitives/banner-root.tsx +++ b/src/components/banner/primitives/banner-root.tsx @@ -60,7 +60,7 @@ export function BannerRoot({
{ - // Clear localStorage on mount localStorage.removeItem(`${id}-hidden-banner`); }, [id]); @@ -54,20 +53,16 @@ const meta: Meta = { docs: { description: { component: ` -A dismissible banner component for displaying promotional messages, announcements, and notifications at the top of a page. +A dismissible banner component for displaying promotional messages, announcements, and notifications at the top of a page. The banner persists its dismissed state in localStorage. ## CSS Variables \`\`\`css :root { - --banner-focus: var(--foreground); - --banner-background: var(--brand); - --banner-text: var(--foreground); - --banner-close-icon: color-mix(in oklab, var(--foreground) 50%, transparent); - --banner-close-icon-hover: var(--foreground); - --banner-close-background: transparent; - --banner-close-background-hover: color-mix(in oklab, var(--background) 40%, transparent); - --banner-font-family: var(--font-family-body); + --banner-text: var(--text-primary); + --banner-fill: var(--brand); + --banner-fill-icon: var(--foreground); + --banner-font: var(--font-body); } \`\`\` `, @@ -78,7 +73,7 @@ A dismissible banner component for displaying promotional messages, announcement argTypes: { id: { control: 'text', - description: 'Unique identifier for localStorage persistence', + description: 'Unique identifier used for localStorage persistence of dismissed state', }, children: { control: 'text', @@ -88,6 +83,12 @@ A dismissible banner component for displaying promotional messages, announcement control: 'boolean', description: 'Whether to hide the dismiss button', }, + onDismiss: { + description: 'Callback function called when the banner is dismissed', + }, + dismissIcon: { + description: 'Configuration for a custom dismiss icon with `asChild` and `children` props', + }, }, }; @@ -118,21 +119,6 @@ export const WithoutDismiss: Story = { }, }; -export const ShippingPromotion: Story = { - render: () => ( - - 🚚 Free shipping on all orders over $50 - - ), - parameters: { - docs: { - description: { - story: 'Banners support rich content including emojis and HTML elements like ``.', - }, - }, - }, -}; - /** * The Banner can be built using composable primitives for full customization. * This example shows the component anatomy using the primitive components. @@ -158,7 +144,9 @@ export const ComposableAnatomy: Story = { Summer Sale: Up to 50% off select items. Shop now! - + + +
@@ -181,7 +169,9 @@ import * as BannerPrimitive from '@/components/banner/primitives'; Banner message - + + + \`\`\` diff --git a/src/components/blog-card/blog-card.tsx b/src/components/blog-card/blog-card.tsx new file mode 100644 index 0000000..ce5e474 --- /dev/null +++ b/src/components/blog-card/blog-card.tsx @@ -0,0 +1,61 @@ +import * as BlogCardPrimitive from '@/components/blog-card'; + +export interface BlogCardProps { + className?: string; + aspectRatio?: '5/6' | '3/4' | '4/3' | '1/1'; + title: string; + author?: string; + content: string; + date: string; + image?: { + src: string; + alt: string; + }; + link: { + href: string; + ariaLabel: string; + }; +} + +/** + * This component supports various CSS variables for theming. Here's a comprehensive list, along + * with their default values: + * + * ```css + * :root { + * --blog-card-text-primary: var(--text-primary); + * --blog-card-text-secondary: var(--text-secondary); + * --blog-card-font-title: var(--font-body); + * --blog-card-font-content: var(--font-body); + * } + * ``` + */ +export function BlogCard({ + author, + aspectRatio = '4/3', + content, + date, + link, + image, + title, + className, +}: BlogCardProps) { + return ( + + + {image ? ( + + ) : ( + {title} + )} + + {title} + {content} + + {date} + {author !== undefined && {author}} + + + + ); +} diff --git a/src/components/blog-card/index.ts b/src/components/blog-card/index.ts new file mode 100644 index 0000000..48ad8a4 --- /dev/null +++ b/src/components/blog-card/index.ts @@ -0,0 +1,2 @@ +export { BlogCard, type BlogCardProps } from '@/components/blog-card/blog-card'; +export * from '@/components/blog-card/primitives'; diff --git a/src/components/blog-card/primitives.ts b/src/components/blog-card/primitives.ts new file mode 100644 index 0000000..61632d7 --- /dev/null +++ b/src/components/blog-card/primitives.ts @@ -0,0 +1,44 @@ +export { + BlogCardRoot as Root, + type BlogCardRootProps as RootProps, +} from '@/components/blog-card/primitives/blog-card-root'; +export { + BlogCardFallback as Fallback, + type BlogCardFallbackProps as FallbackProps, +} from '@/components/blog-card/primitives/blog-card-fallback'; +export { + BlogCardThumbnail as Thumbnail, + type BlogCardThumbnailProps as ThumbnailProps, +} from '@/components/blog-card/primitives/blog-card-thumbnail'; +export { + BlogCardImage as Image, + type BlogCardImageProps as ImageProps, +} from '@/components/blog-card/primitives/blog-card-image'; +export { + BlogCardLink as Link, + type BlogCardLinkProps as LinkProps, +} from '@/components/blog-card/primitives/blog-card-link'; +export { + BlogCardTitle as Title, + type BlogCardTitleProps as TitleProps, +} from '@/components/blog-card/primitives/blog-card-title'; +export { + BlogCardContent as Content, + type BlogCardContentProps as ContentProps, +} from '@/components/blog-card/primitives/blog-card-content'; +export { + BlogCardDetails as Details, + type BlogCardDetailsProps as DetailsProps, +} from '@/components/blog-card/primitives/blog-card-details'; +export { + BlogCardDate as Date, + type BlogCardDateProps as DateProps, +} from '@/components/blog-card/primitives/blog-card-date'; +export { + BlogCardAuthor as Author, + type BlogCardAuthorProps as AuthorProps, +} from '@/components/blog-card/primitives/blog-card-author'; +export { + BlogCardSkeleton as Skeleton, + type BlogCardSkeletonProps as SkeletonProps, +} from '@/components/blog-card/primitives/blog-card-skeleton'; diff --git a/src/components/blog-post-card/primitives/blog-post-card-author.tsx b/src/components/blog-card/primitives/blog-card-author.tsx similarity index 53% rename from src/components/blog-post-card/primitives/blog-post-card-author.tsx rename to src/components/blog-card/primitives/blog-card-author.tsx index 1d6688b..9cafb27 100644 --- a/src/components/blog-post-card/primitives/blog-post-card-author.tsx +++ b/src/components/blog-card/primitives/blog-card-author.tsx @@ -2,13 +2,13 @@ import type { ComponentProps } from 'react'; import { cn } from '@/lib'; -export type BlogPostCardAuthorProps = ComponentProps<'span'>; +export type BlogCardAuthorProps = ComponentProps<'span'>; -export function BlogPostCardAuthor({ className, children, ...props }: BlogPostCardAuthorProps) { +export function BlogCardAuthor({ className, children, ...props }: BlogCardAuthorProps) { return ( {children} diff --git a/src/components/blog-card/primitives/blog-card-content.tsx b/src/components/blog-card/primitives/blog-card-content.tsx new file mode 100644 index 0000000..db4396a --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-content.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export type BlogCardContentProps = ComponentProps<'p'>; + +export function BlogCardContent({ className, children, ...props }: BlogCardContentProps) { + return ( +

+ {children} +

+ ); +} diff --git a/src/components/blog-card/primitives/blog-card-date.tsx b/src/components/blog-card/primitives/blog-card-date.tsx new file mode 100644 index 0000000..5229f24 --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-date.tsx @@ -0,0 +1,17 @@ +import type { ComponentProps } from 'react'; + +export type BlogCardDateProps = ComponentProps<'time'> & { + children: string; +}; + +export function BlogCardDate({ className, children, ...props }: BlogCardDateProps) { + return ( + + ); +} diff --git a/src/components/blog-card/primitives/blog-card-details.tsx b/src/components/blog-card/primitives/blog-card-details.tsx new file mode 100644 index 0000000..6b0b857 --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-details.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export type BlogCardDetailsProps = ComponentProps<'div'>; + +export function BlogCardDetails({ children, className, ...props }: BlogCardDetailsProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/blog-card/primitives/blog-card-fallback.tsx b/src/components/blog-card/primitives/blog-card-fallback.tsx new file mode 100644 index 0000000..14a2ec1 --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-fallback.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export type BlogCardFallbackProps = ComponentProps<'div'>; + +export function BlogCardFallback({ children, className, ...props }: BlogCardFallbackProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/blog-post-card/primitives/blog-post-card-image.tsx b/src/components/blog-card/primitives/blog-card-image.tsx similarity index 53% rename from src/components/blog-post-card/primitives/blog-post-card-image.tsx rename to src/components/blog-card/primitives/blog-card-image.tsx index 7e75ee7..715dbfd 100644 --- a/src/components/blog-post-card/primitives/blog-post-card-image.tsx +++ b/src/components/blog-card/primitives/blog-card-image.tsx @@ -3,26 +3,20 @@ import type { ComponentProps } from 'react'; import { cn } from '@/lib'; -export interface BlogPostCardImageProps extends ComponentProps<'img'> { +export interface BlogCardImageProps extends ComponentProps<'img'> { asChild?: boolean; } -export function BlogPostCardImage({ - className, - asChild = false, - ...props -}: BlogPostCardImageProps) { +export function BlogCardImage({ className, asChild = false, ...props }: BlogCardImageProps) { const Component = asChild ? Slot : 'img'; return ( ); diff --git a/src/components/blog-card/primitives/blog-card-link.tsx b/src/components/blog-card/primitives/blog-card-link.tsx new file mode 100644 index 0000000..fc486f6 --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-link.tsx @@ -0,0 +1,20 @@ +import { Slot } from '@radix-ui/react-slot'; +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export interface BlogCardLinkProps extends ComponentProps<'a'> { + asChild?: boolean; +} + +export function BlogCardLink({ asChild = false, className, ...props }: BlogCardLinkProps) { + const Component = asChild ? Slot : 'a'; + + return ( + + ); +} diff --git a/src/components/blog-card/primitives/blog-card-root.tsx b/src/components/blog-card/primitives/blog-card-root.tsx new file mode 100644 index 0000000..3413082 --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-root.tsx @@ -0,0 +1,29 @@ +import type { ComponentProps, ElementType } from 'react'; + +import { cn } from '@/lib'; + +export type BlogCardRootProps = Omit, 'as'> & { + as?: E; + aspectRatio?: '5/6' | '3/4' | '4/3' | '1/1'; +}; + +export function BlogCardRoot({ + as, + className, + children, + aspectRatio = '4/3', + ...props +}: BlogCardRootProps) { + const BlogCardRootElement = as ?? 'article'; + + return ( + + {children} + + ); +} diff --git a/src/components/blog-post-card/primitives/blog-post-card-skeleton.tsx b/src/components/blog-card/primitives/blog-card-skeleton.tsx similarity index 63% rename from src/components/blog-post-card/primitives/blog-post-card-skeleton.tsx rename to src/components/blog-card/primitives/blog-card-skeleton.tsx index 460f228..8129ec6 100644 --- a/src/components/blog-post-card/primitives/blog-post-card-skeleton.tsx +++ b/src/components/blog-card/primitives/blog-card-skeleton.tsx @@ -3,22 +3,22 @@ import type { ComponentProps } from 'react'; import * as SkeletonPrimitive from '@/components/skeleton'; import { cn } from '@/lib'; -export type BlogPostCardSkeletonProps = ComponentProps<'div'>; +export type BlogCardSkeletonProps = ComponentProps<'div'>; -export function BlogPostCardSkeleton({ className, ...props }: BlogPostCardSkeletonProps) { +export function BlogCardSkeleton({ className, ...props }: BlogCardSkeletonProps) { return (
diff --git a/src/components/blog-card/primitives/blog-card-thumbnail.tsx b/src/components/blog-card/primitives/blog-card-thumbnail.tsx new file mode 100644 index 0000000..3ded25f --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-thumbnail.tsx @@ -0,0 +1,24 @@ +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export type BlogCardThumbnailProps = ComponentProps<'div'>; + +export function BlogCardThumbnail({ className, children, ...props }: BlogCardThumbnailProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/blog-card/primitives/blog-card-title.tsx b/src/components/blog-card/primitives/blog-card-title.tsx new file mode 100644 index 0000000..7668b04 --- /dev/null +++ b/src/components/blog-card/primitives/blog-card-title.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react'; + +import { cn } from '@/lib'; + +export type BlogCardTitleProps = ComponentProps<'h5'>; + +export function BlogCardTitle({ className, children, ...props }: BlogCardTitleProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/blog-card/storybook/blog-card.stories.tsx b/src/components/blog-card/storybook/blog-card.stories.tsx new file mode 100644 index 0000000..3d81c1a --- /dev/null +++ b/src/components/blog-card/storybook/blog-card.stories.tsx @@ -0,0 +1,285 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import * as BlogCardPrimitive from '@/components/blog-card'; +import { BlogCard, type BlogCardProps } from '@/components/blog-card/blog-card'; + +const meta: Meta = { + title: 'Components/BlogCard', + component: BlogCard, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +A card component for displaying blog post previews with image, title, content summary, date, and optional author. + +## CSS Variables + +\`\`\`css +:root { + --blog-card-text-primary: var(--text-primary); + --blog-card-text-secondary: var(--text-secondary); + --blog-card-font-title: var(--font-body); + --blog-card-font-content: var(--font-body); +} +\`\`\` + +## Container Queries + +The component adapts at the \`@lg\` breakpoint (32rem). + +| Element | Below @lg | @lg and above | +|---------|-----------------|------------------| +| Title | text-base, mt-3 | text-xl, mt-4 | +| Content | text-sm, mt-2 | text-base, mt-3 | +| Details | text-sm | text-base | + `, + }, + }, + }, + tags: ['autodocs'], + argTypes: { + aspectRatio: { + control: 'select', + options: ['5/6', '3/4', '4/3', '1/1'], + description: 'Thumbnail image aspect ratio', + }, + title: { + control: 'text', + description: 'Blog post title', + }, + content: { + control: 'text', + description: 'Content summary', + }, + date: { + control: 'text', + description: 'Publication date', + }, + author: { + control: 'text', + description: 'Author name (optional)', + }, + image: { + control: 'object', + description: 'Image object with `src` and `alt`', + }, + link: { + control: 'object', + description: 'Link object with `href` and `ariaLabel`', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { + docs: { + description: { + story: 'Blog card with image, title, content, date, and author.', + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + title: "Sustainable Cleaning: A Beginner's Guide", + content: + 'Discover how small changes in your cleaning routine can make a big impact on the environment. From natural ingredients to reusable tools, we cover everything you need to get started.', + date: 'December 15, 2024', + author: 'Sarah Mitchell', + image: { + src: 'https://images.unsplash.com/photo-1685052392996-5c042ab4c170?w=900', + alt: 'Eco-friendly cleaning supplies arranged on a wooden surface', + }, + link: { + href: '/blog/sustainable-cleaning-guide', + ariaLabel: "Read Sustainable Cleaning: A Beginner's Guide", + }, + }, +}; + +export const WithoutImage: Story = { + parameters: { + docs: { + description: { + story: 'When no image is provided, a fallback displays the title text.', + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + title: 'Natural Cleaning Ingredients You Already Have', + content: + 'Your pantry is full of powerful cleaning agents. Learn how to use vinegar, baking soda, and lemon to keep your home sparkling clean.', + date: 'December 5, 2024', + author: 'James Chen', + link: { + href: '/blog/natural-cleaning-ingredients', + ariaLabel: 'Read Natural Cleaning Ingredients You Already Have', + }, + }, +}; + +export const ContainerQueries: Story = { + parameters: { + docs: { + description: { + story: 'Typography and spacing adapt at the `@lg` breakpoint (32rem).', + }, + source: { + code: ` +// Small container - text-base title, text-sm content +
+ +
+ +// Large container - text-xl title, text-base content +
+ +
+ `, + }, + }, + }, + render: () => ( +
+
+

Small container (below @lg breakpoint)

+
+ +
+
+
+

Large container (at @lg breakpoint)

+
+ +
+
+
+ ), +}; + +export const ComposableAnatomy: Story = { + parameters: { + docs: { + description: { + story: 'Use primitives to build custom blog card layouts.', + }, + source: { + code: ` +import * as BlogCardPrimitive from '@/components/blog-card'; + + + + + + Post Title + Summary text... + + December 15, 2024 + Author Name + + + + `, + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + render: () => ( + + + + + DIY Natural Cleaning Solutions + + Create your own effective cleaning products using simple, natural ingredients. Safe for your + family and the environment. + + + November 20, 2024 + Emma Wilson + + + + ), +}; + +export const Skeleton: Story = { + parameters: { + docs: { + description: { + story: 'Loading state while blog post data loads.', + }, + source: { + code: ` + + + + `, + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + render: () => ( + + + + ), +}; diff --git a/src/components/blog-post-card/blog-post-card.tsx b/src/components/blog-post-card/blog-post-card.tsx deleted file mode 100644 index a826812..0000000 --- a/src/components/blog-post-card/blog-post-card.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as BlogPostCardPrimitive from '@/components/blog-post-card'; - -export interface BlogPostCardProps { - className?: string; - aspectRatio?: '5/6' | '3/4' | '4/3' | '1/1'; - title: string; - author?: string; - content: string; - date: string; - image?: { - src: string; - alt: string; - }; - link: { - href: string; - ariaLabel: string; - }; -} - -/** - * This component supports various CSS variables for theming. Here's a comprehensive list, along - * with their default values: - * - * ```css - * :root { - * --blog-post-card-focus: var(--brand); - * --blog-post-card-image-background: var(--contrast-100); - * --blog-post-card-empty-text: color-mix(in oklab, var(--foreground) 15%, transparent); - * --blog-post-card-title-text: var(--foreground); - * --blog-post-card-content-text: var(--contrast-400); - * --blog-post-card-author-date-text: var(--foreground); - * --blog-post-card-font-family: var(--font-family-body); - * --blog-post-card-summary-text: var(--contrast-400); - * --blog-post-card-author-date-text: var(--foreground); - * } - * ``` - */ -export function BlogPostCard({ - author, - aspectRatio = '4/3', - content, - date, - link, - image, - title, - className, -}: BlogPostCardProps) { - return ( - - - {image ? ( - - ) : ( - {title} - )} - - {title} - {content} - - {date} - {author !== undefined && ( - {author} - )} - - - - ); -} diff --git a/src/components/blog-post-card/index.ts b/src/components/blog-post-card/index.ts deleted file mode 100644 index ad22777..0000000 --- a/src/components/blog-post-card/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BlogPostCard, type BlogPostCardProps } from '@/components/blog-post-card/blog-post-card'; -export * from '@/components/blog-post-card/primitives'; diff --git a/src/components/blog-post-card/primitives.ts b/src/components/blog-post-card/primitives.ts deleted file mode 100644 index 22d280e..0000000 --- a/src/components/blog-post-card/primitives.ts +++ /dev/null @@ -1,44 +0,0 @@ -export { - BlogPostCardRoot as Root, - type BlogPostCardRootProps as RootProps, -} from '@/components/blog-post-card/primitives/blog-post-card-root'; -export { - BlogPostCardFallback as Fallback, - type BlogPostCardFallbackProps as FallbackProps, -} from '@/components/blog-post-card/primitives/blog-post-card-fallback'; -export { - BlogPostCardThumbnail as Thumbnail, - type BlogPostCardThumbnailProps as ThumbnailProps, -} from '@/components/blog-post-card/primitives/blog-post-card-thumbnail'; -export { - BlogPostCardImage as Image, - type BlogPostCardImageProps as ImageProps, -} from '@/components/blog-post-card/primitives/blog-post-card-image'; -export { - BlogPostCardLink as Link, - type BlogPostCardLinkProps as LinkProps, -} from '@/components/blog-post-card/primitives/blog-post-card-link'; -export { - BlogPostCardTitle as Title, - type BlogPostCardTitleProps as TitleProps, -} from '@/components/blog-post-card/primitives/blog-post-card-title'; -export { - BlogPostCardContent as Content, - type BlogPostCardContentProps as ContentProps, -} from '@/components/blog-post-card/primitives/blog-post-card-content'; -export { - BlogPostCardDetails as Details, - type BlogPostCardDetailsProps as DetailsProps, -} from '@/components/blog-post-card/primitives/blog-post-card-details'; -export { - BlogPostCardDate as Date, - type BlogPostCardDateProps as DateProps, -} from '@/components/blog-post-card/primitives/blog-post-card-date'; -export { - BlogPostCardAuthor as Author, - type BlogPostCardAuthorProps as AuthorProps, -} from '@/components/blog-post-card/primitives/blog-post-card-author'; -export { - BlogPostCardSkeleton as Skeleton, - type BlogPostCardSkeletonProps as SkeletonProps, -} from '@/components/blog-post-card/primitives/blog-post-card-skeleton'; diff --git a/src/components/blog-post-card/primitives/blog-post-card-content.tsx b/src/components/blog-post-card/primitives/blog-post-card-content.tsx deleted file mode 100644 index 8e56b80..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-content.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export type BlogPostCardContentProps = ComponentProps<'p'>; - -export function BlogPostCardContent({ className, children, ...props }: BlogPostCardContentProps) { - return ( -

- {children} -

- ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-date.tsx b/src/components/blog-post-card/primitives/blog-post-card-date.tsx deleted file mode 100644 index 17857ff..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-date.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ComponentProps } from 'react'; - -export type BlogPostCardDateProps = ComponentProps<'time'> & { - children: string; -}; - -export function BlogPostCardDate({ className, children, ...props }: BlogPostCardDateProps) { - return ( - - ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-details.tsx b/src/components/blog-post-card/primitives/blog-post-card-details.tsx deleted file mode 100644 index 43335bb..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-details.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export type BlogPostCardDetailsProps = ComponentProps<'div'>; - -export function BlogPostCardDetails({ children, className, ...props }: BlogPostCardDetailsProps) { - return ( -
- {children} -
- ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-fallback.tsx b/src/components/blog-post-card/primitives/blog-post-card-fallback.tsx deleted file mode 100644 index 03b336a..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-fallback.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export type BlogPostCardFallbackProps = ComponentProps<'div'>; - -export function BlogPostCardFallback({ children, className, ...props }: BlogPostCardFallbackProps) { - return ( -
- {children} -
- ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-link.tsx b/src/components/blog-post-card/primitives/blog-post-card-link.tsx deleted file mode 100644 index d78eb8b..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-link.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Slot } from '@radix-ui/react-slot'; -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export interface BlogPostCardLinkProps extends ComponentProps<'a'> { - asChild?: boolean; -} - -export function BlogPostCardLink({ asChild = false, className, ...props }: BlogPostCardLinkProps) { - const Component = asChild ? Slot : 'a'; - - return ( - - ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-root.tsx b/src/components/blog-post-card/primitives/blog-post-card-root.tsx deleted file mode 100644 index 8b362a8..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-root.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { ComponentProps, ElementType } from 'react'; - -import { cn } from '@/lib'; - -export type BlogPostCardRootProps = Omit< - ComponentProps, - 'as' -> & { - as?: E; - aspectRatio?: '5/6' | '3/4' | '4/3' | '1/1'; -}; - -export function BlogPostCardRoot({ - as, - className, - children, - aspectRatio = '4/3', - ...props -}: BlogPostCardRootProps) { - const BlogPostCardRootElement = as ?? 'article'; - - return ( - - {children} - - ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-thumbnail.tsx b/src/components/blog-post-card/primitives/blog-post-card-thumbnail.tsx deleted file mode 100644 index 71bd4ec..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-thumbnail.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export type BlogPostCardThumbnailProps = ComponentProps<'div'>; - -export function BlogPostCardThumbnail({ - className, - children, - ...props -}: BlogPostCardThumbnailProps) { - return ( -
- {children} -
- ); -} diff --git a/src/components/blog-post-card/primitives/blog-post-card-title.tsx b/src/components/blog-post-card/primitives/blog-post-card-title.tsx deleted file mode 100644 index fddd390..0000000 --- a/src/components/blog-post-card/primitives/blog-post-card-title.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { cn } from '@/lib'; - -export type BlogPostCardTitleProps = ComponentProps<'h5'>; - -export function BlogPostCardTitle({ className, children, ...props }: BlogPostCardTitleProps) { - return ( -
- {children} -
- ); -} diff --git a/src/components/blog-post-card/storybook/blog-post-card.stories.tsx b/src/components/blog-post-card/storybook/blog-post-card.stories.tsx deleted file mode 100644 index aabfa6c..0000000 --- a/src/components/blog-post-card/storybook/blog-post-card.stories.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import * as BlogPostCardPrimitive from '@/components/blog-post-card'; -import { BlogPostCard, type BlogPostCardProps } from '@/components/blog-post-card/blog-post-card'; - -const meta: Meta = { - title: 'Components/BlogPostCard', - component: BlogPostCard, - parameters: { - layout: 'centered', - docs: { - description: { - component: ` -The BlogPostCard component displays a blog post preview with image, title, content summary, date, and optional author. - -## CSS Variables - -The following CSS variables can be used to customize the BlogPostCard component: - -\`\`\`css -:root { - --blog-post-card-focus: var(--brand); - --blog-post-card-image-background: var(--contrast-100); - --blog-post-card-empty-text: color-mix(in oklab, var(--foreground) 15%, transparent); - --blog-post-card-title-text: var(--foreground); - --blog-post-card-content-text: var(--contrast-400); - --blog-post-card-author-date-text: var(--foreground); - --blog-post-card-font-family: var(--font-family-body); - --blog-post-card-summary-text: var(--contrast-400); -} -\`\`\` - -## Aspect Ratios - -The component supports four aspect ratios for the thumbnail image: -- \`4/3\` (default) - Landscape, ideal for blog imagery -- \`5/6\` - Slightly tall -- \`3/4\` - Portrait orientation -- \`1/1\` - Square format - `, - }, - }, - }, - tags: ['autodocs'], - argTypes: { - aspectRatio: { - control: 'select', - options: ['5/6', '3/4', '4/3', '1/1'], - description: 'The aspect ratio of the thumbnail image', - }, - title: { - control: 'text', - description: 'The blog post title', - }, - content: { - control: 'text', - description: 'The blog post content summary', - }, - date: { - control: 'text', - description: 'The publication date', - }, - author: { - control: 'text', - description: 'The author name (optional)', - }, - image: { - control: 'object', - description: 'The thumbnail image object with src and alt', - }, - link: { - control: 'object', - description: 'The link object with href and ariaLabel', - }, - }, -}; - -export default meta; -type Story = StoryObj; - -function StoryWrapper({ children }: { children: React.ReactNode }) { - return
{children}
; -} - -/** - * The default BlogPostCard displays a thumbnail, title, content summary, and date. - */ -export const Default: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - args: { - title: "Sustainable Cleaning: A Beginner's Guide", - content: - 'Discover how small changes in your cleaning routine can make a big impact on the environment. From natural ingredients to reusable tools, we cover everything you need to get started.', - date: 'December 15, 2024', - image: { - src: 'https://images.unsplash.com/photo-1685052392996-5c042ab4c170?w=900', - alt: 'Eco-friendly cleaning supplies arranged on a wooden surface', - }, - link: { - href: '/blog/sustainable-cleaning-guide', - ariaLabel: "Read Sustainable Cleaning: A Beginner's Guide", - }, - }, -}; - -/** - * BlogPostCard with an author displayed below the date. - */ -export const WithAuthor: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - args: { - title: 'The Art of Minimalist Home Organization', - content: - 'Learn how to declutter your space and create a serene home environment with our step-by-step guide to minimalist organization principles.', - date: 'December 10, 2024', - author: 'Sarah Mitchell', - image: { - src: 'https://images.unsplash.com/photo-1597816189341-6ed558ab017e?w=900', - alt: 'Minimal ceramic soap dispenser in a clean bathroom', - }, - link: { - href: '/blog/minimalist-home-organization', - ariaLabel: 'Read The Art of Minimalist Home Organization', - }, - }, -}; - -/** - * BlogPostCard without an image displays a fallback with the title. - */ -export const WithoutImage: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - args: { - title: 'Natural Cleaning Ingredients You Already Have', - content: - 'Your pantry is full of powerful cleaning agents. Learn how to use vinegar, baking soda, and lemon to keep your home sparkling clean.', - date: 'December 5, 2024', - author: 'James Chen', - link: { - href: '/blog/natural-cleaning-ingredients', - ariaLabel: 'Read Natural Cleaning Ingredients You Already Have', - }, - }, -}; - -/** - * BlogPostCard with a square aspect ratio. - */ -export const SquareAspectRatio: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - args: { - aspectRatio: '1/1', - title: 'Zero-Waste Kitchen Essentials', - content: - 'Transform your kitchen into an eco-friendly space with these essential zero-waste products and practices.', - date: 'November 28, 2024', - image: { - src: 'https://images.unsplash.com/photo-1590439471364-192aa70c0b53?w=900', - alt: 'Bamboo countertop brush', - }, - link: { - href: '/blog/zero-waste-kitchen', - ariaLabel: 'Read Zero-Waste Kitchen Essentials', - }, - }, -}; - -/** - * ## Composable Anatomy - * - * For advanced customization, you can use the primitive components directly. The primitives include: - * - * - `Root` - Container with aspect ratio configuration - * - `Thumbnail` - Image container - * - `Image` - Blog post image with hover effects - * - `Fallback` - Displayed when no image is available - * - `Title` - Blog post title - * - `Content` - Content summary text - * - `Details` - Container for date and author - * - `Date` - Publication date - * - `Author` - Author name - * - `Link` - Invisible link overlay for click area - * - * ```tsx - * import * as BlogPostCardPrimitive from '@/components/blog-post-card'; - * - * - * - * - * - * Post Title - * Summary text... - * - * December 15, 2024 - * Author Name - * - * - * - * ``` - */ -export const ComposableAnatomy: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - render: () => ( - - - - - DIY Natural Cleaning Solutions - - Create your own effective cleaning products using simple, natural ingredients. Safe for your - family and the environment. - - - November 20, 2024 - Emma Wilson - - - - ), -}; - -/** - * The skeleton state is displayed while blog post data is loading. - */ -export const Skeleton: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - render: () => ( - - - - ), -}; diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index 6f87c46..4aa0692 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -27,10 +27,10 @@ export interface BreadcrumbsProps { * * ```css * :root { - * --breadcrumbs-font-family: var(--font-family-body); - * --breadcrumbs-primary-text: var(--foreground); - * --breadcrumbs-secondary-text: var(--contrast-500); - * --breadcrumbs-icon: var(--contrast-500); + * --breadcrumbs-font: var(--font-body); + * --breadcrumbs-text-primary: var(--primary-text); + * --breadcrumbs-text-secondary: var(--text-secondary); + * --breadcrumbs-fill-icon: var(--contrast-400); * } * ``` */ diff --git a/src/components/breadcrumbs/primitives/breadcrumbs-current.tsx b/src/components/breadcrumbs/primitives/breadcrumbs-current.tsx index 4a1d056..b2d98a1 100644 --- a/src/components/breadcrumbs/primitives/breadcrumbs-current.tsx +++ b/src/components/breadcrumbs/primitives/breadcrumbs-current.tsx @@ -9,7 +9,7 @@ export function BreadcrumbsCurrent({ className, children, ...props }: Breadcrumb