howl/src/stack.tsx
Chris Wanstrath 0cad100197 ________ ______ _______ ______ ________
/        |/      \ /       \  /      \ /        |
$$$$$$$$//$$$$$$  |$$$$$$$  |/$$$$$$  |$$$$$$$$/
$$ |__   $$ |  $$ |$$ |__$$ |$$ | _$$/ $$ |__
$$    |  $$ |  $$ |$$    $$< $$ |/    |$$    |
$$$$$/   $$ |  $$ |$$$$$$$  |$$ |$$$$ |$$$$$/
$$ |     $$ \__$$ |$$ |  $$ |$$ \__$$ |$$ |_____
$$ |     $$    $$/ $$ |  $$ |$$    $$/ $$       |
$$/       $$$$$$/  $$/   $$/  $$$$$$/  $$$$$$$$/
2026-01-16 08:33:38 -08:00

266 lines
9.0 KiB
TypeScript

import { define } from 'forge'
import { theme } from './theme'
import { H2 } from './text'
import { RedBox, GreenBox, BlueBox } from './box'
import { Grid } from './grid'
import { Section } from './section'
export const VStack = define('VStack', {
display: 'flex',
flexDirection: 'column',
variants: {
gap: {
0: { gap: 0 },
1: { gap: theme('spacing-1') },
2: { gap: theme('spacing-2') },
3: { gap: theme('spacing-3') },
4: { gap: theme('spacing-4') },
6: { gap: theme('spacing-6') },
8: { gap: theme('spacing-8') },
12: { gap: theme('spacing-12') },
},
v: {
start: { justifyContent: 'flex-start' },
center: { justifyContent: 'center' },
end: { justifyContent: 'flex-end' },
between: { justifyContent: 'space-between' },
around: { justifyContent: 'space-around' },
evenly: { justifyContent: 'space-evenly' },
},
h: {
start: { alignItems: 'flex-start' },
center: { alignItems: 'center' },
end: { alignItems: 'flex-end' },
stretch: { alignItems: 'stretch' },
baseline: { alignItems: 'baseline' },
},
wrap: {
flexWrap: 'wrap',
},
},
render({ props, parts: { Root } }) {
const { rows, style, ...rest } = props
const gridStyle = rows
? { display: 'grid', gridTemplateRows: rows.map((r: number) => `${r}fr`).join(' '), ...style }
: style
return <Root style={gridStyle} {...rest}>{props.children}</Root>
},
})
export const HStack = define('HStack', {
display: 'flex',
flexDirection: 'row',
variants: {
gap: {
0: { gap: 0 },
1: { gap: theme('spacing-1') },
2: { gap: theme('spacing-2') },
3: { gap: theme('spacing-3') },
4: { gap: theme('spacing-4') },
6: { gap: theme('spacing-6') },
8: { gap: theme('spacing-8') },
12: { gap: theme('spacing-12') },
},
h: {
start: { justifyContent: 'flex-start' },
center: { justifyContent: 'center' },
end: { justifyContent: 'flex-end' },
between: { justifyContent: 'space-between' },
around: { justifyContent: 'space-around' },
evenly: { justifyContent: 'space-evenly' },
},
v: {
start: { alignItems: 'flex-start' },
center: { alignItems: 'center' },
end: { alignItems: 'flex-end' },
stretch: { alignItems: 'stretch' },
baseline: { alignItems: 'baseline' },
},
wrap: {
flexWrap: 'wrap',
},
},
render({ props, parts: { Root } }) {
const { cols, style, ...rest } = props
const gridStyle = cols
? { display: 'grid', gridTemplateColumns: cols.map((c: number) => `${c}fr`).join(' '), ...style }
: style
return <Root style={gridStyle} {...rest}>{props.children}</Root>
},
})
type MainAxisOpts = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
type CrossAxisOpts = 'start' | 'center' | 'end' | 'stretch' | 'baseline'
export const Test = () => {
const mainAxisOpts: MainAxisOpts[] = ['start', 'center', 'end', 'between', 'around', 'evenly']
const crossAxisOpts: CrossAxisOpts[] = ['start', 'center', 'end', 'stretch', 'baseline']
return (
<Section gap={8} style={{ padding: '16px' }}>
{/* HStack layout matrix */}
<VStack gap={2}>
<H2>HStack Layout</H2>
<div style={{ overflowX: 'auto' }}>
<Grid cols={7} gap={1} style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
{/* Header row: blank + h labels */}
<div></div>
{mainAxisOpts.map((h) => (
<div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
h: {h}
</div>
))}
{/* Each row: v label + HStack cells */}
{crossAxisOpts.map((v) => [
<div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
v: {v}
</div>,
...mainAxisOpts.map((h) => (
<HStack
key={`${h}-${v}`}
h={h}
v={v}
style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '96px', border: '1px solid #9ca3af' }}
>
<RedBox>Aa</RedBox>
<GreenBox>Aa</GreenBox>
<BlueBox>Aa</BlueBox>
</HStack>
)),
])}
</Grid>
</div>
</VStack>
{/* VStack layout matrix */}
<VStack gap={2}>
<H2>VStack Layout</H2>
<div style={{ overflowX: 'auto' }}>
<Grid cols={6} gap={1} style={{ gridTemplateColumns: 'auto repeat(5, 1fr)' }}>
{/* Header row: blank + h labels */}
<div></div>
{crossAxisOpts.map((h) => (
<div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
h: {h}
</div>
))}
{/* Each row: v label + VStack cells */}
{mainAxisOpts.map((v) => [
<div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
v: {v}
</div>,
...crossAxisOpts.map((h) => (
<VStack
key={`${h}-${v}`}
v={v}
h={h}
style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '168px', border: '1px solid #9ca3af' }}
>
<RedBox>Aa</RedBox>
<GreenBox>Aa</GreenBox>
<BlueBox>Aa</BlueBox>
</VStack>
)),
])}
</Grid>
</div>
</VStack>
{/* Custom column sizing */}
<VStack gap={4}>
<H2>HStack with Custom Column Sizing</H2>
<VStack gap={4}>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[7, 3] (70%/30% split)
</div>
<HStack gap={4} cols={[7, 3]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
70% width
</div>
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
30% width
</div>
</HStack>
</div>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[2, 1] (66%/33% split)
</div>
<HStack gap={4} cols={[2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#bbf7d0', padding: '16px', textAlign: 'center' }}>
2/3 width
</div>
<div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>
1/3 width
</div>
</HStack>
</div>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[1, 2, 1] (25%/50%/25% split)
</div>
<HStack gap={4} cols={[1, 2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#e9d5ff', padding: '16px', textAlign: 'center' }}>
25%
</div>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
50%
</div>
<div style={{ backgroundColor: '#fbcfe8', padding: '16px', textAlign: 'center' }}>
25%
</div>
</HStack>
</div>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[7, 3] with maxWidth: 600px
</div>
<HStack gap={4} cols={[7, 3]} style={{ maxWidth: '600px', backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
70% of max 600px
</div>
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
30% of max 600px
</div>
</HStack>
</div>
</VStack>
</VStack>
{/* Custom row sizing */}
<VStack gap={4}>
<H2>VStack with Custom Row Sizing</H2>
<VStack gap={4}>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
rows=[2, 1] (2/3 and 1/3 height)
</div>
<VStack
gap={4}
rows={[2, 1]}
style={{ backgroundColor: '#f3f4f6', padding: '16px', height: '300px' }}
>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
2/3 height
</div>
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
1/3 height
</div>
</VStack>
</div>
</VStack>
</VStack>
</Section>
)
}