forked from defunkt/howl
/ |/ \ / \ / \ / | $$$$$$$$//$$$$$$ |$$$$$$$ |/$$$$$$ |$$$$$$$$/ $$ |__ $$ | $$ |$$ |__$$ |$$ | _$$/ $$ |__ $$ | $$ | $$ |$$ $$< $$ |/ |$$ | $$$$$/ $$ | $$ |$$$$$$$ |$$ |$$$$ |$$$$$/ $$ | $$ \__$$ |$$ | $$ |$$ \__$$ |$$ |_____ $$ | $$ $$/ $$ | $$ |$$ $$/ $$ | $$/ $$$$$$/ $$/ $$/ $$$$$$/ $$$$$$$$/
250 lines
6.8 KiB
TypeScript
250 lines
6.8 KiB
TypeScript
import { define } from 'forge'
|
|
import { theme } from './theme'
|
|
import { Section } from './section'
|
|
import { H2 } from './text'
|
|
import { VStack, HStack } from './stack'
|
|
|
|
export type SelectOption = {
|
|
value: string
|
|
label: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
// Custom dropdown arrow as base64 SVG
|
|
const dropdownArrow = `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`
|
|
|
|
export const Select = define('Select', {
|
|
parts: {
|
|
Wrapper: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: theme('spacing-1'),
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
Label: {
|
|
base: 'label',
|
|
fontSize: theme('fontSize-sm'),
|
|
fontWeight: 500,
|
|
color: theme('colors-fg'),
|
|
},
|
|
Field: {
|
|
base: 'select',
|
|
height: 40,
|
|
padding: `${theme('spacing-2')} 32px ${theme('spacing-2')} ${theme('spacing-3')}`,
|
|
borderRadius: theme('radius-md'),
|
|
border: `1px solid ${theme('colors-border')}`,
|
|
background: theme('colors-bg'),
|
|
fontSize: theme('fontSize-sm'),
|
|
outline: 'none',
|
|
appearance: 'none',
|
|
backgroundImage: dropdownArrow,
|
|
backgroundRepeat: 'no-repeat',
|
|
backgroundPosition: 'right 8px center',
|
|
backgroundSize: '16px' as any,
|
|
flex: 1,
|
|
|
|
states: {
|
|
':focus': {
|
|
borderColor: theme('colors-borderActive'),
|
|
},
|
|
':disabled': {
|
|
opacity: 0.5,
|
|
cursor: 'not-allowed',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
variants: {
|
|
labelPosition: {
|
|
above: {
|
|
parts: {
|
|
Wrapper: {
|
|
flexDirection: 'column',
|
|
gap: theme('spacing-1'),
|
|
},
|
|
},
|
|
},
|
|
left: {
|
|
parts: {
|
|
Wrapper: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: theme('spacing-1'),
|
|
},
|
|
},
|
|
},
|
|
right: {
|
|
parts: {
|
|
Wrapper: {
|
|
flexDirection: 'row-reverse',
|
|
alignItems: 'center',
|
|
gap: theme('spacing-1'),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
render({ props, parts: { Root, Wrapper, Label, Field } }) {
|
|
const { children, options, placeholder, labelPosition = 'above', ...selectProps } = props
|
|
|
|
// Generate id for label association if needed
|
|
const elementId = props.id || (children ? `select-${Math.random().toString(36).slice(2)}` : undefined)
|
|
|
|
const selectElement = (
|
|
<Field {...selectProps} id={elementId}>
|
|
{placeholder && (
|
|
<option value="" disabled>
|
|
{placeholder}
|
|
</option>
|
|
)}
|
|
{options?.map((option: SelectOption) => (
|
|
<option
|
|
key={option.value}
|
|
value={option.value}
|
|
disabled={option.disabled}
|
|
>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</Field>
|
|
)
|
|
|
|
if (!children) {
|
|
return selectElement
|
|
}
|
|
|
|
return (
|
|
<Wrapper labelPosition={labelPosition}>
|
|
<Label for={elementId}>{children}</Label>
|
|
{selectElement}
|
|
</Wrapper>
|
|
)
|
|
},
|
|
})
|
|
|
|
export type SelectProps = Parameters<typeof Select>[0]
|
|
|
|
export const Test = () => {
|
|
const months = [
|
|
{ value: '01', label: 'January' },
|
|
{ value: '02', label: 'February' },
|
|
{ value: '03', label: 'March' },
|
|
{ value: '04', label: 'April' },
|
|
{ value: '05', label: 'May' },
|
|
{ value: '06', label: 'June' },
|
|
{ value: '07', label: 'July' },
|
|
{ value: '08', label: 'August' },
|
|
{ value: '09', label: 'September' },
|
|
{ value: '10', label: 'October' },
|
|
{ value: '11', label: 'November' },
|
|
{ value: '12', label: 'December' },
|
|
]
|
|
|
|
const years = Array.from({ length: 10 }, (_, i) => ({
|
|
value: String(2024 + i),
|
|
label: String(2024 + i),
|
|
}))
|
|
|
|
const countries = [
|
|
{ value: 'us', label: 'United States' },
|
|
{ value: 'ca', label: 'Canada' },
|
|
{ value: 'uk', label: 'United Kingdom' },
|
|
{ value: 'de', label: 'Germany' },
|
|
{ value: 'fr', label: 'France' },
|
|
{ value: 'au', label: 'Australia', disabled: true },
|
|
]
|
|
|
|
return (
|
|
<Section style={{ maxWidth: '448px' }}>
|
|
{/* Basic selects */}
|
|
<VStack gap={4}>
|
|
<H2>Basic Selects</H2>
|
|
<VStack gap={4}>
|
|
<Select options={months} placeholder="Select month" />
|
|
<Select options={years} placeholder="Select year" />
|
|
<Select options={countries} placeholder="Select country" />
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* With values */}
|
|
<VStack gap={4}>
|
|
<H2>With Values</H2>
|
|
<VStack gap={4}>
|
|
<Select options={months} value="03" />
|
|
<Select options={years} value="2025" />
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* Disabled state */}
|
|
<VStack gap={4}>
|
|
<H2>Disabled State</H2>
|
|
<VStack gap={4}>
|
|
<Select options={months} disabled placeholder="Disabled select" />
|
|
<Select options={years} disabled value="2024" />
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* Label above */}
|
|
<VStack gap={4}>
|
|
<H2>Label Above</H2>
|
|
<VStack gap={4}>
|
|
<Select options={months} placeholder="Select month">
|
|
Birth Month
|
|
</Select>
|
|
<Select options={years} placeholder="Select year">
|
|
Birth Year
|
|
</Select>
|
|
<Select options={countries} placeholder="Select country">
|
|
Country
|
|
</Select>
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* Label to the left */}
|
|
<VStack gap={4}>
|
|
<H2>Label Left</H2>
|
|
<VStack gap={4}>
|
|
<Select labelPosition="left" options={months} placeholder="Select month">
|
|
Month
|
|
</Select>
|
|
<Select labelPosition="left" options={years} placeholder="Select year">
|
|
Year
|
|
</Select>
|
|
<Select labelPosition="left" options={countries} placeholder="Select country">
|
|
Country
|
|
</Select>
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* Label to the right */}
|
|
<VStack gap={4}>
|
|
<H2>Label Right</H2>
|
|
<VStack gap={4}>
|
|
<Select labelPosition="right" options={months} placeholder="Select month">
|
|
Month
|
|
</Select>
|
|
<Select labelPosition="right" options={years} placeholder="Select year">
|
|
Year
|
|
</Select>
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* Horizontal layout (like card form) */}
|
|
<VStack gap={4}>
|
|
<H2>Horizontal Layout</H2>
|
|
<HStack gap={4}>
|
|
<Select options={months} placeholder="MM">
|
|
Expires
|
|
</Select>
|
|
<Select options={years} placeholder="YYYY">
|
|
Year
|
|
</Select>
|
|
</HStack>
|
|
</VStack>
|
|
</Section>
|
|
)
|
|
}
|