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>