forked from defunkt/howl
276 lines
8.2 KiB
TypeScript
276 lines
8.2 KiB
TypeScript
import { Section } from "./section"
|
|
import { H2, H3, H4, H5, Text, SmallText } from "./text"
|
|
import "hono/jsx"
|
|
import type { JSX, FC } from "hono/jsx"
|
|
import { VStack, HStack } from "./stack"
|
|
|
|
export type SelectOption = {
|
|
value: string
|
|
label: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
export type SelectProps = Omit<JSX.IntrinsicElements["select"], "children"> & {
|
|
options: SelectOption[]
|
|
placeholder?: string
|
|
labelPosition?: "above" | "left" | "right"
|
|
children?: any
|
|
}
|
|
|
|
export const Select: FC<SelectProps> = (props) => {
|
|
const { options, placeholder, labelPosition = "above", children, style, ...selectProps } = props
|
|
|
|
// If a label is provided but no id, generate a random id so the label can be clicked
|
|
if (children && !selectProps.id) {
|
|
selectProps.id = `random-${Math.random().toString(36)}`
|
|
}
|
|
|
|
const selectStyle: JSX.CSSProperties = {
|
|
height: "40px",
|
|
padding: "8px 32px 8px 12px",
|
|
borderRadius: "6px",
|
|
border: "1px solid #d1d5db",
|
|
backgroundColor: "white",
|
|
fontSize: "14px",
|
|
outline: "none",
|
|
appearance: "none",
|
|
backgroundImage: `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`,
|
|
backgroundRepeat: "no-repeat",
|
|
backgroundPosition: "right 8px center",
|
|
backgroundSize: "16px 16px",
|
|
...style,
|
|
}
|
|
|
|
const selectElement = (
|
|
<select style={selectStyle} {...selectProps}>
|
|
{placeholder && (
|
|
<option value="" disabled>
|
|
{placeholder}
|
|
</option>
|
|
)}
|
|
{options.map((option) => (
|
|
<option
|
|
key={option.value}
|
|
value={option.value}
|
|
disabled={option.disabled}
|
|
selected={selectProps.value === option.value}
|
|
>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
|
|
if (!children) {
|
|
return selectElement
|
|
}
|
|
|
|
const labelStyle: JSX.CSSProperties = {
|
|
fontSize: "14px",
|
|
fontWeight: "500",
|
|
color: "#111827",
|
|
}
|
|
|
|
const labelElement = (
|
|
<label for={selectProps.id} style={labelStyle}>
|
|
{children}
|
|
</label>
|
|
)
|
|
|
|
if (labelPosition === "above") {
|
|
return (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}>
|
|
{labelElement}
|
|
{selectElement}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (labelPosition === "left") {
|
|
return (
|
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
|
{labelElement}
|
|
<select style={{ ...selectStyle, flex: 1 }} {...selectProps}>
|
|
{placeholder && (
|
|
<option value="" disabled>
|
|
{placeholder}
|
|
</option>
|
|
)}
|
|
{options.map((option) => (
|
|
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (labelPosition === "right") {
|
|
return (
|
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
|
<select style={{ ...selectStyle, flex: 1 }} {...selectProps}>
|
|
{placeholder && (
|
|
<option value="" disabled>
|
|
{placeholder}
|
|
</option>
|
|
)}
|
|
{options.map((option) => (
|
|
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{labelElement}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export const Test = () => {
|
|
const options = [{ value: "1", label: "Option 1" }, { value: "2", label: "Option 2" }]
|
|
|
|
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 maxWidth="448px">
|
|
{/* API Usage Examples */}
|
|
<VStack gap={2} style={{ backgroundColor: "#f9fafb", padding: "16px", borderRadius: "8px", fontFamily: "monospace", fontSize: "12px" }}>
|
|
<div><Select options={options} /></div>
|
|
<div><Select options={options} placeholder="Choose" /></div>
|
|
<div><Select options={options}>Label</Select></div>
|
|
<div><Select options={options} labelPosition="left">Label</Select></div>
|
|
</VStack>
|
|
|
|
{/* 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"
|
|
style={{ opacity: 0.5, cursor: "not-allowed" }}
|
|
/>
|
|
<Select options={years} disabled value="2024" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
|
</VStack>
|
|
</VStack>
|
|
|
|
{/* Custom styling */}
|
|
<VStack gap={4}>
|
|
<H2>Custom Styling</H2>
|
|
<VStack gap={4}>
|
|
<Select options={countries} style={{ borderColor: "#93c5fd" }} placeholder="Custom styled select" />
|
|
<Select options={months} style={{ height: "32px", fontSize: "12px" }} placeholder="Small select" />
|
|
</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>
|
|
)
|
|
}
|