feat(template): bake shadcn-svelte + Tailwind v4 + TS into fullstack cookiecutter (#265)
* 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 <agent@opencode.local>
This commit is contained in:
parent
e4f9313641
commit
7c3ded601f
104 changed files with 2206 additions and 211 deletions
|
|
@ -81,6 +81,9 @@ STACK_REQUIRED: dict[str, list[str]] = {
|
||||||
"ruff",
|
"ruff",
|
||||||
"mypy",
|
"mypy",
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"tailwind",
|
||||||
|
"shadcn",
|
||||||
|
"typescript",
|
||||||
],
|
],
|
||||||
"mcp-server": ["fastapi", "mcp", "patchright", "uv"],
|
"mcp-server": ["fastapi", "mcp", "patchright", "uv"],
|
||||||
"cli": ["typer", "uv", "hatchling", "ruff", "mypy", "pytest"],
|
"cli": ["typer", "uv", "hatchling", "ruff", "mypy", "pytest"],
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ init flow.
|
||||||
```
|
```
|
||||||
Выбери тип проекта (из spec-status VALID_TYPES):
|
Выбери тип проекта (из spec-status VALID_TYPES):
|
||||||
[1] backend — FastAPI + Tortoise, REST API (cookiecutter template)
|
[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)
|
[3] cli — Python CLI tool, Typer (cookiecutter template)
|
||||||
[4] mcp-server — MCP + REST сервер (нет cookiecutter template — ручная инициализация)
|
[4] mcp-server — MCP + REST сервер (нет cookiecutter template — ручная инициализация)
|
||||||
[5] bot — Telegram bot, aiogram 3 (нет cookiecutter template — ручная инициализация)
|
[5] bot — Telegram bot, aiogram 3 (нет cookiecutter template — ручная инициализация)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Общий для всех типов: 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`)
|
- **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
|
- **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
|
||||||
- **cli**: Typer (default) / click / argparse, hatchling build
|
- **cli**: Typer (default) / click / argparse, hatchling build
|
||||||
- **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
- **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
||||||
|
|
@ -65,7 +65,7 @@ Prompt template A (см. ниже).
|
||||||
```
|
```
|
||||||
Выбери тип проекта:
|
Выбери тип проекта:
|
||||||
[1] backend — FastAPI + Tortoise, REST API, без frontend
|
[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)
|
[3] mcp-server — MCP + REST сервер (Patchright/Playwright over CDP)
|
||||||
[4] cli — Python CLI tool (Typer)
|
[4] cli — Python CLI tool (Typer)
|
||||||
[5] bot — Telegram bot (aiogram 3)
|
[5] bot — Telegram bot (aiogram 3)
|
||||||
|
|
@ -88,7 +88,7 @@ backend:
|
||||||
- Auth: [1] none v1 / [2] JWT / [3] X-API-Key
|
- Auth: [1] none v1 / [2] JWT / [3] X-API-Key
|
||||||
|
|
||||||
fullstack:
|
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)
|
- DB: (same as backend)
|
||||||
- Auth: (same as backend)
|
- Auth: (same as backend)
|
||||||
|
|
||||||
|
|
@ -236,7 +236,7 @@ Spec complete. Issues: #N1, #N2, ...
|
||||||
Default stack для типа (хардкод, добавить всегда):
|
Default stack для типа (хардкод, добавить всегда):
|
||||||
- Общий: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI
|
- Общий: 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`)
|
- 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
|
- mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
|
||||||
- cli: Typer (default) / click / argparse, hatchling build
|
- cli: Typer (default) / click / argparse, hatchling build
|
||||||
- bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
- bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,11 @@
|
||||||
"description": "Fullstack project description",
|
"description": "Fullstack project description",
|
||||||
"use_auth": ["no", "yes"],
|
"use_auth": ["no", "yes"],
|
||||||
"use_db": ["yes", "no"],
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -21,5 +21,17 @@
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignore": ["build", ".svelte-kit", "playwright-report"]
|
"ignore": ["build", ".svelte-kit", "playwright-report"]
|
||||||
}
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"include": ["src/lib/components/ui/**/*.svelte", "src/lib/components/ui/**/*.ts"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"entry": ["src/**/*.{js,svelte}", "!src/**/*.test.js"],
|
"entry": ["src/**/*.{ts,svelte}", "!src/**/*.test.ts"],
|
||||||
"project": ["src/**/*.{js,svelte}"],
|
"project": ["src/**/*.{ts,svelte}"],
|
||||||
"ignore": ["build", ".svelte-kit"],
|
"ignore": ["build", ".svelte-kit"],
|
||||||
"ignoreBinaries": true,
|
"ignoreBinaries": true,
|
||||||
"ignoreDependencies": false
|
"ignoreDependencies": false
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,22 @@
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:e2e": "playwright test",
|
"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 .",
|
"lint": "biome check .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
"knip": "knip"
|
"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": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.0.0",
|
"@sveltejs/adapter-node": "^5.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
|
@ -25,6 +36,11 @@
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vitest": "^2.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,23 +1,123 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #ffffff;
|
--background: oklch(1 0 0);
|
||||||
--fg: #1a1a1a;
|
--foreground: oklch(0.145 0 0);
|
||||||
--accent: #646cff;
|
--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) {
|
.dark {
|
||||||
:root {
|
--background: oklch(0.145 0 0);
|
||||||
--bg: #1a1a1a;
|
--foreground: oklch(0.985 0 0);
|
||||||
--fg: #ffffff;
|
--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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* @prop {string} title
|
|
||||||
* @prop {import('svelte').Snippet} [children]
|
|
||||||
*/
|
|
||||||
let { title, children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<h1>{title}</h1>
|
|
||||||
{@render children?.()}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
header {
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
|
import NavMain from './nav-main.svelte';
|
||||||
|
import NavUser from './nav-user.svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
variant = 'inset',
|
||||||
|
collapsible = 'icon',
|
||||||
|
title = '',
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
variant?: 'sidebar' | 'floating' | 'inset';
|
||||||
|
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||||
|
title?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Provider {variant} {collapsible}>
|
||||||
|
<Sidebar.Root>
|
||||||
|
<Sidebar.Header>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="flex items-center gap-2 px-2 py-1.5 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<span class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 11a9 9 0 0 1 9 9" />
|
||||||
|
<path d="M4 4a16 16 0 0 1 16 16" />
|
||||||
|
<circle cx="5" cy="19" r="1" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="truncate">{title}</span>
|
||||||
|
</a>
|
||||||
|
</Sidebar.Header>
|
||||||
|
<Sidebar.Content>
|
||||||
|
<NavMain />
|
||||||
|
</Sidebar.Content>
|
||||||
|
<Sidebar.Footer>
|
||||||
|
<NavUser />
|
||||||
|
</Sidebar.Footer>
|
||||||
|
</Sidebar.Root>
|
||||||
|
{@render children?.()}
|
||||||
|
</Sidebar.Provider>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
|
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard';
|
||||||
|
import RadioIcon from '@lucide/svelte/icons/radio';
|
||||||
|
import UploadIcon from '@lucide/svelte/icons/upload';
|
||||||
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ title: 'Dashboard', url: '/', icon: LayoutDashboardIcon },
|
||||||
|
{ title: 'Channels', url: '/channels', icon: RadioIcon },
|
||||||
|
{ title: 'Uploads', url: '/uploads', icon: UploadIcon },
|
||||||
|
{ title: 'Settings', url: '/settings', icon: SettingsIcon },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<a href={item.url} class="flex items-center gap-2">
|
||||||
|
<item.icon class="size-4" />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
|
import UserIcon from '@lucide/svelte/icons/user';
|
||||||
|
import LogOutIcon from '@lucide/svelte/icons/log-out';
|
||||||
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||||
|
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: 'Operator',
|
||||||
|
email: 'operator@example.com',
|
||||||
|
avatar: '',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
class="flex w-full items-center gap-2 rounded-md p-2 text-sm outline-none hover:bg-sidebar-accent"
|
||||||
|
>
|
||||||
|
<span class="bg-muted flex size-7 items-center justify-center rounded-md">
|
||||||
|
<UserIcon class="size-4" />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col text-left">
|
||||||
|
<span class="text-sm font-medium">{user.name}</span>
|
||||||
|
<span class="text-xs text-muted-foreground">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronUpIcon class="ml-auto size-4" />
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content side="top" class="w-56">
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item href="/settings">
|
||||||
|
<SettingsIcon class="size-4" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<UserIcon class="size-4" />
|
||||||
|
Profile
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<LogOutIcon class="size-4" />
|
||||||
|
Sign out
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
|
import UsersIcon from '@lucide/svelte/icons/users';
|
||||||
|
import UploadIcon from '@lucide/svelte/icons/upload';
|
||||||
|
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
||||||
|
import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
title: 'Total Channels',
|
||||||
|
value: '12',
|
||||||
|
delta: '+2 this week',
|
||||||
|
icon: UsersIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Active Uploads',
|
||||||
|
value: '4',
|
||||||
|
delta: '+1 today',
|
||||||
|
icon: UploadIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Pending Tasks',
|
||||||
|
value: '7',
|
||||||
|
delta: '-3 since yesterday',
|
||||||
|
icon: ListTodoIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Success Rate',
|
||||||
|
value: '98.4%',
|
||||||
|
delta: '+0.6% mtd',
|
||||||
|
icon: TrendingUpIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 px-4 py-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{#each cards as card (card.title)}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Card.Title class="text-sm font-medium text-muted-foreground">
|
||||||
|
{card.title}
|
||||||
|
</Card.Title>
|
||||||
|
<card.icon class="text-muted-foreground size-4" />
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<div class="text-2xl font-semibold">{card.value}</div>
|
||||||
|
<p class="text-muted-foreground mt-1 text-xs">{card.delta}</p>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
|
import { cn } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
title = '',
|
||||||
|
children,
|
||||||
|
}: { title?: string; children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="bg-background sticky top-0 z-10 flex h-14 items-center gap-2 border-b px-4">
|
||||||
|
<Sidebar.Trigger class="-ml-1" />
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h1 class={cn('text-sm font-medium', !title && 'hidden md:block')}>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center gap-2">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
||||||
|
outline:
|
||||||
|
'border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||||
|
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
|
icon: 'size-9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = 'button',
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? 'link' : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-content"
|
||||||
|
class={cn('px-6', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn('font-medium leading-none', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
class={cn(
|
||||||
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
|
|
||||||
|
type Props = WithoutChildren<Root.Checkbox.RootProps> & {
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Checkbox.Root
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
data-slot="checkbox"
|
||||||
|
class={cn(
|
||||||
|
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<div class="flex items-center justify-center text-current transition-none">
|
||||||
|
{#if checked}
|
||||||
|
<CheckIcon class="size-3.5" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Root.Checkbox.Root>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './checkbox.svelte';
|
||||||
|
export { Root, Root as Checkbox };
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
import {
|
||||||
|
type TableOptions,
|
||||||
|
type Table,
|
||||||
|
createTable,
|
||||||
|
type ColumnDef,
|
||||||
|
} from '@tanstack/table-core';
|
||||||
|
|
||||||
|
export type DataTableProps<TData, TValue> = {
|
||||||
|
data: TData[];
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createTableInstance<TData, TValue>(
|
||||||
|
options: TableOptions<TData>
|
||||||
|
): Table<TData> {
|
||||||
|
return createTable(options);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
import type { CellContext, HeaderContext, RowData } from '@tanstack/table-core';
|
||||||
|
import { type Snippet } from 'svelte';
|
||||||
|
|
||||||
|
export type FlexRenderProps<TData extends RowData, TValue> = {
|
||||||
|
ctx: CellContext<TData, TValue> | HeaderContext<TData, TValue>;
|
||||||
|
render: ((ctx: CellContext<TData, TValue> | HeaderContext<TData, TValue>) => unknown) | undefined;
|
||||||
|
snippet?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let { ctx, render, snippet }: import('./flex-render.svelte').FlexRenderProps<any, any> = $props();
|
||||||
|
|
||||||
|
const cell = $derived(
|
||||||
|
typeof render === 'function' ? (render as any)(ctx) : render
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#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}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type {
|
||||||
|
CellContext,
|
||||||
|
HeaderContext,
|
||||||
|
RowData,
|
||||||
|
} from '@tanstack/table-core';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
export function renderHeader<TData extends RowData, TValue>(
|
||||||
|
ctx: HeaderContext<TData, TValue>,
|
||||||
|
render: ((ctx: HeaderContext<TData, TValue>) => unknown) | undefined,
|
||||||
|
snippet?: Snippet<[{ props: Record<string, unknown> }]>
|
||||||
|
): unknown {
|
||||||
|
if (snippet) return snippet;
|
||||||
|
if (typeof render === 'function') return render(ctx);
|
||||||
|
return render;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderCells<TData extends RowData, TValue>(
|
||||||
|
ctx: CellContext<TData, TValue>,
|
||||||
|
render: ((ctx: CellContext<TData, TValue>) => unknown) | undefined,
|
||||||
|
snippet?: Snippet<[{ props: Record<string, unknown> }]>
|
||||||
|
): unknown {
|
||||||
|
if (snippet) return snippet;
|
||||||
|
if (typeof render === 'function') return render(ctx);
|
||||||
|
return render;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Field.ControlProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Control
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-control"
|
||||||
|
class={cn('flex flex-col gap-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Field.DescriptionProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-description"
|
||||||
|
class={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Field.ErrorsProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Errors
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-errors"
|
||||||
|
class={cn('text-destructive text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Field.LabelProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Label
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-label"
|
||||||
|
class={cn('text-sm leading-none font-medium select-none', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Field.LegendProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Legend
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-legend"
|
||||||
|
class={cn('text-sm leading-none font-medium select-none', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChild } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
type Props = WithoutChild<Root.Field.RootProps> & {
|
||||||
|
class?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-root"
|
||||||
|
class={cn('flex flex-col gap-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Root.Field.Root>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Field.ValidationProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Field.Validation
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-validation"
|
||||||
|
class={cn('text-destructive text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './input.svelte';
|
||||||
|
export { Root, Root as Input };
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
type InputProps = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, 'value'> & {
|
||||||
|
value?: string | number | null;
|
||||||
|
files?: FileList | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: InputProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
bind:value
|
||||||
|
data-slot="input"
|
||||||
|
{type}
|
||||||
|
class={cn(
|
||||||
|
'border-input bg-background ring-offset-background placeholder:text-muted-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
'file:text-foreground file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './label.svelte';
|
||||||
|
export { Root, Root as Label };
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<Root.Label.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Label.Root
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
'flex items-center gap-2 text-sm leading-none font-medium select-none',
|
||||||
|
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50 group-data-[invalid]:text-destructive',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './separator.svelte';
|
||||||
|
export { Root, Root as Separator };
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
|
||||||
|
type Props = WithoutChildren<Root.Separator.RootProps> & {
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = 'horizontal',
|
||||||
|
decorative = true,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Separator.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="separator"
|
||||||
|
{orientation}
|
||||||
|
{decorative}
|
||||||
|
class={cn(
|
||||||
|
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const SIDEBAR_WIDTH = '16rem';
|
||||||
|
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||||
|
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-content"
|
||||||
|
data-sidebar="content"
|
||||||
|
class={cn(
|
||||||
|
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -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<SidebarContextValue>(SIDEBAR_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSidebarContext(value: SidebarContextValue) {
|
||||||
|
setContext(SIDEBAR_ID, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON };
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-footer"
|
||||||
|
data-sidebar="footer"
|
||||||
|
class={cn('flex flex-col gap-2 p-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-group-action"
|
||||||
|
data-sidebar="group-action"
|
||||||
|
class={cn(
|
||||||
|
'text-sidebar-foreground/70 ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-group-content"
|
||||||
|
data-sidebar="group-content"
|
||||||
|
class={cn('w-full text-sm', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-group-label"
|
||||||
|
data-sidebar="group-label"
|
||||||
|
class={cn(
|
||||||
|
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:text-sidebar-accent-foreground',
|
||||||
|
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-group"
|
||||||
|
data-sidebar="group"
|
||||||
|
class={cn(
|
||||||
|
'group/sidebar-group relative flex w-full min-w-0 flex-col p-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-header"
|
||||||
|
data-sidebar="header"
|
||||||
|
class={cn('flex flex-col gap-2 p-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLInputElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-input"
|
||||||
|
data-sidebar="input"
|
||||||
|
class={cn(
|
||||||
|
'bg-background border-input ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-8 w-full min-w-0 rounded-md border px-2 py-1 text-sm shadow-xs outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import { useSidebar } from './context.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-inset"
|
||||||
|
class={cn(
|
||||||
|
'bg-background relative flex min-h-svh flex-1 flex-col',
|
||||||
|
'peer-data-[variant=inset]:shadow-xs peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 peer-data-[variant=inset]:ml-0 peer-data-[variant=inset]:peer-data-[side=right]:ml-2 peer-data-[variant=inset]:peer-data-[side=right]:mr-2 peer-data-[variant=inset]:peer-data-[side=right]:peer-data-[state=collapsed]:mr-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:peer-data-[side=right]:peer-data-[state=collapsed]:mr-2 md:peer-data-[variant=inset]:peer-data-[side=right]:ml-2 md:peer-data-[variant=inset]:peer-data-[side=right]:peer-data-[state=collapsed]:mr-2 md:peer-data-[variant=inset]:peer-data-[side=right]:mr-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||||
|
sidebar?.variant === 'inset' && 'ml-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-action"
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
class={cn(
|
||||||
|
'text-sidebar-foreground/70 ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:text-sidebar-accent-foreground',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-badge"
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
class={cn(
|
||||||
|
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
|
||||||
|
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { useSidebar } from './context.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>> & { children?: Snippet } = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-button"
|
||||||
|
data-sidebar="menu-button"
|
||||||
|
class={cn(
|
||||||
|
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-invalid:border-destructive aria-invalid:text-destructive group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onclick={() => sidebar?.setOpenMobile(false)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLLIElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-item"
|
||||||
|
data-sidebar="menu-item"
|
||||||
|
class={cn(
|
||||||
|
'group/menu-item relative',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-skeleton"
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
class={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
import { useSidebar } from './context.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>> & { children?: Snippet } = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-sub-button"
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
class={cn(
|
||||||
|
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground/70 outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:text-destructive [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onclick={() => sidebar?.setOpenMobile(false)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLLIElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-sub-item"
|
||||||
|
data-sidebar="menu-sub-item"
|
||||||
|
class={cn('group/menu-sub-item relative', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-sub-trigger"
|
||||||
|
data-sidebar="menu-sub-trigger"
|
||||||
|
class={cn(
|
||||||
|
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground/70 outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:text-destructive [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLLIElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu-sub"
|
||||||
|
data-sidebar="menu-sub"
|
||||||
|
class={cn('border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5', 'group-data-[collapsible=icon]:hidden', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLUListElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-menu"
|
||||||
|
data-sidebar="menu"
|
||||||
|
class={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { setSidebarContext, type SidebarContextValue, type SidebarProps } from './context.svelte.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
variant = 'sidebar',
|
||||||
|
collapsible = 'offcanvas',
|
||||||
|
side = 'left',
|
||||||
|
open = $bindable(false),
|
||||||
|
openMobile = $bindable(false),
|
||||||
|
children,
|
||||||
|
}: SidebarProps & {
|
||||||
|
open?: boolean;
|
||||||
|
openMobile?: boolean;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let isMobile = $state(false);
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
const mq = window.matchMedia('(max-width: 767px)');
|
||||||
|
isMobile = mq.matches;
|
||||||
|
const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
|
||||||
|
mq.addEventListener('change', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOpen = (v: boolean) => (open = v);
|
||||||
|
const setOpenMobile = (v: boolean) => (openMobile = v);
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
if (isMobile) {
|
||||||
|
setOpenMobile(!openMobile);
|
||||||
|
} else {
|
||||||
|
setOpen(!open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx: SidebarContextValue = {
|
||||||
|
get state() {
|
||||||
|
return open ? 'expanded' : 'collapsed';
|
||||||
|
},
|
||||||
|
get open() {
|
||||||
|
return open;
|
||||||
|
},
|
||||||
|
setOpen,
|
||||||
|
get openMobile() {
|
||||||
|
return openMobile;
|
||||||
|
},
|
||||||
|
setOpenMobile,
|
||||||
|
get isMobile() {
|
||||||
|
return isMobile;
|
||||||
|
},
|
||||||
|
variant,
|
||||||
|
collapsible,
|
||||||
|
side,
|
||||||
|
toggleSidebar,
|
||||||
|
};
|
||||||
|
setSidebarContext(ctx);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children?.()}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-rail"
|
||||||
|
data-sidebar="rail"
|
||||||
|
class={cn(
|
||||||
|
'bg-sidebar-border hover:bg-sidebar-border/50 absolute inset-y-0 z-20 hidden w-px transition-[opacity]',
|
||||||
|
'group-data-[side=left]:-right-4 group-data[side=right]:-left-4',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import { useSidebar } from './context.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet } = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if sidebar?.isMobile}
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar"
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
class={cn(
|
||||||
|
'bg-background text-foreground fixed inset-y-0 z-50 flex w-(--sidebar-width) flex-col transition-transform duration-300',
|
||||||
|
sidebar.side === 'right' ? 'right-0' : 'left-0',
|
||||||
|
sidebar.openMobile
|
||||||
|
? 'translate-x-0'
|
||||||
|
: sidebar.side === 'right'
|
||||||
|
? 'translate-x-full'
|
||||||
|
: '-translate-x-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar"
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
class={cn(
|
||||||
|
'bg-sidebar text-sidebar-foreground group/sidebar flex h-svh w-(--sidebar-width) flex-col transition-[width,transform] duration-200',
|
||||||
|
sidebar?.collapsible === 'icon' && !sidebar?.open && 'w-(--sidebar-width-icon)',
|
||||||
|
sidebar?.collapsible === 'offcanvas' && !sidebar?.open && '-translate-x-full',
|
||||||
|
sidebar?.side === 'right' ? 'ml-auto' : '',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import SeparatorPrimitive from './separator.svelte';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-separator"
|
||||||
|
data-sidebar="separator"
|
||||||
|
class={cn('bg-sidebar-border mx-2 w-auto', className)}
|
||||||
|
{...restProps}
|
||||||
|
></div>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import { useSidebar } from './context.svelte.js';
|
||||||
|
import type { HTMLAttributes, Snippet } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLButtonElement>> & { children?: Snippet } = $props();
|
||||||
|
|
||||||
|
const sidebar = useSidebar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="sidebar-trigger"
|
||||||
|
data-sidebar="trigger"
|
||||||
|
class={cn('size-7 inline-flex items-center justify-center rounded-md p-0 text-sidebar-foreground/70 outline-none transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring', className)}
|
||||||
|
onclick={sidebar?.toggleSidebar}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6" />
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
|
<line x1="3" y1="18" x2="21" y2="18" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './skeleton.svelte';
|
||||||
|
export { Root, Root as Skeleton };
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="skeleton"
|
||||||
|
class={cn('bg-muted animate-pulse rounded-md', className)}
|
||||||
|
{...restProps}
|
||||||
|
></div>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Sonner } from './sonner.svelte';
|
||||||
|
|
||||||
|
export { Sonner, Sonner as Toaster };
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
export { default as Sonner } from './sonner.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Toaster as SonnerPrimitive } from 'svelte-sonner';
|
||||||
|
import { cn } from '$lib/utils/cn.svelte.js';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: import('svelte-sonner').ToasterProps & { class?: string } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SonnerPrimitive
|
||||||
|
data-slot="sonner"
|
||||||
|
class={cn('toaster group', className)}
|
||||||
|
theme={$mode}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './switch.svelte';
|
||||||
|
export { Root, Root as Switch };
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Root from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildren } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
type Props = WithoutChildren<Root.Switch.RootProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root.Switch.Root
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
data-slot="switch"
|
||||||
|
class={cn(
|
||||||
|
'peer focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] data-[state=unchecked]:bg-input data-[state=checked]:bg-primary disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<Root.Switch.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
class="bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-lg transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</Root.Switch.Thumb>
|
||||||
|
</Root.Switch.Root>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Root from './textarea.svelte';
|
||||||
|
export { Root, Root as Textarea };
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils/cn.svelte.js';
|
||||||
|
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
type TextareaProps = WithElementRef<HTMLTextareaAttributes>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: TextareaProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:this={ref}
|
||||||
|
bind:value
|
||||||
|
data-slot="textarea"
|
||||||
|
class={cn(
|
||||||
|
'border-input bg-background ring-offset-background placeholder:text-muted-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-md border px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
></textarea>
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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, U extends HTMLElement = HTMLElement> = T & {
|
||||||
|
ref?: U | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WithoutChildren<T> = T extends any
|
||||||
|
? 'children' extends keyof T
|
||||||
|
? Omit<T, 'children'>
|
||||||
|
: T
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export type WithoutChild<T> = T extends any
|
||||||
|
? 'child' extends keyof T
|
||||||
|
? Omit<T, 'child'>
|
||||||
|
: T
|
||||||
|
: T;
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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…');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{page.status} — {{ cookiecutter.project_name }}</title>
|
<title>{page.status}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>{page.status}</h1>
|
<h1>{page.status}</h1>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||||
|
import SiteHeader from '$lib/components/site-header.svelte';
|
||||||
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let { data, children }: { data: { title: string }; children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModeWatcher />
|
||||||
|
|
||||||
|
<AppSidebar variant="inset" collapsible="icon" title={data.title}>
|
||||||
|
<Sidebar.Inset>
|
||||||
|
<SiteHeader title={data.title} />
|
||||||
|
<main class="flex flex-1 flex-col">
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</Sidebar.Inset>
|
||||||
|
</AppSidebar>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
|
export function load(): { title: string } {
|
||||||
|
return { title: '{{ cookiecutter.project_name }}' };
|
||||||
|
}
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return { title: '{{ cookiecutter.project_name }}' };
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +1,62 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Header from '$components/Header.svelte';
|
import type { ColumnDef } from '@tanstack/table-core';
|
||||||
import { createCounter } from '$lib/stores/counter.svelte.js';
|
import SectionCards from '$lib/components/section-cards.svelte';
|
||||||
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
|
import * as DataTable from '$lib/components/ui/data-table/index.js';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let counter = $state(createCounter(0));
|
|
||||||
|
type Channel = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: 'active' | 'paused' | 'archived';
|
||||||
|
uploaded: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<Channel>[] = [
|
||||||
|
{ accessorKey: 'id', header: 'ID' },
|
||||||
|
{ accessorKey: 'name', header: 'Name' },
|
||||||
|
{ accessorKey: 'status', header: 'Status' },
|
||||||
|
{ accessorKey: 'uploaded', header: 'Uploads' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows: Channel[] = [
|
||||||
|
{ id: 'CH-001', name: 'Main feed', status: 'active', uploaded: 1248 },
|
||||||
|
{ id: 'CH-002', name: 'Archive', status: 'paused', uploaded: 412 },
|
||||||
|
{ id: 'CH-003', name: 'Outreach', status: 'active', uploaded: 89 },
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.title}</title>
|
<title>{data.title}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Header title={data.title}>
|
<SectionCards />
|
||||||
<p>{{ cookiecutter.description }}</p>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<main>
|
<Card.Root class="mx-4 mb-4">
|
||||||
<p>Counter: {counter.count}</p>
|
<Card.Header>
|
||||||
<button onclick={() => counter.increment()}>+1</button>
|
<Card.Title>Channels</Card.Title>
|
||||||
<button onclick={() => counter.decrement()}>-1</button>
|
<Card.Description>Recent channel activity.</Card.Description>
|
||||||
<button onclick={() => counter.reset()}>reset</button>
|
</Card.Header>
|
||||||
</main>
|
<Card.Content>
|
||||||
|
<table class="w-full text-sm">
|
||||||
<style>
|
<thead>
|
||||||
main {
|
<tr class="text-muted-foreground border-b text-left">
|
||||||
padding: 1rem;
|
{#each columns as col}
|
||||||
}
|
<th class="py-2 pr-4 font-medium">{col.header as string}</th>
|
||||||
button {
|
{/each}
|
||||||
margin: 0 0.25rem;
|
</tr>
|
||||||
}
|
</thead>
|
||||||
</style>
|
<tbody>
|
||||||
|
{#each rows as row (row.id)}
|
||||||
|
<tr class="border-b last:border-0">
|
||||||
|
<td class="py-2 pr-4">{row.id}</td>
|
||||||
|
<td class="py-2 pr-4">{row.name}</td>
|
||||||
|
<td class="py-2 pr-4">{row.status}</td>
|
||||||
|
<td class="py-2 pr-4">{row.uploaded}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export function load(): { title: string } {
|
||||||
|
return { title: '{{ cookiecutter.project_name }}' };
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,15 @@ const config = {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
alias: {
|
alias: {
|
||||||
$lib: 'src/lib',
|
$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/*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue