filters

Filters with custom trigger button

Filters with custom trigger button

Загрузка превью…

src/filters/c-filters-3.vue

<!--
  Порт c-filters-3 ("Filters with custom trigger button") —
  .agent-tmp/reui-upstream/registry-reui/bases/radix/components/filters/c-filters-3.tsx

  Отличия от апстрима (см. docs/PORTING.md §5):
  - `IconPlaceholder` заменён инлайновым `<svg>` (lucide-react v0.545.0 paths).
  - `@/registry/bases/radix/ui/avatar` -> `Avatar`/`AvatarImage`/`AvatarFallback`
    из `@revueui/ui` (тот же примитив).
  - Состояние демо зафиксировано (docs/PORTING.md, задача): `useState` убран,
    `filters` — константа, `onChange` — no-op. `createFilter()` не вызывается
    (использует `Date.now()`/`Math.random()` внутри) — id захардкожен.
-->
<script setup lang="ts">
import { h, type VNodeChild } from "vue"
import { Filters, type Filter, type FilterFieldConfig } from "@/components/reui/filters"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"

function svgIcon(paths: () => VNodeChild[]) {
  return (cls?: string) => () =>
    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: cls,
      },
      paths()
    )
}

const TagIcon = svgIcon(() => [
  h("path", {
    d: "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z",
  }),
  h("circle", { cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor" }),
])
const MailIcon = svgIcon(() => [
  h("path", { d: "m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" }),
  h("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
])
const GlobeIcon = svgIcon(() => [
  h("circle", { cx: "12", cy: "12", r: "10" }),
  h("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
  h("path", { d: "M2 12h20" }),
])
const UserIcon = svgIcon(() => [
  h("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }),
  h("circle", { cx: "12", cy: "7", r: "4" }),
])
const CircleAlertIcon = svgIcon(() => [
  h("circle", { cx: "12", cy: "12", r: "10" }),
  h("line", { x1: "12", x2: "12", y1: "8", y2: "12" }),
  h("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" }),
])
const StarIcon = svgIcon(() => [
  h("path", {
    d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",
  }),
])
const UserRoundXIcon = svgIcon(() => [
  h("path", { d: "M2 21a8 8 0 0 1 11.873-7" }),
  h("circle", { cx: "10", cy: "8", r: "5" }),
  h("path", { d: "m17 17 5 5" }),
  h("path", { d: "m22 17-5 5" }),
])
const ListFilterIcon = svgIcon(() => [
  h("path", { d: "M2 5h20" }),
  h("path", { d: "M6 12h12" }),
  h("path", { d: "M9 19h6" }),
])
const FunnelXIcon = svgIcon(() => [
  h("path", {
    d: "M12.531 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l.427-.473",
  }),
  h("path", { d: "m16.5 3.5 5 5" }),
  h("path", { d: "m21.5 3.5-5 5" }),
])

// Priority option icon: Star colored by priority (upstream `PriorityIcon`).
const priorityStarColors: Record<string, string> = {
  low: "text-green-500",
  medium: "text-yellow-500",
  high: "text-orange-500",
  urgent: "text-red-500",
}
function priorityIcon(priority: keyof typeof priorityStarColors) {
  return StarIcon(priorityStarColors[priority])
}

function avatarIcon(src: string, alt: string, fallback: string) {
  return () =>
    h(
      Avatar,
      { class: "size-5" },
      {
        default: () => [
          h(AvatarImage, { src, alt }),
          h(AvatarFallback, null, { default: () => fallback }),
        ],
      }
    )
}
function unassignedAvatarIcon() {
  return () =>
    h(
      Avatar,
      { class: "size-5" },
      {
        default: () =>
          h(AvatarFallback, null, { default: () => UserRoundXIcon()() }),
      }
    )
}

// Basic filter fields for outline variant demo
const fields: FilterFieldConfig<string>[] = [
  {
    key: "text",
    label: "Text",
    icon: TagIcon("size-3.5"),
    type: "text",
    class: "w-36",
    placeholder: "Search text...",
  },
  {
    key: "email",
    label: "Email",
    icon: MailIcon("size-3.5"),
    type: "text",
    class: "w-40",
    placeholder: "user@example.com",
  },
  {
    key: "website",
    label: "Website",
    icon: GlobeIcon("size-3.5"),
    type: "text",
    class: "w-40",
    placeholder: "https://example.com",
  },
  {
    key: "assignee",
    label: "Assignee",
    icon: UserIcon("size-3.5"),
    type: "multiselect",
    class: "w-[200px]",
    options: [
      { value: "john", label: "John Doe", icon: avatarIcon("https://randomuser.me/api/portraits/men/1.jpg", "John Doe", "JD") },
      { value: "jane", label: "Jane Smith", icon: avatarIcon("https://randomuser.me/api/portraits/women/2.jpg", "Jane Smith", "JS") },
      { value: "bob", label: "Bob Johnson", icon: avatarIcon("https://randomuser.me/api/portraits/men/3.jpg", "Bob Johnson", "BJ") },
      { value: "alice", label: "Alice Brown", icon: avatarIcon("https://randomuser.me/api/portraits/women/4.jpg", "Alice Brown", "AB") },
      { value: "nick", label: "Nick Bold", icon: avatarIcon("https://randomuser.me/api/portraits/men/4.jpg", "Nick Bold", "NB") },
      { value: "unassigned", label: "Unassigned", icon: unassignedAvatarIcon() },
    ],
  },
  {
    key: "priority",
    label: "Priority",
    icon: CircleAlertIcon("size-3.5"),
    type: "multiselect",
    class: "w-[180px]",
    options: [
      { value: "low", label: "Low", icon: priorityIcon("low") },
      { value: "medium", label: "Medium", icon: priorityIcon("medium") },
      { value: "high", label: "High", icon: priorityIcon("high") },
      { value: "urgent", label: "Urgent", icon: priorityIcon("urgent") },
    ],
  },
]

// Стенд: состояние зафиксировано (docs/PORTING.md §5, задача).
const filters: Filter<string>[] = [
  { id: "f1", field: "assignee", operator: "is_any_of", values: ["john", "nick", "alice"] },
]

function noop() {}
</script>

<template>
  <div class="flex grow content-start items-start gap-2.5 self-start">
    <div class="flex-1">
      <Filters :filters="filters" :fields="fields" @change="noop">
        <template #trigger>
          <Button variant="outline" size="icon">
            <component :is="ListFilterIcon()" />
          </Button>
        </template>
      </Filters>
    </div>

    <Button v-if="filters.length > 0" variant="outline" @click="noop">
      <component :is="FunnelXIcon()" />
      Clear
    </Button>
  </div>
</template>

Установка

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

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

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