wip: zen refactor selector

This commit is contained in:
Frank
2025-10-11 07:54:57 -04:00
parent f93e1e5c92
commit 133da0f448
10 changed files with 337 additions and 530 deletions

View File

@@ -0,0 +1,80 @@
[data-component="dropdown"] {
position: relative;
[data-slot="trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: none;
border-radius: var(--border-radius-sm);
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
}
span {
flex: 1;
text-align: left;
font-weight: 500;
}
}
[data-slot="chevron"] {
flex-shrink: 0;
color: var(--color-text-secondary);
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
z-index: 1000;
margin-top: var(--space-1);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 160px;
&[data-align="left"] {
left: 0;
}
&[data-align="right"] {
right: 0;
}
@media (prefers-color-scheme: dark) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
[data-slot="item"] {
display: block;
width: 100%;
padding: var(--space-2-5) var(--space-3);
border: none;
background: none;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
text-align: left;
cursor: pointer;
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
&[data-selected="true"] {
background-color: var(--color-accent-alpha);
}
}
}

View File

@@ -0,0 +1,79 @@
import { JSX, Show, createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { IconChevron } from "./icon"
import "./dropdown.css"
interface DropdownProps {
trigger: JSX.Element | string
children: JSX.Element
open?: boolean
onOpenChange?: (open: boolean) => void
align?: "left" | "right"
class?: string
}
export function Dropdown(props: DropdownProps) {
const [store, setStore] = createStore({
isOpen: props.open ?? false,
})
let dropdownRef: HTMLDivElement | undefined
createEffect(() => {
if (props.open !== undefined) {
setStore("isOpen", props.open)
}
})
createEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
setStore("isOpen", false)
props.onOpenChange?.(false)
}
}
document.addEventListener("click", handleClickOutside)
onCleanup(() => document.removeEventListener("click", handleClickOutside))
})
const toggle = () => {
const newValue = !store.isOpen
setStore("isOpen", newValue)
props.onOpenChange?.(newValue)
}
return (
<div data-component="dropdown" class={props.class} ref={dropdownRef}>
<button data-slot="trigger" type="button" onClick={toggle}>
{typeof props.trigger === "string" ? <span>{props.trigger}</span> : props.trigger}
<IconChevron data-slot="chevron" />
</button>
<Show when={store.isOpen}>
<div data-slot="dropdown" data-align={props.align ?? "left"}>
{props.children}
</div>
</Show>
</div>
)
}
interface DropdownItemProps {
children: JSX.Element
selected?: boolean
onClick?: () => void
type?: "button" | "submit" | "reset"
}
export function DropdownItem(props: DropdownItemProps) {
return (
<button
data-slot="item"
data-selected={props.selected ?? false}
type={props.type ?? "button"}
onClick={props.onClick}
>
{props.children}
</button>
)
}