refactor(know-your-world): extract reusable utilities and components
- Extract regionSizeUtils.ts with size/range conversion functions - Extract RegionListPanel component for scrollable region lists - Add responsiveStyles.ts with breakpoint utilities and preventFlexExpansion - Add comprehensive unit tests for all extracted modules (65 tests) This improves code organization and reusability for the region selector. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/components/Thermometer'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { MapSelectorMap } from './MapSelectorMap'
|
||||
import { RegionListPanel } from './RegionListPanel'
|
||||
import {
|
||||
WORLD_MAP,
|
||||
calculateContinentViewBox,
|
||||
@@ -32,6 +33,8 @@ import {
|
||||
COUNTRY_TO_CONTINENT,
|
||||
type ContinentId,
|
||||
} from '../continents'
|
||||
import { sizesToRange, rangeToSizes } from '../utils/regionSizeUtils'
|
||||
import { preventFlexExpansion } from '../utils/responsiveStyles'
|
||||
|
||||
/**
|
||||
* Size options for the range thermometer, ordered from largest to smallest
|
||||
@@ -43,25 +46,6 @@ const SIZE_OPTIONS: ThermometerOption<RegionSize>[] = ALL_REGION_SIZES.map((size
|
||||
emoji: REGION_SIZE_CONFIG[size].emoji,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Convert an array of sizes to min/max values for the range thermometer
|
||||
*/
|
||||
function sizesToRange(sizes: RegionSize[]): [RegionSize, RegionSize] {
|
||||
const sorted = [...sizes].sort(
|
||||
(a, b) => ALL_REGION_SIZES.indexOf(a) - ALL_REGION_SIZES.indexOf(b)
|
||||
)
|
||||
return [sorted[0], sorted[sorted.length - 1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert min/max range values back to an array of sizes
|
||||
*/
|
||||
function rangeToSizes(min: RegionSize, max: RegionSize): RegionSize[] {
|
||||
const minIdx = ALL_REGION_SIZES.indexOf(min)
|
||||
const maxIdx = ALL_REGION_SIZES.indexOf(max)
|
||||
return ALL_REGION_SIZES.slice(minIdx, maxIdx + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection path for drill-down navigation:
|
||||
* - [] = World level
|
||||
@@ -1105,45 +1089,16 @@ export function DrillDownMapSelector({
|
||||
borderColor: isDark ? 'gray.700' : 'gray.300',
|
||||
marginTop: '2',
|
||||
paddingTop: '2',
|
||||
maxHeight: '200px',
|
||||
/* Prevent this element from expanding the parent - use 0 width + min 100% trick */
|
||||
width: 0,
|
||||
minWidth: '100%',
|
||||
/* Prevent this element from expanding the parent */
|
||||
...preventFlexExpansion,
|
||||
})}
|
||||
>
|
||||
{/* Scrollable list */}
|
||||
<div
|
||||
className={css({
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
{selectedRegionNames
|
||||
.slice()
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
onMouseEnter={() => setPreviewRegionName(name)}
|
||||
onMouseLeave={() => setPreviewRegionName(null)}
|
||||
className={css({
|
||||
px: '2',
|
||||
py: '1',
|
||||
fontSize: 'xs',
|
||||
color: isDark ? 'gray.300' : 'gray.600',
|
||||
cursor: 'pointer',
|
||||
rounded: 'md',
|
||||
overflowWrap: 'break-word',
|
||||
_hover: {
|
||||
bg: isDark ? 'gray.700' : 'gray.200',
|
||||
color: isDark ? 'gray.100' : 'gray.900',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<RegionListPanel
|
||||
regions={selectedRegionNames}
|
||||
onRegionHover={setPreviewRegionName}
|
||||
maxHeight="200px"
|
||||
isDark={isDark}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import { css } from '@styled/css'
|
||||
|
||||
interface RegionListItemProps {
|
||||
name: string
|
||||
onHover?: (name: string | null) => void
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual region list item with hover interaction.
|
||||
* Memoized to prevent unnecessary re-renders when list is large.
|
||||
*/
|
||||
const RegionListItem = memo(function RegionListItem({
|
||||
name,
|
||||
onHover,
|
||||
isDark = false,
|
||||
}: RegionListItemProps) {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => onHover?.(name)}
|
||||
onMouseLeave={() => onHover?.(null)}
|
||||
className={css({
|
||||
px: '2',
|
||||
py: '1',
|
||||
fontSize: 'xs',
|
||||
color: isDark ? 'gray.300' : 'gray.600',
|
||||
cursor: onHover ? 'pointer' : 'default',
|
||||
rounded: 'md',
|
||||
overflowWrap: 'break-word',
|
||||
_hover: {
|
||||
bg: isDark ? 'gray.700' : 'gray.200',
|
||||
color: isDark ? 'gray.100' : 'gray.900',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
interface RegionListPanelProps {
|
||||
/** List of region names to display */
|
||||
regions: string[]
|
||||
/** Callback when hovering over a region name (for map preview) */
|
||||
onRegionHover?: (name: string | null) => void
|
||||
/** Maximum height of the scrollable list */
|
||||
maxHeight?: string
|
||||
/** Dark mode styling */
|
||||
isDark?: boolean
|
||||
/** Sort regions alphabetically (default: true) */
|
||||
sortAlphabetically?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrollable panel displaying a list of region names.
|
||||
* Used in DrillDownMapSelector for displaying selected regions on desktop.
|
||||
*/
|
||||
export function RegionListPanel({
|
||||
regions,
|
||||
onRegionHover,
|
||||
maxHeight = '200px',
|
||||
isDark = false,
|
||||
sortAlphabetically = true,
|
||||
}: RegionListPanelProps) {
|
||||
const displayRegions = sortAlphabetically
|
||||
? [...regions].sort((a, b) => a.localeCompare(b))
|
||||
: regions
|
||||
|
||||
return (
|
||||
<div
|
||||
data-element="region-list-panel"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight,
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
{/* Scrollable list */}
|
||||
<div
|
||||
className={css({
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
{displayRegions.map((name) => (
|
||||
<RegionListItem key={name} name={name} onHover={onRegionHover} isDark={isDark} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
/**
|
||||
* Utility functions extracted from DrillDownMapSelector for testing.
|
||||
* These are copied here since they're defined inline in the component.
|
||||
* In a refactor, these should be moved to a separate utils file.
|
||||
*/
|
||||
|
||||
type RegionSize = 'huge' | 'large' | 'medium' | 'small' | 'tiny'
|
||||
const ALL_REGION_SIZES: RegionSize[] = ['huge', 'large', 'medium', 'small', 'tiny']
|
||||
|
||||
/**
|
||||
* Convert an array of sizes to min/max values for the range thermometer
|
||||
*/
|
||||
function sizesToRange(sizes: RegionSize[]): [RegionSize, RegionSize] {
|
||||
const sorted = [...sizes].sort(
|
||||
(a, b) => ALL_REGION_SIZES.indexOf(a) - ALL_REGION_SIZES.indexOf(b)
|
||||
)
|
||||
return [sorted[0], sorted[sorted.length - 1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert min/max range values back to an array of sizes
|
||||
*/
|
||||
function rangeToSizes(min: RegionSize, max: RegionSize): RegionSize[] {
|
||||
const minIdx = ALL_REGION_SIZES.indexOf(min)
|
||||
const maxIdx = ALL_REGION_SIZES.indexOf(max)
|
||||
return ALL_REGION_SIZES.slice(minIdx, maxIdx + 1)
|
||||
}
|
||||
import {
|
||||
sizesToRange,
|
||||
rangeToSizes,
|
||||
calculateExcludedRegions,
|
||||
calculatePreviewChanges,
|
||||
} from '../../utils/regionSizeUtils'
|
||||
import type { RegionSize } from '../../maps'
|
||||
|
||||
describe('DrillDownMapSelector utility functions', () => {
|
||||
describe('sizesToRange', () => {
|
||||
@@ -205,14 +184,6 @@ describe('Region filtering logic', () => {
|
||||
describe('excluded regions calculation', () => {
|
||||
const allRegionIds = ['region-1', 'region-2', 'region-3', 'region-4', 'region-5']
|
||||
|
||||
function calculateExcludedRegions(
|
||||
allRegions: string[],
|
||||
filteredRegions: string[]
|
||||
): string[] {
|
||||
const filteredSet = new Set(filteredRegions)
|
||||
return allRegions.filter(id => !filteredSet.has(id))
|
||||
}
|
||||
|
||||
it('returns empty when all regions are included', () => {
|
||||
const filtered = ['region-1', 'region-2', 'region-3', 'region-4', 'region-5']
|
||||
const excluded = calculateExcludedRegions(allRegionIds, filtered)
|
||||
@@ -240,31 +211,6 @@ describe('Region filtering logic', () => {
|
||||
})
|
||||
|
||||
describe('Preview add/remove regions calculation', () => {
|
||||
function calculatePreviewChanges(
|
||||
currentIncluded: string[],
|
||||
previewIncluded: string[]
|
||||
): { addRegions: string[]; removeRegions: string[] } {
|
||||
const currentSet = new Set(currentIncluded)
|
||||
const previewSet = new Set(previewIncluded)
|
||||
|
||||
const addRegions: string[] = []
|
||||
const removeRegions: string[] = []
|
||||
|
||||
for (const id of previewSet) {
|
||||
if (!currentSet.has(id)) {
|
||||
addRegions.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of currentSet) {
|
||||
if (!previewSet.has(id)) {
|
||||
removeRegions.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
return { addRegions, removeRegions }
|
||||
}
|
||||
|
||||
it('detects regions to be added', () => {
|
||||
const current = ['a', 'b']
|
||||
const preview = ['a', 'b', 'c', 'd']
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { RegionListPanel } from '../RegionListPanel'
|
||||
|
||||
// Mock Panda CSS
|
||||
vi.mock('@styled/css', () => ({
|
||||
css: vi.fn(() => 'mocked-css-class'),
|
||||
}))
|
||||
|
||||
describe('RegionListPanel', () => {
|
||||
const defaultRegions = ['France', 'Germany', 'Spain', 'Italy', 'Poland']
|
||||
|
||||
describe('rendering', () => {
|
||||
it('renders all region names', () => {
|
||||
render(<RegionListPanel regions={defaultRegions} />)
|
||||
|
||||
expect(screen.getByText('France')).toBeInTheDocument()
|
||||
expect(screen.getByText('Germany')).toBeInTheDocument()
|
||||
expect(screen.getByText('Spain')).toBeInTheDocument()
|
||||
expect(screen.getByText('Italy')).toBeInTheDocument()
|
||||
expect(screen.getByText('Poland')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('sorts regions alphabetically by default', () => {
|
||||
const unsorted = ['Zambia', 'Argentina', 'France']
|
||||
render(<RegionListPanel regions={unsorted} />)
|
||||
|
||||
const items = screen.getAllByText(/^(Argentina|France|Zambia)$/)
|
||||
expect(items[0]).toHaveTextContent('Argentina')
|
||||
expect(items[1]).toHaveTextContent('France')
|
||||
expect(items[2]).toHaveTextContent('Zambia')
|
||||
})
|
||||
|
||||
it('preserves order when sortAlphabetically is false', () => {
|
||||
const unsorted = ['Zambia', 'Argentina', 'France']
|
||||
render(<RegionListPanel regions={unsorted} sortAlphabetically={false} />)
|
||||
|
||||
const items = screen.getAllByText(/^(Argentina|France|Zambia)$/)
|
||||
expect(items[0]).toHaveTextContent('Zambia')
|
||||
expect(items[1]).toHaveTextContent('Argentina')
|
||||
expect(items[2]).toHaveTextContent('France')
|
||||
})
|
||||
|
||||
it('renders empty list when no regions provided', () => {
|
||||
const { container } = render(<RegionListPanel regions={[]} />)
|
||||
|
||||
// Should still render the container
|
||||
expect(container.querySelector('[data-element="region-list-panel"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has correct data-element attribute', () => {
|
||||
const { container } = render(<RegionListPanel regions={defaultRegions} />)
|
||||
|
||||
expect(container.querySelector('[data-element="region-list-panel"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('hover interactions', () => {
|
||||
it('calls onRegionHover with region name on mouse enter', () => {
|
||||
const onRegionHover = vi.fn()
|
||||
render(<RegionListPanel regions={defaultRegions} onRegionHover={onRegionHover} />)
|
||||
|
||||
fireEvent.mouseEnter(screen.getByText('France'))
|
||||
|
||||
expect(onRegionHover).toHaveBeenCalledWith('France')
|
||||
})
|
||||
|
||||
it('calls onRegionHover with null on mouse leave', () => {
|
||||
const onRegionHover = vi.fn()
|
||||
render(<RegionListPanel regions={defaultRegions} onRegionHover={onRegionHover} />)
|
||||
|
||||
fireEvent.mouseLeave(screen.getByText('France'))
|
||||
|
||||
expect(onRegionHover).toHaveBeenCalledWith(null)
|
||||
})
|
||||
|
||||
it('does not throw when onRegionHover is not provided', () => {
|
||||
render(<RegionListPanel regions={defaultRegions} />)
|
||||
|
||||
// Should not throw
|
||||
expect(() => {
|
||||
fireEvent.mouseEnter(screen.getByText('France'))
|
||||
fireEvent.mouseLeave(screen.getByText('France'))
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('styling props', () => {
|
||||
it('accepts isDark prop', () => {
|
||||
// Just verify it renders without error
|
||||
const { container } = render(<RegionListPanel regions={defaultRegions} isDark={true} />)
|
||||
|
||||
expect(container.querySelector('[data-element="region-list-panel"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('accepts maxHeight prop', () => {
|
||||
// Just verify it renders without error
|
||||
const { container } = render(<RegionListPanel regions={defaultRegions} maxHeight="300px" />)
|
||||
|
||||
expect(container.querySelector('[data-element="region-list-panel"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('large lists', () => {
|
||||
it('handles many regions efficiently', () => {
|
||||
const manyRegions = Array.from({ length: 100 }, (_, i) => `Region ${i + 1}`)
|
||||
render(<RegionListPanel regions={manyRegions} />)
|
||||
|
||||
// Should render first and last
|
||||
expect(screen.getByText('Region 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Region 100')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles long region names', () => {
|
||||
const longNames = [
|
||||
'Democratic Republic of the Congo',
|
||||
'United Kingdom of Great Britain and Northern Ireland',
|
||||
]
|
||||
render(<RegionListPanel regions={longNames} />)
|
||||
|
||||
expect(screen.getByText('Democratic Republic of the Congo')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('United Kingdom of Great Britain and Northern Ireland')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Utility functions for converting between region size arrays and min/max ranges.
|
||||
* Used by the DrillDownMapSelector's RangeThermometer component.
|
||||
*/
|
||||
|
||||
import type { RegionSize } from '../maps'
|
||||
import { ALL_REGION_SIZES } from '../maps'
|
||||
|
||||
/**
|
||||
* Convert an array of sizes to min/max values for the range thermometer.
|
||||
* Sorts the sizes by their position in ALL_REGION_SIZES and returns the first and last.
|
||||
*
|
||||
* @example
|
||||
* sizesToRange(['medium', 'small', 'large']) // returns ['large', 'small']
|
||||
* sizesToRange(['medium']) // returns ['medium', 'medium']
|
||||
*/
|
||||
export function sizesToRange(sizes: RegionSize[]): [RegionSize, RegionSize] {
|
||||
const sorted = [...sizes].sort(
|
||||
(a, b) => ALL_REGION_SIZES.indexOf(a) - ALL_REGION_SIZES.indexOf(b)
|
||||
)
|
||||
return [sorted[0], sorted[sorted.length - 1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert min/max range values back to an array of sizes.
|
||||
* Returns all sizes between min and max (inclusive) in ALL_REGION_SIZES order.
|
||||
*
|
||||
* @example
|
||||
* rangeToSizes('large', 'small') // returns ['large', 'medium', 'small']
|
||||
* rangeToSizes('medium', 'medium') // returns ['medium']
|
||||
*/
|
||||
export function rangeToSizes(min: RegionSize, max: RegionSize): RegionSize[] {
|
||||
const minIdx = ALL_REGION_SIZES.indexOf(min)
|
||||
const maxIdx = ALL_REGION_SIZES.indexOf(max)
|
||||
return ALL_REGION_SIZES.slice(minIdx, maxIdx + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate which regions are excluded based on the current size filter.
|
||||
* Returns IDs of regions that exist but are filtered out.
|
||||
*/
|
||||
export function calculateExcludedRegions(
|
||||
allRegionIds: string[],
|
||||
filteredRegionIds: string[]
|
||||
): string[] {
|
||||
const filteredSet = new Set(filteredRegionIds)
|
||||
return allRegionIds.filter((id) => !filteredSet.has(id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate preview changes when hovering over a different size range.
|
||||
* Returns which regions would be added or removed if the user clicked.
|
||||
*/
|
||||
export function calculatePreviewChanges(
|
||||
currentIncludedIds: string[],
|
||||
previewIncludedIds: string[]
|
||||
): { addRegions: string[]; removeRegions: string[] } {
|
||||
const currentSet = new Set(currentIncludedIds)
|
||||
const previewSet = new Set(previewIncludedIds)
|
||||
|
||||
const addRegions: string[] = []
|
||||
const removeRegions: string[] = []
|
||||
|
||||
for (const id of previewSet) {
|
||||
if (!currentSet.has(id)) {
|
||||
addRegions.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of currentSet) {
|
||||
if (!previewSet.has(id)) {
|
||||
removeRegions.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
return { addRegions, removeRegions }
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Responsive style utilities for consistent breakpoint handling.
|
||||
* Centralizes responsive patterns used across Know Your World components.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Breakpoint keys matching Panda CSS convention.
|
||||
* - base: 0px+ (mobile)
|
||||
* - sm: 640px+ (small tablets)
|
||||
* - md: 768px+ (tablets/small desktop)
|
||||
* - lg: 1024px+ (desktop)
|
||||
* - xl: 1280px+ (large desktop)
|
||||
*/
|
||||
export const BREAKPOINTS = {
|
||||
mobile: 'base',
|
||||
tablet: 'sm',
|
||||
desktop: 'md',
|
||||
wide: 'lg',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Creates responsive display styles for showing/hiding elements.
|
||||
*
|
||||
* @example
|
||||
* // Show only on desktop
|
||||
* className={css(responsiveDisplay({ showOn: 'desktop' }))}
|
||||
*
|
||||
* // Show only on mobile
|
||||
* className={css(responsiveDisplay({ hideOn: 'desktop' }))}
|
||||
*/
|
||||
export function responsiveDisplay(options: {
|
||||
showOn?: 'mobile' | 'desktop'
|
||||
hideOn?: 'mobile' | 'desktop'
|
||||
}): { display: Record<string, string> } {
|
||||
if (options.showOn === 'desktop') {
|
||||
return { display: { base: 'none', md: 'flex' } }
|
||||
}
|
||||
if (options.showOn === 'mobile') {
|
||||
return { display: { base: 'flex', md: 'none' } }
|
||||
}
|
||||
if (options.hideOn === 'desktop') {
|
||||
return { display: { base: 'flex', md: 'none' } }
|
||||
}
|
||||
if (options.hideOn === 'mobile') {
|
||||
return { display: { base: 'none', md: 'flex' } }
|
||||
}
|
||||
return { display: { base: 'flex' } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates responsive transform scale styles.
|
||||
* Commonly used for shrinking controls on mobile.
|
||||
*
|
||||
* @example
|
||||
* // 75% on mobile, 100% on tablet+
|
||||
* className={css(responsiveScale(0.75, 1))}
|
||||
*/
|
||||
export function responsiveScale(
|
||||
mobileScale: number,
|
||||
desktopScale: number = 1,
|
||||
origin: string = 'top right'
|
||||
): {
|
||||
transform: Record<string, string>
|
||||
transformOrigin: string
|
||||
} {
|
||||
return {
|
||||
transform: {
|
||||
base: `scale(${mobileScale})`,
|
||||
sm: `scale(${desktopScale})`,
|
||||
},
|
||||
transformOrigin: origin,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates responsive spacing/gap styles.
|
||||
*
|
||||
* @example
|
||||
* className={css(responsiveGap(1, 2))}
|
||||
*/
|
||||
export function responsiveGap(
|
||||
mobileGap: number | string,
|
||||
desktopGap: number | string
|
||||
): { gap: Record<string, number | string> } {
|
||||
return {
|
||||
gap: {
|
||||
base: mobileGap,
|
||||
sm: desktopGap,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates responsive flex direction styles.
|
||||
* Useful for stacking elements vertically on mobile.
|
||||
*
|
||||
* @example
|
||||
* // Stack on mobile, row on desktop
|
||||
* className={css(responsiveFlexDirection('column', 'row'))}
|
||||
*/
|
||||
export function responsiveFlexDirection(
|
||||
mobileDirection: 'row' | 'column',
|
||||
desktopDirection: 'row' | 'column'
|
||||
): { flexDirection: Record<string, 'row' | 'column'> } {
|
||||
return {
|
||||
flexDirection: {
|
||||
base: mobileDirection,
|
||||
sm: desktopDirection,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates responsive font size styles.
|
||||
*
|
||||
* @example
|
||||
* className={css(responsiveFontSize('sm', 'md'))}
|
||||
*/
|
||||
export function responsiveFontSize(
|
||||
mobileSize: string,
|
||||
desktopSize: string
|
||||
): { fontSize: Record<string, string> } {
|
||||
return {
|
||||
fontSize: {
|
||||
base: mobileSize,
|
||||
sm: desktopSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS trick to prevent a flex child from expanding its parent.
|
||||
* Sets width to 0 but min-width to 100% so it fills available space
|
||||
* without contributing to parent's width calculation.
|
||||
*/
|
||||
export const preventFlexExpansion = {
|
||||
width: 0,
|
||||
minWidth: '100%',
|
||||
} as const
|
||||
Reference in New Issue
Block a user