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>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-combobox-22.json

Зависимости реестра

Источник: порт из ReUI (Keenthemes, MIT)