Initial template
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# frontend-svelte
|
||||||
|
|
||||||
|
Copier template for a `frontend/` part: SvelteKit on Bun with Tailwind v4, ultracite (biome) and optional shadcn-svelte, served by a Bun server with SSR, as a static SPA behind `serve.ts`, or as a static SPA that the backend api serves from `frontend/build`.
|
||||||
|
|
||||||
|
Ships `frontend/docker-compose.yml` with root-relative paths, `docker-compose.local.yml` (published ports) and `docker-compose.prod.yml` (the `caddy` network), a `Makefile` with `fmt`, `check`, `build` and `dev`, `.env.example`, `CLAUDE.md`, `.claude/rules` for Svelte and ultracite, the Svelte MCP server and `.impeccable/` scaffolding. Meant to be applied on top of the `infra` template.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
copier copy --trust -d project_name="My Thing" -d serve=bun <this-repo> <dest>
|
||||||
|
copier update -a .copier-answers.frontend.yml
|
||||||
|
```
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
_subdirectory: template
|
||||||
|
_templates_suffix: .jinja
|
||||||
|
_answers_file: .copier-answers.frontend.yml
|
||||||
|
_envops:
|
||||||
|
keep_trailing_newline: true
|
||||||
|
|
||||||
|
project_name:
|
||||||
|
type: str
|
||||||
|
help: Project name
|
||||||
|
|
||||||
|
project_slug:
|
||||||
|
type: str
|
||||||
|
help: Package name, snake_case
|
||||||
|
default: "{{ project_name | lower | replace(' ', '_') | replace('-', '_') | replace('.', '_') }}"
|
||||||
|
|
||||||
|
serve:
|
||||||
|
type: str
|
||||||
|
help: How the frontend is served
|
||||||
|
choices:
|
||||||
|
Bun server with SSR (svelte-adapter-bun): bun
|
||||||
|
Static SPA behind serve.ts (adapter-static): static
|
||||||
|
Static SPA served by the backend api: api
|
||||||
|
default: bun
|
||||||
|
|
||||||
|
ui_library:
|
||||||
|
type: str
|
||||||
|
choices:
|
||||||
|
none (Tailwind v4): none
|
||||||
|
shadcn-svelte: shadcn
|
||||||
|
default: none
|
||||||
|
|
||||||
|
include_impeccable:
|
||||||
|
type: bool
|
||||||
|
help: .impeccable/ scaffolding for the impeccable skill
|
||||||
|
default: true
|
||||||
|
|
||||||
|
install_deps:
|
||||||
|
type: bool
|
||||||
|
help: bun install after generation
|
||||||
|
default: true
|
||||||
|
|
||||||
|
frontend_adapter:
|
||||||
|
type: str
|
||||||
|
when: false
|
||||||
|
default: "{{ 'bun' if serve == 'bun' else 'static' }}"
|
||||||
|
|
||||||
|
_tasks:
|
||||||
|
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd frontend && bun install'{% else %}true{% endif %}"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||||
|
|
||||||
|
## Available Svelte MCP Tools:
|
||||||
|
|
||||||
|
### 1. list-sections
|
||||||
|
|
||||||
|
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||||
|
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||||
|
|
||||||
|
### 2. get-documentation
|
||||||
|
|
||||||
|
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||||
|
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||||
|
|
||||||
|
### 3. svelte-autofixer
|
||||||
|
|
||||||
|
Analyzes Svelte code and returns issues and suggestions.
|
||||||
|
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||||
|
|
||||||
|
### 4. playground-link
|
||||||
|
|
||||||
|
Generates a Svelte Playground link with the provided code.
|
||||||
|
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Ultracite Code Standards
|
||||||
|
|
||||||
|
This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
- **Format code**: `bun x ultracite fix`
|
||||||
|
- **Check for issues**: `bun x ultracite check`
|
||||||
|
- **Diagnose setup**: `bun x ultracite doctor`
|
||||||
|
|
||||||
|
Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity.
|
||||||
|
|
||||||
|
### Type Safety & Explicitness
|
||||||
|
|
||||||
|
- Use explicit types for function parameters and return values when they enhance clarity
|
||||||
|
- Prefer `unknown` over `any` when the type is genuinely unknown
|
||||||
|
- Use const assertions (`as const`) for immutable values and literal types
|
||||||
|
- Leverage TypeScript's type narrowing instead of type assertions
|
||||||
|
- Use meaningful variable names instead of magic numbers - extract constants with descriptive names
|
||||||
|
|
||||||
|
### Modern JavaScript/TypeScript
|
||||||
|
|
||||||
|
- Use arrow functions for callbacks and short functions
|
||||||
|
- Prefer `for...of` loops over `.forEach()` and indexed `for` loops
|
||||||
|
- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access
|
||||||
|
- Prefer template literals over string concatenation
|
||||||
|
- Use destructuring for object and array assignments
|
||||||
|
- Use `const` by default, `let` only when reassignment is needed, never `var`
|
||||||
|
|
||||||
|
### Async & Promises
|
||||||
|
|
||||||
|
- Always `await` promises in async functions - don't forget to use the return value
|
||||||
|
- Use `async/await` syntax instead of promise chains for better readability
|
||||||
|
- Handle errors appropriately in async code with try-catch blocks
|
||||||
|
- Don't use async functions as Promise executors
|
||||||
|
|
||||||
|
### React & JSX
|
||||||
|
|
||||||
|
- Use function components over class components
|
||||||
|
- Call hooks at the top level only, never conditionally
|
||||||
|
- Specify all dependencies in hook dependency arrays correctly
|
||||||
|
- Use the `key` prop for elements in iterables (prefer unique IDs over array indices)
|
||||||
|
- Nest children between opening and closing tags instead of passing as props
|
||||||
|
- Don't define components inside other components
|
||||||
|
- Use semantic HTML and ARIA attributes for accessibility:
|
||||||
|
- Provide meaningful alt text for images
|
||||||
|
- Use proper heading hierarchy
|
||||||
|
- Add labels for form inputs
|
||||||
|
- Include keyboard event handlers alongside mouse events
|
||||||
|
- Use semantic elements (`<button>`, `<nav>`, etc.) instead of divs with roles
|
||||||
|
|
||||||
|
### Error Handling & Debugging
|
||||||
|
|
||||||
|
- Remove `console.log`, `debugger`, and `alert` statements from production code
|
||||||
|
- Throw `Error` objects with descriptive messages, not strings or other values
|
||||||
|
- Use `try-catch` blocks meaningfully - don't catch errors just to rethrow them
|
||||||
|
- Prefer early returns over nested conditionals for error cases
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
|
||||||
|
- Keep functions focused and under reasonable cognitive complexity limits
|
||||||
|
- Extract complex conditions into well-named boolean variables
|
||||||
|
- Use early returns to reduce nesting
|
||||||
|
- Prefer simple conditionals over nested ternary operators
|
||||||
|
- Group related code together and separate concerns
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Add `rel="noopener"` when using `target="_blank"` on links
|
||||||
|
- Avoid `dangerouslySetInnerHTML` unless absolutely necessary
|
||||||
|
- Don't use `eval()` or assign directly to `document.cookie`
|
||||||
|
- Validate and sanitize user input
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Avoid spread syntax in accumulators within loops
|
||||||
|
- Use top-level regex literals instead of creating them in loops
|
||||||
|
- Prefer specific imports over namespace imports
|
||||||
|
- Avoid barrel files (index files that re-export everything)
|
||||||
|
- Use proper image components (e.g., Next.js `<Image>`) over `<img>` tags
|
||||||
|
|
||||||
|
### Framework-Specific Guidance
|
||||||
|
|
||||||
|
**Next.js:**
|
||||||
|
- Use Next.js `<Image>` component for images
|
||||||
|
- Use `next/head` or App Router metadata API for head elements
|
||||||
|
- Use Server Components for async data fetching instead of async Client Components
|
||||||
|
|
||||||
|
**React 19+:**
|
||||||
|
- Use ref as a prop instead of `React.forwardRef`
|
||||||
|
|
||||||
|
**Solid/Svelte/Vue/Qwik:**
|
||||||
|
- Use `class` and `for` attributes (not `className` or `htmlFor`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Write assertions inside `it()` or `test()` blocks
|
||||||
|
- Avoid done callbacks in async tests - use async/await instead
|
||||||
|
- Don't use `.only` or `.skip` in committed code
|
||||||
|
- Keep test suites reasonably flat - avoid excessive `describe` nesting
|
||||||
|
|
||||||
|
## When Biome Can't Help
|
||||||
|
|
||||||
|
Biome's linter will catch most issues automatically. Focus your attention on:
|
||||||
|
|
||||||
|
1. **Business logic correctness** - Biome can't validate your algorithms
|
||||||
|
2. **Meaningful naming** - Use descriptive names for functions, variables, and types
|
||||||
|
3. **Architecture decisions** - Component structure, data flow, and API design
|
||||||
|
4. **Edge cases** - Handle boundary conditions and error states
|
||||||
|
5. **User experience** - Accessibility, performance, and usability considerations
|
||||||
|
6. **Documentation** - Add comments for complex logic, but prefer self-documenting code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Most formatting and common issues are automatically fixed by Biome. Run `bun x ultracite fix` before committing to ensure compliance.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"command": "cd frontend && bun run fix --skip=correctness/noUnusedImports",
|
||||||
|
"type": "command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"matcher": "Write|Edit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"svelte": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://mcp.svelte.dev/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.svelte-kit
|
||||||
|
build
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PUBLIC_API_BASE_URL=/api
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Impeccable
|
||||||
|
**/.impeccable/config.local.json
|
||||||
|
**/.impeccable/hook.cache.json
|
||||||
|
**/.impeccable/hook.pending.json
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## Checking commands
|
||||||
|
After writing code, always run from `frontend/`:
|
||||||
|
```shell
|
||||||
|
bun fix
|
||||||
|
bun check
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
COMPOSE := docker compose --project-directory ..
|
||||||
|
|
||||||
|
.PHONY: fmt check build dev
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
bun run fix
|
||||||
|
|
||||||
|
check:
|
||||||
|
bun run check
|
||||||
|
bun run lint
|
||||||
|
|
||||||
|
build:
|
||||||
|
{% if serve == 'api' %} $(COMPOSE) run --rm frontend-dev sh -c "bun install && bun run build"
|
||||||
|
{% else %} $(COMPOSE) build frontend
|
||||||
|
{% endif %}
|
||||||
|
dev:
|
||||||
|
bun run dev
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
|
"extends": ["ultracite/biome/core", "ultracite/biome/svelte"],
|
||||||
|
"files": {
|
||||||
|
"includes": [
|
||||||
|
"!.claude",
|
||||||
|
"!.svelte-kit",
|
||||||
|
"!build",
|
||||||
|
"!.mcp.json",
|
||||||
|
"!.impeccable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"javascript": { "globals": ["Bun"] },
|
||||||
|
"overrides": [
|
||||||
|
{%- if ui_library == 'shadcn' %}
|
||||||
|
{
|
||||||
|
"includes": ["src/lib/components/ui/**"],
|
||||||
|
"linter": { "enabled": false },
|
||||||
|
"assist": { "enabled": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["src/lib/utils.ts"],
|
||||||
|
"linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } }
|
||||||
|
},
|
||||||
|
{%- endif %}
|
||||||
|
{
|
||||||
|
"includes": ["**/*.svelte"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"style": {
|
||||||
|
"useFilenamingConvention": {
|
||||||
|
"level": "error",
|
||||||
|
"options": { "filenameCases": ["kebab-case", "PascalCase"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["src/app.d.ts"],
|
||||||
|
"linter": { "rules": { "style": { "noNamespace": "off" } } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
{% if serve != 'api' %} frontend:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000"
|
||||||
|
|
||||||
|
{% endif %} frontend-dev:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{%- set dash = project_slug | replace('_', '-') -%}
|
||||||
|
{% if serve != 'api' -%}
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
aliases:
|
||||||
|
- {{ dash }}-frontend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
external: true
|
||||||
|
{%- else -%}
|
||||||
|
services: {}
|
||||||
|
{%- endif %}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
{% if serve != 'api' %} frontend:
|
||||||
|
build: frontend
|
||||||
|
image: {{ project_slug }}/frontend
|
||||||
|
profiles: [frontend, services]
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: false
|
||||||
|
- path: frontend/.env
|
||||||
|
required: false
|
||||||
|
command: {% if serve == 'bun' %}[build/index.js]{% else %}[run, serve.ts]{% endif %}
|
||||||
|
|
||||||
|
{% endif %} frontend-dev:
|
||||||
|
image: oven/bun:alpine
|
||||||
|
profiles: [frontend-dev]
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /app
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: false
|
||||||
|
- path: frontend/.env
|
||||||
|
required: false
|
||||||
|
environment:
|
||||||
|
API_PROXY_TARGET: http://api:8080
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- frontend_node_modules:/app/node_modules
|
||||||
|
- frontend_svelte_kit:/app/.svelte-kit
|
||||||
|
command: [sh, -c, "bun install && bun run dev --host 0.0.0.0 --port 3000"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
frontend_node_modules:
|
||||||
|
frontend_svelte_kit:
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "{{ project_slug }}",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "ultracite check",
|
||||||
|
"fix": "ultracite fix"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.5.2",
|
||||||
|
"@sveltejs/kit": "^2.63.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@types/bun": "^1.3.0",
|
||||||
|
{% if frontend_adapter == 'bun' %} "svelte-adapter-bun": "^1.0.1",
|
||||||
|
{% else %} "@sveltejs/adapter-static": "^3.0.10",
|
||||||
|
{% endif %}{% if ui_library == 'shadcn' %} "@internationalized/date": "^3.12.0",
|
||||||
|
"@lucide/svelte": "^1.23.0",
|
||||||
|
"bits-ui": "^2.16.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"mode-watcher": "^1.1.0",
|
||||||
|
"shadcn-svelte": "^1.3.0",
|
||||||
|
"svelte-sonner": "^1.1.0",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
{% endif %} "svelte": "^5.56.1",
|
||||||
|
"svelte-check": "^4.6.0",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"ultracite": "7.9.0",
|
||||||
|
"vite": "^8.0.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
|
<meta content="scale" name="text-scale">
|
||||||
|
<link href="%sveltekit.assets%/favicon.svg" rel="icon" type="image/svg+xml">
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
|
||||||
|
const BASE = env.PUBLIC_API_BASE_URL ?? "/api";
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
constructor(status: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.name = "ApiError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Json = Record<string, unknown> | unknown[];
|
||||||
|
|
||||||
|
async function handle<T>(res: Response): Promise<T> {
|
||||||
|
if (res.status === 204) {
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
if (res.ok) {
|
||||||
|
return (await res.json()) as T;
|
||||||
|
}
|
||||||
|
let detail = res.statusText;
|
||||||
|
try {
|
||||||
|
const { detail: parsed } = (await res.json()) as { detail?: string };
|
||||||
|
if (parsed) {
|
||||||
|
detail = parsed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
detail = res.statusText;
|
||||||
|
}
|
||||||
|
throw new ApiError(res.status, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: Json
|
||||||
|
): Promise<T> {
|
||||||
|
const init: RequestInit = { credentials: "include", method };
|
||||||
|
if (body !== undefined) {
|
||||||
|
init.headers = { "Content-Type": "application/json" };
|
||||||
|
init.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
const res = await fetch(BASE + path, init);
|
||||||
|
return handle<T>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
del: <T>(path: string) => request<T>("DELETE", path),
|
||||||
|
get: <T>(path: string) => request<T>("GET", path),
|
||||||
|
patch: <T>(path: string, body?: Json) => request<T>("PATCH", path, body),
|
||||||
|
post: <T>(path: string, body?: Json) => request<T>("POST", path, body),
|
||||||
|
put: <T>(path: string, body?: Json) => request<T>("PUT", path, body),
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
|
// biome-ignore-all lint/suspicious/noEmptySource: temporary
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||||
|
export type WithoutChildren<T> = T extends { children?: any }
|
||||||
|
? Omit<T, "children">
|
||||||
|
: T;
|
||||||
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
|
||||||
|
ref?: U | null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import "./layout.css";
|
||||||
|
{%- if ui_library == 'shadcn' %}
|
||||||
|
import { ModeWatcher } from "mode-watcher";
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% if ui_library == 'shadcn' %}<ModeWatcher />
|
||||||
|
|
||||||
|
{% endif %}{@render children()}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>{{ project_name }}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main
|
||||||
|
class="flex min-h-dvh flex-col items-center justify-center gap-4 px-6 text-center {% if ui_library == 'shadcn' %}bg-background text-foreground{% else %}bg-white text-neutral-900 dark:bg-neutral-950 dark:text-neutral-50{% endif %}"
|
||||||
|
>
|
||||||
|
<h1 class="font-bold text-4xl tracking-tight sm:text-5xl">{{ project_name }}</h1>
|
||||||
|
<p class="max-w-prose text-base {% if ui_library == 'shadcn' %}text-muted-foreground{% else %}text-neutral-500{% endif %}">
|
||||||
|
Edit
|
||||||
|
<code class="font-mono">src/routes/+page.svelte</code>.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{% if ui_library == 'shadcn' -%}
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn-svelte/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.922 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.205 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 {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--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-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{%- else -%}
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans:
|
||||||
|
system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:disabled),
|
||||||
|
[role="button"]:not(:disabled),
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
{%- endif %}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const ssr = false;
|
||||||
|
export const prerender = false;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% if frontend_adapter == 'bun' -%}
|
||||||
|
import adapter from "svelte-adapter-bun";
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
compilerOptions: {
|
||||||
|
runes: ({ filename }) =>
|
||||||
|
// biome-ignore lint/performance/useTopLevelRegex: svelte default behaviour
|
||||||
|
filename.split(/[/\\]/).includes("node_modules") ? undefined : true,
|
||||||
|
},
|
||||||
|
kit: {
|
||||||
|
adapter: adapter(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
{%- else -%}
|
||||||
|
import adapter from "@sveltejs/adapter-static";
|
||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
assets: "build",
|
||||||
|
fallback: "index.html",
|
||||||
|
pages: "build",
|
||||||
|
precompress: false,
|
||||||
|
strict: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
{%- endif %}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
changeOrigin: true,
|
||||||
|
target: process.env.API_PROXY_TARGET ?? "http://localhost:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{% if frontend_adapter == 'bun' -%}
|
||||||
|
FROM oven/bun:alpine
|
||||||
|
|
||||||
|
ENV TERM=xterm-256color
|
||||||
|
ENV COLORTERM=truecolor
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock* ./
|
||||||
|
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN bun --bun svelte-kit sync
|
||||||
|
RUN bun --bun run build
|
||||||
|
|
||||||
|
ENTRYPOINT ["bun"]
|
||||||
|
CMD ["build/index.js"]
|
||||||
|
{%- else -%}
|
||||||
|
FROM oven/bun:alpine AS deps
|
||||||
|
|
||||||
|
ENV TERM=xterm-256color
|
||||||
|
ENV COLORTERM=truecolor
|
||||||
|
ENV BUN_INSTALL_CACHE_DIR=/bun-cache
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock* ./
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/bun-cache bun install --frozen-lockfile
|
||||||
|
|
||||||
|
|
||||||
|
FROM deps AS builder
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN bun --bun svelte-kit sync
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM oven/bun:alpine AS runtime
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/build ./build
|
||||||
|
COPY --from=builder /app/serve.ts ./serve.ts
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["bun"]
|
||||||
|
CMD ["run", "serve.ts"]
|
||||||
|
{%- endif %}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const ROOT = `${import.meta.dir}/build`;
|
||||||
|
const PORT = Number(Bun.env.PORT ?? 3000);
|
||||||
|
const IMMUTABLE = "/_app/immutable/";
|
||||||
|
|
||||||
|
const fallback = Bun.file(`${ROOT}/index.html`);
|
||||||
|
|
||||||
|
function headersFor(pathname: string): HeadersInit {
|
||||||
|
if (pathname.startsWith(IMMUTABLE)) {
|
||||||
|
return { "Cache-Control": "public, max-age=31536000, immutable" };
|
||||||
|
}
|
||||||
|
return { "Cache-Control": "no-cache" };
|
||||||
|
}
|
||||||
|
|
||||||
|
Bun.serve({
|
||||||
|
async fetch(request) {
|
||||||
|
const { pathname } = new URL(request.url);
|
||||||
|
|
||||||
|
if (pathname.includes("..")) {
|
||||||
|
return new Response("Bad Request", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = Bun.file(`${ROOT}${pathname}`);
|
||||||
|
if (await file.exists()) {
|
||||||
|
return new Response(file, { headers: headersFor(pathname) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(fallback, {
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
idleTimeout: 30,
|
||||||
|
port: PORT,
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/routes/layout.css",
|
||||||
|
"baseColor": "neutral"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry",
|
||||||
|
"style": "luma",
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"menuColor": "default-translucent",
|
||||||
|
"menuAccent": "subtle"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
config.local.json
|
||||||
|
hook.cache.json
|
||||||
|
hook.pending.json
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{{ _copier_answers|to_nice_yaml -}}
|
||||||
Reference in New Issue
Block a user