UNPKG

@hanamura/react-containers

Version:
619 lines (509 loc) 15.7 kB
# ⚛️ React Containers Flexible and responsive container components for React applications. ## Installation ```bash npm install @hanamura/react-containers # or yarn add @hanamura/react-containers ``` ## Framework Compatibility ### Next.js App Router All components in this library include the `'use client'` directive, making them fully compatible with Next.js App Router without any additional configuration. ```tsx // page.tsx or layout.tsx in Next.js App Router import { Stack, Tile } from '@hanamura/react-containers' export default function Page() { return ( <Stack options={{ gap: 16 }}> <h1>My Page</h1> <Tile options={{ gap: 16 }}> {/* Content */} </Tile> </Stack> ) } ``` ## Basic Usage ```tsx import { Tile, Stack, Cluster, Reel, Switcher } from '@hanamura/react-containers' // Define your breakpoints type MyBreakpoints = 'mobile' | 'tablet' | 'desktop' // Define your media queries const queries: Array<[MyBreakpoints, { query: string }]> = [ ['mobile', { query: '(max-width: 599px)' }], ['tablet', { query: '(min-width: 600px) and (max-width: 1199px)' }], ['desktop', { query: '(min-width: 1200px)' }], ] // Usage example function App() { return ( <div> {/* Combine default settings with context-specific overrides */} <Tile<MyBreakpoints> // Default settings applied to all contexts options={{ columns: 1, gap: 16, }} // Media queries queries={queries} // Context-specific overrides adaptiveOptions={{ tablet: { columns: 2 }, desktop: { columns: 4, gap: 24 }, }} > {/* Content */} </Tile> </div> ) } ``` ## Practical Usage in Projects When using the same media queries throughout your project, it's recommended to create shared breakpoint definitions and container wrappers. This approach reduces repetition and improves consistency across your application. ### 1. Create App Configuration (app-config.ts) ```tsx // Define your application breakpoints export type AppBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' // Define breakpoint values in pixels export const breakpointValues = { xs: 0, // Extra small devices sm: 600, // Small devices like phones md: 960, // Medium devices like tablets lg: 1280, // Large devices like laptops xl: 1920, // Extra large devices like desktops } // Generate media queries // IMPORTANT: Order matters! Queries should be arranged from most general to most specific. // The last matching query in this array will be used for adaptive options. export const queries: Array<[AppBreakpoint, { query: string }]> = [ // Most general query first (matches all viewport widths) ['xs', { query: '(min-width: 0px)' }], // More specific queries follow ['sm', { query: `(min-width: ${breakpointValues.sm}px)` }], ['md', { query: `(min-width: ${breakpointValues.md}px)` }], ['lg', { query: `(min-width: ${breakpointValues.lg}px)` }], ['xl', { query: `(min-width: ${breakpointValues.xl}px)` }], // Most specific, highest priority ] // Define standardized spacing values using CSS variables export type AppSpacing = | 'var(--spacing-xs)' | 'var(--spacing-sm)' | 'var(--spacing-md)' | 'var(--spacing-lg)' | 'var(--spacing-xl)' // Convenience object for accessing spacing values export const spacing: Record<string, AppSpacing> = { xs: 'var(--spacing-xs)', sm: 'var(--spacing-sm)', md: 'var(--spacing-md)', lg: 'var(--spacing-lg)', xl: 'var(--spacing-xl)', } // Helper function for creating adaptive options (optional) export function createAdaptiveOptions<T>( options: Partial<Record<AppBreakpoint, Partial<T>>> ) { return options } ``` ### 2. Define Container Defaults (optional) ```tsx import { createAdaptiveOptions, AppBreakpoint, spacing } from './app-config' // Container default settings export const containerDefaults = { // Tile default settings tile: { // Default values applied to all contexts defaults: { columns: 1, gap: spacing.md, }, // Context-specific overrides adaptive: createAdaptiveOptions<{ columns: number gap: AppSpacing }>({ sm: { columns: 2 }, md: { columns: 3 }, lg: { columns: 4, gap: spacing.lg }, xl: { columns: 6 }, }), }, // Other container settings... } ``` ### 3. Create App-Specific Container Components (AppContainers.tsx) This approach offers significant advantages: - Pre-configured with your app's breakpoints and queries - Standardized spacing across components - No need to repeat type parameters or queries in multiple places - Makes it easier to enforce UI consistency ```tsx import { Cluster, ClusterProps, Reel, ReelProps, Stack, StackProps, Switcher, SwitcherProps, Tile, TileProps, } from '@hanamura/react-containers' import { AppBreakpoint, AppSpacing, queries } from './app-config' // Create pre-configured components with your app's setup export function AppTile(props: TileProps<AppBreakpoint, AppSpacing>) { return <Tile<AppBreakpoint, AppSpacing> queries={queries} {...props} /> } export function AppStack(props: StackProps<AppBreakpoint, AppSpacing>) { return <Stack<AppBreakpoint, AppSpacing> queries={queries} {...props} /> } export function AppCluster(props: ClusterProps<AppBreakpoint, AppSpacing>) { return <Cluster<AppBreakpoint, AppSpacing> queries={queries} {...props} /> } export function AppReel(props: ReelProps<AppBreakpoint, AppSpacing>) { return <Reel<AppBreakpoint, AppSpacing> queries={queries} {...props} /> } export function AppSwitcher(props: SwitcherProps<AppBreakpoint>) { return <Switcher<AppBreakpoint> queries={queries} {...props} /> } ``` ### 4. Use in Your Application (App.tsx) ```tsx import React from 'react' import { AppTile, AppStack } from './components/AppContainers' import { spacing } from './app-config' const App = () => { const images = [ { src: '/photo1.jpg', alt: 'Photo 1' }, { src: '/photo2.jpg', alt: 'Photo 2' }, // ... ] return ( <AppStack options={{ gap: spacing.md, }} adaptiveOptions={{ md: { gap: spacing.lg }, }} > <h1>Photo Gallery</h1> {/* Single column layout without specifying columns */} <AppTile options={{ gap: spacing.md, }} > <img src="/hero-image.jpg" alt="Hero Image" style={{ width: '100%', height: 'auto' }} /> </AppTile> {/* Multi-column grid with responsive behavior */} <AppTile options={{ columns: 2, gap: spacing.md, }} adaptiveOptions={{ sm: { columns: 1 }, md: { columns: 2 }, lg: { columns: 3, gap: spacing.lg }, }} > {images.map((image, index) => ( <img key={index} src={image.src} alt={image.alt} style={{ width: '100%', height: 'auto' }} /> ))} </AppTile> </AppStack> ) } ``` ## Media Query Priority When working with adaptive containers, it's important to understand how media queries are prioritized: 1. **Order Matters**: Queries should be defined from most general to most specific. 2. **Last Match Wins**: When multiple media queries match, the last one in your array will be used. 3. **Priority Example**: ```typescript // In this example, if both '(min-width: 0px)' and '(min-width: 768px)' match: // - The 'desktop' context will be active // - The 'desktop' adaptive options will be applied const queries = [ ['mobile', { query: '(min-width: 0px)' }], // More general ['desktop', { query: '(min-width: 768px)' }], // More specific (higher priority) ] ``` This behavior allows you to create intuitive responsive designs where more specific breakpoints override more general ones. ## Available Containers All containers share common options that can be set both in `options` and `adaptiveOptions`: ### Common Options - `as`: Lets you change the rendered HTML element (default is `div`) - `paddingInline`: Controls horizontal padding - `paddingBlock`: Controls vertical padding Components also support a rich set of accessibility attributes: - Standard HTML attributes: `id`, `role`, `tabIndex` - ARIA attributes: `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-hidden`, `aria-expanded`, `aria-controls`, `aria-disabled` - Testing attributes: `data-testid` Additionally, any other custom attributes (like `data-*` attributes) will be forwarded to the rendered element. Both properties accept the following formats: - Single value: `paddingInline: value` - Single value in array: `paddingBlock: [value]` - Two values in array: `paddingInline: [value1, value2]` (for left/right or top/bottom) Example with adaptive padding: ```tsx <Tile options={{ columns: 3, // Common padding applied to all contexts paddingInline: 16, paddingBlock: [8, 24], // [top, bottom] }} adaptiveOptions={{ mobile: { columns: 1, // Override padding for specific contexts paddingInline: 8, paddingBlock: 8, }, desktop: { columns: 4, paddingBlock: [16, 32], }, }} > {/* Content */} </Tile> ``` ### Tile A container that arranges items in a uniform grid layout. Options: - `columns`: Number of columns in the grid (defaults to 1 if not specified or if value is less than 2) - `gap`: Space between grid items (can be a single value or `[rowGap, columnGap]`) ### Stack A container that stacks items vertically. It's the simplest layout component, focused on doing one thing well. Options: - `gap`: Space between stacked items - `divider`: Either a React element to use as a divider, or a function that returns a divider element (receives `{ index, position }` props) - `dividerPositions`: Object controlling where dividers appear - `start`: Whether to show a divider at the start (default: false) - `between`: Whether to show dividers between items (default: true) - `end`: Whether to show a divider at the end (default: false) Example with function dividers: ```tsx <Stack options={{ gap: 16, divider: ({ index, position }) => ( <hr style={{ margin: 0, borderTop: '1px solid #ccc' }} /> ), dividerPositions: { start: false, between: true, end: false, }, }} adaptiveOptions={{ desktop: { gap: 24, dividerPositions: { between: false, // Disable dividers on desktop }, }, }} > <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </Stack> ``` Example with React element dividers (Next.js server component compatible): ```tsx <Stack options={{ gap: 16, divider: <hr className="my-divider" />, dividerPositions: { start: false, between: true, end: false, }, }} > <div>Item 1</div> <div>Item 2</div> <div>Item 3</div> </Stack> ``` Example with custom HTML element: ```tsx // When changing the HTML element using the 'as' prop <Stack<string, string> as="ul" options={{ gap: 16 }} style={{ listStyle: 'none', padding: 0, margin: 0 }} > <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </Stack> ``` For more complex layouts, use Stack in combination with Tile or Cluster components. ### Cluster A container that arranges items in a wrapping flex layout. Options: - `gap`: Space between flex items (can be a single value or `[rowGap, columnGap]`) - `align`: Cross-axis alignment - `justify`: Main-axis alignment ### Reel A container that creates a horizontally scrollable area for creating carousels, galleries, and other swipeable content. Options: - `gap`: Space between items - `hideScrollbar`: Whether to hide the scrollbar (default: false) - `snap`: Scroll snapping behavior ('none', 'mandatory', or 'proximity') - `columns`: Number of columns or column width: - When number: Container width is divided into that many equal columns - When string: Each column gets the exact width specified (e.g., '150px') **Note**: When using the `snap` option, you need to add `scroll-snap-align` CSS property to child elements yourself: ```css .reel-item { scroll-snap-align: center; /* or start, end */ flex-shrink: 0; /* Prevent items from shrinking */ } ``` Example with horizontal scrolling: ```tsx <Reel options={{ gap: 16, hideScrollbar: true, snap: 'mandatory', }} > <div className="reel-item">Item 1</div> <div className="reel-item">Item 2</div> <div className="reel-item">Item 3</div> </Reel> ``` Example with custom styling: ```tsx <Reel options={{ gap: 8, paddingInline: 16, snap: 'proximity', }} style={{ maxWidth: '100%' }} > <div className="reel-item">Item 1</div> <div className="reel-item">Item 2</div> <div className="reel-item">Item 3</div> </Reel> ``` Example with fixed width columns: ```tsx <Reel options={{ gap: 16, columns: '150px', // All items will be exactly 150px wide snap: 'mandatory', }} > <div className="reel-item">Item 1</div> <div className="reel-item">Item 2</div> <div className="reel-item">Item 3</div> </Reel> ``` Example with equal division columns: ```tsx <Reel options={{ gap: 16, columns: 3, // Container divided into 3 equal columns (accounting for gap) snap: 'proximity', }} > <div className="reel-item">Item 1</div> <div className="reel-item">Item 2</div> <div className="reel-item">Item 3</div> </Reel> ``` ### Switcher A component that renders different content based on the active breakpoint. This is useful for completely changing the UI at different screen sizes or for inline content variations. Options: - `content`: The content to display at the current breakpoint The Switcher component differs from other container components in these ways: - It doesn't take children props; instead, it uses the `content` option to determine what to render - It renders content directly without adding a container element - It can be used inline within text or other components Example with responsive content switching: ```tsx <Switcher options={{ content: <div>Default content for all breakpoints</div> }} adaptiveOptions={{ mobile: { content: <div>Mobile-specific content</div> }, tablet: { content: <div>Tablet-specific content</div> }, desktop: { content: <div>Desktop-specific content</div> } }} /> ``` Example with complex content: ```tsx <Switcher options={{ content: <p>Basic layout for small screens</p> }} adaptiveOptions={{ desktop: { content: ( <Cluster options={{ gap: 24, justify: 'space-between' }} > <div>Left sidebar</div> <div>Main content</div> <div>Right sidebar</div> </Cluster> ) } }} /> ``` Example with inline usage (unique to Switcher): ```tsx <p> This paragraph contains <Switcher options={{ content: <span>default text</span> }} adaptiveOptions={{ tablet: { content: <span className="highlight">highlighted text on tablets</span> }, desktop: { content: <span className="highlight-large">emphasized text on desktop</span> }, }} /> that changes based on screen size. </p> ``` ## Browser Compatibility React Containers is compatible with all modern browsers that support CSS Grid and CSS Custom Properties (CSS Variables): - Chrome 57+ - Firefox 52+ - Safari 10.1+ - Edge 16+ The library is tested against the following browser targets (defined in `.browserslistrc`): - Last 2 versions of major browsers - Browsers with > 0.5% market share - No dead browsers - No IE 11 support ## License MIT