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

245 lines
7.1 KiB
TypeScript

import { Section } from './section'
import { H2, H3, Text, SmallText } from './text'
import { Avatar } from './avatar'
import type { AvatarProps } from './avatar'
import { Image } from './image'
import type { ImageProps } from './image'
import { VStack, HStack } from './stack'
import { Grid } from './grid'
export const Placeholder = {
Avatar(props: PlaceholderAvatarProps) {
const { size = 32, seed = 'seed', type = 'dylan', transparent, alt, style, rounded, id, ref, class: className } = props
// Generate DiceBear avatar URL
const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`)
url.searchParams.set('seed', seed)
url.searchParams.set('size', size.toString())
if (transparent) {
url.searchParams.set('backgroundColor', 'transparent')
}
return <Avatar src={url.toString()} alt={alt} style={style} size={size as any} rounded={rounded} id={id} ref={ref} class={className} />
},
Image(props: PlaceholderImageProps) {
const { width = 200, height = 200, seed = 1, alt = 'Placeholder image', objectFit, style, id, ref, class: className } = props
// Generate Picsum Photos URL with seed for consistent images
const src = `https://picsum.photos/${width}/${height}?random=${seed}`
return <Image src={src} alt={alt} objectFit={objectFit} width={width} height={height} style={style} id={id} ref={ref} class={className} />
},
}
export const Test = () => {
return (
<Section>
{/* Show all available avatar styles */}
<VStack gap={4}>
<H2>
All Avatar Styles ({allStyles.length} total)
</H2>
<Grid cols={6} gap={3}>
{allStyles.slice(0, 12).map((style) => (
<VStack h="center" gap={1} key={style}>
<Placeholder.Avatar type={style} size={48} />
<SmallText style={{ fontWeight: '500' }}>{style}</SmallText>
</VStack>
))}
</Grid>
</VStack>
{/* Avatar size variations */}
<VStack gap={4}>
<H2>Avatar Size Variations</H2>
<HStack gap={4}>
{[24, 32, 48, 64].map((size) => (
<VStack h="center" gap={2} key={size}>
<Placeholder.Avatar size={size} />
<Text>{size}px</Text>
</VStack>
))}
</HStack>
</VStack>
{/* Avatar styling combinations */}
<VStack gap={4}>
<H2>Avatar Styling Options</H2>
<HStack gap={6}>
<VStack h="center" gap={2}>
<Placeholder.Avatar rounded size={64} />
<Text>Rounded + Background</Text>
</VStack>
<VStack h="center" gap={2}>
<div style={{ backgroundColor: '#e5e7eb', padding: '8px' }}>
<Placeholder.Avatar rounded transparent size={64} />
</div>
<Text>Rounded + Transparent</Text>
</VStack>
<VStack h="center" gap={2}>
<Placeholder.Avatar size={64} />
<Text>Square + Background</Text>
</VStack>
<VStack h="center" gap={2}>
<div style={{ backgroundColor: '#e5e7eb', padding: '8px' }}>
<Placeholder.Avatar transparent size={64} />
</div>
<Text>Square + Transparent</Text>
</VStack>
</HStack>
</VStack>
{/* Avatar seed variations */}
<VStack gap={4}>
<H2>
Avatar Seeds (Same Style, Different People)
</H2>
<HStack gap={4}>
{['alice', 'bob', 'charlie', 'diana'].map((seed) => (
<VStack h="center" gap={2} key={seed}>
<Placeholder.Avatar seed={seed} size={64} />
<Text>"{seed}"</Text>
</VStack>
))}
</HStack>
</VStack>
{/* Placeholder Images */}
<VStack gap={6}>
<H2>Placeholder Images</H2>
{/* Size variations */}
<VStack gap={3}>
<H3>Size Variations</H3>
<HStack gap={4}>
{[
{ width: 100, height: 100 },
{ width: 150, height: 100 },
{ width: 200, height: 150 },
{ width: 250, height: 200 },
].map(({ width, height }) => (
<VStack h="center" gap={2} key={`${width}x${height}`}>
<Placeholder.Image width={width} height={height} seed={1} />
<Text>
{width}x{height}
</Text>
</VStack>
))}
</HStack>
</VStack>
{/* Different seeds - show variety */}
<VStack gap={3}>
<H3>
Different Images (Different Seeds)
</H3>
<HStack gap={4}>
{[1, 2, 3, 4, 5].map((seed) => (
<VStack h="center" gap={2} key={seed}>
<Placeholder.Image width={150} height={150} seed={seed} />
<Text>Seed {seed}</Text>
</VStack>
))}
</HStack>
</VStack>
{/* With custom styles */}
<VStack gap={3}>
<H3>With Custom Styles</H3>
<HStack gap={6}>
<VStack h="center" gap={2}>
<Placeholder.Image
width={150}
height={150}
seed={1}
objectFit="cover"
style={{ borderRadius: '8px', border: '4px solid #3b82f6' }}
/>
<Text>Rounded + Border</Text>
</VStack>
<VStack h="center" gap={2}>
<Placeholder.Image
width={150}
height={150}
seed={2}
objectFit="cover"
style={{ boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
/>
<Text>With Shadow</Text>
</VStack>
<VStack h="center" gap={2}>
<Placeholder.Image
width={150}
height={150}
seed={3}
objectFit="cover"
style={{
borderRadius: '9999px',
border: '4px solid #22c55e',
boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)',
}}
/>
<Text>Circular + Effects</Text>
</VStack>
</HStack>
</VStack>
</VStack>
</Section>
)
}
// Type definitions
type PlaceholderAvatarProps = Omit<AvatarProps, 'src'> & {
seed?: string
type?: DicebearStyleName
transparent?: boolean
}
type PlaceholderImageProps = Omit<ImageProps, 'src' | 'alt'> & {
width?: number
height?: number
seed?: number
alt?: string
}
// All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles
const allStyles = [
'adventurer',
'adventurer-neutral',
'avataaars',
'avataaars-neutral',
'big-ears',
'big-ears-neutral',
'big-smile',
'bottts',
'bottts-neutral',
'croodles',
'croodles-neutral',
'dylan',
'fun-emoji',
'glass',
'icons',
'identicon',
'initials',
'lorelei',
'lorelei-neutral',
'micah',
'miniavs',
'notionists',
'notionists-neutral',
'open-peeps',
'personas',
'pixel-art',
'pixel-art-neutral',
'rings',
'shapes',
'thumbs',
] as const
type DicebearStyleName = (typeof allStyles)[number]
// Default export for convenience
export default Placeholder