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

218 lines
6.4 KiB
TypeScript

import { define } from 'forge'
import { theme } from './theme'
import * as icons from 'lucide-static'
import { Grid } from './grid'
import { VStack } from './stack'
import { Section } from './section'
import { H2, Text } from './text'
export type IconName = keyof typeof icons
// Icon wrapper - the SVG is injected via dangerouslySetInnerHTML
const IconWrapper = define('Icon', {
display: 'block',
flexShrink: 0,
})
// IconLink wrapper
const IconLinkWrapper = define('IconLink', {
base: 'a',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'opacity 0.2s',
states: {
':hover': {
opacity: 0.7,
},
},
})
type IconProps = Parameters<typeof IconWrapper>[0] & {
name: IconName
size?: number
}
type IconLinkProps = Parameters<typeof IconLinkWrapper>[0] & {
name: IconName
size?: number
}
function sizeToPixels(size: number): number {
return size * 4
}
export const Icon = (props: IconProps) => {
const { name, size = 6, style, ...rest } = props
const iconSvg = icons[name]
if (!iconSvg) {
throw new Error(`Icon "${name}" not found in Lucide icons`)
}
const pixelSize = sizeToPixels(size)
const iconStyle = {
width: `${pixelSize}px`,
height: `${pixelSize}px`,
...style,
}
// Modify the SVG string to include our custom attributes
const modifiedSvg = iconSvg
.replace(/width="[^"]*"/, '')
.replace(/height="[^"]*"/, '')
.replace(/class="[^"]*"/, '')
.replace(
/<svg([^>]*)>/,
`<svg$1 style="display: block; flex-shrink: 0; width: ${pixelSize}px; height: ${pixelSize}px;">`
)
return <IconWrapper dangerouslySetInnerHTML={{ __html: modifiedSvg }} style={iconStyle} {...rest} />
}
export const IconLink = (props: IconLinkProps) => {
const { href = '#', name, size, ...rest } = props
return (
<IconLinkWrapper href={href} {...rest}>
<Icon name={name} size={size} />
</IconLinkWrapper>
)
}
export const Test = () => {
return (
<Section>
{/* Size variations */}
<VStack gap={4}>
<H2>Icon Size Variations</H2>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => (
<VStack h="center" gap={2} key={size}>
<Icon name="Heart" size={size} />
<Text>{size}</Text>
</VStack>
))}
</div>
</VStack>
{/* Styling with CSS classes */}
<VStack gap={4}>
<H2>Styling with CSS Classes</H2>
<Grid cols={5} gap={6}>
<VStack h="center" gap={2}>
<Icon name="Star" size={12} />
<Text>Default</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Star" size={12} style={{ color: '#3b82f6' }} />
<Text>Blue Color</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Star" size={12} class="stroke-1" />
<Text>Thin Stroke</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Star" size={12} style={{ color: '#fbbf24', fill: 'currentColor', stroke: 'none' }} />
<Text>Filled</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Star" size={12} style={{ color: '#a855f7', transition: 'color 0.2s' }} />
<Text>Hover Effect</Text>
</VStack>
</Grid>
</VStack>
{/* Advanced styling */}
<VStack gap={4}>
<H2>Advanced Styling</H2>
<Grid cols={4} gap={6}>
<VStack h="center" gap={2}>
<Icon name="Heart" size={12} style={{ color: '#ef4444', fill: 'currentColor', stroke: 'none' }} />
<Text>Filled Heart</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Shield" size={12} style={{ color: '#16a34a', strokeWidth: '2' }} />
<Text>Thick Stroke</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Sun" size={12} style={{ color: '#eab308' }} />
<Text>Sun Icon</Text>
</VStack>
<VStack h="center" gap={2}>
<Icon name="Zap" size={12} style={{ color: '#60a5fa', filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.3))' }} />
<Text>Drop Shadow</Text>
</VStack>
</Grid>
</VStack>
{/* Icon links */}
<VStack gap={4}>
<H2>Icon Links</H2>
<div style={{ display: 'flex', gap: '24px' }}>
<VStack h="center" gap={2}>
<IconLink name="Home" size={8} href="/" />
<Text>Home Link</Text>
</VStack>
<VStack h="center" gap={2}>
<IconLink name="ExternalLink" size={8} href="https://example.com" target="_blank" />
<Text>External Link</Text>
</VStack>
<VStack h="center" gap={2}>
<IconLink name="Mail" size={8} href="mailto:hello@example.com" />
<Text>Email Link</Text>
</VStack>
<VStack h="center" gap={2}>
<IconLink name="Phone" size={8} href="tel:+1234567890" />
<Text>Phone Link</Text>
</VStack>
</div>
</VStack>
{/* Styled icon links */}
<VStack gap={4}>
<H2>Styled Icon Links</H2>
<Grid cols={4} gap={6}>
<VStack h="center" gap={2}>
<IconLink
name="Download"
size={8}
href="#"
style={{
backgroundColor: '#3b82f6',
color: 'white',
padding: '8px',
borderRadius: '8px',
}}
/>
<Text>Button Style</Text>
</VStack>
<VStack h="center" gap={2}>
<IconLink
name="Settings"
size={8}
href="#"
style={{
border: '2px solid #d1d5db',
padding: '8px',
borderRadius: '9999px',
}}
/>
<Text>Circle Border</Text>
</VStack>
<VStack h="center" gap={2}>
<IconLink name="Heart" size={8} href="#" style={{ color: '#ef4444' }} />
<Text>Red Heart</Text>
</VStack>
<VStack h="center" gap={2}>
<IconLink name="Star" size={8} href="#" style={{ color: '#fbbf24', fill: 'currentColor' }} />
<Text>Filled Star</Text>
</VStack>
</Grid>
</VStack>
</Section>
)
}