howl/src/select.tsx
2025-11-29 22:44:51 -08:00

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>&lt;Select options=&#123;options&#125; /&gt;</div>
<div>&lt;Select options=&#123;options&#125; placeholder="Choose" /&gt;</div>
<div>&lt;Select options=&#123;options&#125;&gt;Label&lt;/Select&gt;</div>
<div>&lt;Select options=&#123;options&#125; labelPosition="left"&gt;Label&lt;/Select&gt;</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>
)
}