diff --git a/design/briefs/site.md b/design/briefs/site.md new file mode 100644 index 00000000..7c9fafde --- /dev/null +++ b/design/briefs/site.md @@ -0,0 +1,167 @@ +# rowkit.dev 2.0 — бриф на сайт + +8 октября 2026 · бриф для дизайнера. Файл: [rowkit × Windows 98](https://www.figma.com/design/hmDfFpjrDP6WtEary6U6SE/rowkit-%C3%97-Windows-98), новая страница «Site v2». Бренд — страница «Brand» (гайд 173:35331). + +## Зачем + +Сейчас rowkit.dev — эмуляция операционной системы: рабочий стол, Dock, «Пуск», каждая страница в окне Explorer или Finder. Это мило один раз и мешает каждый следующий: + +- главная не говорит, что это и почему это стоит взять — она играет в ОС; +- меню слева набрано мелко (13px), текст зажат рамками окна, на телефоне окна тесные; +- переключатель тем — мелкий текст в панели задач, его не находят. + +Нужен **сайт продукта**: красивый лендинг и удобная документация. Две темы остаются главным аргументом, но как витрина, а не как костюм: один и тот же сайт выглядит как Windows 98 или как modern, и это видно с первого экрана. + +## Аудитория и что она должна понять + +Фронтенд-разработчик или тимлид, который выбирает набор компонентов для продуктового интерфейса на Vue 3 (админка, внутренний инструмент, SaaS). За 10 секунд на главной он должен понять: + +1. **Что это:** профессиональный набор компонентов для продуктовых интерфейсов на Vue 3 и TypeScript. +2. **Чем отличается:** один набор — любой вид. Тема — это только токены; Windows 98 и modern в комплекте, свою можно сделать одним вызовом. +3. **Что он серьёзный:** типы, доступность с тестами, своё поведение без сторонних библиотек, SSR, лимит размера, инструкции для ИИ-агентов в пакете. +4. **Что он растёт:** 21 компонент сегодня, план на остальное — меню, даты, палитра команд и т. д. +5. **Как начать:** `npm i rowkit@beta`, две строки CSS. + +Тон: точный, спокойный, уверенный, с лёгкой игрой — игра в переключении тем, а не в декорациях. + +## Решения, которые уже приняты + +- **Весь сайт** — лендинг и документация — в обычном каркасе: шапка, меню слева, текст по центру, оглавление справа. Рабочий стол, Dock, «Пуск», панель задач, окна вокруг страниц уходят. +- **Лендинг — в двух стилях.** Windows 98 — «шкурка» того же лендинга, а не другая раскладка: серая грань, фаски, PT Sans, синие выделения, пиксельные иконки. +- **Документация — стандартная тема VitePress с брендом** (вариант B, решение 9 октября; вариант A — свой каркас в двух шкурках — остаётся в Figma как отклонённый). Нейтрально и привычно; брендинг — логотип, цвет vermilion на ссылках, активном пункте меню и маркерах, врезка TIP в цвете бренда, меню 15px вместо 14. Светлая и тёмная схема — штатный переключатель VitePress. +- **Переключатель «Windows 98 | Modern» в документации** стоит в шапке VitePress («Components: Windows 98 | Modern») и меняет только живые примеры компонентов, а не оболочку страницы. +- **Сайт открывается в modern**, Windows 98 — переключателем или `?theme=win98`. Выбор запоминается. +- **Modern-лендинг брендирован** — это вид по умолчанию: бумажный фон `#FAFAF8` (в тёмной — `night`), слова «product interfaces» в заголовке, метка над ним, главные кнопки, ссылки со стрелкой, подписи секций (THEMES, COMPONENTS, WHY ROWKIT…), номера шагов и плашки иконок — в vermilion; подвал — чернила `#111114` с белым логотипом. Живые компоненты внутри (демо, превью) — чистый modern, без цвета бренда. Win98-лендинг брендом не красится. +- **Бренд над темами:** знак, вордмарк, vermilion в шапке и подвале одинаковы в обеих темах (в Win98 — пиксельное прочтение знака 16/32). + +## Шрифты и размеры (обе темы) + +Читать должно быть удобно в первую очередь. Ориентир — modern; Win98 повторяет размеры в PT Sans. + +| Роль | Размер / строка | Примечание | +| ------------------------ | --------------- | ----------------------------------------- | +| Заголовок лендинга | 48–56 / 1.1 | на телефоне 34–36 | +| Подзаголовок лендинга | 20 / 30 | | +| H1 страницы документации | 32 / 40 | | +| H2 | 24 / 32 | | +| H3 | 18 / 26 | | +| Основной текст | 16 / 26 | колонка 680–720px, ~70 знаков | +| Меню слева | 15 / 22 | разделы — 13px заглавными или полужирным | +| «На этой странице» | 14 / 20 | | +| Код | 14 / 22 | моно; в Win98 — VT323 крупнее, как сейчас | + +Цели касания на телефоне ≥ 44px. Контраст текста ≥ 4.5:1 в обеих темах и обеих схемах. + +## Каркас документации + +> С 9 октября документация — стандартная тема VitePress с брендом (см. «Решения»). Описание ниже — вариант A, оставлено для истории; для варианта B действуют: шапка VitePress + переключатель компонентов, меню 15px, контент 688px, оглавление справа, «Previous page / Next page», TIP-врезка цветом бренда, код с подсветкой (github-light / github-dark), живые примеры в рамке. + +**Шапка (64px; телефон 56px).** Логотип · Docs · Components · Patterns · Storybook · GitHub · поиск (⌘K) · **переключатель тем** · версия. Переключатель крупный и подписанный: сегменты «Windows 98 | Modern» и кнопка схемы Auto/Light/Dark. В Win98 — как переключатель на панели инструментов (вдавленная кнопка у выбранной), в modern — сегментированный контрол. + +**Меню слева (260px).** Разделы: Guide, Components (Foundations / Forms / Data / Overlays), Patterns, Foundations · Tokens, Decisions, Roadmap, Contributing. Все разделы раскрыты; текущая страница подсвечена; меню прокручивается отдельно от страницы. На телефоне — выезжающая панель из кнопки в шапке. + +**Содержимое (до 720px).** Хлебные крошки → H1 → вводный абзац (крупнее, 18/28) → разделы. Примеры: живой пример над кодом, код со строкой «Copy». Таблицы props — как сейчас (генерируются), но в ширину колонки без горизонтального скролла. Внизу: «Previous / Next», «Edit on GitHub», дата. + +**Справа (220px, ≥1280px).** «On this page» с подсветкой текущего раздела. + +**Поиск.** Одна палитра поиска по ⌘K в обеих темах (сейчас Spotlight в modern, окно Find в Win98): поле, недавние, результаты по группам, «ничего не найдено». В Win98 — диалог с фаской, в modern — карточка со стеклом. + +**404.** Обычная страница в каркасе: «Page not found», адрес, «Go to Home», поиск. + +## Лендинг + +Ширина контента 1200px, секции с воздухом (96–128px между ними, на телефоне 64). Сверху вниз: + +1. **Первый экран.** Слева: заголовок «Vue 3 components for product interfaces», подзаголовок («A professional toolkit — typed, accessible, themeable. Windows 98 and modern in the box, your own in one call.»), команда установки с кнопкой копирования, кнопки «Get started» и «Browse components». Справа: **живое демо** — таблица пользователей с фильтром, выбором и пагинацией в текущей теме (тот же код, что сейчас в «Live demo»). Над демо или под ним — крупный переключатель «Windows 98 | Modern»: клик перекрашивает весь сайт, и это первое, что хочется нажать. +2. **«One set, any look».** Одна и та же форма «New user» дважды рядом — в Windows 98 и в modern (`data-theme` на контейнере, вживую). Ниже — пример `defineTheme` из `rowkit/theme` и третья, «своя» тема рядом (например, фиолетовая): видно, что тема — это значения, а не компоненты. +3. **Что внутри.** Сетка компонентов по разделам (Foundations, Forms, Data, Overlays): у каждого — маленькое живое превью в текущей теме, имя, одна строка «для чего». Отдельной строкой — «Coming next»: меню, Popover, Sheet, даты, палитра команд, Tabs, Switch, Combobox — серыми карточками с пометкой «Planned», ссылка на Roadmap. Это и есть «их будет много». +4. **Почему это профессиональные компоненты.** 6 карточек: + - **Typed** — дженерики в `DataTable` и `Select`, строгий TypeScript, без `any`; + - **Accessible** — WAI-ARIA, клавиатура, a11y-проверка в CI на каждой истории; + - **Own behaviour layer** — фокус, слои, позиционирование без сторонних библиотек; + - **Tokens all the way down** — `@rowkit/tokens`, 313 токенов с описаниями; + - **SSR и Nuxt** — работает без настройки; + - **Ready for coding agents** — `AGENTS.md` в пакете. + Плюс строка цифр: тесты (~1500), размер (39 kB вся библиотека, 2.6 kB одна кнопка), 21 компонент. +5. **Пример кода.** «A data table page in 20 lines»: код слева, результат справа (FilterBar + DataTable + Pagination). +6. **Начать.** Три шага: установить, подключить стили, выбрать тему — и кнопка «Read the guide». +7. **Подвал.** Логотип, слоган, ссылки (Docs, Components, Roadmap, GitHub, npm, Storybook), MIT, «Built with rowkit». + +## Два стиля сайта + +| Часть | Windows 98 | Modern (light / dark) | +| ------------------ | ------------------------------------------------------------------- | --------------------------------------------------------- | +| Фон страницы | серый `#C0C0C0` для рамок; белый под текстом | `background` темы; секции лендинга — едва заметные полосы | +| Шапка | панель инструментов с фаской снизу, кнопки-иконки | полупрозрачная, размытие, линия снизу | +| Меню слева | белая колодезная панель (sunken), синее выделение, пиксельные папки | без рамки, скруглённое серое выделение, контурные иконки | +| Карточки | GroupBox с подписью на рамке или raised-панель | скруглённые карточки с тенью | +| Кнопки, поля | компоненты rowkit в теме — без своих стилей сайта | так же | +| Заголовок лендинга | PT Sans Bold, чёрный; акцент — синий `#000080` | системный шрифт, semibold; акцент — синий темы | +| Ссылки | синие, подчёркнутые | синие, подчёркивание при наведении | + +Вся «шкурка» — токены темы. Свои цвета сайта — только бренд (знак, vermilion) и подсветка кода `site/code-*`. + +## Телефон (390) + +Шапка: логотип, поиск, переключатель (сжатый: «98 | M» или иконка с меню), кнопка меню. Лендинг в одну колонку: демо под заголовком, компактная таблица (Name / Status), сетка компонентов в 2 колонки. Документация: меню и оглавление — выезжающие панели. + +## Что нарисовать + +Страница «Site v2» в Figma (254:9584): + +- [x] Компоненты сайта: Header, HeaderMobile, ThemeSwitch (win98 / modern), Search, NavItem, SideNav, Toc, FeatureCard, ComponentCard, Footer, SearchPalette; иконки «Site icons» (контурные 24 и пиксельные 16/32). +- [x] Лендинг: 1440 и 390 × Windows 98, Modern Light, Modern Dark. +- [x] Вариант A документации (отклонён): страница компонента, статья, список, поиск, 404 — 1440 и 390 × три вида. +- [x] Вариант B документации: стандартная тема VitePress с брендом — страница компонента, светлая и тёмная. +- [x] Лист «Site v2 — decisions». +- [ ] Вариант B: телефон 390 и страница-статья — по необходимости, VitePress задаёт их сам. + +## Правки после ревью (9 октября) + +- [x] Карточки в одном ряду — одной высоты (Start in a minute, Built like a professional tool, сетки компонентов). +- [x] Рамка карточек: в modern — тонкая обводка 1px (`border-subtle`) без тени; в Win98 — фаска без обводки. Код в шагах — на своей подложке. +- [x] Свои иконки для «Built like a professional tool»: скобки, доступность, слои, образцы цвета, сервер, искра — контурные в modern, пиксельные 32px в Win98. +- [x] Подсветка синтаксиса во всех блоках кода лендинга (`site/code-*`: keyword, string, tag, attr, comment, number). +- [x] Иконки modern-темы тоньше: линия 2.25 → 1.75 (на сетке 24, то есть ≈1.2px в 16px), глифы 3 → 2.5, 32-px 1.5 → 1.25. **Это изменение библиотеки:** выгрузить SVG из Figma в `packages/ui/src/icons/modern/`, `pnpm icons:modern`, changeset (`rowkit` minor — «modern icons draw a lighter line»), проверить кадры Storybook в modern. +- [x] Брендирование modern-лендинга vermilion (см. «Решения»). +- [x] Документация → стандартная тема VitePress с брендом (см. «Решения»). + +## Правки «полезнее для людей» (9 октября) + +- [x] Под установкой — требования: «Vue 3.5+ · Tailwind CSS v4 · TypeScript · MIT». +- [x] Под живым демо — «Live — sort, select and filter this table.» и «View source →». +- [x] После сетки компонентов — кнопка «See all 21 components» (на телефоне была). +- [x] Новая секция «Before you install» (FAQ) перед «Start in a minute»: готово ли к продакшену, нужен ли Tailwind, какая тема по умолчанию (Windows 98 без атрибута — путает, раз сайт открывается в modern), Nuxt и SSR, размер, лицензия. +- [x] Документация: под вводным абзацем — строка импорта (`import { DataTable } from 'rowkit'`) и ссылки «Storybook ↗ · Source ↗ · Report an issue ↗». + +## Уборка в Figma (9 октября) + +- Удалены кадры варианта A документации (страница компонента, меню, статья, список, поиск, 404 — все виды) и компоненты, нужные только им: `SiteV2/NavItem`, `SideNav`, `Toc`. +- Страницы старого сайта переименованы в «Archive — Site v1 · Windows 98 desktop» и «Archive — Site v1 · Modern desktop» и перенесены в конец файла: код сайта до вёрстки 2.0 ещё на них. + +## План вёрстки + +1. [x] **Иконки** — выгрузка и changeset (отдельный PR, библиотека). +2. [x] **Документация** — вернуть стандартную тему VitePress: убрать `SiteLayout`/`ExplorerLayout`, рабочий стол и окна; бренд через `--vp-c-brand-*`, логотип, меню 15px; переключатель «Components: Windows 98 | Modern» в `nav-bar-content-after`, ставит `data-theme` на живые примеры (`DemoBox`), не на страницу; поиск — штатный local search; 404 — штатный с брендом. +3. [x] **Лендинг** — `layout: home` со своим компонентом: секции по макету, живое демо и форма «New user» из кода, `data-theme` на корне лендинга по переключателю, бренд-цвета сайта. Код — `docs/.vitepress/theme/components/landing/`, значения сайта — `landing.css`. Отступления от макета: + - vermilion как цвет текста — `#c8361c` (у `#d63a1f` на бумаге 4.48:1, чуть ниже AA); кнопки и заголовок — `#d63a1f`; + - «Your theme» задаёт пять значений, а не два: без hover, active и `--color-checked` третья панель была бы наполовину синей, а код под ней — неправдой; + - «A data table page in twenty lines»: `columns` — массив объектов, как его на самом деле принимает `DataTable` (в макете — массив строк); + - данные демо — из `home-users.ts` (адреса `@example.com`, колонка Seats вместо Logins). +4. [x] **Проверка** — скриншоты 390/1440 в обеих схемах и в Win98-лендинге, контраст, Lighthouse, нет горизонтального скролла. Lighthouse (главная, Introduction, Installation, Button, DataTable, Themes; телефон и десктоп): Accessibility и SEO — 100 везде, Best Practices — 96 (локально нет скрипта Vercel Analytics). По ходу: + - подсветка кода в документации — `github-light-high-contrast` / `github-dark-default`: у штатных github-light и github-dark цвета токенов 3.2–4.3:1; + - строчный код — `brand-2` (у `brand-1` на сером 4.3:1), подпись языка блока — `text-2`; + - «Your theme» и конструктор тем отдают ` diff --git a/docs/.vitepress/theme/components/home/DesktopHome.vue b/docs/.vitepress/theme/components/home/DesktopHome.vue deleted file mode 100644 index c7ec269f..00000000 --- a/docs/.vitepress/theme/components/home/DesktopHome.vue +++ /dev/null @@ -1,417 +0,0 @@ - - - diff --git a/docs/.vitepress/theme/components/home/DesktopIcon.vue b/docs/.vitepress/theme/components/home/DesktopIcon.vue deleted file mode 100644 index 5cf27b60..00000000 --- a/docs/.vitepress/theme/components/home/DesktopIcon.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/docs/.vitepress/theme/components/home/shortcuts.ts b/docs/.vitepress/theme/components/home/shortcuts.ts deleted file mode 100644 index edfb1b3f..00000000 --- a/docs/.vitepress/theme/components/home/shortcuts.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Component } from 'vue' -import { withBase } from 'vitepress' -import { - Book32Icon, - Code32Icon, - Computer32Icon, - Document32Icon, - Folder32Icon, - Trash32Icon, -} from 'rowkit' - -export interface Shortcut { - label: string - href: string - icon: Component -} - -/** The desktop's shortcuts, top to bottom: the home page's and the 404's. */ -export const shortcuts: Shortcut[] = [ - { label: 'Guide', href: withBase('/introduction'), icon: Book32Icon }, - { label: 'Components', href: withBase('/components/'), icon: Folder32Icon }, - { label: 'Patterns', href: withBase('/patterns/data-table-page'), icon: Folder32Icon }, - { label: 'Storybook', href: 'https://storybook.rowkit.dev', icon: Computer32Icon }, - { label: 'GitHub', href: 'https://github.com/NikolaiKushner/rowkit', icon: Code32Icon }, - { label: 'Decisions', href: withBase('/decisions/001-typescript-pin'), icon: Document32Icon }, - { - label: 'Old versions', - href: 'https://github.com/NikolaiKushner/rowkit/releases', - icon: Trash32Icon, - }, -] diff --git a/docs/.vitepress/theme/components/home/useHomeDemo.ts b/docs/.vitepress/theme/components/home/useHomeDemo.ts deleted file mode 100644 index 5a0751ba..00000000 --- a/docs/.vitepress/theme/components/home/useHomeDemo.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { computed, ref, watch } from 'vue' -import { useClientSort } from 'rowkit' -import type { DataTableColumn, DataTableSort, FilterChip } from 'rowkit' -import { homeUsers, type HomeUser } from '../home-users' - -/** - * The live demo on the home page: a Users list with search, a status filter, - * sorting, selection, paging, and actions that really act — Export downloads - * the selected rows, Delete removes them from this tab's copy of the list, - * and in the modern theme New user adds one. - */ -export function useHomeDemo() { - const people = ref([...homeUsers]) - - const search = ref('') - // Seeded so the window opens with a chip, as the Figma desktop shows it. - const status = ref('active') - const sort = ref | undefined>({ key: 'name', direction: 'asc' }) - // Alan Turing and Katherine Johnson: on page one under the seeded sort. - const selected = ref([3, 5]) - const page = ref(1) - const pageSize = 25 - - const filtered = computed(() => { - const query = search.value.trim().toLowerCase() - return people.value.filter((row) => { - if (status.value && row.status !== status.value) return false - if (!query) return true - return row.name.toLowerCase().includes(query) || row.email.toLowerCase().includes(query) - }) - }) - - // Pagination never moves the page itself; the application does, so a - // narrowed list does not leave the reader on an empty page. - watch([search, status], () => { - page.value = 1 - }) - - const chips = computed(() => - status.value ? [{ id: 'status', label: 'Status', value: label[status.value] }] : [] - ) - - const sorted = useClientSort(filtered, sort, columns) - - const pageRows = computed(() => { - const start = (page.value - 1) * pageSize - return sorted.value.slice(start, start + pageSize) - }) - - const range = computed(() => { - const total = filtered.value.length - if (total === 0) return '0 of 0' - const from = (page.value - 1) * pageSize + 1 - return `${String(from)}–${String(Math.min(total, from + pageSize - 1))} of ${String(total)}` - }) - - function removeFilter(): void { - status.value = undefined - } - - function clearFilters(): void { - search.value = '' - status.value = undefined - } - - function remove(ids: number[]): void { - people.value = people.value.filter((row) => !ids.includes(row.id)) - selected.value = selected.value.filter((id) => !ids.includes(id)) - } - - /** A row not yet in the list, for New user: saved through `add`. */ - function blank(): HomeUser { - const id = Math.max(0, ...people.value.map((row) => row.id)) + 1 - return { - id, - name: '', - email: '', - role: 'Member', - status: 'invited', - seats: 0, - lastActive: 'never', - } - } - - /** Adds a new row and selects it, so it is easy to find in the list. */ - function add(user: HomeUser): void { - people.value = [...people.value, user] - selected.value = [user.id] - } - - /** Writes an edited row back, by id. */ - function update(user: HomeUser): void { - people.value = people.value.map((row) => (row.id === user.id ? user : row)) - } - - /** The selected rows as a CSV file, downloaded. */ - function exportSelected(): void { - const rows = people.value.filter((row) => selected.value.includes(row.id)) - const lines = [ - 'Name,Email,Role,Status,Seats', - ...rows.map((row) => [row.name, row.email, row.role, row.status, row.seats].join(',')), - ] - const url = URL.createObjectURL(new Blob([lines.join('\n')], { type: 'text/csv' })) - const link = Object.assign(document.createElement('a'), { href: url, download: 'users.csv' }) - link.click() - URL.revokeObjectURL(url) - } - - return { - search, - status, - sort, - selected, - page, - pageSize, - filtered, - chips, - pageRows, - range, - removeFilter, - clearFilters, - remove, - blank, - add, - update, - exportSelected, - } -} - -export const columns: DataTableColumn[] = [ - // The modern window has 12px margins round the table; `modern.css` narrows - // Name and Seats there so the actions column is not cut off. - { key: 'name', header: 'Name', sortable: true, width: 'var(--rk-demo-name-width, 150px)' }, - { key: 'email', header: 'Email', width: '170px' }, - { key: 'role', header: 'Role', sortable: true, width: '96px' }, - { key: 'status', header: 'Status', width: '96px' }, - { - key: 'seats', - header: 'Seats', - sortable: true, - numeric: true, - width: 'var(--rk-demo-seats-width, 80px)', - }, - { id: 'actions', header: 'Actions', headerSrOnly: true, width: '56px' }, -] - -/** Status as the badges read it. */ -export const label = { active: 'Active', invited: 'Invited', suspended: 'Suspended' } as const - -export const tone = { active: 'success', invited: 'warning', suspended: 'danger' } as const diff --git a/docs/.vitepress/theme/components/landing/CodeBlock.vue b/docs/.vitepress/theme/components/landing/CodeBlock.vue new file mode 100644 index 00000000..5eb42f6b --- /dev/null +++ b/docs/.vitepress/theme/components/landing/CodeBlock.vue @@ -0,0 +1,39 @@ + + + diff --git a/docs/.vitepress/theme/components/home/EditUserDialog.vue b/docs/.vitepress/theme/components/landing/EditUserDialog.vue similarity index 85% rename from docs/.vitepress/theme/components/home/EditUserDialog.vue rename to docs/.vitepress/theme/components/landing/EditUserDialog.vue index da7b355a..df48970f 100644 --- a/docs/.vitepress/theme/components/home/EditUserDialog.vue +++ b/docs/.vitepress/theme/components/landing/EditUserDialog.vue @@ -16,18 +16,16 @@ import { SelectTrigger, } from 'rowkit' import type { HomeUser } from '../home-users' -import { label } from './useHomeDemo' +import { label } from './useLandingDemo' /** - * The pencil on a row of the live demo: the row's fields in a dialog, edited - * as a copy. Save writes it back into the demo; Cancel, Escape and ✕ drop it. - * With `isNew`, the same form adds a user. + * The pencil on a row of the landing page's live table: the row's fields in a + * dialog, edited as a copy. Save writes it back into the table; Cancel, Escape + * and ✕ drop it. */ const props = defineProps<{ /** The row being edited; the dialog is open while there is one. */ user: HomeUser | undefined - /** The row is not in the list yet: New user rather than Edit. */ - isNew?: boolean }>() const emit = defineEmits<{ @@ -71,10 +69,10 @@ function save(): void {