feat: add new design primitives

This commit is contained in:
2026-08-27 19:41:32 +05:00
parent 418a091505
commit 0acfa3aeb9
8 changed files with 198 additions and 1 deletions
+51
View File
@@ -0,0 +1,51 @@
# AGENTS.md
Инструкции для агентов (и IDE-ассистентов) по работе с этим репозиторием.
## Стек
- **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes).
- Менеджер пакетов: **pnpm**. Запускать команды из `web/`, либо через
обёртки в корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`).
Не использовать npm.
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`,
тесты: `pnpm --dir web test`.
## Правила кода
### Svelte 5: типизация props через `interface Props`
Все типизированные пропсы компонентов описываются через локальный
`interface Props`, а деструктуризация идёт через аннотацию типа при `$props()`:
```svelte
<script lang="ts">
interface Props {
label: string;
accent?: boolean;
children?: import('svelte').Snippet;
}
let { label, accent = false, children }: Props = $props();
</script>
```
Не использовать инлайн-дженерик `$props<{ ... }>()` — он тяжело читается и
разносит тип и деструктуризацию по разным местам.
> Позже планируется линтер/правило (eslint-plugin-svelte / custom rule),
> запрещающее `$props<...>()` и требующее `interface Props`.
### Дизайн: новый визуальный язык
Переезд на новый дизайн описан в `docs/plan-redesign.md`. Ключевые
ограничения при работе с веткой `feat/redesign`:
- Глобальные стили старого и нового дизайна разнесены по layout без
наследования: старое в `web/src/app.css` (группа `(old)/`), новое в
`web/src/lib/styles/design2.css` (ветка `preview/*`). Префикс `.ds2` не
используется.
- Все повторяющиеся визуальные элементы — отдельные компоненты в
`web/src/lib/components/kit/`, даже «просто div с двумя стилями».
- Токены нового дизайна живут в `design2.css`; `refs/` и `refs-html/`
одноразовый источник, не поддерживаемый (правки дизайна — сразу в код).
- Коммиты мелкие (< ~500 строк), атомарные
+1 -1
View File
@@ -304,7 +304,7 @@ src/routes/preview/tools/[id]/+page.svelte
подключить шрифты в `design2.css` (self-host, offline-first). подключить шрифты в `design2.css` (self-host, offline-first).
- [x] **[C4]** _(слит в C3: шрифты нельзя подключить до создания `design2.css`)_ - [x] **[C4]** _(слит в C3: шрифты нельзя подключить до создания `design2.css`)_
`[data-theme='dark']` (таблица §2). Без утилитарных классов. `[data-theme='dark']` (таблица §2). Без утилитарных классов.
- [ ] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`, - [x] **[C5]** Примитивы batch 1: `AppShell`, `BlueprintGrid`, `CheckerCanvas`,
`MonoLabel`, `StatusDot`/`StatusLine`. `MonoLabel`, `StatusDot`/`StatusLine`.
- [ ] **[C6]** Примитивы batch 2: `Panel`, `PanelHeading`, `SettingGroup`, - [ ] **[C6]** Примитивы batch 2: `Panel`, `PanelHeading`, `SettingGroup`,
`SettingsFooter`, `MetaRow`/`MetaList`, `Segmented`, `Badge`, `StepCard`, `SettingsFooter`, `MetaRow`/`MetaList`, `Segmented`, `Badge`, `StepCard`,
@@ -0,0 +1,27 @@
<script lang="ts">
import BlueprintGrid from './BlueprintGrid.svelte';
let { children } = $props();
</script>
<div class="app-shell">
<BlueprintGrid />
<div class="app-content">
{@render children()}
</div>
</div>
<style>
.app-shell {
position: relative;
min-height: 100dvh;
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
}
.app-content {
position: relative;
z-index: 1;
}
</style>
@@ -0,0 +1,14 @@
<div class="blueprint-grid" aria-hidden="true"></div>
<style>
.blueprint-grid {
position: absolute;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(to right, color-mix(in srgb, var(--line) 45%, transparent) 1px, transparent 1px),
linear-gradient(to bottom, color-mix(in srgb, var(--line) 45%, transparent) 1px, transparent 1px);
background-size: 32px 32px;
opacity: 0.55;
}
</style>
@@ -0,0 +1,40 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
size?: 'sm' | 'large';
}
let { children, size = 'large' }: Props = $props();
</script>
<div class="checker-canvas checker-canvas--{size}">
{#if children}
{@render children()}
{/if}
</div>
<style>
.checker-canvas {
position: relative;
display: grid;
place-items: center;
overflow: hidden;
background-color: #0b0f14;
background-image: linear-gradient(45deg, #1a2129 25%, transparent 25%),
linear-gradient(-45deg, #1a2129 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1a2129 75%),
linear-gradient(-45deg, transparent 75%, #1a2129 75%);
background-size: 20px 20px;
background-position:
0 0,
0 10px,
10px -10px,
-10px 0;
}
.checker-canvas--sm {
min-height: 180px;
}
.checker-canvas--large {
min-height: 360px;
}
</style>
@@ -0,0 +1,30 @@
<script lang="ts">
interface Props {
accent?: boolean;
children?: import('svelte').Snippet;
}
let { accent = false, children }: Props = $props();
</script>
<span class="mono-label" class:accent={accent}>
{#if children}
{@render children()}
{/if}
</span>
<style>
.mono-label {
font-family: var(--font-mono);
font-size: 10px;
line-height: 1.2;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.accent {
color: var(--blue);
}
</style>
@@ -0,0 +1,12 @@
<span class="status-dot" aria-hidden="true"></span>
<style>
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--success) 25%, transparent);
}
</style>
@@ -0,0 +1,23 @@
<script lang="ts">
import StatusDot from './StatusDot.svelte';
import MonoLabel from './MonoLabel.svelte';
interface Props {
label: string;
}
let { label }: Props = $props();
</script>
<span class="status-line">
<StatusDot />
<MonoLabel>{label}</MonoLabel>
</span>
<style>
.status-line {
display: inline-flex;
align-items: center;
gap: 6px;
}
</style>