filters
Filters with various field types
Filters with various field types
Загрузка превью…
src/filters/c-filters-1.vue
<!--
Порт c-filters-1 ("Filters with various field types") —
.agent-tmp/reui-upstream/registry-reui/bases/radix/components/filters/c-filters-1.tsx
Отличия от апстрима (см. docs/PORTING.md §5, прецедент filters/rating/tree):
- `IconPlaceholder` заменён инлайновым `<svg>` (пути lucide-react v0.545.0),
как и в самом примитиве `packages/ui/src/reui/filters/`.
- `@/registry/bases/radix/ui/avatar` -> `Avatar`/`AvatarImage`/`AvatarFallback`
из `@revueui/ui` (тот же примитив, Radix Avatar / reka-ui AvatarRoot).
- Состояние демо зафиксировано (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 FilterFieldsConfig } from "@/components/reui/filters"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
// ---------------------------------------------------------------------------
// Инлайновые SVG вместо IconPlaceholder (lucide-react v0.545.0 paths).
// `svgIcon` — фабрика фабрик: `svgIcon(paths)` даёт функцию, которая по
// опциональному классу возвращает `() => VNodeChild` — то, что ожидает
// `FilterFieldConfig.icon`/`FilterOption.icon` (см. context.ts).
// ---------------------------------------------------------------------------
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 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 TypeIcon = svgIcon(() => [
h("path", { d: "M12 4v16" }),
h("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }),
h("path", { d: "M9 20h6" }),
])
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 PhoneIcon = svgIcon(() => [
h("path", {
d: "M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384",
}),
])
const BellIcon = svgIcon(() => [
h("path", { d: "M10.268 21a2 2 0 0 0 3.464 0" }),
h("path", {
d: "M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326",
}),
])
const ClockIcon = svgIcon(() => [
h("path", { d: "M12 6v6l4 2" }),
h("circle", { cx: "12", cy: "12", r: "10" }),
])
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 CircleCheckIcon = svgIcon(() => [
h("circle", { cx: "12", cy: "12", r: "10" }),
h("path", { d: "m9 12 2 2 4-4" }),
])
const BanIcon = svgIcon(() => [
h("path", { d: "M4.929 4.929 19.07 19.071" }),
h("circle", { cx: "12", cy: "12", r: "10" }),
])
const UserRoundCheckIcon = svgIcon(() => [
h("path", { d: "M2 21a8 8 0 0 1 13.292-6" }),
h("circle", { cx: "10", cy: "8", r: "5" }),
h("path", { d: "m16 19 2 2 4-4" }),
])
const UsersIcon = svgIcon(() => [
h("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
h("path", { d: "M16 3.128a4 4 0 0 1 0 7.744" }),
h("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
h("circle", { cx: "9", cy: "7", r: "4" }),
])
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 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 BuildingIcon = svgIcon(() => [
h("path", { d: "M12 10h.01" }),
h("path", { d: "M12 14h.01" }),
h("path", { d: "M12 6h.01" }),
h("path", { d: "M16 10h.01" }),
h("path", { d: "M16 14h.01" }),
h("path", { d: "M16 6h.01" }),
h("path", { d: "M8 10h.01" }),
h("path", { d: "M8 14h.01" }),
h("path", { d: "M8 6h.01" }),
h("path", { d: "M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" }),
h("rect", { x: "4", y: "2", width: "16", height: "20", rx: "2" }),
])
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: plain colored dot (upstream `PriorityIcon`, not an
// IconPlaceholder — no icon-library swap needed here).
const priorityDotColors: Record<string, string> = {
low: "bg-green-500",
medium: "bg-yellow-500",
high: "bg-violet-500",
urgent: "bg-orange-500",
critical: "bg-red-500",
}
function priorityDot(priority: keyof typeof priorityDotColors) {
return () => h("div", { class: cn("size-2.25 shrink-0 rounded-full", priorityDotColors[priority]) })
}
function avatarIcon(src: string, alt: string, fallback: string, cls = "size-5") {
return () =>
h(
Avatar,
{ class: cls },
{
default: () => [
h(AvatarImage, { src, alt }),
h(AvatarFallback, null, { default: () => fallback }),
],
}
)
}
function unassignedAvatarIcon() {
return () =>
h(
Avatar,
{ class: "size-5" },
{
default: () =>
h(AvatarFallback, null, { default: () => UserRoundXIcon()() }),
}
)
}
function flagIcon(code: string) {
return () =>
h("img", {
src: `https://flagcdn.com/${code.toLowerCase()}.svg`,
alt: code,
class: "size-4 rounded-full object-cover",
})
}
const countryFlags = [
{ code: "AF", name: "Afghanistan" },
{ code: "AL", name: "Albania" },
{ code: "DZ", name: "Algeria" },
{ code: "AS", name: "American Samoa" },
{ code: "AD", name: "Andorra" },
{ code: "AO", name: "Angola" },
{ code: "AI", name: "Anguilla" },
{ code: "AG", name: "Antigua and Barbuda" },
{ code: "AR", name: "Argentina" },
{ code: "AM", name: "Armenia" },
{ code: "AU", name: "Australia" },
{ code: "AT", name: "Austria" },
{ code: "AZ", name: "Azerbaijan" },
{ code: "BS", name: "Bahamas" },
{ code: "BH", name: "Bahrain" },
{ code: "BD", name: "Bangladesh" },
{ code: "BB", name: "Barbados" },
{ code: "BY", name: "Belarus" },
{ code: "BE", name: "Belgium" },
{ code: "BZ", name: "Belize" },
{ code: "BJ", name: "Benin" },
{ code: "BM", name: "Bermuda" },
{ code: "BT", name: "Bhutan" },
{ code: "BO", name: "Bolivia" },
{ code: "BA", name: "Bosnia and Herzegovina" },
{ code: "BW", name: "Botswana" },
{ code: "BR", name: "Brazil" },
{ code: "IO", name: "British Indian Ocean Territory" },
{ code: "BN", name: "Brunei Darussalam" },
{ code: "BG", name: "Bulgaria" },
{ code: "BF", name: "Burkina Faso" },
{ code: "BI", name: "Burundi" },
{ code: "KH", name: "Cambodia" },
{ code: "CM", name: "Cameroon" },
{ code: "CA", name: "Canada" },
{ code: "CV", name: "Cape Verde" },
{ code: "KY", name: "Cayman Islands" },
{ code: "CF", name: "Central African Republic" },
{ code: "TD", name: "Chad" },
{ code: "CL", name: "Chile" },
{ code: "CN", name: "China" },
{ code: "CO", name: "Colombia" },
{ code: "KM", name: "Comoros" },
{ code: "CG", name: "Congo" },
{ code: "CR", name: "Costa Rica" },
{ code: "CI", name: "Cote D'Ivoire" },
{ code: "HR", name: "Croatia" },
{ code: "CU", name: "Cuba" },
{ code: "CY", name: "Cyprus" },
{ code: "CZ", name: "Czech Republic" },
{ code: "DK", name: "Denmark" },
{ code: "DJ", name: "Djibouti" },
{ code: "DM", name: "Dominica" },
{ code: "DO", name: "Dominican Republic" },
{ code: "EC", name: "Ecuador" },
{ code: "EG", name: "Egypt" },
{ code: "SV", name: "El Salvador" },
{ code: "GQ", name: "Equatorial Guinea" },
{ code: "ER", name: "Eritrea" },
{ code: "EE", name: "Estonia" },
{ code: "SZ", name: "Eswatini" },
{ code: "ET", name: "Ethiopia" },
{ code: "FI", name: "Finland" },
{ code: "FR", name: "France" },
{ code: "GA", name: "Gabon" },
{ code: "GM", name: "Gambia" },
{ code: "GE", name: "Georgia" },
{ code: "DE", name: "Germany" },
{ code: "GH", name: "Ghana" },
{ code: "GR", name: "Greece" },
{ code: "GD", name: "Grenada" },
{ code: "GT", name: "Guatemala" },
{ code: "GN", name: "Guinea" },
{ code: "GW", name: "Guinea-Bissau" },
{ code: "GY", name: "Guyana" },
{ code: "HT", name: "Haiti" },
{ code: "HN", name: "Honduras" },
{ code: "HK", name: "Hong Kong" },
{ code: "HU", name: "Hungary" },
{ code: "IS", name: "Iceland" },
{ code: "IN", name: "India" },
{ code: "ID", name: "Indonesia" },
{ code: "IR", name: "Iran" },
{ code: "IQ", name: "Iraq" },
{ code: "IE", name: "Ireland" },
{ code: "IL", name: "Israel" },
{ code: "IT", name: "Italy" },
{ code: "JM", name: "Jamaica" },
{ code: "JP", name: "Japan" },
{ code: "JO", name: "Jordan" },
{ code: "KZ", name: "Kazakhstan" },
{ code: "KE", name: "Kenya" },
{ code: "KR", name: "South Korea" },
{ code: "KW", name: "Kuwait" },
{ code: "KG", name: "Kyrgyzstan" },
{ code: "LA", name: "Laos" },
{ code: "LV", name: "Latvia" },
{ code: "LB", name: "Lebanon" },
{ code: "LS", name: "Lesotho" },
{ code: "LR", name: "Liberia" },
{ code: "LY", name: "Libya" },
{ code: "LT", name: "Lithuania" },
{ code: "LU", name: "Luxembourg" },
{ code: "MO", name: "Macao" },
{ code: "MG", name: "Madagascar" },
{ code: "MW", name: "Malawi" },
{ code: "MY", name: "Malaysia" },
{ code: "MV", name: "Maldives" },
{ code: "ML", name: "Mali" },
{ code: "MT", name: "Malta" },
{ code: "MH", name: "Marshall Islands" },
{ code: "MR", name: "Mauritania" },
{ code: "MU", name: "Mauritius" },
{ code: "MX", name: "Mexico" },
{ code: "FM", name: "Micronesia" },
{ code: "MD", name: "Moldova" },
{ code: "MC", name: "Monaco" },
{ code: "MN", name: "Mongolia" },
{ code: "ME", name: "Montenegro" },
{ code: "MA", name: "Morocco" },
{ code: "MZ", name: "Mozambique" },
{ code: "MM", name: "Myanmar" },
{ code: "NA", name: "Namibia" },
{ code: "NP", name: "Nepal" },
{ code: "NL", name: "Netherlands" },
{ code: "NZ", name: "New Zealand" },
{ code: "NI", name: "Nicaragua" },
{ code: "NG", name: "Nigeria" },
{ code: "NO", name: "Norway" },
{ code: "OM", name: "Oman" },
{ code: "PK", name: "Pakistan" },
{ code: "PA", name: "Panama" },
{ code: "PG", name: "Papua New Guinea" },
{ code: "PY", name: "Paraguay" },
{ code: "PE", name: "Peru" },
{ code: "PH", name: "Philippines" },
{ code: "PL", name: "Poland" },
{ code: "PT", name: "Portugal" },
{ code: "QA", name: "Qatar" },
{ code: "RO", name: "Romania" },
{ code: "RU", name: "Russia" },
{ code: "RW", name: "Rwanda" },
{ code: "WS", name: "Samoa" },
{ code: "SM", name: "San Marino" },
{ code: "SA", name: "Saudi Arabia" },
{ code: "SN", name: "Senegal" },
{ code: "RS", name: "Serbia" },
{ code: "SG", name: "Singapore" },
{ code: "SK", name: "Slovakia" },
{ code: "SI", name: "Slovenia" },
{ code: "ZA", name: "South Africa" },
{ code: "ES", name: "Spain" },
{ code: "LK", name: "Sri Lanka" },
{ code: "SE", name: "Sweden" },
{ code: "CH", name: "Switzerland" },
{ code: "SY", name: "Syria" },
{ code: "TW", name: "Taiwan" },
{ code: "TJ", name: "Tajikistan" },
{ code: "TZ", name: "Tanzania" },
{ code: "TH", name: "Thailand" },
{ code: "TR", name: "Turkey" },
{ code: "UG", name: "Uganda" },
{ code: "UA", name: "Ukraine" },
{ code: "AE", name: "United Arab Emirates" },
{ code: "GB", name: "United Kingdom" },
{ code: "US", name: "United States" },
{ code: "UY", name: "Uruguay" },
{ code: "UZ", name: "Uzbekistan" },
{ code: "VN", name: "Vietnam" },
{ code: "ZM", name: "Zambia" },
{ code: "ZW", name: "Zimbabwe" },
]
// Example: All Possible Filter Field Types with Grouping
const fields: FilterFieldsConfig<string> = [
{
group: "Basic",
fields: [
{
key: "text",
label: "Text",
type: "text",
icon: MailIcon(),
placeholder: "Search text...",
},
{
key: "email",
label: "Email",
type: "text",
icon: TypeIcon(),
placeholder: "user@example.com",
},
{
key: "website",
label: "Website",
icon: GlobeIcon(),
type: "text",
placeholder: "https://example.com",
},
{
key: "phone",
label: "Phone",
icon: PhoneIcon(),
type: "text",
placeholder: "+1 (123) 456-7890",
},
],
},
{
group: "Select",
fields: [
{
key: "status",
label: "Status",
icon: BellIcon(),
type: "select",
searchable: false,
class: "w-[200px]",
options: [
{ value: "todo", label: "To Do", icon: ClockIcon("stroke-violet-500") },
{ value: "in-progress", label: "In Progress", icon: CircleAlertIcon("stroke-yellow-500") },
{ value: "done", label: "Done", icon: CircleCheckIcon("stroke-green-500") },
{ value: "cancelled", label: "Cancelled", icon: BanIcon("stroke-destructive") },
],
},
{
key: "priority",
label: "Priority",
icon: BanIcon(),
type: "multiselect",
class: "w-[180px]",
options: [
{ value: "low", label: "Low", icon: priorityDot("low") },
{ value: "medium", label: "Medium", icon: priorityDot("medium") },
{ value: "high", label: "High", icon: priorityDot("high") },
{ value: "urgent", label: "Urgent", icon: priorityDot("urgent") },
{ value: "critical", label: "Critical", icon: priorityDot("critical") },
],
},
{
key: "assignee",
label: "Assignee",
icon: UserRoundCheckIcon(),
type: "multiselect",
maxSelections: 5,
options: [
{ value: "john", label: "John Doe", icon: avatarIcon("https://randomuser.me/api/portraits/men/1.jpg", "John Doe", "JD", "size-5 border") },
{ 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: "sarah", label: "Sarah Wilson", icon: avatarIcon("https://randomuser.me/api/portraits/women/5.jpg", "Sarah Wilson", "SW") },
{ value: "michael", label: "Michael Scott", icon: avatarIcon("https://randomuser.me/api/portraits/men/6.jpg", "Michael Scott", "MS") },
{ value: "emily", label: "Emily Blunt", icon: avatarIcon("https://randomuser.me/api/portraits/women/7.jpg", "Emily Blunt", "EB") },
{ value: "david", label: "David Gandy", icon: avatarIcon("https://randomuser.me/api/portraits/men/8.jpg", "David Gandy", "DG") },
{ value: "laura", label: "Laura Palmer", icon: avatarIcon("https://randomuser.me/api/portraits/women/9.jpg", "Laura Palmer", "LP") },
{ value: "kevin", label: "Kevin Hart", icon: avatarIcon("https://randomuser.me/api/portraits/men/10.jpg", "Kevin Hart", "KH") },
{ value: "anna", label: "Anna Kendrick", icon: avatarIcon("https://randomuser.me/api/portraits/women/11.jpg", "Anna Kendrick", "AK") },
{ value: "tom", label: "Tom Cruise", icon: avatarIcon("https://randomuser.me/api/portraits/men/12.jpg", "Tom Cruise", "TC") },
{ value: "lisa", label: "Lisa Kudrow", icon: avatarIcon("https://randomuser.me/api/portraits/women/13.jpg", "Lisa Kudrow", "LK") },
{ value: "james", label: "James Bond", icon: avatarIcon("https://randomuser.me/api/portraits/men/14.jpg", "James Bond", "JB") },
{ value: "unassigned", label: "Unassigned", icon: unassignedAvatarIcon() },
],
},
{
key: "userType",
label: "User Type",
icon: UsersIcon(),
type: "select",
searchable: false,
class: "w-[200px]",
options: [
{ value: "premium", label: "Premium", icon: StarIcon("size-3 text-yellow-500") },
{ value: "standard", label: "Standard", icon: BuildingIcon("size-3 text-blue-500") },
{ value: "trial", label: "Trial", icon: ClockIcon("size-3 text-gray-500") },
],
},
{
key: "country",
label: "Country",
icon: GlobeIcon(),
type: "select",
searchable: true,
class: "w-[220px]",
options: countryFlags.map((country) => ({
value: country.code,
label: country.name,
icon: flagIcon(country.code),
})),
},
],
},
]
// Стенд: состояние зафиксировано (docs/PORTING.md §5, задача) — фильтры не
// раскрываются, `filters`/`onChange` не интерактивны.
const filters: Filter<string>[] = [
{ id: "f1", field: "priority", operator: "is_any_of", values: ["low", "medium", "critical"] },
]
function noop() {}
</script>
<template>
<div class="flex grow content-start items-start gap-2.5 self-start">
<div class="grow space-y-5">
<!-- Filters Section -->
<div class="flex items-start gap-2.5">
<div class="flex-1">
<Filters
:filters="filters"
:fields="fields"
@change="noop"
shortcut-key="f"
shortcut-label="F"
:enable-shortcut="true"
>
<template #trigger>
<Button variant="outline">
<component :is="ListFilterIcon()" />
Add Filter
</Button>
</template>
</Filters>
</div>
<Button v-if="filters.length > 0" variant="outline" @click="noop">
<component :is="FunnelXIcon()" />
Clear
</Button>
</div>
<!-- Debug Block -->
<pre class="bg-muted dark:bg-muted/60 mt-2 max-h-[400px] w-full max-w-[500px] overflow-auto overflow-x-auto rounded-md border p-3 text-xs">{{ JSON.stringify(filters, null, 2) }}</pre>
</div>
</div>
</template>