Inputs
Saved Views
Сохранённые наборы фильтров: каждый вид показывает не только имя, но и расшифровку условий, а общие виды помечены — их правка меняет экран всей команде.
- filters
- presets
- views
- saved
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/filters-007
Сохранённые виды
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «filters-007» (Saved Views) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/filters-007.json
Registry item: https://vibeui.ru/r/filters-007.json
Ставится в: components/vibeui/filters-007.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Сохранённые наборы фильтров: каждый вид показывает не только имя, но и расшифровку условий, а общие виды помечены — их правка меняет экран всей команде.
Список сохранённых наборов фильтров: имя, расшифровка условий, метка общего вида и кнопка сохранения текущего отбора. Активный вид объявлен через aria-pressed. Ноль зависимостей, один файл, своя палитра.
## 3. Как использовать
import { Filters007 } from "@/components/vibeui/filters-007"
<Filters007
views={[{ id: "week", name: "Мои за неделю", summary: "Автор: я · за 7 дней" }]}
onChange={(id) => applyView(id)}
/>
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-filters-007-* и не подменять её токенами вашей темы (bg-accent, border-input и подобными)
- расшифровку условий под именем: через неделю по одному названию не вспомнить, что внутри вида
- aria-pressed у кнопки вида — активный набор обязан объявляться, а не только подсвечиваться
- кнопку во всю строку: мишень в одно слово неудобна и мышью, и пальцем
- метку общего вида: его правка меняет экран всей команде, и это надо сказать до нажатия
- точку-индикатор рядом с именем как второй, нецветовой признак выбранного вида
- блок <style> внутри компонента — в нём вся палитра, строки видов и кнопка сохранения
## 6. Можно менять
- заголовок title над списком
- массив views: имена, расшифровки и метки общих наборов
- подпись кнопки сохранения saveLabel
- обработчик onChange: он получает идентификатор выбранного вида
- акцент через проп accent — цвет активного вида
## 7. Правила
- Сохранение в компоненте только добавляет строку: настоящее сохранение на сервере делает вызывающий код.
- Переименование и удаление видов не входят в компонент — для них нужно отдельное меню строки.
- Общий вид стоит защищать подтверждением при правке: пользователь редко понимает, что меняет чужой экран.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/filters-007.jsonhttps://vibeui.ru/r/filters-007.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-filters-007-*. Клиентский: "use client" ради выбора и сохранения. Вид — кнопка во всю строку с aria-pressed: мишень крупная, а состояние объявлено ролью, а не только цветом. Под именем стоит расшифровка условий, потому что по одному имени через неделю не вспомнить содержимое. Общие виды помечены отдельной меткой.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
"use client"
import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Filters007View = {
id: string
name: string
summary: string
shared?: boolean
}
export type Filters007Props = Omit<
ComponentPropsWithoutRef<"div">,
"children" | "onChange"
> & {
title?: string
views?: Filters007View[]
saveLabel?: string
onChange?: (id: string) => void
accent?: string
}
// Идея компонента: набор фильтров как именованный вид. Один и тот же отбор
// собирают заново каждое утро, и это самая частая ручная работа в любой
// таблице. Здесь каждый вид показывает не только имя, но и расшифровку
// условий — по одному имени через неделю не вспомнить, что внутри. Общие
// виды помечены отдельно: их правка меняет экран всей команде.
const STYLES = `
:where([data-vibeui-block="filters-007"]){
--vibeui-filters-007-surface:oklch(1 0 0);
--vibeui-filters-007-fill:oklch(0.975 0.004 265);
--vibeui-filters-007-fg:oklch(0.23 0.014 265);
--vibeui-filters-007-muted:oklch(0.55 0.014 265);
--vibeui-filters-007-border:oklch(0.89 0.008 265);
--vibeui-filters-007-shell:oklch(0.91 0.006 265);
--vibeui-filters-007-accent:oklch(0.5 0.16 210);
--vibeui-filters-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: список показывают поверх любого фона. */
[data-vibeui-block="filters-007"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:20rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-filters-007-surface);
border:1px solid var(--vibeui-filters-007-shell);border-radius:0.875rem;
font-family:var(--vibeui-filters-007-font);color:var(--vibeui-filters-007-fg);
}
[data-vibeui-block="filters-007"] *{box-sizing:border-box}
[data-vibeui-block="filters-007"] h3{margin:0;font-size:0.8125rem;font-weight:650}
[data-vibeui-block="filters-007"] ul{
display:flex;flex-direction:column;gap:0.25rem;margin:0;padding:0;list-style:none;
}
/* Вид — одна кнопка целиком: мишень в строку, а не крошечное имя. */
[data-vibeui-block="filters-007"] [data-part="view"]{
appearance:none;cursor:pointer;width:100%;text-align:start;
display:flex;flex-direction:column;gap:0.125rem;
padding:0.5rem 0.625rem;border-radius:0.625rem;
border:1px solid var(--vibeui-filters-007-border);
background:var(--vibeui-filters-007-surface);color:inherit;font:inherit;
transition:border-color .16s ease,background-color .16s ease;
}
[data-vibeui-block="filters-007"] [data-part="view"]:hover{background:var(--vibeui-filters-007-fill)}
[data-vibeui-block="filters-007"] [data-part="view"]:focus-visible{outline:2px solid var(--vibeui-filters-007-accent);outline-offset:2px}
[data-vibeui-block="filters-007"] [data-part="view"][aria-pressed="true"]{
border-color:var(--vibeui-filters-007-accent);
background:color-mix(in oklab,var(--vibeui-filters-007-accent) 8%,oklch(1 0 0));
}
[data-vibeui-block="filters-007"] [data-part="line"]{
display:flex;align-items:center;gap:0.375rem;
font-size:0.8125rem;font-weight:650;
}
[data-vibeui-block="filters-007"] [data-part="dot"]{
flex:none;width:0.4375rem;height:0.4375rem;border-radius:9999px;
background:var(--vibeui-filters-007-border);
}
[data-vibeui-block="filters-007"] [data-part="view"][aria-pressed="true"] [data-part="dot"]{background:var(--vibeui-filters-007-accent)}
/* Расшифровка условий: по одному имени через неделю не вспомнить, что внутри. */
[data-vibeui-block="filters-007"] [data-part="summary"]{
font-size:0.6875rem;line-height:1.4;color:var(--vibeui-filters-007-muted);
}
[data-vibeui-block="filters-007"] [data-part="shared"]{
margin-inline-start:auto;flex:none;
padding:0.0625rem 0.375rem;border-radius:9999px;
background:var(--vibeui-filters-007-fill);
font-size:0.5625rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;
color:var(--vibeui-filters-007-muted);
}
[data-vibeui-block="filters-007"] [data-part="save"]{
appearance:none;cursor:pointer;align-self:flex-start;
padding:0.375rem 0.75rem;border-radius:0.5rem;
border:1px dashed var(--vibeui-filters-007-border);
background:none;color:var(--vibeui-filters-007-accent);
font:inherit;font-size:0.75rem;font-weight:650;
}
[data-vibeui-block="filters-007"] [data-part="save"]:hover{border-style:solid;border-color:var(--vibeui-filters-007-accent)}
[data-vibeui-block="filters-007"] [data-part="save"]:focus-visible{outline:2px solid var(--vibeui-filters-007-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="filters-007"] *{animation:none!important;transition:none!important}}
`
const DEFAULT_VIEWS: Filters007View[] = [
{
id: "week",
name: "Мои за неделю",
summary: "Автор: я · Обновлён: за 7 дней",
},
{
id: "review",
name: "Ждут проверки",
summary: "Статус: на ревью · Приоритет: высокий",
shared: true,
},
{
id: "free",
name: "Бесплатные шаблоны",
summary: "Цена: 0 ₽ · Лицензия: MIT",
shared: true,
},
]
/**
* Сохранённые наборы фильтров: имя, расшифровка условий и метка общего вида.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Filters007({
title = "Сохранённые виды",
views = DEFAULT_VIEWS,
saveLabel = "Сохранить текущий отбор",
onChange,
accent,
className,
style,
...props
}: Filters007Props) {
const [list, setList] = useState(views)
const [active, setActive] = useState(views[0]?.id ?? "")
const palette = {
...(accent ? { "--vibeui-filters-007-accent": accent } : null),
...style,
} as CSSProperties
const pick = (id: string) => {
setActive(id)
onChange?.(id)
}
const save = () => {
const id = `view-${list.length + 1}`
setList([
...list,
{
id,
name: `Новый вид ${list.length + 1}`,
summary: "Текущие условия отбора",
},
])
pick(id)
}
return (
<>
<style href="vibeui-filters-007" precedence="medium">
{STYLES}
</style>
<div
{...props}
data-vibeui-block="filters-007"
className={className}
style={palette}
>
<h3>{title}</h3>
<ul>
{list.map((view) => (
<li key={view.id}>
<button
type="button"
data-part="view"
aria-pressed={active === view.id}
onClick={() => pick(view.id)}
>
<span data-part="line">
<span data-part="dot" aria-hidden="true" />
{view.name}
{view.shared ? <span data-part="shared">общий</span> : null}
</span>
<span data-part="summary">{view.summary}</span>
</button>
</li>
))}
</ul>
<button type="button" data-part="save" onClick={save}>
{saveLabel}
</button>
</div>
</>
)
}