kodu 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,484 @@
1
+ ---
2
+ name: litefront-prototype
3
+ description: >
4
+ Создание интерактивных прототипов на базе LiteFront (Vite, React 19, TanStack Router,
5
+ URQL, FSD, Tailwind v4 + DaisyUI v5). Приоритет: скорость прототипирования
6
+ и визуальное качество через существующую инфраструктуру проекта.
7
+ ---
8
+
9
+ # LiteFront Prototype Skill
10
+
11
+ > **ПРОТОТИП — всё симулируется.** Нет реального бэкенда, нет реального API, нет реальной авторизации, нет реальной загрузки файлов. Цель — интерактивный UI, который работает офлайн без внешних зависимостей.
12
+
13
+ ## Правила прототипа (строго соблюдать)
14
+
15
+ | Что | Правило |
16
+ |-----|---------|
17
+ | **Авторизация** | `VITE_MOCK_AUTH=true` → `MockAuthProvider`. **Не подключать** Logto / любой OIDC-сервер |
18
+ | **GraphQL API** | Только `createMockExchange` — никаких реальных HTTP-запросов к бэкенду |
19
+ | **Загрузка файлов** | Симулировать через `mockUpload` — никакого реального upload-сервера |
20
+ | **Внешние сервисы** | Любые внешние вызовы — только через моки в памяти |
21
+ | **Генерация типов** | `npm run gen` не нужен — типы писать вручную |
22
+ | **Переменные окружения** | `VITE_GRAPHQL_API_URL` и другие API-переменные игнорируются при мокинге |
23
+
24
+ ---
25
+
26
+ ## Директория и инициализация
27
+
28
+ Все файлы создавать внутри папки **`prototype/`**.
29
+
30
+ Если `prototype/` пуста или не существует — инициализируй через LiteFront:
31
+
32
+ ```bash
33
+ npx degit uxname/litefront prototype
34
+ cd prototype
35
+ npm install
36
+ cp .env.example .env
37
+ ```
38
+
39
+ Сразу после инициализации добавить в `.env`:
40
+ ```
41
+ VITE_MOCK_AUTH=true
42
+ ```
43
+
44
+ **После инициализации (обязательно):**
45
+ 1. **Прочитай `prototype/AGENTS.md`** — главный источник конвенций проекта: структура файлов, архитектура, команды, паттерны кода. Без этого нельзя правильно генерировать код.
46
+ 2. Изучи `src/` — какие слои FSD уже есть, какие компоненты существуют.
47
+ 3. Проверь `package.json` — список доступных скриптов и зависимостей.
48
+ 4. Убедись, что в `.env` установлено `VITE_MOCK_AUTH=true`.
49
+
50
+ ## Технологический стек
51
+
52
+ | Слой | Технология |
53
+ |------|-----------|
54
+ | **Билд** | Vite 8 + React 19 + TypeScript 6 (strict) |
55
+ | **Роутинг** | TanStack Router v1 (file-based, авто-код-сплиттинг) |
56
+ | **Данные** | URQL + `createMockExchange` (никакого реального GraphQL API) |
57
+ | **Стейт** | Zustand 5 (локальный), URQL с моками (серверный) |
58
+ | **Стили** | Tailwind v4 + DaisyUI v5 (themes: cmyk / dark) |
59
+ | **Аутентификация** | `MockAuthProvider` через `VITE_MOCK_AUTH=true` (без реального OIDC) |
60
+ | **i18n** | ParaglideJS (Inlang), `messages/{locale}.json` |
61
+ | **UI-кит** | DaisyUI, Lucide-React (иконки), Sonner (тосты) |
62
+ | **Архитектура** | Feature-Sliced Design (FSD) |
63
+
64
+ ## Архитектура FSD (Feature-Sliced Design)
65
+
66
+ ```
67
+ src/
68
+ ├── app/ # Инициализация, провайдеры, ErrorBoundary
69
+ ├── entities/ # Бизнес-сущности (counter, user, project…)
70
+ ├── features/ # Пользовательские сценарии (auth, createProject…)
71
+ ├── pages/ # Компоненты страниц
72
+ ├── routes/ # Определения роутов TanStack Router
73
+ ├── shared/ # Переиспользуемое: api, ui, config, lib
74
+ ├── widgets/ # Композиционные блоки (Header, Sidebar…)
75
+ ├── graphql/ # .graphql-файлы (queries/, mutations/, fragments/)
76
+ └── generated/ # Авто-генерация (роуты, GraphQL-типы)
77
+ ```
78
+
79
+ **Правила FSD:**
80
+ - Слой может импортировать только нижележащие слои: `pages` → `widgets` → `features` → `entities` → `shared`.
81
+ - Слайс (папка внутри слоя) не импортирует другие слайсы того же слоя.
82
+ - Публичное API слайса — через `index.ts` (barrel export).
83
+
84
+ **Path aliases** (уже настроены в `tsconfig.json`):
85
+ `@shared/*`, `@entities/*`, `@features/*`, `@widgets/*`, `@pages/*`, `@generated/*`, `@public/*`
86
+
87
+ ---
88
+
89
+ ## 1. Существующие компоненты (уже есть в проекте)
90
+
91
+ Не создавай их заново — используй готовые:
92
+
93
+ - **`@features/auth`** — `useAuth()`, `AuthGuard`, `MockAuthProvider`
94
+ - **`@entities/counter`** — `useCounterStore`, `Counter`
95
+ - **`@widgets/Header`** — `Header`
96
+ - **`@shared/ui/ErrorFallback`** — готовая страница ошибки с категориями (AUTH, ACCESS, NETWORK, SERVER, UNKNOWN)
97
+ - **`@shared/ui/Toaster`** — `toast` из Sonner + кастомные стили
98
+ - **`@pages/404`** — страница 404
99
+
100
+ ## 2. Роутинг (TanStack Router)
101
+
102
+ Файлы роутов — в `src/routes/`. Роуты разделяют логику (`index.tsx`) и ленивый компонент (`index.lazy.tsx`).
103
+
104
+ **Новый роут:**
105
+ ```tsx
106
+ // src/routes/projects.tsx
107
+ import { createFileRoute } from '@tanstack/react-router';
108
+
109
+ export const Route = createFileRoute('/projects')({
110
+ head: () => ({
111
+ meta: [
112
+ { title: 'Projects | LiteFront' },
113
+ { name: 'description', content: 'Manage projects' },
114
+ ],
115
+ }),
116
+ });
117
+ ```
118
+
119
+ ```tsx
120
+ // src/routes/projects.lazy.tsx
121
+ import { ProjectsPage } from '@pages/projects';
122
+ import { createLazyFileRoute } from '@tanstack/react-router';
123
+
124
+ export const Route = createLazyFileRoute('/projects')({
125
+ component: ProjectsPage,
126
+ });
127
+ ```
128
+
129
+ **Страница:**
130
+ ```tsx
131
+ // src/pages/projects/ui/index.tsx
132
+ import { useAuth } from '@features/auth';
133
+ import { Link, useNavigate } from '@tanstack/react-router';
134
+ import { Plus } from 'lucide-react';
135
+
136
+ export function ProjectsPage() {
137
+ const auth = useAuth();
138
+ const navigate = useNavigate();
139
+
140
+ return (
141
+ <div>
142
+ <div className="flex items-center justify-between mb-6">
143
+ <h1 className="text-2xl font-bold">Projects</h1>
144
+ <button onClick={() => navigate({ to: '/projects/new' })}
145
+ className="btn btn-primary">
146
+ <Plus className="size-4" /> New
147
+ </button>
148
+ </div>
149
+ {auth.isAuthenticated && (
150
+ <p className="text-sm text-base-content/60 mb-4">
151
+ Signed in as {auth.user?.profile?.email}
152
+ </p>
153
+ )}
154
+ <Link to="/projects/$id" params={{ id: '123' }}>Project</Link>
155
+ </div>
156
+ );
157
+ }
158
+ ```
159
+
160
+ **Auth guard на роут (симулированный):**
161
+ ```tsx
162
+ // src/routes/protected/index.tsx
163
+ import { createFileRoute, redirect } from '@tanstack/react-router';
164
+
165
+ export const Route = createFileRoute('/protected/')({
166
+ beforeLoad: ({ context }) => {
167
+ if (!context.auth.isAuthenticated) {
168
+ throw redirect({ to: '/' });
169
+ }
170
+ },
171
+ });
172
+ ```
173
+
174
+ С `VITE_MOCK_AUTH=true` `context.auth.isAuthenticated` всегда `true` — редирект не произойдёт.
175
+
176
+ **Навигация в коде:** `useNavigate()` из `@tanstack/react-router`.
177
+ **Запрещено** использовать `<a href>`.
178
+
179
+ **После создания нового файла роута — запусти `npm run gen:routes`**
180
+ для перегенерации `src/generated/routeTree.gen.ts`.
181
+
182
+ ## 3. Авторизация (только симуляция)
183
+
184
+ `VITE_MOCK_AUTH=true` в `.env` активирует `MockAuthProvider` из `@features/auth`.
185
+ **Не подключать** реальный OIDC-сервер (Logto или любой другой) — прототип работает без него.
186
+
187
+ ```tsx
188
+ // Мок-пользователь доступен сразу без логина:
189
+ const auth = useAuth();
190
+ // auth.isAuthenticated === true
191
+ // auth.user?.profile?.email === 'mock@example.com'
192
+ ```
193
+
194
+ `AuthGuard` и защищённые роуты работают через мок — дополнительной настройки не нужно.
195
+
196
+ ## 4. Данные (только моки, без GraphQL API)
197
+
198
+ В прототипе **нет реального GraphQL API**. Все данные хранятся в памяти и возвращаются через `createMockExchange`.
199
+ Никаких HTTP-запросов к бэкенду. `npm run gen` не нужен.
200
+
201
+ ### Mock exchange
202
+
203
+ ```ts
204
+ // src/shared/api/mock-exchange.ts
205
+ import { Exchange, makeResult } from 'urql';
206
+ import { pipe, mergeMap, fromValue } from 'wonka';
207
+
208
+ type Mocks = Record<
209
+ string,
210
+ (variables?: Record<string, unknown>) => Record<string, unknown>
211
+ >;
212
+
213
+ export const createMockExchange = (mocks: Mocks): Exchange => {
214
+ return ({ forward }) => (ops$) =>
215
+ pipe(
216
+ ops$,
217
+ mergeMap((operation) => {
218
+ const queryName = operation.query.definitions[0]?.name?.value;
219
+ const handler = queryName ? mocks[queryName] : undefined;
220
+
221
+ if (handler) {
222
+ const data = handler(operation.variables as Record<string, unknown>);
223
+ return fromValue(makeResult(operation, { data }));
224
+ }
225
+
226
+ return pipe(fromValue(operation), forward);
227
+ }),
228
+ );
229
+ };
230
+ ```
231
+
232
+ ### Подключение в GraphQL-клиент
233
+
234
+ ```ts
235
+ // src/shared/api/graphql-client.ts
236
+ import { createMockExchange } from './mock-exchange';
237
+ import type { Mocks } from './mock-exchange';
238
+
239
+ export const createGraphQLClient = (
240
+ accessToken?: string,
241
+ mocks?: Mocks,
242
+ ): Client => {
243
+ const exchanges = [
244
+ cacheExchange,
245
+ errorExchange({ onError: (error) => { /* ... */ } }),
246
+ ];
247
+
248
+ if (mocks) exchanges.push(createMockExchange(mocks));
249
+
250
+ exchanges.push(fetchExchange);
251
+
252
+ return new Client({
253
+ url: import.meta.env.VITE_GRAPHQL_API_URL,
254
+ exchanges,
255
+ fetchOptions: {
256
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
257
+ },
258
+ requestPolicy: 'cache-and-network',
259
+ });
260
+ };
261
+ ```
262
+
263
+ ### Мок-данные в `__root.tsx`
264
+
265
+ ```tsx
266
+ // src/routes/__root.tsx
267
+ import { GraphQLProvider, createGraphQLClient } from '@shared/api';
268
+
269
+ const mockData = {
270
+ GetProjects: () => ({
271
+ projects: [
272
+ { id: 'p-1', name: 'Project Alpha', status: 'active' },
273
+ { id: 'p-2', name: 'Project Beta', status: 'draft' },
274
+ ],
275
+ }),
276
+ CreateProject: (vars) => ({
277
+ createProject: { id: 'p-new', name: vars?.input?.name || 'New' },
278
+ }),
279
+ };
280
+
281
+ function RootComponent() {
282
+ const auth = useAuth();
283
+ const client = useMemo(
284
+ () => createGraphQLClient(auth.user?.id_token, mockData),
285
+ [auth.user?.id_token],
286
+ );
287
+ // ...
288
+ }
289
+ ```
290
+
291
+ ### Пример компонента с мок-данными
292
+
293
+ ```tsx
294
+ import { useGetProjectsQuery } from '@generated/graphql';
295
+
296
+ function ProjectsList() {
297
+ const [{ data, fetching, error }] = useGetProjectsQuery();
298
+
299
+ if (fetching) return <span className="loading loading-spinner loading-lg" />;
300
+ if (error) return <p className="text-error">{error.message}</p>;
301
+ if (!data?.projects.length)
302
+ return (
303
+ <div className="text-center py-20 bg-base-100 rounded-xl border-2 border-dashed border-base-300">
304
+ <p className="text-base-content/50">No projects yet</p>
305
+ </div>
306
+ );
307
+
308
+ return (
309
+ <div className="overflow-x-auto bg-base-100 rounded-xl shadow-sm border border-base-300">
310
+ <table className="table table-zebra">
311
+ <thead>
312
+ <tr>
313
+ <th>Name</th>
314
+ </tr>
315
+ </thead>
316
+ <tbody>
317
+ {data.projects.map((project) => (
318
+ <tr key={project.id} className="hover">
319
+ <td>{project.name}</td>
320
+ </tr>
321
+ ))}
322
+ </tbody>
323
+ </table>
324
+ </div>
325
+ );
326
+ }
327
+ ```
328
+
329
+ ### Правила мок-данных
330
+
331
+ 1. Все данные — в памяти, никаких внешних сервисов.
332
+ 2. UI должен выглядеть как с реальными данными: таблицы непустые, навигация работает, тосты показываются.
333
+ 3. Для списков — минимум 2–3 элемента (не пусто, не единичная запись).
334
+ 4. Состояния loading/empty/error должны быть визуально видны при соответствующих условиях.
335
+
336
+ ### Типы без codegen
337
+
338
+ `npm run gen` не нужен. Типы писать вручную:
339
+
340
+ ```ts
341
+ // src/shared/api/mock-types.ts
342
+ export interface Project {
343
+ id: string;
344
+ name: string;
345
+ status: 'active' | 'draft' | 'archived';
346
+ }
347
+ ```
348
+
349
+ Либо использовать локальную схему в `codegen.yml`:
350
+ ```yaml
351
+ schema: "./src/generated/schema.graphql"
352
+ documents: "./src/graphql/**/*.graphql"
353
+ ```
354
+
355
+ ## 5. Загрузка файлов (только симуляция)
356
+
357
+ В прототипе файлы **не загружаются на сервер**. Симулировать через `mockUpload`:
358
+
359
+ ```ts
360
+ // src/shared/api/mock-upload.ts
361
+ export async function mockUpload(file: File): Promise<{ url: string; name: string }> {
362
+ await new Promise((r) => setTimeout(r, 800)); // имитация задержки
363
+ return {
364
+ url: URL.createObjectURL(file), // временный локальный URL для превью
365
+ name: file.name,
366
+ };
367
+ }
368
+ ```
369
+
370
+ ```tsx
371
+ import { mockUpload } from '@shared/api/mock-upload';
372
+ import { toast } from '@shared/ui/Toaster';
373
+
374
+ function FileUploader() {
375
+ const [preview, setPreview] = useState<string | null>(null);
376
+
377
+ const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
378
+ const file = e.target.files?.[0];
379
+ if (!file) return;
380
+ const result = await mockUpload(file);
381
+ setPreview(result.url);
382
+ toast.success(`Файл загружен: ${result.name}`);
383
+ };
384
+
385
+ return (
386
+ <div>
387
+ <input type="file" onChange={handleFile} className="file-input file-input-bordered" />
388
+ {preview && <img src={preview} alt="preview" className="mt-2 max-h-48 rounded-lg" />}
389
+ </div>
390
+ );
391
+ }
392
+ ```
393
+
394
+ Для файлов не-изображений — показывать имя и размер, возвращать мок-URL `/mock/uploaded/<filename>`.
395
+
396
+ ## 6. Локальный стейт (Zustand)
397
+
398
+ Для стейта, который не идёт через GraphQL:
399
+
400
+ ```tsx
401
+ import { create } from 'zustand';
402
+ import { devtools } from 'zustand/middleware';
403
+
404
+ interface UIState {
405
+ sidebarOpen: boolean;
406
+ toggleSidebar: () => void;
407
+ }
408
+
409
+ export const useUIStore = create(
410
+ devtools<UIState>((set) => ({
411
+ sidebarOpen: false,
412
+ toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
413
+ })),
414
+ );
415
+ ```
416
+
417
+ ## 7. Стили (Tailwind + DaisyUI)
418
+
419
+ - **DaisyUI:** `btn`, `card`, `input`, `badge`, `modal`, `table`, `table-zebra`, `loading loading-spinner`, `toggle`, `select`, `alert`, `avatar`, `tooltip`.
420
+ - **Модалки:** используй тег `<dialog>` + `useRef`. Открытие: `.showModal()`.
421
+ Закрытие: `<form method="dialog">`.
422
+ - **Иконки:** `lucide-react`.
423
+ - **Темы:** `cmyk` (светлая, по умолчанию), `dark` (тёмная, prefers-color-scheme).
424
+ DaisyUI-темы подключаются через атрибут `data-theme` на `<html>`.
425
+ - **Цвета DaisyUI:** `bg-base-100`, `text-base-content`, `text-primary`,
426
+ `border-base-300`, `bg-primary`, `text-primary-content` и т.д.
427
+ Не используй хардкодные цвета — только семантические токены темы.
428
+
429
+ ## 8. i18n (ParaglideJS)
430
+
431
+ Сообщения — в `messages/{locale}.json`. Используй готовые или добавляй новые.
432
+
433
+ ```json
434
+ {
435
+ "projects_title": "Projects",
436
+ "project_created": "Project created successfully"
437
+ }
438
+ ```
439
+
440
+ Импорт в коде:
441
+ ```tsx
442
+ import * as m from '@generated/paraglide/messages';
443
+ // m.projects_title() → "Projects"
444
+ ```
445
+
446
+ Для прототипа i18n можно временно пропустить — используй прямые строки.
447
+
448
+ ## 9. Порядок действий при генерации кода
449
+
450
+ 1. **Прочитай `prototype/AGENTS.md`** — конвенции проекта, структура, паттерны.
451
+ 2. Изучи структуру `src/` — какие слайсы FSD уже есть.
452
+ 3. Определи, в какой слой FSD ложится новая фича.
453
+ 4. **Настрой моки данных** — все операции через `createMockExchange` (никаких реальных API-вызовов).
454
+ 5. **Авторизация — только через `VITE_MOCK_AUTH=true`**, не подключать Logto.
455
+ 6. **Загрузка файлов — только через `mockUpload`**, без реального сервера.
456
+ 7. Добавь мок-данные для новых операций в `mockData` в `__root.tsx`.
457
+ 8. Используй существующие компоненты где возможно.
458
+ 9. Следуй паттернам из существующих страниц.
459
+
460
+ ## 10. Команды
461
+
462
+ ```bash
463
+ npm run start:dev # Dev-сервер (localhost:3000)
464
+ npm run gen:routes # Генерация routeTree.gen.ts (нужен после нового роута)
465
+ npm run check # Линтинг + typecheck + Knip
466
+ npm run lint:fix # Biome с автофиксом
467
+ npm run build # Production-сборка
468
+ npm run test:dev # Тесты (Vitest, watch)
469
+ npm run test:e2e:dev # Playwright UI mode
470
+ ```
471
+
472
+ `npm run gen` (GraphQL codegen) в прототипе **не нужен** — типы пишутся вручную.
473
+
474
+ ## 11. Формат ответа
475
+
476
+ Отвечай **только** блоками кода. Перед каждым блоком указывай путь
477
+ (относительно `prototype/`). Выводи файлы целиком.
478
+
479
+ ```
480
+ **`src/pages/projects/ui/index.tsx`**
481
+ \`\`\`tsx
482
+ // полный код файла
483
+ \`\`\`
484
+ ```