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.
- package/package.json +2 -2
- package/src/cli/add.ts +3 -4
- package/src/cli/block.ts +16 -10
- package/src/cli/create.ts +6 -11
- package/src/cli/feat.ts +19 -22
- package/src/cli/index.ts +1 -2
- package/src/cli/manifest.ts +1 -1
- package/src/cli/registry.ts +3 -1
- package/src/core/build.ts +1 -3
- package/src/core/client/App.svelte +3 -8
- package/src/core/client/router.svelte.ts +3 -8
- package/src/core/config.ts +1 -4
- package/src/core/cookies.ts +1 -2
- package/src/core/dev-500.ts +1 -1
- package/src/core/html.ts +1 -2
- package/src/core/plugin.ts +1 -3
- package/src/core/plugins/inspector/bun-plugin.ts +1 -4
- package/src/core/plugins/inspector/index.ts +45 -59
- package/src/core/renderer.ts +3 -10
- package/src/core/routeTypes.ts +3 -9
- package/src/core/scanner.ts +1 -3
- package/src/core/server.ts +9 -34
- package/src/core/staticManifest.ts +1 -3
- package/src/core/svelteAudit.ts +2 -5
- package/src/core/svelteCompiler.ts +2 -8
- package/templates/default/.prettierignore +1 -0
- package/templates/demo/.prettierignore +1 -0
- package/templates/shop/.env.example +12 -0
- package/templates/shop/.prettierignore +7 -0
- package/templates/shop/.prettierrc.json +9 -0
- package/templates/shop/README.md +62 -0
- package/templates/shop/_gitignore +12 -0
- package/templates/shop/bosia.config.ts +10 -0
- package/templates/shop/instructions.txt +8 -0
- package/templates/shop/package.json +27 -0
- package/templates/shop/public/favicon.svg +14 -0
- package/templates/shop/public/logo-dark.svg +14 -0
- package/templates/shop/public/logo-light.svg +14 -0
- package/templates/shop/src/app.css +132 -0
- package/templates/shop/src/app.d.ts +14 -0
- package/templates/shop/src/app.html +11 -0
- package/templates/shop/src/hooks.server.ts +21 -0
- package/templates/shop/src/lib/utils.ts +1 -0
- package/templates/shop/src/routes/(private)/+layout.server.ts +10 -0
- package/templates/shop/src/routes/(private)/+layout.svelte +14 -0
- package/templates/shop/src/routes/(private)/dashboard/+page.svelte +11 -0
- package/templates/shop/src/routes/(public)/+layout.svelte +13 -0
- package/templates/shop/src/routes/(public)/+page.svelte +30 -0
- package/templates/shop/src/routes/+error.svelte +19 -0
- package/templates/shop/src/routes/+layout.server.ts +9 -0
- package/templates/shop/src/routes/+layout.svelte +6 -0
- package/templates/shop/template.json +10 -0
- package/templates/shop/tsconfig.json +22 -0
- package/templates/todo/.prettierignore +1 -0
- package/templates/todo/template.json +4 -1
package/src/core/scanner.ts
CHANGED
|
@@ -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({
|
package/src/core/server.ts
CHANGED
|
@@ -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
|
}
|
package/src/core/svelteAudit.ts
CHANGED
|
@@ -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" };
|
|
@@ -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,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,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,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>
|