ui/tagSelect: simplify by requiring objects with value key

This commit is contained in:
Johannes Kirschbauer
2025-08-28 10:10:25 +02:00
parent 640f15d55e
commit c574b84278
3 changed files with 83 additions and 63 deletions

View File

@@ -1,22 +1,3 @@
.dummybg {
padding: 1rem;
width: 20rem;
min-height: 10rem;
border-radius: 8px;
border: 1px solid #2e4a4b;
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%),
linear-gradient(
180deg,
theme(colors.bg.inv.2) 0%,
theme(colors.bg.inv.3) 100%
);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.trigger { .trigger {
@apply rounded-md bg-inv-4 w-full min-h-11; @apply rounded-md bg-inv-4 w-full min-h-11;

View File

@@ -3,6 +3,7 @@ import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { TagSelect, TagSelectProps } from "./TagSelect"; import { TagSelect, TagSelectProps } from "./TagSelect";
import { Tag } from "../Tag/Tag"; import { Tag } from "../Tag/Tag";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";
import { createSignal } from "solid-js";
const meta = { const meta = {
title: "Components/Custom/SelectStepper", title: "Components/Custom/SelectStepper",
@@ -11,28 +12,51 @@ const meta = {
export default meta; export default meta;
type Story = StoryObj<TagSelectProps<string>>; interface Item {
value: string;
label: string;
}
const Item = (item: string) => ( type Story = StoryObj<TagSelectProps<Item>>;
const Item = (item: Item) => (
<Tag <Tag
inverted inverted
icon={(tag) => ( icon={(tag) => (
<Icon icon={"Machine"} size="0.5rem" inverted={tag.inverted} /> <Icon icon={"Machine"} size="0.5rem" inverted={tag.inverted} />
)} )}
> >
{item} {item.label}
</Tag> </Tag>
); );
export const Default: Story = { export const Default: Story = {
args: { args: {
renderItem: Item, renderItem: Item,
values: ["foo", "bar"], label: "Peer",
options: ["foo", "bar", "baz", "qux", "quux"], options: [
onChange: (values: string[]) => { { value: "foo", label: "Foo" },
console.log("Selected values:", values); { value: "bar", label: "Bar" },
}, { value: "baz", label: "Baz" },
onClick: () => { { value: "qux", label: "Qux" },
console.log("Combobox clicked"); { value: "quux", label: "Quux" },
}, { value: "corge", label: "Corge" },
{ value: "grault", label: "Grault" },
],
} satisfies Partial<TagSelectProps<Item>>,
render: (args: TagSelectProps<Item>) => {
const [state, setState] = createSignal<Item[]>([]);
return (
<TagSelect<Item>
{...args}
values={state()}
onClick={() => {
console.log("Clicked, current values:");
setState(() => [
{ value: "baz", label: "Baz" },
{ value: "qux", label: "Qux" },
]);
}}
/>
);
}, },
}; };

View File

@@ -5,43 +5,58 @@ import styles from "./TagSelect.module.css";
import { Combobox } from "@kobalte/core/combobox"; import { Combobox } from "@kobalte/core/combobox";
import { Button } from "../Button/Button"; import { Button } from "../Button/Button";
// Base props common to both modes
export interface TagSelectProps<T> { export interface TagSelectProps<T> {
// Define any props needed for the SelectStepper component onClick: () => void;
label: string;
values: T[]; values: T[];
options: T[]; options: T[];
onChange: (values: T[]) => void;
onClick: () => void;
renderItem: (item: T) => JSX.Element; renderItem: (item: T) => JSX.Element;
} }
export function TagSelect<T>(props: TagSelectProps<T>) { /**
* Shallowly interactive field for selecting multiple tags / machines.
* It does only handle click and focus interactions
* Displays the selected items as tags
*/
export function TagSelect<T extends { value: unknown }>(
props: TagSelectProps<T>,
) {
const optionValue = "value";
return ( return (
<div class={styles.dummybg}>
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
<div class="flex w-full items-center gap-2 px-1.5"> <div class="flex w-full items-center gap-2 px-1.5 py-0">
<Typography <Typography
hierarchy="body" hierarchy="label"
weight="medium" weight="medium"
class="flex gap-2 uppercase" class="flex gap-2 uppercase"
size="s" size="xs"
inverted inverted
color="secondary" color="secondary"
> >
Servers {props.label}
</Typography> </Typography>
<Icon icon="Info" color="tertiary" inverted /> <Icon icon="Info" color="tertiary" inverted size={11} />
<Button icon="Settings" hierarchy="primary" ghost class="ml-auto" /> <Button
icon="Settings"
hierarchy="primary"
ghost
class="ml-auto"
size="xs"
/>
</div> </div>
<Combobox<T> <Combobox<T>
multiple multiple
optionValue={optionValue}
value={props.values} value={props.values}
onChange={props.onChange}
options={props.options} options={props.options}
allowsEmptyCollection allowsEmptyCollection
class="w-full" class="w-full"
> >
<Combobox.Control<T> aria-label="Fruits"> <Combobox.Control<T> aria-label="Fruits">
{(state) => ( {(state) => {
console.log("combobox state selected", state.selectedOptions());
return (
<Combobox.Trigger <Combobox.Trigger
tabIndex={1} tabIndex={1}
class={styles.trigger} class={styles.trigger}
@@ -62,10 +77,10 @@ export function TagSelect<T>(props: TagSelectProps<T>) {
<For each={state.selectedOptions()}>{props.renderItem}</For> <For each={state.selectedOptions()}>{props.renderItem}</For>
</div> </div>
</Combobox.Trigger> </Combobox.Trigger>
)} );
}}
</Combobox.Control> </Combobox.Control>
</Combobox> </Combobox>
</div> </div>
</div>
); );
} }