bosia 0.6.23 → 0.6.25
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 +1 -1
- package/src/cli/add.ts +15 -4
- package/src/cli/addRouter.ts +17 -6
- package/src/cli/block.ts +7 -0
- package/src/cli/create.ts +0 -1
- package/src/cli/feat.ts +1 -1
- package/src/cli/index.ts +10 -3
- package/src/cli/manifest.ts +16 -1
- package/src/cli/page.ts +141 -0
- package/src/cli/theme.ts +20 -0
- package/src/core/dev.ts +98 -2
- package/src/core/html.ts +10 -4
- package/templates/default/package.json +1 -1
- package/templates/default/src/app.css +6 -0
- package/templates/demo/package.json +1 -1
- package/templates/demo/src/app.css +6 -0
- package/templates/shop/package.json +1 -1
- package/templates/shop/src/app.css +6 -0
- package/templates/todo/.env.example +0 -2
- package/templates/todo/.prettierignore +0 -7
- package/templates/todo/.prettierrc.json +0 -9
- package/templates/todo/README.md +0 -69
- package/templates/todo/_gitignore +0 -12
- package/templates/todo/bosia.config.ts +0 -10
- package/templates/todo/instructions.txt +0 -3
- package/templates/todo/package.json +0 -24
- package/templates/todo/public/.gitkeep +0 -0
- package/templates/todo/public/favicon.svg +0 -14
- package/templates/todo/public/logo-dark.svg +0 -14
- package/templates/todo/public/logo-light.svg +0 -14
- package/templates/todo/src/app.css +0 -134
- package/templates/todo/src/app.d.ts +0 -14
- package/templates/todo/src/app.html +0 -11
- package/templates/todo/src/hooks.server.ts +0 -20
- package/templates/todo/src/lib/utils.ts +0 -1
- package/templates/todo/src/routes/(public)/+page.svelte +0 -53
- package/templates/todo/src/routes/+error.svelte +0 -19
- package/templates/todo/src/routes/+layout.server.ts +0 -8
- package/templates/todo/src/routes/+layout.svelte +0 -6
- package/templates/todo/template.json +0 -6
- package/templates/todo/tsconfig.json +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
package/src/cli/add.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface RegistryIndex {
|
|
|
33
33
|
components: string[];
|
|
34
34
|
features: string[];
|
|
35
35
|
blocks?: string[];
|
|
36
|
+
pages?: string[];
|
|
36
37
|
themes?: string[];
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -75,7 +76,7 @@ export async function runAdd(names: string[], flags: string[] = []) {
|
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* Resolve the full registry path for a component using the index.
|
|
78
|
-
* - "
|
|
79
|
+
* - "<name>" → "<name>" (exact match in index)
|
|
79
80
|
* - "button" → "ui/button" (suffix match in index)
|
|
80
81
|
* - "shop/cart" → "shop/cart" (explicit path used as-is)
|
|
81
82
|
*/
|
|
@@ -83,7 +84,7 @@ function resolveDestPath(name: string): string {
|
|
|
83
84
|
if (name.includes("/")) return name;
|
|
84
85
|
|
|
85
86
|
if (registryIndex) {
|
|
86
|
-
// Exact match (
|
|
87
|
+
// Exact match (bare name present in index)
|
|
87
88
|
if (registryIndex.components.includes(name)) return name;
|
|
88
89
|
// Suffix match (e.g. "button" → "ui/button")
|
|
89
90
|
const match = registryIndex.components.find((c) => c.endsWith(`/${name}`));
|
|
@@ -181,9 +182,10 @@ export function runAddList(): void {
|
|
|
181
182
|
const manifest = readManifest();
|
|
182
183
|
const components = Object.entries(manifest.components);
|
|
183
184
|
const blocks = Object.entries(manifest.blocks);
|
|
185
|
+
const pages = Object.entries(manifest.pages);
|
|
184
186
|
|
|
185
|
-
if (components.length === 0 && blocks.length === 0) {
|
|
186
|
-
console.log("No components or
|
|
187
|
+
if (components.length === 0 && blocks.length === 0 && pages.length === 0) {
|
|
188
|
+
console.log("No components, blocks, or pages installed.");
|
|
187
189
|
console.log("Install one with: bun x bosia@latest add <component>");
|
|
188
190
|
return;
|
|
189
191
|
}
|
|
@@ -204,6 +206,15 @@ export function runAddList(): void {
|
|
|
204
206
|
console.log(` ${name.padEnd(w)} ${entry.installedAt.slice(0, 10)}`);
|
|
205
207
|
}
|
|
206
208
|
}
|
|
209
|
+
|
|
210
|
+
if (pages.length > 0) {
|
|
211
|
+
if (components.length > 0 || blocks.length > 0) console.log("");
|
|
212
|
+
console.log(`⬡ Installed pages (${pages.length}):\n`);
|
|
213
|
+
const w = Math.max(...pages.map(([n]) => n.length));
|
|
214
|
+
for (const [name, entry] of pages) {
|
|
215
|
+
console.log(` ${name.padEnd(w)} ${entry.installedAt.slice(0, 10)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
// ─── Ensure $lib/utils.ts exists ─────────────────────────────
|
package/src/cli/addRouter.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export interface AddRunners {
|
|
6
6
|
runAdd: (names: string[], flags: string[]) => Promise<void> | void;
|
|
7
7
|
runAddBlock: (name: string | undefined, flags: string[]) => Promise<void> | void;
|
|
8
|
+
runAddPage: (name: string | undefined, flags: string[]) => Promise<void> | void;
|
|
8
9
|
runAddTheme: (name: string | undefined, flags: string[]) => Promise<void> | void;
|
|
9
10
|
runAddFont: (family: string | undefined, url: string | undefined) => Promise<void> | void;
|
|
10
11
|
runAddList: () => Promise<void> | void;
|
|
@@ -19,6 +20,10 @@ export async function routeAdd(args: string[], runners: AddRunners): Promise<voi
|
|
|
19
20
|
await runners.runAddBlock(positional[1], flags);
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
23
|
+
if (sub === "page") {
|
|
24
|
+
await runners.runAddPage(positional[1], flags);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
22
27
|
if (sub === "theme") {
|
|
23
28
|
const themeFlags = args.filter((a) => a.startsWith("--"));
|
|
24
29
|
await runners.runAddTheme(positional[1], themeFlags);
|
|
@@ -33,19 +38,25 @@ export async function routeAdd(args: string[], runners: AddRunners): Promise<voi
|
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
// Alias: `blocks/<cat>/<name>` tokens dispatch to
|
|
37
|
-
// Skills/AI agents frequently emit
|
|
38
|
-
// `ui/*` components; route those
|
|
39
|
-
//
|
|
41
|
+
// Alias: `blocks/<cat>/<name>` / `pages/<cat>/<name>` tokens dispatch to the
|
|
42
|
+
// matching installer. Skills/AI agents frequently emit these plural forms
|
|
43
|
+
// alongside `ui/*` components; route those transparently and let any
|
|
44
|
+
// remaining plain component names fall through to runAdd.
|
|
40
45
|
const blockTokens = positional.filter((p) => p.startsWith("blocks/"));
|
|
41
|
-
const
|
|
42
|
-
|
|
46
|
+
const pageTokens = positional.filter((p) => p.startsWith("pages/"));
|
|
47
|
+
const componentTokens = positional.filter(
|
|
48
|
+
(p) => !p.startsWith("blocks/") && !p.startsWith("pages/"),
|
|
49
|
+
);
|
|
50
|
+
if (blockTokens.length > 0 || pageTokens.length > 0) {
|
|
43
51
|
if (componentTokens.length > 0) {
|
|
44
52
|
await runners.runAdd(componentTokens, flags);
|
|
45
53
|
}
|
|
46
54
|
for (const token of blockTokens) {
|
|
47
55
|
await runners.runAddBlock(token.slice("blocks/".length), flags);
|
|
48
56
|
}
|
|
57
|
+
for (const token of pageTokens) {
|
|
58
|
+
await runners.runAddPage(token.slice("pages/".length), flags);
|
|
59
|
+
}
|
|
49
60
|
return;
|
|
50
61
|
}
|
|
51
62
|
|
package/src/cli/block.ts
CHANGED
|
@@ -30,6 +30,10 @@ interface BlockMeta {
|
|
|
30
30
|
npmDeps: Record<string, string>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Track already-installed blocks within a session to avoid redundant work
|
|
34
|
+
// when multiple features/blocks declare the same block dependency.
|
|
35
|
+
const installed = new Set<string>();
|
|
36
|
+
|
|
33
37
|
export async function runAddBlock(
|
|
34
38
|
name: string | undefined,
|
|
35
39
|
flags: string[] = [],
|
|
@@ -58,6 +62,9 @@ export async function runAddBlock(
|
|
|
58
62
|
await initAddRegistry(registryRoot);
|
|
59
63
|
ensureUtils(resolvedOptions.cwd);
|
|
60
64
|
|
|
65
|
+
if (installed.has(name)) return;
|
|
66
|
+
installed.add(name);
|
|
67
|
+
|
|
61
68
|
console.log(`⬡ Installing block: ${name}\n`);
|
|
62
69
|
|
|
63
70
|
const meta = await readRegistryJSON<BlockMeta>(registryRoot, "blocks", name, "meta.json");
|
package/src/cli/create.ts
CHANGED
|
@@ -14,7 +14,6 @@ const BOSIA_VERSION: string = BOSIA_PKG.version;
|
|
|
14
14
|
const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
|
|
15
15
|
default: "Minimal starter with routing and Tailwind",
|
|
16
16
|
demo: "Full-featured demo with hooks, API routes, form actions, and more",
|
|
17
|
-
todo: "Todo app with PostgreSQL + Drizzle ORM",
|
|
18
17
|
shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart",
|
|
19
18
|
};
|
|
20
19
|
|
package/src/cli/feat.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { recordFeature, readManifest } from "./manifest.ts";
|
|
|
16
16
|
// ─── bun x bosia@latest feat <feature> [--local] ─────────
|
|
17
17
|
// Fetches a feature scaffold from the GitHub registry (or local
|
|
18
18
|
// registry with --local) and copies route/lib files, installs npm deps.
|
|
19
|
-
// Supports nested feature dependencies (e.g.
|
|
19
|
+
// Supports nested feature dependencies (e.g. shop → auth).
|
|
20
20
|
|
|
21
21
|
type FileStrategy =
|
|
22
22
|
| "write" // overwrite (prompt if interactive)
|
package/src/cli/index.ts
CHANGED
|
@@ -69,6 +69,10 @@ async function main() {
|
|
|
69
69
|
const { runAddBlock } = await import("./block.ts");
|
|
70
70
|
await runAddBlock(name, flags);
|
|
71
71
|
},
|
|
72
|
+
runAddPage: async (name, flags) => {
|
|
73
|
+
const { runAddPage } = await import("./page.ts");
|
|
74
|
+
await runAddPage(name, flags);
|
|
75
|
+
},
|
|
72
76
|
runAddTheme: async (name, flags) => {
|
|
73
77
|
const { runAddTheme } = await import("./theme.ts");
|
|
74
78
|
await runAddTheme(name, flags);
|
|
@@ -116,6 +120,7 @@ Commands:
|
|
|
116
120
|
test [args] Run tests with bun test (auto-loads .env.test, sets BOSIA_ENV=test)
|
|
117
121
|
add <component...> [-y] Add one or more UI components from the registry
|
|
118
122
|
add block <cat>/<name> Add a composed block from the registry
|
|
123
|
+
add page <cat>/<name> Add a full page (a group of blocks) from the registry
|
|
119
124
|
add theme <name> Add a theme (tokens.css) from the registry
|
|
120
125
|
add font <family> <url> Prepend an @import url(...) for a font family to src/app.css
|
|
121
126
|
add list List installed components and blocks (reads bosia.json)
|
|
@@ -126,7 +131,7 @@ Commands:
|
|
|
126
131
|
|
|
127
132
|
Examples:
|
|
128
133
|
bun x bosia@latest create my-app
|
|
129
|
-
bun x bosia@latest create my-app --template
|
|
134
|
+
bun x bosia@latest create my-app --template shop
|
|
130
135
|
bun x bosia dev
|
|
131
136
|
bun x bosia build
|
|
132
137
|
bun x bosia start
|
|
@@ -137,8 +142,10 @@ Examples:
|
|
|
137
142
|
bun x bosia@latest add button card input → install multiple at once
|
|
138
143
|
bun x bosia@latest add -y button card → auto-confirm overwrites (CI / scripts)
|
|
139
144
|
bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
|
|
140
|
-
bun x bosia@latest add block cards/feature
|
|
141
|
-
bun x bosia@latest add blocks/cards/feature
|
|
145
|
+
bun x bosia@latest add block cards/feature
|
|
146
|
+
bun x bosia@latest add blocks/cards/feature (alias for: add block cards/feature)
|
|
147
|
+
bun x bosia@latest add page storefront/home
|
|
148
|
+
bun x bosia@latest add pages/storefront/home (alias for: add page storefront/home)
|
|
142
149
|
bun x bosia@latest add theme editorial
|
|
143
150
|
bun x bosia@latest add font "Fredoka" "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"
|
|
144
151
|
bun x bosia@latest feat login
|
package/src/cli/manifest.ts
CHANGED
|
@@ -38,15 +38,19 @@ export interface BlockManifestEntry {
|
|
|
38
38
|
fonts?: string[];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// A page is a group of blocks (no backend); reuse the block entry shape.
|
|
42
|
+
export type PageManifestEntry = BlockManifestEntry;
|
|
43
|
+
|
|
41
44
|
export interface Manifest {
|
|
42
45
|
version: number;
|
|
43
46
|
features: Record<string, FeatureManifestEntry>;
|
|
44
47
|
components: Record<string, ComponentManifestEntry>;
|
|
45
48
|
blocks: Record<string, BlockManifestEntry>;
|
|
49
|
+
pages: Record<string, PageManifestEntry>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function emptyManifest(): Manifest {
|
|
49
|
-
return { version: MANIFEST_VERSION, features: {}, components: {}, blocks: {} };
|
|
53
|
+
return { version: MANIFEST_VERSION, features: {}, components: {}, blocks: {}, pages: {} };
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export function readManifest(cwd: string = process.cwd()): Manifest {
|
|
@@ -59,6 +63,7 @@ export function readManifest(cwd: string = process.cwd()): Manifest {
|
|
|
59
63
|
features: parsed.features ?? {},
|
|
60
64
|
components: parsed.components ?? {},
|
|
61
65
|
blocks: parsed.blocks ?? {},
|
|
66
|
+
pages: parsed.pages ?? {},
|
|
62
67
|
};
|
|
63
68
|
} catch {
|
|
64
69
|
return emptyManifest();
|
|
@@ -99,3 +104,13 @@ export function recordBlock(
|
|
|
99
104
|
manifest.blocks[name] = { installedAt: new Date().toISOString(), ...entry };
|
|
100
105
|
writeManifest(manifest, cwd);
|
|
101
106
|
}
|
|
107
|
+
|
|
108
|
+
export function recordPage(
|
|
109
|
+
cwd: string,
|
|
110
|
+
name: string,
|
|
111
|
+
entry: Omit<PageManifestEntry, "installedAt">,
|
|
112
|
+
): void {
|
|
113
|
+
const manifest = readManifest(cwd);
|
|
114
|
+
manifest.pages[name] = { installedAt: new Date().toISOString(), ...entry };
|
|
115
|
+
writeManifest(manifest, cwd);
|
|
116
|
+
}
|
package/src/cli/page.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { mkdirSync, existsSync } from "fs";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import {
|
|
5
|
+
type InstallOptions,
|
|
6
|
+
resolveLocalRegistryOrExit,
|
|
7
|
+
readRegistryJSON,
|
|
8
|
+
readRegistryFile,
|
|
9
|
+
writeRegistryFile,
|
|
10
|
+
mergePkgJson,
|
|
11
|
+
bunAdd,
|
|
12
|
+
} from "./registry.ts";
|
|
13
|
+
import { addComponent, initAddRegistry, ensureUtils } from "./add.ts";
|
|
14
|
+
import { runAddBlock } from "./block.ts";
|
|
15
|
+
import { mergeFontImports } from "./fonts.ts";
|
|
16
|
+
import { recordPage } from "./manifest.ts";
|
|
17
|
+
|
|
18
|
+
// ─── bun x bosia@latest add page <category>/<name> ───────
|
|
19
|
+
// Installs a full page (a group of blocks, no backend) into
|
|
20
|
+
// src/lib/pages/<path>/. Recursively installs the block and
|
|
21
|
+
// component dependencies the page composes, plus npm deps and
|
|
22
|
+
// optional Google Fonts @imports into app.css.
|
|
23
|
+
|
|
24
|
+
interface PageMeta {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
category: string;
|
|
28
|
+
themes?: string[];
|
|
29
|
+
dependencies: string[]; // "blocks/..." or "ui/..." entries
|
|
30
|
+
files: string[];
|
|
31
|
+
fonts?: Record<string, string>; // family → @import URL
|
|
32
|
+
npmDeps: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Track already-installed pages within a session to avoid redundant work.
|
|
36
|
+
const installed = new Set<string>();
|
|
37
|
+
|
|
38
|
+
export async function runAddPage(
|
|
39
|
+
name: string | undefined,
|
|
40
|
+
flags: string[] = [],
|
|
41
|
+
options?: InstallOptions,
|
|
42
|
+
) {
|
|
43
|
+
if (!name || !name.includes("/")) {
|
|
44
|
+
console.error(
|
|
45
|
+
"❌ Please provide a page path.\n Usage: bun x bosia@latest add page <category>/<name> [-y] [--local]",
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const local = flags.includes("--local");
|
|
51
|
+
const flagYes = flags.includes("-y") || flags.includes("--yes");
|
|
52
|
+
const inheritedRoot = options?.registryRoot ?? null;
|
|
53
|
+
const registryRoot = inheritedRoot ?? (local ? resolveLocalRegistryOrExit() : null);
|
|
54
|
+
if (local && !inheritedRoot) console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
55
|
+
|
|
56
|
+
const resolvedOptions: InstallOptions = {
|
|
57
|
+
...(options ?? {}),
|
|
58
|
+
registryRoot,
|
|
59
|
+
skipPrompts: options?.skipPrompts ?? flagYes,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await initAddRegistry(registryRoot);
|
|
63
|
+
ensureUtils(resolvedOptions.cwd);
|
|
64
|
+
|
|
65
|
+
if (installed.has(name)) return;
|
|
66
|
+
installed.add(name);
|
|
67
|
+
|
|
68
|
+
console.log(`⬡ Installing page: ${name}\n`);
|
|
69
|
+
|
|
70
|
+
const meta = await readRegistryJSON<PageMeta>(registryRoot, "pages", name, "meta.json");
|
|
71
|
+
|
|
72
|
+
// 1. Install dependencies first.
|
|
73
|
+
// Block deps (e.g. "blocks/storefront/header") recurse into runAddBlock.
|
|
74
|
+
// Component deps (e.g. "ui/button") go through addComponent.
|
|
75
|
+
for (const dep of meta.dependencies ?? []) {
|
|
76
|
+
if (dep.startsWith("blocks/")) {
|
|
77
|
+
await runAddBlock(dep.slice("blocks/".length), [], resolvedOptions);
|
|
78
|
+
} else {
|
|
79
|
+
await addComponent(dep, false, resolvedOptions);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Copy page files to src/lib/pages/<path>/
|
|
84
|
+
const cwd = resolvedOptions.cwd ?? process.cwd();
|
|
85
|
+
const destDir = join(cwd, "src", "lib", "pages", name);
|
|
86
|
+
|
|
87
|
+
if (!resolvedOptions.skipPrompts && existsSync(destDir)) {
|
|
88
|
+
const replace = await p.confirm({
|
|
89
|
+
message: `Page "${name}" already exists at src/lib/pages/${name}/. Replace it?`,
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(replace) || !replace) {
|
|
92
|
+
console.log(` ⏭️ Skipped ${name}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
mkdirSync(destDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
for (const file of meta.files) {
|
|
100
|
+
const content = await readRegistryFile(registryRoot, "pages", name, file);
|
|
101
|
+
const dest = join(destDir, file);
|
|
102
|
+
if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
|
|
103
|
+
writeRegistryFile(dest, content);
|
|
104
|
+
console.log(` ✍️ src/lib/pages/${name}/${file}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Merge font @imports into app.css (idempotent)
|
|
108
|
+
if (meta.fonts && Object.keys(meta.fonts).length > 0) {
|
|
109
|
+
const cssPath = join(cwd, "src", "app.css");
|
|
110
|
+
if (existsSync(cssPath)) {
|
|
111
|
+
const added = mergeFontImports(cssPath, meta.fonts);
|
|
112
|
+
if (added.length > 0) {
|
|
113
|
+
console.log(` 🔤 Added fonts to app.css: ${added.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. npm deps
|
|
119
|
+
if (meta.npmDeps && Object.keys(meta.npmDeps).length > 0) {
|
|
120
|
+
if (resolvedOptions.skipInstall) {
|
|
121
|
+
const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
|
|
122
|
+
if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
123
|
+
} else {
|
|
124
|
+
await bunAdd(cwd, meta.npmDeps);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 5. Record install in bosia.json manifest.
|
|
129
|
+
recordPage(cwd, name, {
|
|
130
|
+
files: meta.files,
|
|
131
|
+
...(meta.npmDeps && Object.keys(meta.npmDeps).length > 0
|
|
132
|
+
? { npmDeps: Object.keys(meta.npmDeps) }
|
|
133
|
+
: {}),
|
|
134
|
+
...(meta.dependencies && meta.dependencies.length > 0
|
|
135
|
+
? { dependencies: meta.dependencies }
|
|
136
|
+
: {}),
|
|
137
|
+
...(meta.fonts && Object.keys(meta.fonts).length > 0 ? { fonts: Object.keys(meta.fonts) } : {}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`\n✅ ${name} installed at src/lib/pages/${name}/`);
|
|
141
|
+
}
|
package/src/cli/theme.ts
CHANGED
|
@@ -18,6 +18,9 @@ interface ThemeMeta {
|
|
|
18
18
|
|
|
19
19
|
const THEME_IMPORT_RE = /^@import\s+["']\.\/lib\/themes\/[^"']+["'];?\s*$/m;
|
|
20
20
|
const THEME_MARKER = "/* bosia-theme */";
|
|
21
|
+
// Sentinel-bounded :root/.dark block the templates ship (see templates/*/src/app.css).
|
|
22
|
+
// Removing it targets exactly the template defaults and never user-authored :root rules.
|
|
23
|
+
const THEME_VARS_RE = /\/\* bosia-theme-vars[\s\S]*?\/\* \/bosia-theme-vars \*\//;
|
|
21
24
|
|
|
22
25
|
export async function runAddTheme(name: string | undefined, flags: string[] = []) {
|
|
23
26
|
if (!name) {
|
|
@@ -52,6 +55,11 @@ export async function runAddTheme(name: string | undefined, flags: string[] = []
|
|
|
52
55
|
if (existsSync(appCssPath)) {
|
|
53
56
|
patchAppCssThemeImport(appCssPath, name);
|
|
54
57
|
console.log(` 🎨 app.css → @import "./lib/themes/${name}.css"`);
|
|
58
|
+
// The installed theme css owns :root/.dark now — drop the template's default
|
|
59
|
+
// block so two same-specificity :root rules don't fight (template wins by order).
|
|
60
|
+
if (stripTemplateThemeVars(appCssPath)) {
|
|
61
|
+
console.log(` 🧹 app.css → removed template :root/.dark vars (theme owns them)`);
|
|
62
|
+
}
|
|
55
63
|
} else {
|
|
56
64
|
console.warn(` ⚠️ src/app.css not found — theme import not wired automatically.`);
|
|
57
65
|
}
|
|
@@ -86,3 +94,15 @@ function patchAppCssThemeImport(appCssPath: string, themeName: string) {
|
|
|
86
94
|
}
|
|
87
95
|
writeFileSync(appCssPath, next, "utf-8");
|
|
88
96
|
}
|
|
97
|
+
|
|
98
|
+
// Strip the sentinel-bounded template :root/.dark block. Idempotent: once removed
|
|
99
|
+
// (or on an app that never had it), the marker is gone and this is a no-op. Only
|
|
100
|
+
// the raw token blocks go — `@theme {}` (Tailwind mapping) lives above the markers
|
|
101
|
+
// and is untouched.
|
|
102
|
+
function stripTemplateThemeVars(appCssPath: string): boolean {
|
|
103
|
+
const src = readFileSync(appCssPath, "utf-8");
|
|
104
|
+
if (!THEME_VARS_RE.test(src)) return false;
|
|
105
|
+
const next = src.replace(THEME_VARS_RE, "").replace(/\n{3,}/g, "\n\n");
|
|
106
|
+
writeFileSync(appCssPath, next, "utf-8");
|
|
107
|
+
return true;
|
|
108
|
+
}
|
package/src/core/dev.ts
CHANGED
|
@@ -42,7 +42,27 @@ const BACKOFF_SCHEDULE_MS = [500, 1_000, 2_000, 4_000, 5_000];
|
|
|
42
42
|
|
|
43
43
|
// ─── SSE Broadcast ────────────────────────────────────────
|
|
44
44
|
|
|
45
|
+
// Reload-hold control (driven by titoko via /__bosia/hold + /__bosia/resume).
|
|
46
|
+
// While held, rebuilds keep happening (latest code stays ready) but the reload
|
|
47
|
+
// broadcast is suppressed; on resume a single reload is flushed if any rebuild
|
|
48
|
+
// fired meanwhile. Defaults to off, so a plain `bosia dev` developer never sees
|
|
49
|
+
// any behaviour change.
|
|
50
|
+
let reloadHeld = false;
|
|
51
|
+
let reloadQueuedWhileHeld = false;
|
|
52
|
+
let holdSafetyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
// Safety net for a *dead* orchestrator only — NOT a task duration cap. While an
|
|
54
|
+
// agent run is healthy the orchestrator heartbeats `/__bosia/hold` (re-arming
|
|
55
|
+
// this timer), so it never fires mid-task no matter how long the task runs. It
|
|
56
|
+
// fires only if the heartbeats stop (titoko crash, network partition), so a
|
|
57
|
+
// missed resume can't freeze the preview forever. Must comfortably exceed the
|
|
58
|
+
// heartbeat interval so one dropped ping doesn't trip it.
|
|
59
|
+
const HOLD_SAFETY_MS = 90_000;
|
|
60
|
+
|
|
45
61
|
function broadcastReload() {
|
|
62
|
+
if (reloadHeld) {
|
|
63
|
+
reloadQueuedWhileHeld = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
46
66
|
const msg = new TextEncoder().encode("event: reload\ndata: ok\n\n");
|
|
47
67
|
for (const ctrl of sseClients) {
|
|
48
68
|
try {
|
|
@@ -56,6 +76,41 @@ function broadcastReload() {
|
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
78
|
|
|
79
|
+
// 503 body served while the inner app server is mid-restart (rebuild after an
|
|
80
|
+
// edit). Must be HTML carrying the SAME SSE reload client as the dev-500 page —
|
|
81
|
+
// the bare text/plain version had no live `/__bosia/sse` connection, so once an
|
|
82
|
+
// iframe landed here (e.g. a reload racing into a rebuild window) it stayed stuck
|
|
83
|
+
// until a manual reload. With the client, the next `broadcastReload()` once the
|
|
84
|
+
// app binds reloads this page automatically. Keep the literal phrase
|
|
85
|
+
// "App server is starting" in the body: titoko's proxy retry matches on it.
|
|
86
|
+
const STARTING_PAGE = `<!doctype html>
|
|
87
|
+
<html lang="en">
|
|
88
|
+
<head>
|
|
89
|
+
<meta charset="utf-8" />
|
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
91
|
+
<title>Starting…</title>
|
|
92
|
+
<style>
|
|
93
|
+
html,body{margin:0;padding:0;height:100%;background:#0a0a0a;color:#e5e5e5;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
|
|
94
|
+
.wrap{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
|
|
95
|
+
.dot{display:inline-block;width:10px;height:10px;background:#16a34a;border-radius:50%;margin-right:8px;vertical-align:middle;animation:p 1.4s ease-in-out infinite}
|
|
96
|
+
@keyframes p{0%,100%{opacity:1}50%{opacity:.3}}
|
|
97
|
+
h1{font-size:16px;font-weight:600;margin:0}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div class="wrap"><h1><span class="dot"></span>App server is starting…</h1></div>
|
|
102
|
+
<script>
|
|
103
|
+
!function r(){
|
|
104
|
+
try{
|
|
105
|
+
var e=new EventSource("/__bosia/sse");
|
|
106
|
+
e.addEventListener("reload",function(){location.reload()});
|
|
107
|
+
e.onerror=function(){e.close();setTimeout(r,2000)};
|
|
108
|
+
}catch(_){setTimeout(r,2000)}
|
|
109
|
+
}();
|
|
110
|
+
</script>
|
|
111
|
+
</body>
|
|
112
|
+
</html>`;
|
|
113
|
+
|
|
59
114
|
// ─── Build ────────────────────────────────────────────────
|
|
60
115
|
|
|
61
116
|
const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
|
|
@@ -235,6 +290,47 @@ const devServer = Bun.serve({
|
|
|
235
290
|
);
|
|
236
291
|
}
|
|
237
292
|
|
|
293
|
+
// Reload-hold control — host orchestrator (titoko) brackets an AI agent run
|
|
294
|
+
// so the preview reloads once when the agent finishes, not once per file
|
|
295
|
+
// edit. Both routes are idempotent and return small JSON.
|
|
296
|
+
//
|
|
297
|
+
// POST /__bosia/hold doubles as the heartbeat: the FIRST hold opens a fresh
|
|
298
|
+
// window (clears any stale queued reload); subsequent holds only re-arm the
|
|
299
|
+
// safety timer and MUST preserve `reloadQueuedWhileHeld`, or a heartbeat
|
|
300
|
+
// landing after a suppressed rebuild would drop the pending reload and
|
|
301
|
+
// resume would flush nothing.
|
|
302
|
+
if (url.pathname === "/__bosia/hold" && req.method === "POST") {
|
|
303
|
+
if (!reloadHeld) {
|
|
304
|
+
reloadHeld = true;
|
|
305
|
+
reloadQueuedWhileHeld = false;
|
|
306
|
+
}
|
|
307
|
+
if (holdSafetyTimer) clearTimeout(holdSafetyTimer);
|
|
308
|
+
holdSafetyTimer = setTimeout(() => {
|
|
309
|
+
holdSafetyTimer = null;
|
|
310
|
+
if (!reloadHeld) return;
|
|
311
|
+
console.warn("⏱️ Reload hold safety timeout — auto-resuming");
|
|
312
|
+
reloadHeld = false;
|
|
313
|
+
if (reloadQueuedWhileHeld) {
|
|
314
|
+
reloadQueuedWhileHeld = false;
|
|
315
|
+
broadcastReload();
|
|
316
|
+
}
|
|
317
|
+
}, HOLD_SAFETY_MS);
|
|
318
|
+
holdSafetyTimer.unref?.();
|
|
319
|
+
return Response.json({ ok: true, held: true });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (url.pathname === "/__bosia/resume" && req.method === "POST") {
|
|
323
|
+
if (holdSafetyTimer) {
|
|
324
|
+
clearTimeout(holdSafetyTimer);
|
|
325
|
+
holdSafetyTimer = null;
|
|
326
|
+
}
|
|
327
|
+
reloadHeld = false;
|
|
328
|
+
const flushed = reloadQueuedWhileHeld;
|
|
329
|
+
reloadQueuedWhileHeld = false;
|
|
330
|
+
if (flushed) broadcastReload();
|
|
331
|
+
return Response.json({ ok: true, held: false, flushed });
|
|
332
|
+
}
|
|
333
|
+
|
|
238
334
|
// Proxy everything else to the app server. Inject X-Forwarded-Host/Proto so
|
|
239
335
|
// the app's CSRF origin check (gated behind TRUST_PROXY=true, also set in the
|
|
240
336
|
// app env above) reconstructs the public-facing origin from the dev proxy
|
|
@@ -283,9 +379,9 @@ const devServer = Bun.serve({
|
|
|
283
379
|
await Bun.sleep(250);
|
|
284
380
|
continue;
|
|
285
381
|
}
|
|
286
|
-
return new Response(
|
|
382
|
+
return new Response(STARTING_PAGE, {
|
|
287
383
|
status: 503,
|
|
288
|
-
headers: { "Content-Type": "text/
|
|
384
|
+
headers: { "Content-Type": "text/html; charset=utf-8", "Retry-After": "1" },
|
|
289
385
|
});
|
|
290
386
|
}
|
|
291
387
|
}
|
package/src/core/html.ts
CHANGED
|
@@ -19,6 +19,12 @@ export const distManifest: { js: string[]; css: string[]; entry: string } = (()
|
|
|
19
19
|
export const isDev = process.env.NODE_ENV !== "production";
|
|
20
20
|
const cacheBust = isDev ? `?v=${Date.now()}` : "";
|
|
21
21
|
|
|
22
|
+
/** Inline theme bootstrap — runs before paint to avoid FOUC. theme ∈ light|dark|system (missing = system). */
|
|
23
|
+
const THEME_INIT_JS =
|
|
24
|
+
"try{var t=localStorage.getItem('theme');" +
|
|
25
|
+
"document.documentElement.classList.toggle('dark'," +
|
|
26
|
+
"t==='dark'||((t===null||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches))}catch(_){}";
|
|
27
|
+
|
|
22
28
|
// ─── Safe JSON Serialization ──────────────────────────────
|
|
23
29
|
|
|
24
30
|
/** Escapes JSON for safe embedding inside <script> tags. Prevents XSS via </script> injection. */
|
|
@@ -156,7 +162,7 @@ export function buildHtml(
|
|
|
156
162
|
headOpenInterpolated +
|
|
157
163
|
`\n ${faviconLine}${cssLinks}\n` +
|
|
158
164
|
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
159
|
-
` <script${n}
|
|
165
|
+
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
160
166
|
` ${fallbackTitle}${head}` +
|
|
161
167
|
headCloseInterpolated +
|
|
162
168
|
(body ? "" : `\n${SPINNER}`) +
|
|
@@ -175,7 +181,7 @@ export function buildHtml(
|
|
|
175
181
|
${head}
|
|
176
182
|
${cssLinks}
|
|
177
183
|
<link rel="stylesheet" href="/bosia-tw.css${cacheBust}">
|
|
178
|
-
<script${n}
|
|
184
|
+
<script${n}>${THEME_INIT_JS}</script>
|
|
179
185
|
</head>
|
|
180
186
|
<body>
|
|
181
187
|
<div id="app">${body}</div>${scripts}${bodyEnd}
|
|
@@ -208,7 +214,7 @@ export function buildHtmlShellOpen(
|
|
|
208
214
|
headOpenInterpolated +
|
|
209
215
|
`\n ${faviconLine}${cssLinks}\n` +
|
|
210
216
|
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
211
|
-
` <script${n}
|
|
217
|
+
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
212
218
|
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
|
|
213
219
|
);
|
|
214
220
|
}
|
|
@@ -220,7 +226,7 @@ export function buildHtmlShellOpen(
|
|
|
220
226
|
` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
|
|
221
227
|
` ${cssLinks}\n` +
|
|
222
228
|
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
223
|
-
` <script${n}
|
|
229
|
+
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
224
230
|
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
|
|
225
231
|
);
|
|
226
232
|
}
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
--radius-xl: calc(var(--radius) + 4px);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
|
|
48
|
+
between these markers (the installed theme owns these tokens). Do NOT add your
|
|
49
|
+
own :root rules inside the markers — put custom overrides after the close tag. */
|
|
50
|
+
|
|
47
51
|
/* ─── Light Theme (Default) ─────────────────────────────── */
|
|
48
52
|
|
|
49
53
|
:root {
|
|
@@ -110,6 +114,8 @@
|
|
|
110
114
|
--ring: 212.7 26.8% 83.9%;
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
/* /bosia-theme-vars */
|
|
118
|
+
|
|
113
119
|
/* ─── Base Styles ────────────────────────────────────────── */
|
|
114
120
|
|
|
115
121
|
@layer base {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
--radius-xl: calc(var(--radius) + 4px);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
|
|
48
|
+
between these markers (the installed theme owns these tokens). Do NOT add your
|
|
49
|
+
own :root rules inside the markers — put custom overrides after the close tag. */
|
|
50
|
+
|
|
47
51
|
/* ─── Light Theme (Default) ─────────────────────────────── */
|
|
48
52
|
|
|
49
53
|
:root {
|
|
@@ -110,6 +114,8 @@
|
|
|
110
114
|
--ring: 212.7 26.8% 83.9%;
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
/* /bosia-theme-vars */
|
|
118
|
+
|
|
113
119
|
/* ─── Base Styles ────────────────────────────────────────── */
|
|
114
120
|
|
|
115
121
|
@layer base {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
--radius-xl: calc(var(--radius) + 4px);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
|
|
48
|
+
between these markers (the installed theme owns these tokens). Do NOT add your
|
|
49
|
+
own :root rules inside the markers — put custom overrides after the close tag. */
|
|
50
|
+
|
|
47
51
|
/* ─── Light Theme (Default) ─────────────────────────────── */
|
|
48
52
|
|
|
49
53
|
:root {
|
|
@@ -110,6 +114,8 @@
|
|
|
110
114
|
--ring: 212.7 26.8% 83.9%;
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
/* /bosia-theme-vars */
|
|
118
|
+
|
|
113
119
|
/* ─── Base Styles ────────────────────────────────────────── */
|
|
114
120
|
|
|
115
121
|
@layer base {
|
package/templates/todo/README.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# {{PROJECT_NAME}}
|
|
2
|
-
|
|
3
|
-
A fullstack app built with [Bosia](https://github.com/bosapi/bosia) + Drizzle ORM + PostgreSQL.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
- [Bun](https://bun.sh/) v1.1+
|
|
8
|
-
- [PostgreSQL](https://www.postgresql.org/) running locally or remotely
|
|
9
|
-
|
|
10
|
-
## Getting Started
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
# Copy env and set your DATABASE_URL
|
|
14
|
-
cp .env.example .env
|
|
15
|
-
|
|
16
|
-
# Push schema to database
|
|
17
|
-
bun run db:push
|
|
18
|
-
|
|
19
|
-
# Seed initial data
|
|
20
|
-
bun run db:seed
|
|
21
|
-
|
|
22
|
-
# Start dev server
|
|
23
|
-
bun x bosia dev
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Visit [http://localhost:9000](http://localhost:9000) to see the app.
|
|
27
|
-
|
|
28
|
-
## Scripts
|
|
29
|
-
|
|
30
|
-
| Command | Description |
|
|
31
|
-
| --------------------- | -------------------------------------- |
|
|
32
|
-
| `bun x bosia dev` | Start dev server with HMR |
|
|
33
|
-
| `bun x bosia build` | Production build |
|
|
34
|
-
| `bun x bosia start` | Start production server |
|
|
35
|
-
| `bun run db:generate` | Generate migration from schema changes |
|
|
36
|
-
| `bun run db:migrate` | Apply pending migrations |
|
|
37
|
-
| `bun run db:push` | Push schema directly (dev shortcut) |
|
|
38
|
-
| `bun run db:studio` | Open Drizzle Studio GUI |
|
|
39
|
-
| `bun run db:seed` | Run pending seed files |
|
|
40
|
-
|
|
41
|
-
## Project Structure
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
src/
|
|
45
|
-
├── features/
|
|
46
|
-
│ ├── drizzle/ # DB infrastructure
|
|
47
|
-
│ │ ├── index.ts # Connection singleton
|
|
48
|
-
│ │ ├── schemas.ts # Schema aggregator
|
|
49
|
-
│ │ ├── migrations/ # Drizzle Kit output
|
|
50
|
-
│ │ └── seeds/ # Seed files + runner
|
|
51
|
-
│ └── todo/ # Business feature
|
|
52
|
-
│ ├── schemas/ # Table definitions
|
|
53
|
-
│ ├── queries.ts # Typed CRUD
|
|
54
|
-
│ └── types.ts # Inferred types
|
|
55
|
-
├── lib/components/todo/ # UI components
|
|
56
|
-
└── routes/
|
|
57
|
-
├── todos/ # CRUD page with form actions
|
|
58
|
-
└── api/todos/ # REST API
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Adding Features
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
# Add DB support to any Bosia app
|
|
65
|
-
bosia feat drizzle
|
|
66
|
-
|
|
67
|
-
# Add the todo feature (requires drizzle)
|
|
68
|
-
bosia feat todo
|
|
69
|
-
```
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
},
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"@types/bun": "latest",
|
|
20
|
-
"prettier": "^3.3.0",
|
|
21
|
-
"prettier-plugin-svelte": "^3.2.0",
|
|
22
|
-
"typescript": "^5"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
File without changes
|
|
@@ -1,14 +0,0 @@
|
|
|
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>
|
|
@@ -1,14 +0,0 @@
|
|
|
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>
|
|
@@ -1,14 +0,0 @@
|
|
|
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>
|
|
@@ -1,134 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { sequence } from "bosia";
|
|
2
|
-
import type { Handle } from "bosia";
|
|
3
|
-
import { db } from "./features/drizzle";
|
|
4
|
-
|
|
5
|
-
const dbHandle: Handle = async ({ event, resolve }) => {
|
|
6
|
-
event.locals.db = db;
|
|
7
|
-
return resolve(event);
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const loggingHandle: Handle = async ({ event, resolve }) => {
|
|
11
|
-
const start = Date.now();
|
|
12
|
-
event.locals.requestTime = start;
|
|
13
|
-
const res = await resolve(event);
|
|
14
|
-
const ms = Date.now() - start;
|
|
15
|
-
console.log(`[${event.request.method}] ${event.url.pathname} ${res.status} (${ms}ms)`);
|
|
16
|
-
res.headers.set("X-Response-Time", `${ms}ms`);
|
|
17
|
-
return res;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const handle = sequence(dbHandle, loggingHandle);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { cn } from "bosia";
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { LayoutData } from "./$types";
|
|
3
|
-
let { data }: { data: LayoutData } = $props();
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<svelte:head>
|
|
7
|
-
<title>{data.appName}</title>
|
|
8
|
-
</svelte:head>
|
|
9
|
-
|
|
10
|
-
<div class="flex min-h-screen flex-col bg-background text-foreground">
|
|
11
|
-
<div class="flex flex-1 flex-col items-center justify-center gap-8 px-4">
|
|
12
|
-
<div class="text-center">
|
|
13
|
-
<img src="/favicon.svg" alt="" class="mx-auto mb-6 size-16" />
|
|
14
|
-
<h1 class="text-4xl font-bold tracking-tight">{data.appName}</h1>
|
|
15
|
-
<p class="mt-2 text-lg text-muted-foreground">
|
|
16
|
-
Fullstack app powered by Bosia + Drizzle ORM + PostgreSQL
|
|
17
|
-
</p>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<div class="flex gap-4">
|
|
21
|
-
<a
|
|
22
|
-
href="/todos"
|
|
23
|
-
class="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
24
|
-
>
|
|
25
|
-
Todo App
|
|
26
|
-
</a>
|
|
27
|
-
<a
|
|
28
|
-
href="/api/todos"
|
|
29
|
-
target="_blank"
|
|
30
|
-
class="inline-flex items-center justify-center rounded-md border border-input bg-background px-6 py-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
31
|
-
>
|
|
32
|
-
REST API
|
|
33
|
-
</a>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div class="mt-8 grid max-w-2xl grid-cols-1 gap-4 sm:grid-cols-3">
|
|
37
|
-
<div class="rounded-lg border border-border p-4 text-center">
|
|
38
|
-
<p class="text-sm font-medium">Bosia</p>
|
|
39
|
-
<p class="mt-1 text-xs text-muted-foreground">Bun + Svelte 5 + Elysia</p>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="rounded-lg border border-border p-4 text-center">
|
|
42
|
-
<p class="text-sm font-medium">Drizzle ORM</p>
|
|
43
|
-
<p class="mt-1 text-xs text-muted-foreground">Type-safe SQL queries</p>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="rounded-lg border border-border p-4 text-center">
|
|
46
|
-
<p class="text-sm font-medium">PostgreSQL</p>
|
|
47
|
-
<p class="mt-1 text-xs text-muted-foreground">Production-ready database</p>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<footer class="border-t py-4 text-center text-sm text-muted-foreground">Powered by Bosia</footer>
|
|
53
|
-
</div>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { ErrorProps } from "./$types";
|
|
3
|
-
let { error }: ErrorProps = $props();
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<svelte:head>
|
|
7
|
-
<title>{error.status} — {error.message}</title>
|
|
8
|
-
</svelte:head>
|
|
9
|
-
|
|
10
|
-
<div class="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
|
|
11
|
-
<p class="text-8xl font-bold text-gray-200">{error.status}</p>
|
|
12
|
-
<p class="text-2xl font-semibold text-gray-700">{error.message}</p>
|
|
13
|
-
<a
|
|
14
|
-
href="/"
|
|
15
|
-
class="mt-4 px-5 py-2 rounded-lg bg-gray-900 text-white text-sm hover:bg-gray-700 transition-colors"
|
|
16
|
-
>
|
|
17
|
-
Go home
|
|
18
|
-
</a>
|
|
19
|
-
</div>
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"allowJs": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"allowImportingTsExtensions": true,
|
|
10
|
-
"noEmit": true,
|
|
11
|
-
"verbatimModuleSyntax": true,
|
|
12
|
-
"types": ["bun-types"],
|
|
13
|
-
"lib": ["dom", "dom.iterable", "esnext"],
|
|
14
|
-
"rootDirs": [".", ".bosia/types"],
|
|
15
|
-
"paths": {
|
|
16
|
-
"$lib": ["./src/lib"],
|
|
17
|
-
"$lib/*": ["./src/lib/*"]
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
21
|
-
"exclude": ["node_modules", "dist"]
|
|
22
|
-
}
|