combobox
A priority change combobox
A priority change combobox
Загрузка превью…
src/combobox/c-combobox-22.vue
<!--
`IconPlaceholder` заменён инлайновым `<svg>` (lucide-react v0.545.0:
"ellipsis" он же "more-horizontal", "triangle-alert"); Low/Medium/High
иконки в оригинале уже инлайновый `<svg>` — перенесены дословно.
`StatusGlyph`/`PriorityLabel`-аналоги через `<component :is="() => fn()" />`,
как в `c-combobox-21.vue`.
-->
<script setup lang="ts">
import { h, ref, type VNodeChild } from "vue"
import { buttonVariants } from "@/components/ui/button"
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue } from "@/components/ui/combobox"
import { Field } from "@/components/ui/field"
import { cn } from "@/lib/utils"
const priorityOptions = [
{ value: "none", label: "No Priority", tone: "none" },
{ value: "low", label: "Low", tone: "low" },
{ value: "medium", label: "Medium", tone: "medium" },
{ value: "high", label: "High", tone: "high" },
{ value: "urgent", label: "Urgent", tone: "urgent" },
] as const
type PriorityOption = (typeof priorityOptions)[number]
function lowPriorityIcon(): VNodeChild {
return h(
"svg",
{ "aria-hidden": "true", viewBox: "0 0 16 16", fill: "none", class: "size-4 shrink-0 text-foreground! [&_*]:text-foreground!" },
[
h("rect", { x: "3", y: "9", width: "2.5", height: "4", rx: "1", fill: "currentColor" }),
h("rect", { x: "7", y: "6", width: "2.5", height: "7", rx: "1", fill: "currentColor", opacity: "0.28" }),
h("rect", { x: "11", y: "3", width: "2.5", height: "10", rx: "1", fill: "currentColor", opacity: "0.28" }),
]
)
}
function mediumPriorityIcon(): VNodeChild {
return h(
"svg",
{ "aria-hidden": "true", viewBox: "0 0 16 16", fill: "none", class: "size-4 shrink-0 text-foreground! [&_*]:text-foreground!" },
[
h("rect", { x: "3", y: "9", width: "2.5", height: "4", rx: "1", fill: "currentColor" }),
h("rect", { x: "7", y: "6", width: "2.5", height: "7", rx: "1", fill: "currentColor" }),
h("rect", { x: "11", y: "3", width: "2.5", height: "10", rx: "1", fill: "currentColor", opacity: "0.28" }),
]
)
}
function highPriorityIcon(): VNodeChild {
return h(
"svg",
{ "aria-hidden": "true", viewBox: "0 0 16 16", fill: "none", class: "size-4 shrink-0 text-foreground! [&_*]:text-foreground!" },
[
h("rect", { x: "3", y: "9", width: "2.5", height: "4", rx: "1", fill: "currentColor" }),
h("rect", { x: "7", y: "6", width: "2.5", height: "7", rx: "1", fill: "currentColor" }),
h("rect", { x: "11", y: "3", width: "2.5", height: "10", rx: "1", fill: "currentColor" }),
]
)
}
function priorityGlyph(tone: PriorityOption["tone"]): VNodeChild {
switch (tone) {
case "none":
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "text-muted-foreground! [&_*]:text-muted-foreground! size-4 shrink-0",
},
[
h("circle", { cx: "12", cy: "12", r: "1" }),
h("circle", { cx: "19", cy: "12", r: "1" }),
h("circle", { cx: "5", cy: "12", r: "1" }),
]
)
case "low":
return lowPriorityIcon()
case "medium":
return mediumPriorityIcon()
case "high":
return highPriorityIcon()
case "urgent":
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-4 shrink-0 text-yellow-500! [&_*]:text-yellow-500!",
},
[
h("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
h("path", { d: "M12 9v4" }),
h("path", { d: "M12 17h.01" }),
]
)
}
}
function priorityLabel(priority: PriorityOption): VNodeChild {
const labelClassName = priority.tone === "urgent" ? "text-yellow-500!" : "text-foreground!"
return h(
"span",
{ class: cn("text-foreground! flex min-w-0 items-center gap-2", priority.tone === "urgent" && "text-yellow-500!") },
[priorityGlyph(priority.tone), h("span", { class: cn("truncate", labelClassName) }, priority.label)]
)
}
const priority = ref<PriorityOption | null>(priorityOptions[0])
</script>
<template>
<Field class="max-w-xs">
<Combobox
:model-value="priority"
@update:model-value="(v) => (priority = v as PriorityOption | null)"
>
<ComboboxTrigger
:class="cn(buttonVariants({ variant: 'outline' }), 'w-full justify-between font-normal')"
>
<ComboboxValue v-slot="{ value }">
<component :is="() => ((value as PriorityOption | null) ? priorityLabel(value as PriorityOption) : h('span', { class: 'text-muted-foreground' }, 'Set priority'))" />
</ComboboxValue>
</ComboboxTrigger>
<ComboboxContent class="max-w-(--anchor-width) min-w-(--anchor-width)">
<ComboboxInput :show-trigger="false" placeholder="Change priority" class="mb-1" />
<ComboboxEmpty>No priorities found.</ComboboxEmpty>
<ComboboxList>
<ComboboxItem v-for="item in priorityOptions" :key="item.value" :value="item">
<component :is="() => priorityLabel(item)" />
</ComboboxItem>
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
</template>
<style scoped>
/* reka-ui's ComboboxRoot renders a real DOM wrapper div (via internal
ListboxRoot Primitive) around its slot content, unlike Base UI's
headless ComboboxPrimitive.Root, which renders nothing. When the only
child is a ComboboxTrigger-as-button (no sibling ComboboxInput), this
unstyled div participates in an inline formatting context and can add
a stray 1-3px of height depending on the theme's font metrics --
invisible but breaks pixel parity with the React reference, which has
no such wrapper. display:contents removes the anonymous box without
touching layout/functionality (the div stays in the DOM, still used
internally by reka-ui as the popper anchor ref). */
:deep(div[dir]) {
display: contents;
}
</style>