bosia 0.6.21 → 0.6.22

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 (55) 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 +9 -34
  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/demo/.prettierignore +1 -0
  28. package/templates/shop/.env.example +12 -0
  29. package/templates/shop/.prettierignore +7 -0
  30. package/templates/shop/.prettierrc.json +9 -0
  31. package/templates/shop/README.md +62 -0
  32. package/templates/shop/_gitignore +12 -0
  33. package/templates/shop/bosia.config.ts +10 -0
  34. package/templates/shop/instructions.txt +8 -0
  35. package/templates/shop/package.json +27 -0
  36. package/templates/shop/public/favicon.svg +14 -0
  37. package/templates/shop/public/logo-dark.svg +14 -0
  38. package/templates/shop/public/logo-light.svg +14 -0
  39. package/templates/shop/src/app.css +132 -0
  40. package/templates/shop/src/app.d.ts +14 -0
  41. package/templates/shop/src/app.html +11 -0
  42. package/templates/shop/src/hooks.server.ts +21 -0
  43. package/templates/shop/src/lib/utils.ts +1 -0
  44. package/templates/shop/src/routes/(private)/+layout.server.ts +10 -0
  45. package/templates/shop/src/routes/(private)/+layout.svelte +14 -0
  46. package/templates/shop/src/routes/(private)/dashboard/+page.svelte +11 -0
  47. package/templates/shop/src/routes/(public)/+layout.svelte +13 -0
  48. package/templates/shop/src/routes/(public)/+page.svelte +30 -0
  49. package/templates/shop/src/routes/+error.svelte +19 -0
  50. package/templates/shop/src/routes/+layout.server.ts +9 -0
  51. package/templates/shop/src/routes/+layout.svelte +6 -0
  52. package/templates/shop/template.json +10 -0
  53. package/templates/shop/tsconfig.json +22 -0
  54. package/templates/todo/.prettierignore +1 -0
  55. 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(
@@ -461,9 +447,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
461
447
  if (hit) {
462
448
  return new Response(
463
449
  Bun.file(hit.absPath),
464
- hit.cacheControl
465
- ? { headers: { "Cache-Control": hit.cacheControl } }
466
- : undefined,
450
+ hit.cacheControl ? { headers: { "Cache-Control": hit.cacheControl } } : undefined,
467
451
  );
468
452
  }
469
453
  return new Response("Not Found", { status: 404 });
@@ -510,9 +494,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
510
494
  if (!isDev) {
511
495
  // Try both `<path>/index.html` (always/ignore mode) and `<path>.html` (never mode)
512
496
  const prerenderCandidates =
513
- path === "/"
514
- ? ["index.html"]
515
- : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
497
+ path === "/" ? ["index.html"] : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
516
498
  for (const candidate of prerenderCandidates) {
517
499
  const prerenderPath = safePath(`${OUT_DIR}/prerendered`, candidate);
518
500
  if (!prerenderPath) continue;
@@ -533,10 +515,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
533
515
 
534
516
  // Trailing-slash canonicalization — 308 preserves method (form POSTs included)
535
517
  if (pageMatch) {
536
- const canonical = canonicalPathname(
537
- path,
538
- (pageMatch.route as any).trailingSlash ?? "never",
539
- );
518
+ const canonical = canonicalPathname(path, (pageMatch.route as any).trailingSlash ?? "never");
540
519
  if (canonical !== null) {
541
520
  return new Response(null, {
542
521
  status: 308,
@@ -854,15 +833,11 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
854
833
  function parseCorsMaxAge(value?: string): number | undefined {
855
834
  if (!value) return undefined;
856
835
  if (!/^\d+$/.test(value)) {
857
- throw new Error(
858
- `Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`,
859
- );
836
+ throw new Error(`Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`);
860
837
  }
861
838
  const n = parseInt(value, 10);
862
839
  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
- );
840
+ throw new Error(`Invalid CORS_MAX_AGE: "${value}" — must be a non-negative integer (seconds)`);
866
841
  }
867
842
  return n;
868
843
  }
@@ -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
@@ -4,3 +4,4 @@ build
4
4
  .bosia
5
5
  bun.lock
6
6
  public/bosia-tw.css
7
+ bosia.json
@@ -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,27 @@
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
+ "postgres": "^3.4.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/bun": "latest",
22
+ "prettier": "^3.3.0",
23
+ "prettier-plugin-svelte": "^3.2.0",
24
+ "typescript": "^5",
25
+ "drizzle-kit": "^0.31.0"
26
+ }
27
+ }
@@ -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,132 @@
1
+ @import "tailwindcss";
2
+ @source "../src";
3
+
4
+ /*
5
+ * ─── shadcn-inspired Design Tokens ──────────────────────
6
+ * CSS custom properties for light & dark themes.
7
+ * Uses HSL values so Tailwind can apply opacity modifiers.
8
+ */
9
+
10
+ @theme {
11
+ --color-background: hsl(var(--background));
12
+ --color-foreground: hsl(var(--foreground));
13
+
14
+ --color-card: hsl(var(--card));
15
+ --color-card-foreground: hsl(var(--card-foreground));
16
+
17
+ --color-popover: hsl(var(--popover));
18
+ --color-popover-foreground: hsl(var(--popover-foreground));
19
+
20
+ --color-primary: hsl(var(--primary));
21
+ --color-primary-foreground: hsl(var(--primary-foreground));
22
+
23
+ --color-secondary: hsl(var(--secondary));
24
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
25
+
26
+ --color-muted: hsl(var(--muted));
27
+ --color-muted-foreground: hsl(var(--muted-foreground));
28
+
29
+ --color-accent: hsl(var(--accent));
30
+ --color-accent-foreground: hsl(var(--accent-foreground));
31
+
32
+ --color-destructive: hsl(var(--destructive));
33
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
34
+
35
+ --color-border: hsl(var(--border));
36
+ --color-input: hsl(var(--input));
37
+ --color-ring: hsl(var(--ring));
38
+
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
+ }
44
+
45
+ /* ─── Light Theme (Default) ─────────────────────────────── */
46
+
47
+ :root {
48
+ --background: 0 0% 100%;
49
+ --foreground: 222.2 84% 4.9%;
50
+
51
+ --card: 0 0% 100%;
52
+ --card-foreground: 222.2 84% 4.9%;
53
+
54
+ --popover: 0 0% 100%;
55
+ --popover-foreground: 222.2 84% 4.9%;
56
+
57
+ --primary: 222.2 47.4% 11.2%;
58
+ --primary-foreground: 210 40% 98%;
59
+
60
+ --secondary: 210 40% 96.1%;
61
+ --secondary-foreground: 222.2 47.4% 11.2%;
62
+
63
+ --muted: 210 40% 96.1%;
64
+ --muted-foreground: 215.4 16.3% 46.9%;
65
+
66
+ --accent: 210 40% 96.1%;
67
+ --accent-foreground: 222.2 47.4% 11.2%;
68
+
69
+ --destructive: 0 84.2% 60.2%;
70
+ --destructive-foreground: 210 40% 98%;
71
+
72
+ --border: 214.3 31.8% 91.4%;
73
+ --input: 214.3 31.8% 91.4%;
74
+ --ring: 222.2 84% 4.9%;
75
+
76
+ --radius: 0.5rem;
77
+ }
78
+
79
+ /* ─── Dark Theme ─────────────────────────────────────────── */
80
+
81
+ .dark {
82
+ --background: 222.2 84% 4.9%;
83
+ --foreground: 210 40% 98%;
84
+
85
+ --card: 222.2 84% 4.9%;
86
+ --card-foreground: 210 40% 98%;
87
+
88
+ --popover: 222.2 84% 4.9%;
89
+ --popover-foreground: 210 40% 98%;
90
+
91
+ --primary: 210 40% 98%;
92
+ --primary-foreground: 222.2 47.4% 11.2%;
93
+
94
+ --secondary: 217.2 32.6% 17.5%;
95
+ --secondary-foreground: 210 40% 98%;
96
+
97
+ --muted: 217.2 32.6% 17.5%;
98
+ --muted-foreground: 215 20.2% 65.1%;
99
+
100
+ --accent: 217.2 32.6% 17.5%;
101
+ --accent-foreground: 210 40% 98%;
102
+
103
+ --destructive: 0 62.8% 30.6%;
104
+ --destructive-foreground: 210 40% 98%;
105
+
106
+ --border: 217.2 32.6% 17.5%;
107
+ --input: 217.2 32.6% 17.5%;
108
+ --ring: 212.7 26.8% 83.9%;
109
+ }
110
+
111
+ /* ─── Base Styles ────────────────────────────────────────── */
112
+
113
+ @layer base {
114
+ * {
115
+ border-color: theme(--color-border);
116
+ }
117
+
118
+ body {
119
+ background-color: theme(--color-background);
120
+ color: theme(--color-foreground);
121
+ font-family:
122
+ "Inter",
123
+ system-ui,
124
+ -apple-system,
125
+ BlinkMacSystemFont,
126
+ "Segoe UI",
127
+ Roboto,
128
+ "Helvetica Neue",
129
+ Arial,
130
+ sans-serif;
131
+ }
132
+ }
@@ -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
+ }
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { page } from "bosia/client";
3
+ import AdminSidebar from "$lib/components/AdminSidebar.svelte";
4
+
5
+ let { data, children }: { data: { user: { id: string; email: string } }; children: any } =
6
+ $props();
7
+ </script>
8
+
9
+ <div class="flex min-h-screen">
10
+ <AdminSidebar currentPath={page.url.pathname} user={data.user} />
11
+ <main class="flex-1 overflow-x-hidden p-6">
12
+ {@render children()}
13
+ </main>
14
+ </div>
@@ -0,0 +1,11 @@
1
+ <!-- EDIT THIS FILE: add cards, KPIs, recent orders, sales charts, etc. -->
2
+ <svelte:head>
3
+ <title>Dashboard</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Dashboard</h1>
8
+ <p class="text-muted-foreground text-sm">
9
+ This is your admin home. Add widgets, KPI cards, and recent activity here.
10
+ </p>
11
+ </div>
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { page } from "bosia/client";
3
+ import PublicNavbar from "$lib/components/PublicNavbar.svelte";
4
+
5
+ let { data, children }: { data: { user: any }; children: any } = $props();
6
+ </script>
7
+
8
+ <div class="flex min-h-screen flex-col">
9
+ <PublicNavbar currentPath={page.url.pathname} user={data.user} />
10
+ <div class="flex-1">
11
+ {@render children()}
12
+ </div>
13
+ </div>
@@ -0,0 +1,30 @@
1
+ <main class="flex min-h-[80vh] flex-col items-center justify-center gap-6 p-8">
2
+ <div class="flex flex-col items-center gap-3 text-center">
3
+ <img src="/favicon.svg" alt="" class="size-16" />
4
+ <h1 class="text-4xl font-bold tracking-tight">Welcome to your shop</h1>
5
+ <p class="text-muted-foreground text-lg">
6
+ A Bosia shop starter — auth, RBAC, S3 uploads, products & cart.
7
+ </p>
8
+ </div>
9
+
10
+ <div class="mt-4 flex gap-3">
11
+ <a
12
+ href="/products"
13
+ class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
14
+ >
15
+ Browse products
16
+ </a>
17
+ <a
18
+ href="/login"
19
+ class="border-border bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-md border px-4 py-2 text-sm font-medium transition-colors"
20
+ >
21
+ Sign in
22
+ </a>
23
+ </div>
24
+
25
+ <p class="text-muted-foreground mt-6 text-sm">
26
+ Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
27
+ >src/routes/(public)/+page.svelte</code
28
+ > to get started
29
+ </p>
30
+ </main>