From 7c3ded601fb41ebb3574fe2ebb5307f162d29ce1 Mon Sep 17 00:00:00 2001 From: Sergey <93754860+slaid098@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:08:58 +0300 Subject: [PATCH] feat(template): bake shadcn-svelte + Tailwind v4 + TS into fullstack cookiecutter (#265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(template): fullstack cookiecutter — Tailwind v4 + TS + shadcn-svelte scaffold * feat(template): fullstack — shadcn-svelte ui components + dashboard layout * refactor(template): fullstack — remove counter/Header demo files * feat(spec): add tailwind/shadcn/ts to STACK_REQUIRED fullstack * test(template): update fullstack cookiecutter assertions for shadcn-svelte * fix(ci): ruff format test_cookiecutter_templates --------- Co-authored-by: opencode-agent --- .opencode/scripts/spec-status.py | 3 + .opencode/skills/project-template/SKILL.md | 2 +- .opencode/skills/spec/SKILL.md | 8 +- .../templates/fullstack/cookiecutter.json | 8 +- .../frontend/biome.json | 14 +- .../frontend/components.json | 17 +++ .../frontend/jsconfig.json | 22 --- .../frontend/knip.json | 4 +- .../frontend/package.json | 20 ++- .../frontend/src/app.css | 136 +++++++++++++++--- .../frontend/src/lib/components/Header.svelte | 23 --- .../src/lib/components/app-sidebar.svelte | 45 ++++++ .../src/lib/components/nav-main.svelte | 30 ++++ .../src/lib/components/nav-user.svelte | 55 +++++++ .../src/lib/components/section-cards.svelte | 53 +++++++ .../src/lib/components/site-header.svelte | 22 +++ .../lib/components/ui/button/button.svelte | 80 +++++++++++ .../src/lib/components/ui/button/index.ts | 15 ++ .../lib/components/ui/card/card-action.svelte | 20 +++ .../components/ui/card/card-content.svelte | 20 +++ .../ui/card/card-description.svelte | 20 +++ .../lib/components/ui/card/card-footer.svelte | 20 +++ .../lib/components/ui/card/card-header.svelte | 23 +++ .../lib/components/ui/card/card-title.svelte | 20 +++ .../src/lib/components/ui/card/card.svelte | 23 +++ .../src/lib/components/ui/card/index.ts | 24 ++++ .../components/ui/checkbox/checkbox.svelte | 35 +++++ .../src/lib/components/ui/checkbox/index.ts | 2 + .../ui/data-table/create-table.svelte | 19 +++ .../ui/data-table/flex-render.svelte | 26 ++++ .../src/lib/components/ui/data-table/index.ts | 5 + .../ui/data-table/render-helpers.svelte.ts | 26 ++++ .../src/lib/components/ui/dialog/index.ts | 13 ++ .../lib/components/ui/dropdown-menu/index.ts | 20 +++ .../components/ui/field/field-control.svelte | 17 +++ .../ui/field/field-description.svelte | 17 +++ .../components/ui/field/field-errors.svelte | 17 +++ .../components/ui/field/field-label.svelte | 17 +++ .../components/ui/field/field-legend.svelte | 17 +++ .../lib/components/ui/field/field-root.svelte | 26 ++++ .../ui/field/field-validation.svelte | 17 +++ .../src/lib/components/ui/field/index.ts | 24 ++++ .../src/lib/components/ui/input/index.ts | 2 + .../src/lib/components/ui/input/input.svelte | 32 +++++ .../src/lib/components/ui/label/index.ts | 2 + .../src/lib/components/ui/label/label.svelte | 20 +++ .../src/lib/components/ui/separator/index.ts | 2 + .../components/ui/separator/separator.svelte | 28 ++++ .../src/lib/components/ui/sheet/index.ts | 13 ++ .../lib/components/ui/sidebar/constants.ts | 3 + .../lib/components/ui/sidebar/content.svelte | 24 ++++ .../components/ui/sidebar/context.svelte.ts | 37 +++++ .../lib/components/ui/sidebar/footer.svelte | 21 +++ .../components/ui/sidebar/group-action.svelte | 25 ++++ .../ui/sidebar/group-content.svelte | 21 +++ .../components/ui/sidebar/group-label.svelte | 25 ++++ .../lib/components/ui/sidebar/group.svelte | 24 ++++ .../lib/components/ui/sidebar/header.svelte | 21 +++ .../src/lib/components/ui/sidebar/index.ts | 82 +++++++++++ .../lib/components/ui/sidebar/input.svelte | 21 +++ .../lib/components/ui/sidebar/inset.svelte | 28 ++++ .../components/ui/sidebar/menu-action.svelte | 26 ++++ .../components/ui/sidebar/menu-badge.svelte | 26 ++++ .../components/ui/sidebar/menu-button.svelte | 28 ++++ .../components/ui/sidebar/menu-item.svelte | 24 ++++ .../ui/sidebar/menu-skeleton.svelte | 21 +++ .../ui/sidebar/menu-sub-button.svelte | 29 ++++ .../ui/sidebar/menu-sub-item.svelte | 21 +++ .../ui/sidebar/menu-sub-trigger.svelte | 25 ++++ .../lib/components/ui/sidebar/menu-sub.svelte | 21 +++ .../src/lib/components/ui/sidebar/menu.svelte | 21 +++ .../lib/components/ui/sidebar/provider.svelte | 60 ++++++++ .../src/lib/components/ui/sidebar/rail.svelte | 26 ++++ .../src/lib/components/ui/sidebar/root.svelte | 55 +++++++ .../components/ui/sidebar/separator.svelte | 19 +++ .../lib/components/ui/sidebar/trigger.svelte | 31 ++++ .../src/lib/components/ui/skeleton/index.ts | 2 + .../components/ui/skeleton/skeleton.svelte | 17 +++ .../src/lib/components/ui/sonner/index.ts | 3 + .../lib/components/ui/sonner/sonner.svelte | 21 +++ .../src/lib/components/ui/switch/index.ts | 2 + .../lib/components/ui/switch/switch.svelte | 36 +++++ .../src/lib/components/ui/textarea/index.ts | 2 + .../components/ui/textarea/textarea.svelte | 24 ++++ .../src/lib/components/ui/tooltip/index.ts | 9 ++ .../src/lib/hooks/is-mobile.svelte.ts | 26 ++++ .../frontend/src/lib/stores/counter.svelte.js | 22 --- .../frontend/src/lib/stores/counter.test.js | 29 ---- .../frontend/src/lib/utils/cn.svelte.ts | 22 +++ .../frontend/src/lib/utils/format.js | 11 -- .../frontend/src/lib/utils/format.test.js | 20 --- .../frontend/src/routes/+error.svelte | 4 +- .../frontend/src/routes/+layout.svelte | 20 +++ .../frontend/src/routes/+layout.ts | 5 + .../frontend/src/routes/+page.js | 4 - .../frontend/src/routes/+page.svelte | 75 +++++++--- .../frontend/src/routes/+page.ts | 5 + .../frontend/svelte.config.js | 10 +- .../frontend/tests/e2e/app.spec.js | 12 -- .../frontend/tests/e2e/app.spec.ts | 6 + .../frontend/tsconfig.json | 36 +++++ .../{vite.config.js => vite.config.ts} | 3 +- .../{vitest.config.js => vitest.config.ts} | 4 +- tests/test_cookiecutter_templates.py | 98 +++++++++++-- 104 files changed, 2206 insertions(+), 211 deletions(-) create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/components.json delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/jsconfig.json delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/Header.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts delete mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts create mode 100644 .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json rename .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/{vite.config.js => vite.config.ts} (60%) rename .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/{vitest.config.js => vitest.config.ts} (56%) diff --git a/.opencode/scripts/spec-status.py b/.opencode/scripts/spec-status.py index 1f17c70..723955e 100644 --- a/.opencode/scripts/spec-status.py +++ b/.opencode/scripts/spec-status.py @@ -81,6 +81,9 @@ STACK_REQUIRED: dict[str, list[str]] = { "ruff", "mypy", "pytest", + "tailwind", + "shadcn", + "typescript", ], "mcp-server": ["fastapi", "mcp", "patchright", "uv"], "cli": ["typer", "uv", "hatchling", "ruff", "mypy", "pytest"], diff --git a/.opencode/skills/project-template/SKILL.md b/.opencode/skills/project-template/SKILL.md index 8069b0b..7e645d3 100644 --- a/.opencode/skills/project-template/SKILL.md +++ b/.opencode/skills/project-template/SKILL.md @@ -61,7 +61,7 @@ init flow. ``` Выбери тип проекта (из spec-status VALID_TYPES): [1] backend — FastAPI + Tortoise, REST API (cookiecutter template) -[2] fullstack — backend + SvelteKit frontend (cookiecutter template) +[2] fullstack — backend + SvelteKit + Tailwind v4 + shadcn-svelte dashboard (TypeScript, cookiecutter template) [3] cli — Python CLI tool, Typer (cookiecutter template) [4] mcp-server — MCP + REST сервер (нет cookiecutter template — ручная инициализация) [5] bot — Telegram bot, aiogram 3 (нет cookiecutter template — ручная инициализация) diff --git a/.opencode/skills/spec/SKILL.md b/.opencode/skills/spec/SKILL.md index 706c97a..e45478b 100644 --- a/.opencode/skills/spec/SKILL.md +++ b/.opencode/skills/spec/SKILL.md @@ -42,7 +42,7 @@ truth для порядка и действий — `spec-status` tool. На в Общий для всех типов: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI. - **backend**: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich — legacy), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`) -- **fullstack**: backend + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip) +- **fullstack**: backend + frontend/ (SvelteKit + Svelte 5 runes (TS) + Tailwind v4 + shadcn-svelte + Biome + Vitest + Knip) - **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key - **cli**: Typer (default) / click / argparse, hatchling build - **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.) @@ -65,7 +65,7 @@ Prompt template A (см. ниже). ``` Выбери тип проекта: [1] backend — FastAPI + Tortoise, REST API, без frontend -[2] fullstack — backend + SvelteKit/Svelte 5 dashboard (monorepo) +[2] fullstack — backend + SvelteKit/Svelte 5 + Tailwind v4 + shadcn-svelte dashboard (TypeScript, monorepo) [3] mcp-server — MCP + REST сервер (Patchright/Playwright over CDP) [4] cli — Python CLI tool (Typer) [5] bot — Telegram bot (aiogram 3) @@ -88,7 +88,7 @@ backend: - Auth: [1] none v1 / [2] JWT / [3] X-API-Key fullstack: -- frontend: [1] SvelteKit + Svelte 5 (default) / [2] add later +- frontend: [1] SvelteKit + Svelte 5 + Tailwind v4 + shadcn-svelte (default) / [2] add later - DB: (same as backend) - Auth: (same as backend) @@ -236,7 +236,7 @@ Spec complete. Issues: #N1, #N2, ... Default stack для типа (хардкод, добавить всегда): - Общий: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI - backend: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`) -- fullstack: + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip) +- fullstack: + frontend/ (SvelteKit + Svelte 5 runes (TS) + Tailwind v4 + shadcn-svelte + Biome + Vitest + Knip) - mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key - cli: Typer (default) / click / argparse, hatchling build - bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.) diff --git a/.opencode/templates/fullstack/cookiecutter.json b/.opencode/templates/fullstack/cookiecutter.json index 2702b94..9e2e932 100644 --- a/.opencode/templates/fullstack/cookiecutter.json +++ b/.opencode/templates/fullstack/cookiecutter.json @@ -4,5 +4,11 @@ "description": "Fullstack project description", "use_auth": ["no", "yes"], "use_db": ["yes", "no"], - "python_version": "3.13" + "python_version": "3.13", + "_copy_without_render": [ + "frontend/src/lib/components", + "frontend/src/routes/+page.svelte", + "frontend/src/routes/+layout.svelte", + "frontend/src/routes/+error.svelte" + ] } \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/biome.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/biome.json index b226c35..219ad71 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/biome.json +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/biome.json @@ -21,5 +21,17 @@ }, "files": { "ignore": ["build", ".svelte-kit", "playwright-report"] - } + }, + "overrides": [ + { + "include": ["src/lib/components/ui/**/*.svelte", "src/lib/components/ui/**/*.ts"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ] } \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/components.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/components.json new file mode 100644 index 0000000..a4445af --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "new-york", + "tailwind": { + "css": "src/app.css", + "baseColor": "neutral" + }, + "typescript": true, + "aliases": { + "lib": "$lib", + "utils": "$lib/utils", + "components": "$lib/components", + "hooks": "$lib/hooks", + "ui": "$lib/components/ui" + }, + "registry": "https://shadcn-svelte.com/registry" +} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/jsconfig.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/jsconfig.json deleted file mode 100644 index 50a0836..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/jsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "checkJs": true, - "allowJs": true, - "moduleResolution": "bundler", - "target": "ESNext", - "module": "ESNext", - "resolveJsonModule": true, - "strict": true, - "skipLibCheck": true, - "esModuleInterop": true, - "verbatimModuleSyntax": false, - "baseUrl": ".", - "paths": { - "$lib": ["src/lib"], - "$lib/*": ["src/lib/*"], - "$components": ["src/lib/components"], - "$components/*": ["src/lib/components/*"] - } - }, - "include": ["src/**/*.js", "src/**/*.svelte", "vite.config.js", "vitest.config.js"] -} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/knip.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/knip.json index 2a45a62..0664d93 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/knip.json +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/knip.json @@ -1,6 +1,6 @@ { - "entry": ["src/**/*.{js,svelte}", "!src/**/*.test.js"], - "project": ["src/**/*.{js,svelte}"], + "entry": ["src/**/*.{ts,svelte}", "!src/**/*.test.ts"], + "project": ["src/**/*.{ts,svelte}"], "ignore": ["build", ".svelte-kit"], "ignoreBinaries": true, "ignoreDependencies": false diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/package.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/package.json index e5c9c75..9e6fb76 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/package.json +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/package.json @@ -10,11 +10,22 @@ "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test", - "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "lint": "biome check .", "format": "biome format --write .", "knip": "knip" }, + "dependencies": { + "bits-ui": "^2.18.1", + "tailwind-variants": "^3.3.1", + "clsx": "^2.1.1", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "@lucide/svelte": "^1.28.0", + "@tanstack/table-core": "^8.21.3", + "svelte-sonner": "^1.1.1", + "mode-watcher": "^1.1.0" + }, "devDependencies": { "@sveltejs/adapter-node": "^5.0.0", "@sveltejs/kit": "^2.0.0", @@ -25,6 +36,11 @@ "svelte-check": "^4.0.0", "vite": "^5.0.0", "vitest": "^2.0.0", - "@playwright/test": "^1.48.0" + "@playwright/test": "^1.48.0", + "tailwindcss": "^4.3.3", + "@tailwindcss/vite": "^4.3.3", + "typescript": "^5.9.0", + "@tsconfig/svelte": "^5.0.0", + "tslib": "^2.8.0" } } \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/app.css b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/app.css index 896926e..c8558ea 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/app.css +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/app.css @@ -1,23 +1,123 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + :root { - --bg: #ffffff; - --fg: #1a1a1a; - --accent: #646cff; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -@media (prefers-color-scheme: dark) { - :root { - --bg: #1a1a1a; - --fg: #ffffff; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } -} - -body { - margin: 0; - background: var(--bg); - color: var(--fg); - font-family: system-ui, sans-serif; -} - -a { - color: var(--accent); } \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/Header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/Header.svelte deleted file mode 100644 index a15f41d..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/Header.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
-

{title}

- {@render children?.()} -
- - \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte new file mode 100644 index 0000000..66deef7 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/app-sidebar.svelte @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + {title} + + + + + + + + + + {@render children?.()} + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte new file mode 100644 index 0000000..b95323f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-main.svelte @@ -0,0 +1,30 @@ + + + + Platform + + + {#each items as item (item.title)} + + + + {item.title} + + + {/each} + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte new file mode 100644 index 0000000..93c6c66 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/nav-user.svelte @@ -0,0 +1,55 @@ + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Settings + + + + Profile + + + + + + Sign out + + + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte new file mode 100644 index 0000000..6c72244 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/section-cards.svelte @@ -0,0 +1,53 @@ + + +
+ {#each cards as card (card.title)} + + +
+ + {card.title} + + +
+
+ +
{card.value}
+

{card.delta}

+
+
+ {/each} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte new file mode 100644 index 0000000..6d3e71b --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/site-header.svelte @@ -0,0 +1,22 @@ + + +
+ +
+

+ {title} +

+
+
+ {@render children?.()} +
+
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..6e58233 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/button.svelte @@ -0,0 +1,80 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..39b5e2e --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/button/index.ts @@ -0,0 +1,15 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from './button.svelte'; + +export { + Root, + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, + Root as Button, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..318b374 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..aa5d8ef --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,20 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..ca1858f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..220f5f5 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..10e3fce --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..e636f70 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..ffaf46b --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..c298ff5 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/card/index.ts @@ -0,0 +1,24 @@ +import Root from './card.svelte'; +import Header from './card-header.svelte'; +import Title from './card-title.svelte'; +import Description from './card-description.svelte'; +import Action from './card-action.svelte'; +import Content from './card-content.svelte'; +import Footer from './card-footer.svelte'; + +export { + Root, + Header, + Title, + Description, + Action, + Content, + Footer, + Root as Card, + Header as CardHeader, + Title as CardTitle, + Description as CardDescription, + Action as CardAction, + Content as CardContent, + Footer as CardFooter, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..130a308 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,35 @@ + + + + {#snippet children({ checked })} +
+ {#if checked} + + {/if} +
+ {/snippet} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..2675e62 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,2 @@ +import Root from './checkbox.svelte'; +export { Root, Root as Checkbox }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte new file mode 100644 index 0000000..3b65b83 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/create-table.svelte @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte new file mode 100644 index 0000000..d6c8cfb --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/flex-render.svelte @@ -0,0 +1,26 @@ + + + + +{#if snippet} + {@render snippet({ props: ctx as any })} +{:else if typeof cell === 'string' || typeof cell === 'number' || typeof cell === 'boolean'} + {cell} +{:else if cell} + {@html (cell as { html?: string }).html ?? cell} +{/if} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts new file mode 100644 index 0000000..236d8ab --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/index.ts @@ -0,0 +1,5 @@ +import { CreateTable } from './create-table.svelte'; +import FlexRender from './flex-render.svelte'; +import { renderCells, renderHeader } from './render-helpers.svelte'; + +export { CreateTable, FlexRender, renderCells, renderHeader, CreateTable as DataTable }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts new file mode 100644 index 0000000..c00e6a5 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/data-table/render-helpers.svelte.ts @@ -0,0 +1,26 @@ +import type { + CellContext, + HeaderContext, + RowData, +} from '@tanstack/table-core'; +import type { Snippet } from 'svelte'; + +export function renderHeader( + ctx: HeaderContext, + render: ((ctx: HeaderContext) => unknown) | undefined, + snippet?: Snippet<[{ props: Record }]> +): unknown { + if (snippet) return snippet; + if (typeof render === 'function') return render(ctx); + return render; +} + +export function renderCells( + ctx: CellContext, + render: ((ctx: CellContext) => unknown) | undefined, + snippet?: Snippet<[{ props: Record }]> +): unknown { + if (snippet) return snippet; + if (typeof render === 'function') return render(ctx); + return render; +} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..638ec31 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,13 @@ +import Root from 'bits-ui'; + +export { + Root, + Root as Dialog, + Root.Trigger as DialogTrigger, + Root.Portal as DialogPortal, + Root.Overlay as DialogOverlay, + Root.Content as DialogContent, + Root.Title as DialogTitle, + Root.Description as DialogDescription, + Root.Close as DialogClose, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..725008d --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,20 @@ +import Root from 'bits-ui'; + +export { + Root, + Root as DropdownMenu, + Root.Trigger as DropdownMenuTrigger, + Root.Portal as DropdownMenuPortal, + Root.Content as DropdownMenuContent, + Root.Arrow as DropdownMenuArrow, + Root.Separator as DropdownMenuSeparator, + Root.Group as DropdownMenuGroup, + Root.GroupHeading as DropdownMenuGroupHeading, + Root.Item as DropdownMenuItem, + Root.CheckboxItem as DropdownMenuCheckboxItem, + Root.RadioGroup as DropdownMenuRadioGroup, + Root.RadioItem as DropdownMenuRadioItem, + Root.Sub as DropdownMenuSub, + Root.SubTrigger as DropdownMenuSubTrigger, + Root.SubContent as DropdownMenuSubContent, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte new file mode 100644 index 0000000..db3cc65 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-control.svelte @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 0000000..aee212d --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte new file mode 100644 index 0000000..bcface5 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-errors.svelte @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 0000000..0c0c421 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 0000000..3a6c343 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte new file mode 100644 index 0000000..8f1137f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-root.svelte @@ -0,0 +1,26 @@ + + + + {children} + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte new file mode 100644 index 0000000..c10163e --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/field-validation.svelte @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts new file mode 100644 index 0000000..07a79a0 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/field/index.ts @@ -0,0 +1,24 @@ +import Root from 'bits-ui'; + +import FieldRoot from './field-root.svelte'; +import FieldLabel from './field-label.svelte'; +import FieldDescription from './field-description.svelte'; +import FieldValidation from './field-validation.svelte'; +import FieldControl from './field-control.svelte'; +import FieldLegend from './field-legend.svelte'; +import FieldErrors from './field-errors.svelte'; + +export { + Root, + FieldRoot, + FieldLabel, + FieldDescription, + FieldValidation, + FieldControl, + FieldLegend, + FieldErrors, + Root as Field, + FieldRoot as FieldRoot, + Root.Field.Set as FieldSet, + Root.Field.Set as FieldSetRoot, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..f1530a2 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/index.ts @@ -0,0 +1,2 @@ +import Root from './input.svelte'; +export { Root, Root as Input }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..723b0b3 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/input/input.svelte @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..9765015 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/index.ts @@ -0,0 +1,2 @@ +import Root from './label.svelte'; +export { Root, Root as Label }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..938c85c --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..2ac4a4e --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/index.ts @@ -0,0 +1,2 @@ +import Root from './separator.svelte'; +export { Root, Root as Separator }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..9ea8ee9 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..cfec717 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,13 @@ +import Root from 'bits-ui'; + +export { + Root, + Root as Sheet, + Root.Trigger as SheetTrigger, + Root.Portal as SheetPortal, + Root.Overlay as SheetOverlay, + Root.Content as SheetContent, + Root.Title as SheetTitle, + Root.Description as SheetDescription, + Root.Close as SheetClose, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..0706a1c --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,3 @@ +export const SIDEBAR_WIDTH = '16rem'; +export const SIDEBAR_WIDTH_ICON = '3rem'; +export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte new file mode 100644 index 0000000..c877f4f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/content.svelte @@ -0,0 +1,24 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..db31c4d --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,37 @@ +import { getContext, setContext } from 'svelte'; +import { browser } from '$app/environment'; +import { SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './constants.js'; + +export const SIDEBAR_ID = Symbol('sidebar'); + +export type SidebarVariant = 'sidebar' | 'floating' | 'inset'; +export type SidebarCollapsible = 'offcanvas' | 'icon' | 'none'; + +export type SidebarProps = { + variant?: SidebarVariant; + collapsible?: SidebarCollapsible; + side?: 'left' | 'right'; +}; + +export type SidebarContextValue = { + state: 'expanded' | 'collapsed'; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + variant: SidebarVariant; + collapsible: SidebarCollapsible; + side: 'left' | 'right'; + toggleSidebar: () => void; +}; + +export function useSidebar() { + return getContext(SIDEBAR_ID); +} + +export function setSidebarContext(value: SidebarContextValue) { + setContext(SIDEBAR_ID, value); +} + +export { SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte new file mode 100644 index 0000000..94c3b3d --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/footer.svelte @@ -0,0 +1,21 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte new file mode 100644 index 0000000..8550ee2 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-action.svelte @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte new file mode 100644 index 0000000..9979ad9 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte new file mode 100644 index 0000000..cb0c28f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group-label.svelte @@ -0,0 +1,25 @@ + + +
svg]:size-4 [&>svg]:text-sidebar-accent-foreground', + 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', + className + )} + {...restProps} +> + {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte new file mode 100644 index 0000000..a00ec0a --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/group.svelte @@ -0,0 +1,24 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte new file mode 100644 index 0000000..5f51f93 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/header.svelte @@ -0,0 +1,21 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..aa36ec3 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,82 @@ +import { SIDEBAR_ID, setSidebarContext, useSidebar, type SidebarContextValue, type SidebarVariant, type SidebarCollapsible, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './context.svelte.js'; + +import Provider from './provider.svelte'; +import Root from './root.svelte'; +import Header from './header.svelte'; +import Footer from './footer.svelte'; +import Content from './content.svelte'; +import Group from './group.svelte'; +import GroupLabel from './group-label.svelte'; +import GroupAction from './group-action.svelte'; +import GroupContent from './group-content.svelte'; +import Menu from './menu.svelte'; +import MenuItem from './menu-item.svelte'; +import MenuButton from './menu-button.svelte'; +import MenuAction from './menu-action.svelte'; +import MenuBadge from './menu-badge.svelte'; +import MenuSkeleton from './menu-skeleton.svelte'; +import MenuSub from './menu-sub.svelte'; +import MenuSubItem from './menu-sub-item.svelte'; +import MenuSubButton from './menu-sub-button.svelte'; +import MenuSubTrigger from './menu-sub-trigger.svelte'; +import Rail from './rail.svelte'; +import Separator from './separator.svelte'; +import Input from './input.svelte'; +import Trigger from './trigger.svelte'; +import Inset from './inset.svelte'; + +export { + Provider, + Root, + Header, + Footer, + Content, + Group, + GroupLabel, + GroupAction, + GroupContent, + Menu, + MenuItem, + MenuButton, + MenuAction, + MenuBadge, + MenuSkeleton, + MenuSub, + MenuSubItem, + MenuSubButton, + MenuSubTrigger, + Rail, + Separator, + Input, + Trigger, + Inset, + useSidebar, + setSidebarContext, + SIDEBAR_ID, + SIDEBAR_WIDTH, + SIDEBAR_WIDTH_ICON, + Root as Sidebar, + Provider as SidebarProvider, + Header as SidebarHeader, + Footer as SidebarFooter, + Content as SidebarContent, + Group as SidebarGroup, + GroupLabel as SidebarGroupLabel, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + Menu as SidebarMenu, + MenuItem as SidebarMenuItem, + MenuButton as SidebarMenuButton, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubItem as SidebarMenuSubItem, + MenuSubButton as SidebarMenuSubButton, + MenuSubTrigger as SidebarMenuSubTrigger, + Rail as SidebarRail, + Separator as SidebarSeparator, + Input as SidebarInput, + Trigger as SidebarTrigger, + Inset as SidebarInset, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte new file mode 100644 index 0000000..dfa43d2 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/input.svelte @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte new file mode 100644 index 0000000..43fb041 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/inset.svelte @@ -0,0 +1,28 @@ + + +
+ {children} +
\ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte new file mode 100644 index 0000000..87b5cc1 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-action.svelte @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte new file mode 100644 index 0000000..5bf6da8 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-badge.svelte @@ -0,0 +1,26 @@ + + + + {children} + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte new file mode 100644 index 0000000..abcea33 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-button.svelte @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte new file mode 100644 index 0000000..6639687 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-item.svelte @@ -0,0 +1,24 @@ + + +
  • + {children} +
  • \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte new file mode 100644 index 0000000..0b2d871 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-skeleton.svelte @@ -0,0 +1,21 @@ + + +
    + {children} +
    \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte new file mode 100644 index 0000000..bb4dc58 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-button.svelte @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte new file mode 100644 index 0000000..5645aa4 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {children} +
  • \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte new file mode 100644 index 0000000..43acdd7 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub-trigger.svelte @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte new file mode 100644 index 0000000..e62f33d --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu-sub.svelte @@ -0,0 +1,21 @@ + + +
  • + {children} +
  • \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte new file mode 100644 index 0000000..f11f3b1 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/menu.svelte @@ -0,0 +1,21 @@ + + +
      + {children} +
    \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte new file mode 100644 index 0000000..f15f24f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/provider.svelte @@ -0,0 +1,60 @@ + + +{@render children?.()} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte new file mode 100644 index 0000000..6f76476 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/rail.svelte @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte new file mode 100644 index 0000000..8e7a032 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/root.svelte @@ -0,0 +1,55 @@ + + +{#if sidebar?.isMobile} +
    + {children} +
    +{:else} +
    + {children} +
    +{/if} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte new file mode 100644 index 0000000..5f9aa12 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/separator.svelte @@ -0,0 +1,19 @@ + + +
    \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte new file mode 100644 index 0000000..45542a2 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sidebar/trigger.svelte @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..c3bc460 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,2 @@ +import Root from './skeleton.svelte'; +export { Root, Root as Skeleton }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..16980fc --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..cee8169 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/index.ts @@ -0,0 +1,3 @@ +import { Sonner } from './sonner.svelte'; + +export { Sonner, Sonner as Toaster }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..3465b61 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000..64ed57f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/index.ts @@ -0,0 +1,2 @@ +import Root from './switch.svelte'; +export { Root, Root as Switch }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000..f38ed28 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,36 @@ + + + + + {@render children?.()} + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..a998b0e --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,2 @@ +import Root from './textarea.svelte'; +export { Root, Root as Textarea }; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..28ec49d --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..3e80708 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,9 @@ +import Root from 'bits-ui'; + +export { + Root, + Root as Tooltip, + Root.Provider as TooltipProvider, + Root.Trigger as TooltipTrigger, + Root.Content as TooltipContent, +}; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..f901a2f --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,26 @@ +import { browser } from '$app/environment'; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const isMobile = $state( + browser ? window.innerWidth < MOBILE_BREAKPOINT : false + ); + + if (browser) { + const media = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const handler = (event: MediaQueryListEvent) => { + isMobile = event.matches; + }; + media.addEventListener('change', handler); + $effect(() => { + return () => media.removeEventListener('change', handler); + }); + } + + return { + get current() { + return isMobile; + }, + }; +} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js deleted file mode 100644 index 21a30f4..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.svelte.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Counter store — Svelte 5 runes ($state) in a *.svelte.js module. - * Import the store and use `.count` and `.increment()`. - */ - -export function createCounter(initial = 0) { - let count = $state(initial); - return { - get count() { - return count; - }, - increment() { - count += 1; - }, - decrement() { - count -= 1; - }, - reset() { - count = initial; - }, - }; -} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js deleted file mode 100644 index 1b031e6..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/stores/counter.test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { createCounter } from './counter.svelte.js'; - -describe('createCounter', () => { - it('starts at the initial value', () => { - const c = createCounter(5); - expect(c.count).toBe(5); - }); - - it('increments', () => { - const c = createCounter(0); - c.increment(); - expect(c.count).toBe(1); - }); - - it('decrements', () => { - const c = createCounter(3); - c.decrement(); - expect(c.count).toBe(2); - }); - - it('resets to initial', () => { - const c = createCounter(7); - c.increment(); - c.increment(); - c.reset(); - expect(c.count).toBe(7); - }); -}); \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts new file mode 100644 index 0000000..1f58194 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/cn.svelte.ts @@ -0,0 +1,22 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} + +export type WithElementRef = T & { + ref?: U | null; +}; + +export type WithoutChildren = T extends any + ? 'children' extends keyof T + ? Omit + : T + : T; + +export type WithoutChild = T extends any + ? 'child' extends keyof T + ? Omit + : T + : T; \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js deleted file mode 100644 index b0635aa..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Misc utility helpers. - */ - -export function formatDate(date) { - return new Date(date).toLocaleDateString(); -} - -export function truncate(str, n) { - return str.length > n ? `${str.slice(0, n)}…` : str; -} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js deleted file mode 100644 index de25021..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/lib/utils/format.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { formatDate, truncate } from './format.js'; - -describe('formatDate', () => { - it('formats a date string', () => { - const out = formatDate('2024-01-15'); - expect(typeof out).toBe('string'); - expect(out.length).toBeGreaterThan(0); - }); -}); - -describe('truncate', () => { - it('returns the string when shorter than n', () => { - expect(truncate('abc', 10)).toBe('abc'); - }); - - it('truncates with ellipsis when longer', () => { - expect(truncate('abcdef', 3)).toBe('abc…'); - }); -}); \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte index c142e42..7e45317 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+error.svelte @@ -1,9 +1,9 @@ - - {page.status} — {{ cookiecutter.project_name }} + {page.status}

    {page.status}

    diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..74987a1 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.svelte @@ -0,0 +1,20 @@ + + + + + + + +
    + {@render children?.()} +
    +
    +
    \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts new file mode 100644 index 0000000..c7ec014 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+layout.ts @@ -0,0 +1,5 @@ +import type { LayoutLoad } from './$types'; + +export function load(): { title: string } { + return { title: '{{ cookiecutter.project_name }}' }; +} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js deleted file mode 100644 index 3d58f98..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('./$types').PageLoad} */ -export function load() { - return { title: '{{ cookiecutter.project_name }}' }; -} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte index 1e34cf2..068c63e 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.svelte @@ -1,31 +1,62 @@ - {data.title} -
    -

    {{ cookiecutter.description }}

    -
    + -
    -

    Counter: {counter.count}

    - - - -
    - - \ No newline at end of file + + + Channels + Recent channel activity. + + + + + + {#each columns as col} + + {/each} + + + + {#each rows as row (row.id)} + + + + + + + {/each} + +
    {col.header as string}
    {row.id}{row.name}{row.status}{row.uploaded}
    +
    +
    \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts new file mode 100644 index 0000000..ee4a97b --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/src/routes/+page.ts @@ -0,0 +1,5 @@ +import type { PageLoad } from './$types'; + +export function load(): { title: string } { + return { title: '{{ cookiecutter.project_name }}' }; +} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js index 41f6844..123b0d6 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/svelte.config.js @@ -8,7 +8,15 @@ const config = { adapter: adapter(), alias: { $lib: 'src/lib', - $components: 'src/lib/components', + '$lib/*': 'src/lib/*', + '$lib/utils': 'src/lib/utils', + '$lib/utils/*': 'src/lib/utils/*', + '$lib/hooks': 'src/lib/hooks', + '$lib/hooks/*': 'src/lib/hooks/*', + '$lib/components': 'src/lib/components', + '$lib/components/*': 'src/lib/components/*', + '$lib/components/ui': 'src/lib/components/ui', + '$lib/components/ui/*': 'src/lib/components/ui/*', }, }, }; diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js deleted file mode 100644 index 2402220..0000000 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('homepage shows the project title', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('h1')).toContainText('{{ cookiecutter.project_name }}'); -}); - -test('counter increments on click', async ({ page }) => { - await page.goto('/'); - const buttons = page.getByRole('button'); - await expect(buttons.first()).toBeVisible(); -}); \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts new file mode 100644 index 0000000..0dee708 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tests/e2e/app.spec.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test'; + +test('sidebar visible on dashboard load', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('[data-sidebar="sidebar"]')).toBeVisible(); +}); \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json new file mode 100644 index 0000000..6fa1127 --- /dev/null +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "@tsconfig/svelte", + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "verbatimModuleSyntax": false, + "baseUrl": ".", + "paths": { + "$lib": ["src/lib"], + "$lib/*": ["src/lib/*"], + "$lib/utils": ["src/lib/utils"], + "$lib/utils/*": ["src/lib/utils/*"], + "$lib/hooks": ["src/lib/hooks"], + "$lib/hooks/*": ["src/lib/hooks/*"], + "$lib/components": ["src/lib/components"], + "$lib/components/*": ["src/lib/components/*"], + "$lib/components/ui": ["src/lib/components/ui"], + "$lib/components/ui/*": ["src/lib/components/ui/*"] + } + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.ts", + "src/**/*.js", + "src/**/*.svelte", + "tests/**/*.ts", + "vite.config.ts", + "vitest.config.ts", + "playwright.config.js" + ] +} \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.ts similarity index 60% rename from .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.js rename to .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.ts index 822e24e..09bde61 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.js +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vite.config.ts @@ -1,6 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/vite-plugin-svelte'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()], + plugins: [tailwindcss(), sveltekit()], }); \ No newline at end of file diff --git a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.js b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.ts similarity index 56% rename from .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.js rename to .opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.ts index 6b6bc7c..4ef1c32 100644 --- a/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.js +++ b/.opencode/templates/fullstack/{{cookiecutter.project_name}}/frontend/vitest.config.ts @@ -1,12 +1,10 @@ import { defineConfig } from 'vitest/config'; import { svelte } from '@sveltejs/vite-plugin-svelte'; -// Separate vitest config — the sveltekit() plugin forces SSR which -// breaks jsdom-based component tests. Use the plain svelte() plugin. export default defineConfig({ plugins: [svelte({ hot: !process.env.VITEST })], test: { environment: 'jsdom', - include: ['src/**/*.test.js'], + include: ['src/**/*.test.ts'], }, }); \ No newline at end of file diff --git a/tests/test_cookiecutter_templates.py b/tests/test_cookiecutter_templates.py index 0ed94a1..d8d20e5 100644 --- a/tests/test_cookiecutter_templates.py +++ b/tests/test_cookiecutter_templates.py @@ -241,19 +241,26 @@ def test_fullstack_structure(render): assert (render / "backend/src/fs/api/v1/routes/users.py").exists() assert (render / "backend/src/fs/services/auth_service.py").exists() assert (render / "backend/migrations").is_dir() - # frontend has the SvelteKit stack + # frontend has the SvelteKit + Tailwind v4 + shadcn-svelte stack (TS) assert (render / "frontend/package.json").exists() assert (render / "frontend/svelte.config.js").exists() - assert (render / "frontend/vite.config.js").exists() - assert (render / "frontend/vitest.config.js").exists() + assert (render / "frontend/vite.config.ts").exists() + assert (render / "frontend/vitest.config.ts").exists() assert (render / "frontend/biome.json").exists() assert (render / "frontend/knip.json").exists() - assert (render / "frontend/jsconfig.json").exists() + assert (render / "frontend/tsconfig.json").exists() + assert (render / "frontend/components.json").exists() assert (render / "frontend/src/app.html").exists() + assert (render / "frontend/src/app.css").exists() assert (render / "frontend/src/hooks.server.js").exists() + assert (render / "frontend/src/routes/+layout.svelte").exists() assert (render / "frontend/src/routes/+page.svelte").exists() - assert (render / "frontend/src/lib/stores/counter.svelte.js").exists() - assert (render / "frontend/tests/e2e/app.spec.js").exists() + assert (render / "frontend/src/routes/+page.ts").exists() + assert (render / "frontend/src/lib/components/app-sidebar.svelte").exists() + assert (render / "frontend/src/lib/utils/cn.svelte.ts").exists() + assert (render / "frontend/src/lib/hooks/is-mobile.svelte.ts").exists() + assert (render / "frontend/tests/e2e/app.spec.ts").exists() + # removed: counter.svelte.js, Header.svelte, jsconfig.json, +page.js # root CI runs both assert (render / ".github/workflows/ci.yml").exists() assert (render / "README.md").exists() @@ -282,9 +289,10 @@ def test_fullstack_conditional_stripped(render): @pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})]) def test_fullstack_frontend_uses_svelte5(render): - """package.json pins svelte 5.""" + """package.json pins svelte 5 (in dependencies or devDependencies).""" pkg = json.loads((render / "frontend/package.json").read_text()) - svelte_dep = pkg.get("devDependencies", {}).get("svelte", "") + deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})} + svelte_dep = deps.get("svelte", "") assert svelte_dep.startswith("^5.") or svelte_dep.startswith("5"), svelte_dep @@ -308,9 +316,9 @@ def test_fullstack_error_page_is_fragment(render): def test_fullstack_page_uses_data_title(render): """+page.svelte must consume ``data.title`` from the load function.""" page_svelte = (render / "frontend/src/routes/+page.svelte").read_text() - page_js = (render / "frontend/src/routes/+page.js").read_text() + page_ts = (render / "frontend/src/routes/+page.ts").read_text() # load returns { title: ... } - assert "title" in page_js + assert "title" in page_ts # +page.svelte consumes data.title (not a hardcoded cookiecutter literal) assert "data.title" in page_svelte, "+page.svelte must use data.title from load" @@ -322,6 +330,76 @@ def test_fullstack_hooks_no_unused_redirect(render): assert "redirect" not in hooks, "hooks.server.js must not import unused redirect" +@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})]) +def test_fullstack_has_tailwind_config(render): + """vite.config.ts wires @tailwindcss/vite; package.json pins tailwindcss.""" + vite = (render / "frontend/vite.config.ts").read_text() + assert "tailwindcss" in vite, "vite.config.ts must wire @tailwindcss/vite" + # tailwindcss() must run BEFORE sveltekit() (order matters) + assert vite.index("tailwindcss") < vite.index("sveltekit") + pkg = json.loads((render / "frontend/package.json").read_text()) + deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})} + assert "tailwindcss" in deps, "package.json must depend on tailwindcss" + assert "@tailwindcss/vite" in deps, "package.json must depend on @tailwindcss/vite" + + +@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})]) +def test_fullstack_has_shadcn_components_json(render): + """components.json configures shadcn-svelte (new-york, neutral, TS).""" + cfg_path = render / "frontend/components.json" + assert cfg_path.exists(), "components.json must exist" + cfg = json.loads(cfg_path.read_text()) + assert cfg["style"] == "new-york", f"style must be new-york, got {cfg.get('style')}" + assert cfg["tailwind"]["baseColor"] == "neutral" + assert cfg["typescript"] is True, "typescript must be true" + aliases = cfg["aliases"] + assert aliases["lib"] == "$lib" + assert aliases["utils"] == "$lib/utils" + assert aliases["ui"] == "$lib/components/ui" + assert aliases["hooks"] == "$lib/hooks" + + +@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})]) +def test_fullstack_layout_has_sidebar_provider(render): + """+layout.svelte wires the sidebar around the content (directly or via + AppSidebar which embeds Sidebar.Provider).""" + layout = (render / "frontend/src/routes/+layout.svelte").read_text() + has_provider = "Sidebar.Provider" in layout or "SidebarProvider" in layout + has_app_sidebar = "AppSidebar" in layout + assert has_provider or has_app_sidebar, ( + "+layout.svelte must wrap content in Sidebar.Provider (directly or via AppSidebar)" + ) + # AppSidebar itself must contain the Sidebar.Provider + app_sidebar = (render / "frontend/src/lib/components/app-sidebar.svelte").read_text() + assert "Sidebar.Provider" in app_sidebar or "SidebarProvider" in app_sidebar, ( + "app-sidebar.svelte must embed Sidebar.Provider" + ) + + +@pytest.mark.parametrize("template_name, extra_context", [("fullstack", {"project_name": "fs"})]) +def test_fullstack_uses_typescript(render): + """Frontend is TypeScript: tsconfig.json, no jsconfig.json, +page.ts, lang=ts.""" + assert (render / "frontend/tsconfig.json").exists(), "tsconfig.json must exist" + assert not (render / "frontend/jsconfig.json").exists(), "jsconfig.json must be removed" + assert (render / "frontend/src/routes/+page.ts").exists(), "+page.ts must exist" + assert not (render / "frontend/src/routes/+page.js").exists(), "+page.js must be removed" + page_svelte = (render / "frontend/src/routes/+page.svelte").read_text() + assert 'lang="ts"' in page_svelte, '+page.svelte must use