@podosoft/podokit 0.9.0 → 0.10.0
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/dist/add.d.ts +20 -0
- package/dist/add.js +121 -8
- package/dist/assemble.d.ts +4 -1
- package/dist/assemble.js +35 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -4
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.js +5 -0
- package/dist/lockfile.d.ts +20 -1
- package/dist/lockfile.js +48 -6
- package/dist/remove.js +3 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +42 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +6 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/fullstack-nest-svelte/infra/traefik/dynamic.yml +4 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +60 -1
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +42 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +730 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/appearance-card.svelte +371 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +12 -7
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/apply-theme.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/themes.ts +81 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +8 -717
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +17 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +0 -7
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +4 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +56 -0
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +9 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +13 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +21 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +14 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +12 -4
- package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
- package/dist/templates/modules/api-key-auth/files/tests/api/machine.api.spec.ts +25 -0
- package/dist/templates/modules/bullmq/files/tests/api/jobs.api.spec.ts +38 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +31 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +22 -0
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +28 -0
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +35 -0
- package/dist/templates/modules/redis/files/tests/api/redis.api.spec.ts +30 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/todo/apps/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +3 -1
- package/dist/templates/todo/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/todo/dot-gitignore +6 -0
- package/dist/templates/todo/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/todo/infra/traefik/dynamic.yml +4 -0
- package/dist/update.js +74 -8
- package/package.json +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +0 -61
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
|
+
const origin = { origin: base };
|
|
5
|
+
|
|
6
|
+
async function session(playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"]) {
|
|
7
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
8
|
+
const email = `storage-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
9
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "S" } }).catch(() => undefined);
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("object storage: put, get, and presign @smoke", async ({ playwright }) => {
|
|
14
|
+
const ctx = await session(playwright);
|
|
15
|
+
const key = `obj-${Date.now()}`;
|
|
16
|
+
const put = await ctx.put(`/api/storage/${key}`, { data: { content: "hello world" } });
|
|
17
|
+
test.skip(put.status() >= 500, "object storage (MinIO/S3) not reachable");
|
|
18
|
+
expect(put.ok()).toBeTruthy();
|
|
19
|
+
expect(await put.json()).toMatchObject({ key });
|
|
20
|
+
|
|
21
|
+
const got = await (await ctx.get(`/api/storage/${key}`)).json();
|
|
22
|
+
expect(got).toMatchObject({ key, content: "hello world" });
|
|
23
|
+
|
|
24
|
+
const pre = await (await ctx.get(`/api/storage/${key}/presigned`)).json();
|
|
25
|
+
expect(String(pre.url)).toContain(key);
|
|
26
|
+
|
|
27
|
+
await ctx.dispose();
|
|
28
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
|
+
const origin = { origin: base };
|
|
5
|
+
|
|
6
|
+
// rate-limit installs a global throttler guard (default RATE_LIMIT_MAX requests
|
|
7
|
+
// per RATE_LIMIT_TTL seconds, per client). This module ships a global guard, so
|
|
8
|
+
// it never joins the shared Outer smoke app (it would throttle every other
|
|
9
|
+
// spec); this spec ships with the module and runs against a dedicated app.
|
|
10
|
+
//
|
|
11
|
+
// Honor RATE_LIMIT_MAX when the test runner has it (a dedicated app sets a small
|
|
12
|
+
// value so the window is crossed in a handful of requests); otherwise fall back
|
|
13
|
+
// to the module default and allow a little headroom.
|
|
14
|
+
const limit = Number(process.env.RATE_LIMIT_MAX ?? 100);
|
|
15
|
+
|
|
16
|
+
test("rate limit: exceeding the window returns 429 @smoke", async ({ playwright }) => {
|
|
17
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
18
|
+
// /health is public — no session needed. If the first hit is a 5xx the
|
|
19
|
+
// throttler's Redis backing isn't reachable; nothing to assert.
|
|
20
|
+
const probe = await ctx.get("/api/health");
|
|
21
|
+
test.skip(probe.status() >= 500, "throttler storage (redis) not reachable");
|
|
22
|
+
|
|
23
|
+
let seen200 = probe.status() === 200;
|
|
24
|
+
let got429 = false;
|
|
25
|
+
for (let i = 0; i < limit + 5 && !got429; i++) {
|
|
26
|
+
const r = await ctx.get("/api/health");
|
|
27
|
+
if (r.status() === 200) seen200 = true;
|
|
28
|
+
if (r.status() === 429) got429 = true;
|
|
29
|
+
}
|
|
30
|
+
// A fresh window serves the first requests (200) and rejects once the limit is
|
|
31
|
+
// crossed (429).
|
|
32
|
+
expect(seen200).toBe(true);
|
|
33
|
+
expect(got429).toBe(true);
|
|
34
|
+
await ctx.dispose();
|
|
35
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
|
+
const origin = { origin: base };
|
|
5
|
+
|
|
6
|
+
// Self-contained: sign up + sign in a disposable account so the test works when
|
|
7
|
+
// `auth`'s global guard is installed (the common case). Harmless if the API is open.
|
|
8
|
+
async function session(playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"]) {
|
|
9
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
10
|
+
const email = `redis-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
11
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "R" } }).catch(() => undefined);
|
|
12
|
+
return ctx;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("redis cache: set, get, and miss @smoke", async ({ playwright }) => {
|
|
16
|
+
const ctx = await session(playwright);
|
|
17
|
+
const key = `k-${Date.now()}`;
|
|
18
|
+
const put = await ctx.put(`/api/cache/${key}`, { data: { value: "hello", ttl: 60 } });
|
|
19
|
+
test.skip(put.status() >= 500, "redis not reachable");
|
|
20
|
+
expect(put.ok()).toBeTruthy();
|
|
21
|
+
expect(await put.json()).toMatchObject({ key });
|
|
22
|
+
|
|
23
|
+
const got = await (await ctx.get(`/api/cache/${key}`)).json();
|
|
24
|
+
expect(got).toMatchObject({ key, value: "hello" });
|
|
25
|
+
|
|
26
|
+
const miss = await (await ctx.get(`/api/cache/missing-${Date.now()}`)).json();
|
|
27
|
+
expect(miss.value).toBeNull();
|
|
28
|
+
|
|
29
|
+
await ctx.dispose();
|
|
30
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
|
+
const origin = { origin: base };
|
|
5
|
+
|
|
6
|
+
async function session(playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"]) {
|
|
7
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
8
|
+
const email = `sse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
9
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "E" } }).catch(() => undefined);
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("sse: publish accepts a message @smoke", async ({ playwright }) => {
|
|
14
|
+
const ctx = await session(playwright);
|
|
15
|
+
const pub = await ctx.post("/api/events", { data: { message: `hi-${Date.now()}` } });
|
|
16
|
+
expect(pub.ok()).toBeTruthy();
|
|
17
|
+
expect(await pub.json()).toMatchObject({ ok: true });
|
|
18
|
+
await ctx.dispose();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("sse: a published message is delivered over the event stream @smoke", async ({ playwright }) => {
|
|
22
|
+
const ctx = await session(playwright);
|
|
23
|
+
// The stream is behind the auth guard — carry the session cookie on the raw fetch.
|
|
24
|
+
const cookies = (await ctx.storageState()).cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
25
|
+
const ac = new AbortController();
|
|
26
|
+
const stream = await fetch(`${base}/api/events/stream`, { headers: { ...origin, cookie: cookies }, signal: ac.signal }).catch(() => null);
|
|
27
|
+
test.skip(!stream || !stream.ok || !stream.body, "sse stream not reachable");
|
|
28
|
+
expect(stream!.headers.get("content-type")).toContain("text/event-stream");
|
|
29
|
+
|
|
30
|
+
// Subscriber is attached now; publish, then read until the message arrives.
|
|
31
|
+
const msg = `sse-${Date.now()}`;
|
|
32
|
+
await ctx.post("/api/events", { data: { message: msg } });
|
|
33
|
+
|
|
34
|
+
const reader = stream!.body!.getReader();
|
|
35
|
+
const decoder = new TextDecoder();
|
|
36
|
+
let seen = false;
|
|
37
|
+
const deadline = Date.now() + 8000;
|
|
38
|
+
while (Date.now() < deadline && !seen) {
|
|
39
|
+
const { value, done } = await reader.read();
|
|
40
|
+
if (done) break;
|
|
41
|
+
if (decoder.decode(value).includes(msg)) seen = true;
|
|
42
|
+
}
|
|
43
|
+
ac.abort();
|
|
44
|
+
expect(seen).toBe(true);
|
|
45
|
+
await ctx.dispose();
|
|
46
|
+
});
|
|
@@ -37,7 +37,9 @@ export async function proxyRequest(
|
|
|
37
37
|
const upstream = await fetch(targetUrl, {
|
|
38
38
|
method: request.method,
|
|
39
39
|
headers,
|
|
40
|
-
|
|
40
|
+
// Preserve multipart uploads and other binary bodies byte-for-byte. Reading
|
|
41
|
+
// them as text corrupts bytes that are not valid UTF-8 before forwarding.
|
|
42
|
+
body: hasBody ? await request.arrayBuffer() : undefined,
|
|
41
43
|
redirect: "manual",
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
apiVersion: traefik.io/v1alpha1
|
|
2
|
+
kind: Middleware
|
|
3
|
+
metadata:
|
|
4
|
+
name: compression
|
|
5
|
+
namespace: podokit
|
|
6
|
+
spec:
|
|
7
|
+
compress: {}
|
|
8
|
+
---
|
|
1
9
|
apiVersion: networking.k8s.io/v1
|
|
2
10
|
kind: Ingress
|
|
3
11
|
metadata:
|
|
4
12
|
name: app
|
|
5
13
|
namespace: podokit
|
|
14
|
+
annotations:
|
|
15
|
+
traefik.ingress.kubernetes.io/router.middlewares: podokit-compression@kubernetescrd
|
|
6
16
|
spec:
|
|
7
17
|
rules:
|
|
8
18
|
- host: example.com
|
package/dist/update.js
CHANGED
|
@@ -9,6 +9,7 @@ const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
|
9
9
|
const assemble_1 = require("./assemble");
|
|
10
10
|
const lockfile_1 = require("./lockfile");
|
|
11
11
|
const inspect_1 = require("./inspect");
|
|
12
|
+
const add_1 = require("./add");
|
|
12
13
|
function treeText(tree, path) {
|
|
13
14
|
const file = tree.get(path);
|
|
14
15
|
if (!file)
|
|
@@ -32,7 +33,8 @@ function planUpdate(projectRoot, templatesDir) {
|
|
|
32
33
|
templatesDir,
|
|
33
34
|
template: manifest.template,
|
|
34
35
|
answers: manifest.answers,
|
|
35
|
-
modules: manifest.modules
|
|
36
|
+
modules: manifest.modules,
|
|
37
|
+
projectRoot,
|
|
36
38
|
});
|
|
37
39
|
const changes = [];
|
|
38
40
|
const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
|
|
@@ -92,6 +94,40 @@ function writeFile(projectRoot, path, content) {
|
|
|
92
94
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(abs), { recursive: true });
|
|
93
95
|
(0, node_fs_1.writeFileSync)(abs, content);
|
|
94
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Refresh update metadata without teaching the lock that a clean 3-way merge is
|
|
99
|
+
* PodoKit's new baseline. Managed files keep the newly assembled template hash,
|
|
100
|
+
* so the user's merged lines are still recognised as edits on the next update.
|
|
101
|
+
* Files that are neither in the assembled tree nor explicitly owned stay out of
|
|
102
|
+
* the lock instead of being accidentally adopted as managed files.
|
|
103
|
+
*/
|
|
104
|
+
function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs) {
|
|
105
|
+
const next = (0, lockfile_1.computeFilesLock)(projectRoot, ownedGlobs);
|
|
106
|
+
for (const [path, entry] of Object.entries(next.files)) {
|
|
107
|
+
const newText = treeText(newTree, path);
|
|
108
|
+
if (newText !== null) {
|
|
109
|
+
if (entry.tier !== "owned") {
|
|
110
|
+
entry.tier = previous.files[path]?.tier ?? entry.tier;
|
|
111
|
+
entry.outHash = (0, podokit_template_engine_1.hashContent)(newText);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.tier === "owned")
|
|
116
|
+
continue;
|
|
117
|
+
const oldEntry = previous.files[path];
|
|
118
|
+
if (oldEntry && oldEntry.tier !== "owned") {
|
|
119
|
+
// Removed upstream but kept on disk because it was edited: continue to
|
|
120
|
+
// report the removal conflict until the user deletes or ejects it.
|
|
121
|
+
entry.tier = oldEntry.tier;
|
|
122
|
+
entry.outHash = oldEntry.outHash;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// A file created by the application is not implicitly PodoKit-managed.
|
|
126
|
+
delete next.files[path];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
95
131
|
/**
|
|
96
132
|
* Apply an update to the working copy. Clean updates and additions are written,
|
|
97
133
|
* upstream removals deleted, and user-edited files 3-way merged against the old
|
|
@@ -101,13 +137,26 @@ function writeFile(projectRoot, path, content) {
|
|
|
101
137
|
*/
|
|
102
138
|
function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
103
139
|
const manifest = (0, lockfile_1.readManifest)(projectRoot);
|
|
104
|
-
|
|
140
|
+
const previousLock = (0, lockfile_1.readFilesLock)(projectRoot);
|
|
141
|
+
if (!manifest || !previousLock)
|
|
105
142
|
throw new inspect_1.NotAProjectError();
|
|
106
143
|
const plan = planUpdate(projectRoot, templatesDir);
|
|
107
|
-
const modules = manifest.modules
|
|
108
|
-
const newTree = (0, assemble_1.assembleProject)({
|
|
144
|
+
const modules = manifest.modules;
|
|
145
|
+
const newTree = (0, assemble_1.assembleProject)({
|
|
146
|
+
templatesDir,
|
|
147
|
+
template: manifest.template,
|
|
148
|
+
answers: manifest.answers,
|
|
149
|
+
modules,
|
|
150
|
+
projectRoot,
|
|
151
|
+
});
|
|
109
152
|
const oldTree = options.oldTemplatesDir
|
|
110
|
-
? (0, assemble_1.assembleProject)({
|
|
153
|
+
? (0, assemble_1.assembleProject)({
|
|
154
|
+
templatesDir: options.oldTemplatesDir,
|
|
155
|
+
template: manifest.template,
|
|
156
|
+
answers: manifest.answers,
|
|
157
|
+
modules,
|
|
158
|
+
projectRoot,
|
|
159
|
+
})
|
|
111
160
|
: null;
|
|
112
161
|
const result = { written: [], removed: [], merged: [], conflicts: [] };
|
|
113
162
|
for (const change of plan.changes) {
|
|
@@ -136,9 +185,26 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
136
185
|
result.merged.push(change.path);
|
|
137
186
|
}
|
|
138
187
|
}
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
188
|
+
// Keep the assembled target as the managed baseline. A merged working file
|
|
189
|
+
// intentionally remains drifted so a future update performs another 3-way
|
|
190
|
+
// merge instead of treating the user's lines as clean generated output.
|
|
191
|
+
(0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, manifest.ownedGlobs));
|
|
192
|
+
const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
|
|
193
|
+
const refreshedModules = manifest.modules.map((module) => {
|
|
194
|
+
const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
|
|
195
|
+
return resolved?.packageName
|
|
196
|
+
? {
|
|
197
|
+
...module,
|
|
198
|
+
packageName: resolved.packageName,
|
|
199
|
+
moduleVersion: resolved.moduleVersion,
|
|
200
|
+
}
|
|
201
|
+
: module;
|
|
202
|
+
});
|
|
203
|
+
(0, lockfile_1.writeManifest)(projectRoot, {
|
|
204
|
+
...manifest,
|
|
205
|
+
modules: refreshedModules,
|
|
206
|
+
podokitVersion: (0, lockfile_1.podokitVersion)(),
|
|
207
|
+
});
|
|
142
208
|
return result;
|
|
143
209
|
}
|
|
144
210
|
/** Count actions for a one-line summary. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "An opinionated but extensible starter toolkit and CLI for full-stack TypeScript apps with NestJS, SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import "../app.css";
|
|
3
|
-
import { ModeWatcher } from "mode-watcher";
|
|
4
|
-
import { setI18nContext } from "$lib/i18n";
|
|
5
|
-
import { messages, resolveLocale } from "$lib/i18n/messages";
|
|
6
|
-
import { site } from "$lib/site.svelte";
|
|
7
|
-
|
|
8
|
-
let { children, data } = $props();
|
|
9
|
-
const locale = $derived(resolveLocale(data.locale));
|
|
10
|
-
setI18nContext({
|
|
11
|
-
get t() {
|
|
12
|
-
return messages[locale];
|
|
13
|
-
},
|
|
14
|
-
get locale() {
|
|
15
|
-
return locale;
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// Seed the reactive site branding; the general-settings form patches it live.
|
|
20
|
-
$effect.pre(() => site.init(data.site));
|
|
21
|
-
// Prefer live store values (patched after a save); fall back to the server load
|
|
22
|
-
// so SSR renders the saved branding too. Empty falls back to the app.html default.
|
|
23
|
-
const branding = $derived({
|
|
24
|
-
name: site.value.name ?? data.site?.name ?? "",
|
|
25
|
-
description: site.value.description ?? data.site?.description ?? "",
|
|
26
|
-
brandColor: site.value.brandColor ?? data.site?.brandColor ?? "",
|
|
27
|
-
hasFavicon: site.value.hasFavicon || Boolean(data.site?.hasFavicon),
|
|
28
|
-
faviconVersion: site.value.faviconVersion ?? data.site?.faviconVersion ?? "",
|
|
29
|
-
});
|
|
30
|
-
// Apply the site name to the browser tab live (avoids a duplicate <title> vs
|
|
31
|
-
// the app.html default; the default project name stays as the SSR fallback).
|
|
32
|
-
$effect(() => {
|
|
33
|
-
if (branding.name) document.title = branding.name;
|
|
34
|
-
});
|
|
35
|
-
// Apply the brand color as the theme's primary accent live. Clearing it in
|
|
36
|
-
// Settings restores the stylesheet default (remove the inline override).
|
|
37
|
-
$effect(() => {
|
|
38
|
-
const root = document.documentElement;
|
|
39
|
-
if (branding.brandColor) {
|
|
40
|
-
root.style.setProperty("--primary", branding.brandColor);
|
|
41
|
-
root.style.setProperty("--sidebar-primary", branding.brandColor);
|
|
42
|
-
root.style.setProperty("--ring", branding.brandColor);
|
|
43
|
-
} else {
|
|
44
|
-
root.style.removeProperty("--primary");
|
|
45
|
-
root.style.removeProperty("--sidebar-primary");
|
|
46
|
-
root.style.removeProperty("--ring");
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<svelte:head>
|
|
52
|
-
{#if branding.description}
|
|
53
|
-
<meta name="description" content={branding.description} />
|
|
54
|
-
{/if}
|
|
55
|
-
{#if branding.hasFavicon}
|
|
56
|
-
<link rel="icon" href={`/api/site/favicon?v=${branding.faviconVersion}`} />
|
|
57
|
-
{/if}
|
|
58
|
-
</svelte:head>
|
|
59
|
-
|
|
60
|
-
<ModeWatcher />
|
|
61
|
-
{@render children()}
|