import { For, onCleanup, onMount, Show, type JSX } from 'solid-js'
import {
Check,
ChevronDown,
CircleAlert,
CircleCheck,
Info,
Moon,
Sun,
TriangleAlert,
} from 'lucide-solid'
import {
CheckboxRoot,
CheckboxControl,
CheckboxIndicator,
CheckboxLabel,
CheckboxHiddenInput,
} from '@ark-ui/solid/checkbox'
import {
RadioGroupRoot,
RadioGroupItem,
RadioGroupItemControl,
RadioGroupItemText,
RadioGroupItemHiddenInput,
RadioGroupLabel,
} from '@ark-ui/solid/radio-group'
import { theme, toggleTheme } from '../lib/theme'
export function Card(props: { class?: string; children: JSX.Element }) {
return
{props.children}
}
export function CardHeader(props: { class?: string; children: JSX.Element }) {
return {props.children}
}
export function CardTitle(props: { class?: string; children: JSX.Element }) {
return {props.children}
}
export function CardDescription(props: { class?: string; children: JSX.Element }) {
return {props.children}
}
export function CardContent(props: { class?: string; children: JSX.Element }) {
return {props.children}
}
export function CardFooter(props: { class?: string; children: JSX.Element }) {
return {props.children}
}
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
export type ButtonSize = 'sm' | 'default' | 'lg' | 'icon'
const BTN_VARIANT: Record = {
primary: 'btn-primary',
secondary: 'btn-secondary',
outline: 'btn-outline',
ghost: 'btn-ghost',
danger: 'btn-destructive',
}
const BTN_SIZE: Record = {
sm: 'btn-sm',
default: '',
lg: 'btn-lg',
icon: 'btn-icon',
}
export function Button(props: {
onClick?: () => void
variant?: ButtonVariant
size?: ButtonSize
disabled?: boolean
class?: string
type?: 'button' | 'submit'
'aria-label'?: string
title?: string
children: JSX.Element
}) {
return (
)
}
export function TextInput(props: {
id?: string
value?: string
onInput?: (v: string) => void
onKeyDown?: (e: KeyboardEvent) => void
placeholder?: string
type?: string
class?: string
disabled?: boolean
name?: string
autoComplete?: string
'aria-label'?: string
}) {
return (
props.onInput?.((e.currentTarget as HTMLInputElement).value)}
onKeyDown={props.onKeyDown}
/>
)
}
export function Select(props: {
value: string
onChange: (v: string) => void
options: { value: string; label: string }[]
class?: string
disabled?: boolean
'aria-label'?: string
}) {
return (
)
}
export type BadgeVariant = 'default' | 'secondary' | 'outline' | 'success' | 'warning' | 'danger' | 'muted'
const BADGE_TONE: Record = {
default: 'border-transparent bg-primary text-primary-foreground',
secondary: 'border-line bg-secondary text-secondary-foreground',
outline: 'border-line text-foreground',
success: 'border-success/30 bg-success/10 text-success',
warning: 'border-warning/30 bg-warning/10 text-warning',
danger: 'border-destructive/30 bg-destructive/10 text-destructive',
muted: 'border-line bg-transparent text-muted-foreground',
}
export function Badge(props: { variant?: BadgeVariant; class?: string; children: JSX.Element }) {
return {props.children}
}
const ALERT_TONE = {
error: {
box: 'border-destructive/40 bg-destructive/10 text-destructive',
icon: CircleAlert,
},
warning: {
box: 'border-warning/40 bg-warning/10 text-warning',
icon: TriangleAlert,
},
success: {
box: 'border-success/40 bg-success/10 text-success',
icon: CircleCheck,
},
info: {
box: 'border-acid-cyan/40 bg-acid-cyan/10 text-accent-foreground',
icon: Info,
},
}
export function Alert(props: {
tone?: 'error' | 'warning' | 'success' | 'info'
class?: string
children: JSX.Element
}) {
const tone = props.tone ?? 'info'
const style = ALERT_TONE[tone]
const Icon = style.icon
return (
)
}
export function LiveDot(props: { active?: boolean; class?: string }) {
return (
)
}
export function Separator(props: { class?: string }) {
return
}
export function ThemeToggle(props: { class?: string }) {
const next = () => (theme() === 'dark' ? '亮色' : '暗色')
return (
)
}
export function CheckboxRow(props: { label: string; checked: boolean; onChange: (v: boolean) => void; disabled?: boolean }) {
return (
props.onChange(!!e.checked)}
class="group flex cursor-pointer items-center gap-2.5 select-none"
>
{props.label}
)
}
function RadioRow(props: { value: string; label: string }) {
return (
{props.label}
)
}
export function RadioPicker(props: {
label: string
value: string
onChange: (v: string) => void
options: { value: string; label: string }[]
}) {
return (
props.onChange(e.value)}
class="flex flex-col gap-2"
>
{props.label}
{(o) => }
)
}
export function Segmented(props: {
label?: string
value: T
onChange: (v: T) => void
options: { value: T; label: string; icon?: JSX.Element }[]
class?: string
}) {
return (
{props.label}
{(o) => (
)}
)
}
/* ===== HUD 四角(父元素需 relative) ===== */
export function Corners(props: { class?: string }) {
const c = props.class ?? ''
return (
<>
>
)
}
/* ===== 信号电平条 ===== */
export function Meter(props: { active?: boolean; class?: string }) {
return (
)
}
/* ===== 滚动入场(IntersectionObserver) ===== */
export function Reveal(props: { class?: string; delay?: number; children: JSX.Element }) {
let el: HTMLDivElement | undefined
onMount(() => {
const node = el
if (!node) return
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
node.classList.add('is-in')
return
}
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
node.classList.add('is-in')
io.disconnect()
}
}
},
{ threshold: 0.08, rootMargin: '0px 0px -6% 0px' },
)
io.observe(node)
onCleanup(() => io.disconnect())
})
return (
(el = n)} class={`reveal ${props.class ?? ''}`} style={{ '--i': String(props.delay ?? 0) } as any}>
{props.children}
)
}
/* ===== 跑马灯(内容复制两份实现无缝循环) ===== */
export function Marquee(props: { items: string[]; class?: string }) {
const row = (aria: boolean) => (
{(t) => (
<>
{t}
>
)}
)
return (
)
}