bosia 0.6.21 → 0.6.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +2 -2
  2. package/src/cli/add.ts +3 -4
  3. package/src/cli/block.ts +16 -10
  4. package/src/cli/create.ts +6 -11
  5. package/src/cli/feat.ts +19 -22
  6. package/src/cli/index.ts +1 -2
  7. package/src/cli/manifest.ts +1 -1
  8. package/src/cli/registry.ts +3 -1
  9. package/src/core/build.ts +1 -3
  10. package/src/core/client/App.svelte +3 -8
  11. package/src/core/client/router.svelte.ts +3 -8
  12. package/src/core/config.ts +1 -4
  13. package/src/core/cookies.ts +1 -2
  14. package/src/core/dev-500.ts +1 -1
  15. package/src/core/html.ts +1 -2
  16. package/src/core/plugin.ts +1 -3
  17. package/src/core/plugins/inspector/bun-plugin.ts +1 -4
  18. package/src/core/plugins/inspector/index.ts +45 -59
  19. package/src/core/renderer.ts +3 -10
  20. package/src/core/routeTypes.ts +3 -9
  21. package/src/core/scanner.ts +1 -3
  22. package/src/core/server.ts +30 -35
  23. package/src/core/staticManifest.ts +1 -3
  24. package/src/core/svelteAudit.ts +2 -5
  25. package/src/core/svelteCompiler.ts +2 -8
  26. package/templates/default/.prettierignore +1 -0
  27. package/templates/default/src/app.css +2 -0
  28. package/templates/demo/.prettierignore +1 -0
  29. package/templates/demo/src/app.css +2 -0
  30. package/templates/shop/.env.example +12 -0
  31. package/templates/shop/.prettierignore +7 -0
  32. package/templates/shop/.prettierrc.json +9 -0
  33. package/templates/shop/README.md +62 -0
  34. package/templates/shop/_gitignore +12 -0
  35. package/templates/shop/bosia.config.ts +10 -0
  36. package/templates/shop/instructions.txt +8 -0
  37. package/templates/shop/package.json +26 -0
  38. package/templates/shop/public/favicon.svg +14 -0
  39. package/templates/shop/public/logo-dark.svg +14 -0
  40. package/templates/shop/public/logo-light.svg +14 -0
  41. package/templates/shop/src/app.css +134 -0
  42. package/templates/shop/src/app.d.ts +14 -0
  43. package/templates/shop/src/app.html +11 -0
  44. package/templates/shop/src/hooks.server.ts +21 -0
  45. package/templates/shop/src/lib/utils.ts +1 -0
  46. package/templates/shop/src/routes/(private)/+layout.server.ts +10 -0
  47. package/templates/shop/src/routes/(private)/+layout.svelte +44 -0
  48. package/templates/shop/src/routes/(private)/dashboard/+page.svelte +11 -0
  49. package/templates/shop/src/routes/(public)/+layout.svelte +13 -0
  50. package/templates/shop/src/routes/(public)/+page.svelte +38 -0
  51. package/templates/shop/src/routes/+error.svelte +19 -0
  52. package/templates/shop/src/routes/+layout.server.ts +9 -0
  53. package/templates/shop/src/routes/+layout.svelte +6 -0
  54. package/templates/shop/template.json +10 -0
  55. package/templates/shop/tsconfig.json +22 -0
  56. package/templates/todo/.prettierignore +1 -0
  57. package/templates/todo/src/app.css +2 -0
  58. package/templates/todo/template.json +4 -1
@@ -93,9 +93,7 @@ export function scanRoutes(): RouteManifest {
93
93
  ? join(dir, "+page.server.ts")
94
94
  : null;
95
95
 
96
- const pageTs = pageServerFile
97
- ? readTrailingSlash(join(ROUTES_DIR, pageServerFile))
98
- : null;
96
+ const pageTs = pageServerFile ? readTrailingSlash(join(ROUTES_DIR, pageServerFile)) : null;
99
97
  const effectiveTs: TrailingSlash = pageTs ?? currentTrailingSlash;
100
98
 
101
99
  pages.push({
@@ -234,21 +234,11 @@ async function resolve(event: RequestEvent): Promise<Response> {
234
234
  const mask = invalidatedBits
235
235
  ? buildMaskFromBits(
236
236
  invalidatedBits,
237
- pageMatch?.route
238
- ? ((pageMatch.route as any).layoutModules?.length ?? 0)
239
- : 0,
237
+ pageMatch?.route ? ((pageMatch.route as any).layoutModules?.length ?? 0) : 0,
240
238
  )
241
239
  : undefined;
242
240
  const runLoad = async () => {
243
- const data = await loadRouteData(
244
- routeUrl,
245
- locals,
246
- request,
247
- cookies,
248
- null,
249
- pageMatch,
250
- mask,
251
- );
241
+ const data = await loadRouteData(routeUrl, locals, request, cookies, null, pageMatch, mask);
252
242
 
253
243
  let metadata = null;
254
244
  if (pageMatch) {
@@ -278,14 +268,10 @@ async function resolve(event: RequestEvent): Promise<Response> {
278
268
  ? `${dedupKey(routeUrl)}|m=${invalidatedBits}`
279
269
  : dedupKey(routeUrl);
280
270
  const result =
281
- pageMatch?.route.scope === "private"
282
- ? await runLoad()
283
- : await dedup(dedupK, runLoad);
271
+ pageMatch?.route.scope === "private" ? await runLoad() : await dedup(dedupK, runLoad);
284
272
 
285
273
  const cookiesWereAccessed = (cookies as CookieJar).accessed || result.cookiesAccessed;
286
- const cc = cookiesWereAccessed
287
- ? "private, no-cache"
288
- : "public, max-age=0, must-revalidate";
274
+ const cc = cookiesWereAccessed ? "private, no-cache" : "public, max-age=0, must-revalidate";
289
275
 
290
276
  if (!result.data) {
291
277
  return compress(
@@ -380,7 +366,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
380
366
  }
381
367
  }
382
368
 
383
- const response = await handler({
369
+ const handlerResult = await handler({
384
370
  request,
385
371
  params: apiMatch.params,
386
372
  url,
@@ -388,6 +374,15 @@ async function resolve(event: RequestEvent): Promise<Response> {
388
374
  cookies,
389
375
  });
390
376
 
377
+ // Redirect returned (not thrown) — convert to a 303 Response.
378
+ if (handlerResult instanceof Redirect) {
379
+ return new Response(null, {
380
+ status: handlerResult.status,
381
+ headers: { Location: handlerResult.location },
382
+ });
383
+ }
384
+
385
+ const response = handlerResult as Response;
391
386
  const responseContentType = response.headers.get("content-type") ?? "";
392
387
  // SSE responses are long-lived pub/sub streams — caching the buffered
393
388
  // bytes would serve a stale finite snapshot to future subscribers and
@@ -437,6 +432,17 @@ async function resolve(event: RequestEvent): Promise<Response> {
437
432
 
438
433
  return response;
439
434
  } catch (err) {
435
+ // `throw redirect(303, "/")` from a +server.ts handler — turn it into
436
+ // a real 303 instead of a 500. Mirrors the page-action handler below.
437
+ if (err instanceof Redirect) {
438
+ return new Response(null, {
439
+ status: err.status,
440
+ headers: { Location: err.location },
441
+ });
442
+ }
443
+ if (err instanceof HttpError) {
444
+ return Response.json({ error: err.message }, { status: err.status });
445
+ }
440
446
  if (isDev) console.error("API route error:", err);
441
447
  else console.error("API route error:", (err as Error).message ?? err);
442
448
  if (isDev) reportDevErrorFromCatch(err);
@@ -461,9 +467,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
461
467
  if (hit) {
462
468
  return new Response(
463
469
  Bun.file(hit.absPath),
464
- hit.cacheControl
465
- ? { headers: { "Cache-Control": hit.cacheControl } }
466
- : undefined,
470
+ hit.cacheControl ? { headers: { "Cache-Control": hit.cacheControl } } : undefined,
467
471
  );
468
472
  }
469
473
  return new Response("Not Found", { status: 404 });
@@ -510,9 +514,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
510
514
  if (!isDev) {
511
515
  // Try both `<path>/index.html` (always/ignore mode) and `<path>.html` (never mode)
512
516
  const prerenderCandidates =
513
- path === "/"
514
- ? ["index.html"]
515
- : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
517
+ path === "/" ? ["index.html"] : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
516
518
  for (const candidate of prerenderCandidates) {
517
519
  const prerenderPath = safePath(`${OUT_DIR}/prerendered`, candidate);
518
520
  if (!prerenderPath) continue;
@@ -533,10 +535,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
533
535
 
534
536
  // Trailing-slash canonicalization — 308 preserves method (form POSTs included)
535
537
  if (pageMatch) {
536
- const canonical = canonicalPathname(
537
- path,
538
- (pageMatch.route as any).trailingSlash ?? "never",
539
- );
538
+ const canonical = canonicalPathname(path, (pageMatch.route as any).trailingSlash ?? "never");
540
539
  if (canonical !== null) {
541
540
  return new Response(null, {
542
541
  status: 308,
@@ -854,15 +853,11 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
854
853
  function parseCorsMaxAge(value?: string): number | undefined {
855
854
  if (!value) return undefined;
856
855
  if (!/^\d+$/.test(value)) {
857
- throw new Error(
858
- `Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`,
859
- );
856
+ throw new Error(`Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`);
860
857
  }
861
858
  const n = parseInt(value, 10);
862
859
  if (!Number.isFinite(n) || n > Number.MAX_SAFE_INTEGER) {
863
- throw new Error(
864
- `Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`,
865
- );
860
+ throw new Error(`Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`);
866
861
  }
867
862
  return n;
868
863
  }
@@ -50,9 +50,7 @@ export function buildStaticManifest(outDir: string): StaticManifest {
50
50
  const clientRoot = join(outAbs, "client");
51
51
  if (existsSync(clientRoot)) {
52
52
  for (const { abs, rel } of walk(clientRoot)) {
53
- const cacheControl = HASHED_BASENAME.test(basename(rel))
54
- ? IMMUTABLE_CACHE
55
- : DEFAULT_CACHE;
53
+ const cacheControl = HASHED_BASENAME.test(basename(rel)) ? IMMUTABLE_CACHE : DEFAULT_CACHE;
56
54
  addOnce(manifest, `/dist/client/${rel}`, { absPath: abs, cacheControl });
57
55
  }
58
56
  }
@@ -121,9 +121,7 @@ function extractBindings(ast: AnyNode): Binding[] {
121
121
  case "ImportDeclaration": {
122
122
  const sourceNode = stmt.source as AnyNode | undefined;
123
123
  const source =
124
- sourceNode && typeof sourceNode.value === "string"
125
- ? (sourceNode.value as string)
126
- : "";
124
+ sourceNode && typeof sourceNode.value === "string" ? (sourceNode.value as string) : "";
127
125
  const specs = stmt.specifiers as AnyNode[] | undefined;
128
126
  if (!Array.isArray(specs)) break;
129
127
  for (const spec of specs) {
@@ -344,8 +342,7 @@ function collectTemplateRefs(source: string, fragment: AnyNode): TemplateRef[] {
344
342
  // name into the surrounding scope so `<MySnippet/>` doesn't false-
345
343
  // positive. The expression's name is the snippet's identifier.
346
344
  const expr = n.expression as AnyNode | undefined;
347
- const snippetName =
348
- expr && typeof expr.name === "string" ? (expr.name as string) : null;
345
+ const snippetName = expr && typeof expr.name === "string" ? (expr.name as string) : null;
349
346
  if (snippetName && scopeStack.length > 0) {
350
347
  scopeStack[scopeStack.length - 1].add(snippetName);
351
348
  } else if (snippetName) {
@@ -113,10 +113,7 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
113
113
  // Server (Bun) compile output has different line numbers and would
114
114
  // clobber the client entry under the same cache key.
115
115
  if (dev && target === "browser" && result.js.map) {
116
- const m =
117
- typeof result.js.map === "string"
118
- ? JSON.parse(result.js.map)
119
- : result.js.map;
116
+ const m = typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map;
120
117
  svelteMapCache.set(args.path, m);
121
118
  }
122
119
  const contents = dev ? fixBindShadow(result.js.code) : result.js.code;
@@ -134,10 +131,7 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
134
131
  filename: args.path,
135
132
  });
136
133
  if (dev && target === "browser" && result.js.map) {
137
- const m =
138
- typeof result.js.map === "string"
139
- ? JSON.parse(result.js.map)
140
- : result.js.map;
134
+ const m = typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map;
141
135
  svelteMapCache.set(args.path, m);
142
136
  }
143
137
  return { contents: result.js.code, loader: "js" };
@@ -4,3 +4,4 @@ build
4
4
  .bosia
5
5
  bun.lock
6
6
  public/bosia-tw.css
7
+ bosia.json
@@ -1,6 +1,8 @@
1
1
  @import "tailwindcss";
2
2
  @source "../src";
3
3
 
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
4
6
  /*
5
7
  * ─── shadcn-inspired Design Tokens ──────────────────────
6
8
  * CSS custom properties for light & dark themes.
@@ -4,3 +4,4 @@ build
4
4
  .bosia
5
5
  bun.lock
6
6
  public/bosia-tw.css
7
+ bosia.json
@@ -1,6 +1,8 @@
1
1
  @import "tailwindcss";
2
2
  @source "../src";
3
3
 
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
4
6
  /*
5
7
  * ─── shadcn-inspired Design Tokens ──────────────────────
6
8
  * CSS custom properties for light & dark themes.
@@ -0,0 +1,12 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/shop
2
+
3
+ SESSION_SECRET=change-me-in-production
4
+
5
+ STORAGE_DRIVER=s3
6
+ S3_BUCKET=
7
+ S3_REGION=auto
8
+ S3_ACCESS_KEY_ID=
9
+ S3_SECRET_ACCESS_KEY=
10
+ S3_ENDPOINT=
11
+
12
+ PUBLIC_BASE_URL=
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ .bosia
5
+ bun.lock
6
+ public/bosia-tw.css
7
+ bosia.json
@@ -0,0 +1,9 @@
1
+ {
2
+ "useTabs": true,
3
+ "tabWidth": 2,
4
+ "singleQuote": false,
5
+ "trailingComma": "all",
6
+ "printWidth": 100,
7
+ "plugins": ["prettier-plugin-svelte"],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
@@ -0,0 +1,62 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ An online-store starter built with [Bosia](https://github.com/bosapi/bosia) — auth, RBAC, S3-backed uploads, and the shop domain (products / orders / cart).
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh/) v1.1+
8
+ - PostgreSQL running locally or remotely
9
+ - An S3-compatible bucket (AWS S3, Cloudflare R2, MinIO, ...)
10
+
11
+ ## Getting Started
12
+
13
+ ```bash
14
+ cp .env.example .env
15
+ # fill DATABASE_URL, SESSION_SECRET, and S3_* in .env
16
+
17
+ bun run db:generate
18
+ bun run db:migrate
19
+ bun run db:seed
20
+
21
+ bun x bosia dev
22
+ ```
23
+
24
+ Visit [http://localhost:9000](http://localhost:9000). The **first account you register becomes the admin** (gets `('*','*')` via the RBAC bootstrap seed).
25
+
26
+ ## What ships
27
+
28
+ | Feature | Path |
29
+ | ------------- | ---------------------------------------------------------------------- |
30
+ | `auth` | `src/features/auth/`, `(public)/login`, `(public)/register`, `/logout` |
31
+ | `rbac` | `src/features/rbac/`, `locals.can(r,a,scope?)` |
32
+ | `file-upload` | `src/features/file-upload/`, `POST /api/files` (S3 via `Bun.s3`) |
33
+ | `shop` | `src/features/shop/` (products / orders / cart services) |
34
+
35
+ ## Routes
36
+
37
+ - `/` — public landing
38
+ - `/login`, `/register`, `POST /logout`
39
+ - `/dashboard` — gated; redirects to `/login` if unauthenticated
40
+
41
+ ## Scripts
42
+
43
+ | Command | Description |
44
+ | --------------------- | --------------------------------------------- |
45
+ | `bun x bosia dev` | Dev server with HMR |
46
+ | `bun x bosia build` | Production build |
47
+ | `bun run db:generate` | Generate migration from schema changes |
48
+ | `bun run db:migrate` | Apply pending migrations |
49
+ | `bun run db:seed` | Run pending seed files (incl. RBAC bootstrap) |
50
+
51
+ ## S3 storage
52
+
53
+ Uses native `Bun.s3` (no `@aws-sdk/*` dependency). Set the standard env vars:
54
+
55
+ ```
56
+ STORAGE_DRIVER=s3
57
+ S3_BUCKET=...
58
+ S3_REGION=...
59
+ S3_ACCESS_KEY_ID=...
60
+ S3_SECRET_ACCESS_KEY=...
61
+ S3_ENDPOINT= # optional, for R2/MinIO
62
+ ```
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ dist/
3
+ .bosia/
4
+ .DS_Store
5
+ *.log
6
+
7
+ # Generated Tailwind output
8
+ public/bosia-tw.css
9
+
10
+ # Local env overrides — never commit secrets
11
+ .env*.local
12
+ .env
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "bosia";
2
+ import { inspector } from "bosia/plugins/inspector";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ // Dev-only: Alt+click any element on the page to open its source in your editor.
7
+ // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
+ inspector({ editor: "code" }),
9
+ ],
10
+ });
@@ -0,0 +1,8 @@
1
+ Update .env with your DATABASE_URL (PostgreSQL) and S3_* credentials.
2
+ Pick a strong SESSION_SECRET.
3
+
4
+ bun run db:generate
5
+ bun run db:migrate
6
+ bun run db:seed
7
+
8
+ The first account you register becomes the admin.
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bosia dev",
7
+ "build": "bosia build",
8
+ "start": "bosia start",
9
+ "check": "tsc --noEmit && prettier --check .",
10
+ "format": "prettier --write .",
11
+ "format:check": "prettier --check ."
12
+ },
13
+ "dependencies": {
14
+ "bosia": "^{{BOSIA_VERSION}}",
15
+ "svelte": "^5.20.0",
16
+ "tailwind-merge": "^3.5.0",
17
+ "drizzle-orm": "^0.44.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "prettier": "^3.3.0",
22
+ "prettier-plugin-svelte": "^3.2.0",
23
+ "typescript": "^5",
24
+ "drizzle-kit": "^0.31.0"
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="currentColor" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="currentColor" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="currentColor" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="currentColor" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="currentColor" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,134 @@
1
+ @import "tailwindcss";
2
+ @source "../src";
3
+
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
6
+ /*
7
+ * ─── shadcn-inspired Design Tokens ──────────────────────
8
+ * CSS custom properties for light & dark themes.
9
+ * Uses HSL values so Tailwind can apply opacity modifiers.
10
+ */
11
+
12
+ @theme {
13
+ --color-background: hsl(var(--background));
14
+ --color-foreground: hsl(var(--foreground));
15
+
16
+ --color-card: hsl(var(--card));
17
+ --color-card-foreground: hsl(var(--card-foreground));
18
+
19
+ --color-popover: hsl(var(--popover));
20
+ --color-popover-foreground: hsl(var(--popover-foreground));
21
+
22
+ --color-primary: hsl(var(--primary));
23
+ --color-primary-foreground: hsl(var(--primary-foreground));
24
+
25
+ --color-secondary: hsl(var(--secondary));
26
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
27
+
28
+ --color-muted: hsl(var(--muted));
29
+ --color-muted-foreground: hsl(var(--muted-foreground));
30
+
31
+ --color-accent: hsl(var(--accent));
32
+ --color-accent-foreground: hsl(var(--accent-foreground));
33
+
34
+ --color-destructive: hsl(var(--destructive));
35
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
36
+
37
+ --color-border: hsl(var(--border));
38
+ --color-input: hsl(var(--input));
39
+ --color-ring: hsl(var(--ring));
40
+
41
+ --radius-sm: calc(var(--radius) - 4px);
42
+ --radius-md: calc(var(--radius) - 2px);
43
+ --radius-lg: var(--radius);
44
+ --radius-xl: calc(var(--radius) + 4px);
45
+ }
46
+
47
+ /* ─── Light Theme (Default) ─────────────────────────────── */
48
+
49
+ :root {
50
+ --background: 0 0% 100%;
51
+ --foreground: 222.2 84% 4.9%;
52
+
53
+ --card: 0 0% 100%;
54
+ --card-foreground: 222.2 84% 4.9%;
55
+
56
+ --popover: 0 0% 100%;
57
+ --popover-foreground: 222.2 84% 4.9%;
58
+
59
+ --primary: 222.2 47.4% 11.2%;
60
+ --primary-foreground: 210 40% 98%;
61
+
62
+ --secondary: 210 40% 96.1%;
63
+ --secondary-foreground: 222.2 47.4% 11.2%;
64
+
65
+ --muted: 210 40% 96.1%;
66
+ --muted-foreground: 215.4 16.3% 46.9%;
67
+
68
+ --accent: 210 40% 96.1%;
69
+ --accent-foreground: 222.2 47.4% 11.2%;
70
+
71
+ --destructive: 0 84.2% 60.2%;
72
+ --destructive-foreground: 210 40% 98%;
73
+
74
+ --border: 214.3 31.8% 91.4%;
75
+ --input: 214.3 31.8% 91.4%;
76
+ --ring: 222.2 84% 4.9%;
77
+
78
+ --radius: 0.5rem;
79
+ }
80
+
81
+ /* ─── Dark Theme ─────────────────────────────────────────── */
82
+
83
+ .dark {
84
+ --background: 222.2 84% 4.9%;
85
+ --foreground: 210 40% 98%;
86
+
87
+ --card: 222.2 84% 4.9%;
88
+ --card-foreground: 210 40% 98%;
89
+
90
+ --popover: 222.2 84% 4.9%;
91
+ --popover-foreground: 210 40% 98%;
92
+
93
+ --primary: 210 40% 98%;
94
+ --primary-foreground: 222.2 47.4% 11.2%;
95
+
96
+ --secondary: 217.2 32.6% 17.5%;
97
+ --secondary-foreground: 210 40% 98%;
98
+
99
+ --muted: 217.2 32.6% 17.5%;
100
+ --muted-foreground: 215 20.2% 65.1%;
101
+
102
+ --accent: 217.2 32.6% 17.5%;
103
+ --accent-foreground: 210 40% 98%;
104
+
105
+ --destructive: 0 62.8% 30.6%;
106
+ --destructive-foreground: 210 40% 98%;
107
+
108
+ --border: 217.2 32.6% 17.5%;
109
+ --input: 217.2 32.6% 17.5%;
110
+ --ring: 212.7 26.8% 83.9%;
111
+ }
112
+
113
+ /* ─── Base Styles ────────────────────────────────────────── */
114
+
115
+ @layer base {
116
+ * {
117
+ border-color: theme(--color-border);
118
+ }
119
+
120
+ body {
121
+ background-color: theme(--color-background);
122
+ color: theme(--color-foreground);
123
+ font-family:
124
+ "Inter",
125
+ system-ui,
126
+ -apple-system,
127
+ BlinkMacSystemFont,
128
+ "Segoe UI",
129
+ Roboto,
130
+ "Helvetica Neue",
131
+ Arial,
132
+ sans-serif;
133
+ }
134
+ }
@@ -0,0 +1,14 @@
1
+ /// <reference types="svelte" />
2
+
3
+ declare module "*.svelte" {
4
+ import type { Component } from "svelte";
5
+ const component: Component<Record<string, any>, Record<string, any>, any>;
6
+ export default component;
7
+ }
8
+
9
+ declare namespace App {
10
+ interface Locals {
11
+ db: import("./features/drizzle").Database;
12
+ requestTime: number;
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="%bosia.lang%">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ %bosia.head%
7
+ </head>
8
+ <body>
9
+ %bosia.body%
10
+ </body>
11
+ </html>
@@ -0,0 +1,21 @@
1
+ import { sequence } from "bosia";
2
+ import type { Handle } from "bosia";
3
+ import { db } from "./features/drizzle";
4
+ import { authHandle } from "./features/auth";
5
+
6
+ const dbHandle: Handle = async ({ event, resolve }) => {
7
+ event.locals.db = db;
8
+ return resolve(event);
9
+ };
10
+
11
+ const loggingHandle: Handle = async ({ event, resolve }) => {
12
+ const start = Date.now();
13
+ event.locals.requestTime = start;
14
+ const res = await resolve(event);
15
+ const ms = Date.now() - start;
16
+ console.log(`[${event.request.method}] ${event.url.pathname} ${res.status} (${ms}ms)`);
17
+ res.headers.set("X-Response-Time", `${ms}ms`);
18
+ return res;
19
+ };
20
+
21
+ export const handle = sequence(dbHandle, authHandle, loggingHandle);
@@ -0,0 +1 @@
1
+ export { cn } from "bosia";
@@ -0,0 +1,10 @@
1
+ import { redirect } from "bosia";
2
+ import type { LoadEvent } from "bosia";
3
+
4
+ export async function load({ locals, url }: LoadEvent) {
5
+ if (!locals.user) {
6
+ const next = encodeURIComponent(url.pathname + url.search);
7
+ throw redirect(303, `/login?next=${next}`);
8
+ }
9
+ return { user: locals.user };
10
+ }