import type { Meta, StoryObj } from "@storybook/react"; import React, { useRef, useEffect } from "react"; import UI from "./ui"; /** * The UI component is a polymorphic React primitive that can render as any HTML element * while maintaining full TypeScript type safety. It serves as the foundation for 25+ * components across the fpkit library. * * ## Key Features * - Polymorphic rendering with the `as` prop * - Full TypeScript type safety for element-specific props * - Style merging with `defaultStyles` and `styles` * - Proper ref forwarding with typed refs * - Zero runtime overhead */ const meta = { title: "FP/UI", component: UI, tags: ["stable", "autodocs", "primitive"], parameters: { docs: { description: { component: "A foundational polymorphic component that can render as any HTML element with complete type safety.", }, }, }, argTypes: { as: { control: "select", options: [ "div", "span", "button", "a", "section", "article", "nav", "main", "header", "footer", ], description: "The HTML element type to render", table: { type: { summary: "React.ElementType" }, defaultValue: { summary: "div" }, }, }, styles: { control: "object", description: "Inline styles to apply (overrides defaultStyles)", table: { type: { summary: "React.CSSProperties" }, }, }, classes: { control: "text", description: "CSS class names to apply", table: { type: { summary: "string" }, }, }, children: { control: "text", description: "Content to render inside the component", }, }, } satisfies Meta; export default meta; type Story = StoryObj; /** * Default story showing the UI component rendering as a div with basic styling. */ export const Default: Story = { args: { children: "Default UI Component (renders as div)", styles: { padding: "1rem", backgroundColor: "#f0f0f0", borderRadius: "0.25rem", }, }, }; /** * Demonstrates the UI component rendering as a button with button-specific props. */ export const AsButton: Story = { args: { as: "button", children: "Click Me", styles: { padding: "0.75rem 1.5rem", backgroundColor: "#007bff", color: "white", border: "none", borderRadius: "0.25rem", cursor: "pointer", fontSize: "1rem", }, }, }; /** * Demonstrates the UI component rendering as a span element. */ export const AsSpan: Story = { args: { as: "span", children: "Inline Span Element", styles: { fontWeight: "bold", color: "#28a745", padding: "0.25rem 0.5rem", backgroundColor: "#d4edda", borderRadius: "0.25rem", }, }, }; /** * Demonstrates the UI component rendering as an anchor link with href. */ export const AsAnchor: Story = { args: { as: "a", href: "https://example.com", target: "_blank", rel: "noopener noreferrer", children: "External Link", styles: { color: "#007bff", textDecoration: "underline", padding: "0.5rem", display: "inline-block", }, }, }; /** * Demonstrates the UI component rendering as a semantic section element. */ export const AsSection: Story = { args: { as: "section", children: ( <>

Section Title

This demonstrates the UI component rendering as a semantic section element.

), styles: { padding: "1.5rem", backgroundColor: "#fff3cd", border: "1px solid #ffc107", borderRadius: "0.25rem", }, }, }; /** * Shows how the `styles` prop applies inline styles. */ export const WithStyles: Story = { args: { children: "Styled with inline CSS", styles: { padding: "1rem 2rem", backgroundColor: "#6f42c1", color: "white", borderRadius: "0.5rem", fontWeight: "bold", textAlign: "center", }, }, }; /** * Shows how the `classes` prop applies CSS class names. */ export const WithClasses: Story = { args: { children: "Element with CSS classes", classes: "custom-class another-class", styles: { padding: "1rem", border: "2px dashed #17a2b8", }, }, }; /** * Demonstrates how `styles` overrides `defaultStyles`. */ export const StyleMerging: Story = { args: { children: "Style Merging Example", defaultStyles: { padding: "1rem", backgroundColor: "lightblue", color: "blue", fontSize: "1rem", borderRadius: "0.25rem", }, styles: { color: "red", // This overrides the blue color fontWeight: "bold", // This is added }, }, parameters: { docs: { description: { story: "The `defaultStyles` provide base styling (blue text, light blue background), while `styles` overrides specific properties (text becomes red and bold).", }, }, }, }; /** * Demonstrates using CSS custom properties for theming. */ export const CSSCustomProperties: Story = { args: { children: "Themed with CSS Variables", styles: { "--primary-color": "#28a745", "--secondary-color": "#ffffff", padding: "1rem 1.5rem", backgroundColor: "var(--primary-color)", color: "var(--secondary-color)", borderRadius: "0.25rem", } as React.CSSProperties, }, parameters: { docs: { description: { story: "CSS custom properties (variables) can be set dynamically through the styles prop for theming.", }, }, }, }; /** * Demonstrates ref forwarding with proper typing. */ export const RefForwarding: Story = { render: () => { const RefExample = () => { const buttonRef = useRef(null); useEffect(() => { // Focus the button on mount if (buttonRef.current) { buttonRef.current.focus(); } }, []); return ( Auto-focused Button ); }; return ; }, parameters: { docs: { description: { story: "The UI component forwards refs with proper typing. This button is automatically focused when mounted.", }, }, }, }; /** * Example of building a Button component using UI as a primitive. */ export const ButtonPattern: Story = { render: () => { interface ButtonProps { variant?: "primary" | "secondary" | "danger"; children: React.ReactNode; onClick?: () => void; } const Button = ({ variant = "primary", children, ...props }: ButtonProps) => { const variantStyles = { primary: { backgroundColor: "#007bff", color: "white", }, secondary: { backgroundColor: "#6c757d", color: "white", }, danger: { backgroundColor: "#dc3545", color: "white", }, }; return ( {children} ); }; return (
); }, parameters: { docs: { description: { story: "This shows how to build a Button component with variants using UI as the primitive.", }, }, }, }; /** * Example of building a Badge component using UI as a primitive. */ export const BadgePattern: Story = { render: () => { interface BadgeProps { variant?: "info" | "success" | "warning" | "error"; children: React.ReactNode; } const Badge = ({ variant = "info", children, ...props }: BadgeProps) => { const variantStyles = { info: { backgroundColor: "#d1ecf1", color: "#0c5460", }, success: { backgroundColor: "#d4edda", color: "#155724", }, warning: { backgroundColor: "#fff3cd", color: "#856404", }, error: { backgroundColor: "#f8d7da", color: "#721c24", }, }; return ( {children} ); }; return (
Info Success Warning Error
); }, parameters: { docs: { description: { story: "This shows how to build a Badge component with variants using UI as the primitive.", }, }, }, }; /** * Demonstrates TypeScript type safety - element-specific props are correctly typed. */ export const TypeSafeProps: Story = { render: () => { return (
{/* Button with disabled prop (only valid for buttons) */} Disabled Button {/* Anchor with href and target (only valid for anchors) */} GitHub Link {/* Form with onSubmit (only valid for forms) */} { e.preventDefault(); alert("Form submitted!"); }} styles={{ padding: "1rem", border: "1px solid #dee2e6", borderRadius: "0.25rem", }} >
); }, parameters: { docs: { description: { story: "TypeScript ensures that only valid props for each element type are accepted. Try changing the `as` prop to see IntelliSense update!", }, }, }, }; /** * Demonstrates accessible interactive elements with proper ARIA attributes. * All examples pass WCAG 2.1 AA accessibility checks. */ export const AccessibleInteractiveElements: Story = { render: function AccessibleInteractiveElementsStory() { const [isExpanded, setIsExpanded] = React.useState(false); return (
{/* Accessible button with aria-label for icon-only button */}

Icon Button with aria-label

alert("Dialog closed")} styles={{ padding: "0.5rem", backgroundColor: "#dc3545", color: "white", border: "none", borderRadius: "0.25rem", cursor: "pointer", fontSize: "1.25rem", lineHeight: 1, }} > ×
{/* Accessible link with descriptive text */}

Accessible Link

View all products
{/* Toggle button with aria-expanded */}

Expandable Section

setIsExpanded(!isExpanded)} styles={{ padding: "0.75rem 1rem", backgroundColor: "#007bff", color: "white", border: "none", borderRadius: "0.25rem", cursor: "pointer", width: "100%", textAlign: "left", }} > {isExpanded ? "▼" : "▶"} Toggle Content {isExpanded && ( This content is now visible and announced to screen readers. )}
{/* Custom interactive element with proper role and keyboard support */}

Custom Interactive (div with role="button")

alert("Clicked!")} onKeyDown={(e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); alert("Activated via keyboard!"); } }} styles={{ padding: "0.75rem 1rem", backgroundColor: "#28a745", color: "white", borderRadius: "0.25rem", cursor: "pointer", userSelect: "none", display: "inline-block", }} > Press Enter or Space
); }, parameters: { docs: { description: { story: "Examples of accessible interactive elements using proper ARIA attributes, semantic HTML, and keyboard support. Run the Storybook a11y addon to verify these pass accessibility checks.", }, }, }, tags: ["a11y"], }; /** * Demonstrates accessibility patterns including focus management and ARIA attributes. */ export const AccessibilityPatterns: Story = { render: function AccessibilityPatternsStory() { const buttonRef = useRef(null); const [count, setCount] = React.useState(0); useEffect(() => { // Auto-focus on mount for keyboard navigation buttonRef.current?.focus(); }, []); return (
{/* Focus management example */}

Auto-focused Button (Focus Management)

alert("Focused button clicked")} styles={{ padding: "0.75rem 1.5rem", backgroundColor: "#007bff", color: "white", border: "none", borderRadius: "0.25rem", cursor: "pointer", outline: "2px solid transparent", outlineOffset: "2px", }} // Custom focus indicator with WCAG 2.4.7 compliant contrast onFocus={(e: React.FocusEvent) => { e.currentTarget.style.outline = "2px solid #0056b3"; }} onBlur={(e: React.FocusEvent) => { e.currentTarget.style.outline = "2px solid transparent"; }} > This button auto-focused on mount
{/* ARIA live region for dynamic content */}

ARIA Live Region (Dynamic Updates)

setCount((c) => c + 1)} aria-describedby="counter-description" styles={{ padding: "0.75rem 1.5rem", backgroundColor: "#28a745", color: "white", border: "none", borderRadius: "0.25rem", cursor: "pointer", }} > Increment Counter Current count: {count}
{/* Semantic vs generic elements */}

Semantic HTML Choice

✅ Semantic <button> ✅ Semantic <nav>
); }, parameters: { docs: { description: { story: "Demonstrates accessibility patterns including focus management, ARIA live regions for dynamic content, custom focus indicators, and semantic HTML usage.", }, }, }, tags: ["a11y"], }; /** * ⚠️ Shows common accessibility mistakes to avoid. * These examples intentionally violate accessibility guidelines to demonstrate what NOT to do. */ export const CommonAccessibilityMistakes: Story = { render: () => { return (
⚠️ Warning: These examples show common accessibility violations. Do NOT copy these patterns. They are shown for educational purposes only.
{/* Missing accessible name */}

❌ BAD: Icon button without accessible name

{}} styles={{ padding: "0.5rem", backgroundColor: "#dc3545", color: "white", border: "none", borderRadius: "0.25rem", cursor: "pointer", fontSize: "1.25rem", }} > ×

Problem: Screen readers cannot identify this button's purpose. Fix: Add{" "} aria-label="Close"

{/* Non-semantic clickable div */}

❌ BAD: Clickable div without keyboard support

alert("This is not keyboard accessible!")} styles={{ padding: "0.75rem 1rem", backgroundColor: "#6c757d", color: "white", borderRadius: "0.25rem", cursor: "pointer", display: "inline-block", }} > Click me (but you can't use keyboard!)

Problem: Not keyboard accessible or announced to screen readers. Fix: Use as="button"{" "} or add role="button", tabIndex=0, and keyboard handlers.

{/* Poor contrast focus indicator */}

❌ BAD: Insufficient focus indicator contrast

Low contrast focus

Problem: Focus indicator contrast ratio is less than 3:1 (WCAG 2.4.7). Fix: Use a contrasting color like dark blue on light blue background.

{/* Vague link text */}

❌ BAD: Non-descriptive link text

Click here

Problem: "Click here" doesn't describe the link's destination. Fix: Use descriptive text like "View product documentation".

); }, parameters: { docs: { description: { story: "⚠️ Educational examples showing common accessibility violations. These patterns should be avoided. Each example includes an explanation of the problem and how to fix it. Run the Storybook a11y addon to see these violations detected automatically.", }, }, }, tags: ["a11y"], };