342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
import type { TailwindSize } from "./types"
|
|
import "hono/jsx"
|
|
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
|
import { Grid } from "./grid"
|
|
import { Section } from "./section"
|
|
import { H2 } from "./text"
|
|
import { RedBox, GreenBox, BlueBox } from "./box"
|
|
import { CodeExamples } from "./code"
|
|
import { cn } from "./cn"
|
|
|
|
export const VStack: FC<VStackProps> = (props) => {
|
|
return (
|
|
<Stack
|
|
direction="col"
|
|
mainAxis={props.v}
|
|
crossAxis={props.h}
|
|
wrap={props.wrap}
|
|
gap={props.gap}
|
|
maxWidth={props.maxWidth}
|
|
gridSizes={props.rows}
|
|
componentName="VStack"
|
|
class={props.class}
|
|
style={props.style}
|
|
>
|
|
{props.children}
|
|
</Stack>
|
|
)
|
|
}
|
|
|
|
export const HStack: FC<HStackProps> = (props) => {
|
|
return (
|
|
<Stack
|
|
direction="row"
|
|
mainAxis={props.h}
|
|
crossAxis={props.v}
|
|
wrap={props.wrap}
|
|
gap={props.gap}
|
|
maxWidth={props.maxWidth}
|
|
gridSizes={props.cols}
|
|
componentName="HStack"
|
|
class={props.class}
|
|
style={props.style}
|
|
>
|
|
{props.children}
|
|
</Stack>
|
|
)
|
|
}
|
|
|
|
const Stack: FC<StackProps> = (props) => {
|
|
const gapPx = props.gap ? props.gap * 4 : 0
|
|
|
|
// Use CSS Grid when gridSizes (cols/rows) is provided
|
|
if (props.gridSizes) {
|
|
const gridTemplate = props.gridSizes.map(size => `${size}fr`).join(" ")
|
|
|
|
const gridStyles: JSX.CSSProperties = {
|
|
display: "grid",
|
|
gap: `${gapPx}px`,
|
|
maxWidth: props.maxWidth,
|
|
}
|
|
|
|
if (props.direction === "row") {
|
|
gridStyles.gridTemplateColumns = gridTemplate
|
|
} else {
|
|
gridStyles.gridTemplateRows = gridTemplate
|
|
}
|
|
|
|
const combinedStyles = {
|
|
...gridStyles,
|
|
...props.style,
|
|
}
|
|
|
|
return <div class={cn(props.componentName, props.class)} style={combinedStyles}>{props.children}</div>
|
|
}
|
|
|
|
// Default flexbox behavior
|
|
const baseStyles: JSX.CSSProperties = {
|
|
display: "flex",
|
|
flexDirection: props.direction === "row" ? "row" : "column",
|
|
flexWrap: props.wrap ? "wrap" : "nowrap",
|
|
gap: `${gapPx}px`,
|
|
maxWidth: props.maxWidth,
|
|
}
|
|
|
|
if (props.mainAxis) {
|
|
baseStyles.justifyContent = getJustifyContent(props.mainAxis)
|
|
}
|
|
|
|
if (props.crossAxis) {
|
|
baseStyles.alignItems = getAlignItems(props.crossAxis)
|
|
}
|
|
|
|
const combinedStyles = {
|
|
...baseStyles,
|
|
...props.style,
|
|
}
|
|
|
|
return <div class={cn(props.componentName, props.class)} style={combinedStyles}>{props.children}</div>
|
|
}
|
|
|
|
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" }}>
|
|
{/* API Usage Examples */}
|
|
<CodeExamples
|
|
examples={[
|
|
'<VStack>...</VStack>',
|
|
'<VStack gap={4} v="center">...</VStack>',
|
|
'<HStack>...</HStack>',
|
|
'<HStack gap={6} h="between" v="center">...</HStack>',
|
|
'<HStack cols={[7, 3]} maxWidth="1200px" gap={4}>...</HStack>',
|
|
'<VStack rows={[2, 1]} maxWidth="800px">...</VStack>',
|
|
]}
|
|
/>
|
|
|
|
{/* 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 cols={[7, 3]} gap={4} 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 cols={[2, 1]} gap={4} 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 cols={[1, 2, 1]} gap={4} 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" }}>
|
|
With maxWidth="600px"
|
|
</div>
|
|
<HStack cols={[7, 3]} maxWidth="600px" gap={4} style={{ 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
|
|
rows={[2, 1]}
|
|
gap={4}
|
|
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>
|
|
)
|
|
}
|
|
|
|
type StackDirection = "row" | "col"
|
|
|
|
type StackProps = {
|
|
direction: StackDirection
|
|
mainAxis?: string
|
|
crossAxis?: string
|
|
wrap?: boolean
|
|
gap?: TailwindSize
|
|
maxWidth?: string
|
|
gridSizes?: number[] // cols for row, rows for col
|
|
componentName?: string // for data-howl attribute
|
|
class?: string
|
|
style?: JSX.CSSProperties
|
|
children?: any
|
|
}
|
|
|
|
type MainAxisOpts = "start" | "center" | "end" | "between" | "around" | "evenly"
|
|
type CrossAxisOpts = "start" | "center" | "end" | "stretch" | "baseline"
|
|
|
|
type CommonStackProps = PropsWithChildren & {
|
|
wrap?: boolean
|
|
gap?: TailwindSize
|
|
maxWidth?: string
|
|
class?: string
|
|
style?: JSX.CSSProperties
|
|
}
|
|
|
|
type VStackProps = CommonStackProps & {
|
|
v?: MainAxisOpts // main axis for vertical stack
|
|
h?: CrossAxisOpts // cross axis for vertical stack
|
|
rows?: number[] // custom row sizing (e.g., [7, 3] for 70%/30%)
|
|
}
|
|
|
|
type HStackProps = CommonStackProps & {
|
|
h?: MainAxisOpts // main axis for horizontal stack
|
|
v?: CrossAxisOpts // cross axis for horizontal stack
|
|
cols?: number[] // custom column sizing (e.g., [7, 3] for 70%/30%)
|
|
}
|
|
|
|
function getJustifyContent(axis: string): string {
|
|
const map: Record<string, string> = {
|
|
start: "flex-start",
|
|
center: "center",
|
|
end: "flex-end",
|
|
between: "space-between",
|
|
around: "space-around",
|
|
evenly: "space-evenly",
|
|
}
|
|
return map[axis] || "flex-start"
|
|
}
|
|
|
|
function getAlignItems(axis: string): string {
|
|
const map: Record<string, string> = {
|
|
start: "flex-start",
|
|
center: "center",
|
|
end: "flex-end",
|
|
stretch: "stretch",
|
|
baseline: "baseline",
|
|
}
|
|
return map[axis] || "stretch"
|
|
}
|