bosia 0.6.24 → 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 +13 -2
- package/src/cli/addRouter.ts +17 -6
- package/src/cli/index.ts +7 -0
- package/src/cli/manifest.ts +16 -1
- package/src/cli/page.ts +141 -0
- package/src/cli/theme.ts +20 -0
- package/templates/default/src/app.css +6 -0
- package/templates/demo/src/app.css +6 -0
- package/templates/shop/src/app.css +6 -0
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
|
|
|
@@ -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/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)
|
|
@@ -139,6 +144,8 @@ Examples:
|
|
|
139
144
|
bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
|
|
140
145
|
bun x bosia@latest add block cards/feature
|
|
141
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
|
+
}
|
|
@@ -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 {
|