@podosoft/podokit 0.8.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.
Files changed (84) hide show
  1. package/dist/add.d.ts +20 -0
  2. package/dist/add.js +121 -8
  3. package/dist/assemble.d.ts +4 -1
  4. package/dist/assemble.js +35 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +20 -4
  7. package/dist/inspect.d.ts +5 -0
  8. package/dist/inspect.js +5 -0
  9. package/dist/lockfile.d.ts +20 -1
  10. package/dist/lockfile.js +48 -6
  11. package/dist/remove.js +3 -2
  12. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +42 -0
  13. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/site-runtime.svelte +7 -0
  14. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +3 -1
  15. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+layout.svelte +2 -2
  16. package/dist/templates/fullstack-nest-svelte/dot-gitignore +6 -0
  17. package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +10 -0
  18. package/dist/templates/fullstack-nest-svelte/infra/traefik/dynamic.yml +4 -0
  19. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +60 -1
  20. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +3 -0
  21. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +3 -1
  22. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +42 -0
  23. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +46 -0
  24. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +730 -0
  25. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/appearance-card.svelte +371 -0
  26. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
  27. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +39 -0
  28. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +12 -7
  29. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +90 -0
  30. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +29 -0
  31. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/apply-theme.ts +124 -0
  32. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/themes.ts +81 -0
  33. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +6 -0
  34. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +17 -0
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +8 -680
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +10 -1
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +30 -11
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +0 -7
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +96 -14
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +1 -0
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +4 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +35 -0
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.server.ts +11 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +122 -0
  45. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +106 -0
  46. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +56 -0
  47. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +10 -0
  48. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +74 -0
  49. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +9 -2
  50. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +63 -0
  51. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +11 -0
  52. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +13 -5
  53. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +21 -2
  54. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +23 -0
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +41 -0
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +12 -8
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +35 -0
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
  59. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +63 -0
  60. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +23 -7
  61. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +137 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/ui/verify-email.ui.spec.ts +17 -0
  63. package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
  64. package/dist/templates/modules/api-key-auth/files/tests/api/machine.api.spec.ts +25 -0
  65. package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +8 -0
  66. package/dist/templates/modules/auth/files/apps/api/src/auth/two-factor-required.guard.ts +68 -0
  67. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +5 -1
  68. package/dist/templates/modules/auth/module.manifest.json +10 -0
  69. package/dist/templates/modules/bullmq/files/tests/api/jobs.api.spec.ts +38 -0
  70. package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +31 -0
  71. package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +22 -0
  72. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +28 -0
  73. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +35 -0
  74. package/dist/templates/modules/redis/files/tests/api/redis.api.spec.ts +30 -0
  75. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
  76. package/dist/templates/todo/apps/web/src/lib/components/site-runtime.svelte +7 -0
  77. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +3 -1
  78. package/dist/templates/todo/apps/web/src/routes/+layout.svelte +2 -2
  79. package/dist/templates/todo/dot-gitignore +6 -0
  80. package/dist/templates/todo/infra/k3s/ingress.yaml +10 -0
  81. package/dist/templates/todo/infra/traefik/dynamic.yml +4 -0
  82. package/dist/update.js +74 -8
  83. package/package.json +1 -1
  84. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +0 -61
@@ -0,0 +1,124 @@
1
+ // Computes the effective theme (preset neutrals ⊕ brand-color accent ⊕ radius ⊕
2
+ // per-token overrides) and applies it. Two outputs share one var builder:
3
+ // - applyTheme(): injects a mode-scoped <style> (`:root:not(.dark)` / `:root.dark`)
4
+ // so light edits never touch dark and vice-versa. Removes it when nothing is set.
5
+ // - buildVarString(): the same CSS var text, for an inline preview container.
6
+ import { findPreset, type ThemeTokens } from "./themes";
7
+ import type { SiteSettings } from "../site.svelte";
8
+
9
+ export type ThemeMode = "light" | "dark";
10
+ export interface ThemeOverrides {
11
+ light?: Partial<ThemeTokens>;
12
+ dark?: Partial<ThemeTokens>;
13
+ }
14
+ export interface ThemeConfig {
15
+ preset: string | null;
16
+ brandColor: string | null;
17
+ radius: string | null;
18
+ overrides: ThemeOverrides | null;
19
+ }
20
+
21
+ const STYLE_ID = "podokit-theme";
22
+
23
+ /** Merge preset base with per-token overrides for one mode. */
24
+ export function effectiveTokens(config: ThemeConfig, mode: ThemeMode): Partial<ThemeTokens> {
25
+ const preset = findPreset(config.preset);
26
+ const base = (preset ? preset[mode] : null) ?? {};
27
+ const over = config.overrides?.[mode] ?? {};
28
+ return { ...base, ...over };
29
+ }
30
+
31
+ /** Build the CSS custom-property text for a mode. brandColor overrides primary;
32
+ * radius (a plain rem number) is appended with its unit. Only present tokens emit. */
33
+ export function buildVarString(tokens: Partial<ThemeTokens>, brandColor?: string | null, radius?: string | null): string {
34
+ const lines: string[] = [];
35
+ const set = (name: string, value?: string): void => {
36
+ if (value) lines.push(`--${name}: ${value};`);
37
+ };
38
+ set("background", tokens.background);
39
+ set("foreground", tokens.foreground);
40
+ set("card", tokens.card);
41
+ set("card-foreground", tokens.foreground);
42
+ set("popover", tokens.card);
43
+ set("popover-foreground", tokens.foreground);
44
+ set("muted-foreground", tokens.mutedForeground);
45
+ set("border", tokens.border);
46
+ set("input", tokens.border);
47
+ set("secondary", tokens.secondary);
48
+ set("secondary-foreground", tokens.foreground);
49
+ set("muted", tokens.secondary);
50
+ set("accent", tokens.accent);
51
+ set("accent-foreground", tokens.foreground);
52
+ const primary = (brandColor ?? "").trim() || tokens.primary;
53
+ set("primary", primary);
54
+ set("ring", primary);
55
+ set("primary-foreground", tokens.primaryForeground);
56
+ // Sidebar tokens (admin dashboard) — derive the whole set so the sidebar
57
+ // re-themes with the rest of the app, not just its accent.
58
+ set("sidebar", tokens.card);
59
+ set("sidebar-foreground", tokens.foreground);
60
+ set("sidebar-primary", primary);
61
+ set("sidebar-primary-foreground", tokens.primaryForeground);
62
+ set("sidebar-accent", tokens.secondary);
63
+ set("sidebar-accent-foreground", tokens.foreground);
64
+ set("sidebar-border", tokens.border);
65
+ set("sidebar-ring", primary);
66
+ const r = (radius ?? "").trim();
67
+ if (r) lines.push(`--radius: ${r}rem;`);
68
+ return lines.join(" ");
69
+ }
70
+
71
+ /** True when the config would set no custom properties (falls back to app.css). */
72
+ export function isEmptyTheme(config: ThemeConfig): boolean {
73
+ return (
74
+ !buildVarString(effectiveTokens(config, "light"), config.brandColor, config.radius) &&
75
+ !buildVarString(effectiveTokens(config, "dark"), config.brandColor, config.radius)
76
+ );
77
+ }
78
+
79
+ /** Apply the theme globally via a mode-scoped stylesheet (client only). */
80
+ export function applyTheme(config: ThemeConfig): void {
81
+ if (typeof document === "undefined") return;
82
+ const lightVars = buildVarString(effectiveTokens(config, "light"), config.brandColor, config.radius);
83
+ const darkVars = buildVarString(effectiveTokens(config, "dark"), config.brandColor, config.radius);
84
+ const existing = document.getElementById(STYLE_ID);
85
+ if (!lightVars && !darkVars) {
86
+ existing?.remove();
87
+ return;
88
+ }
89
+ const css =
90
+ (lightVars ? `:root:not(.dark) { ${lightVars} }\n` : "") + (darkVars ? `:root.dark { ${darkVars} }` : "");
91
+ const el = existing ?? document.head.appendChild(Object.assign(document.createElement("style"), { id: STYLE_ID }));
92
+ el.textContent = css;
93
+ }
94
+
95
+ /** Safely parse the stored themeOverrides JSON string into a typed object. */
96
+ export function parseOverrides(json: string | null | undefined): ThemeOverrides | null {
97
+ if (!json) return null;
98
+ try {
99
+ const parsed: unknown = JSON.parse(json);
100
+ if (!parsed || typeof parsed !== "object") return null;
101
+ const pick = (v: unknown): Partial<ThemeTokens> | undefined => {
102
+ if (!v || typeof v !== "object") return undefined;
103
+ const out: Record<string, string> = {};
104
+ for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
105
+ if (typeof val === "string") out[k] = val;
106
+ }
107
+ return out as Partial<ThemeTokens>;
108
+ };
109
+ const rec = parsed as Record<string, unknown>;
110
+ return { light: pick(rec.light), dark: pick(rec.dark) };
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /** Build a ThemeConfig from persisted site settings. */
117
+ export function themeConfigFromSettings(s: SiteSettings): ThemeConfig {
118
+ return {
119
+ preset: s.themePreset,
120
+ brandColor: s.brandColor,
121
+ radius: s.themeRadius,
122
+ overrides: parseOverrides(s.themeOverrides),
123
+ };
124
+ }
@@ -0,0 +1,81 @@
1
+ // Built-in theme presets for the admin Appearance settings. A preset is a full
2
+ // neutral+surface+accent base for light and dark; the admin can layer a
3
+ // brand-color accent, a radius, and per-token overrides on top (see
4
+ // apply-theme.ts). Two families ship:
5
+ // - shadcn bases/accents (hex from the official shadcn theme registry), and
6
+ // - popular code-editor palettes (Dracula, Nord, Solarized, Gruvbox,
7
+ // Catppuccin, Tokyo Night, Rosé Pine, One Dark, Everforest, Monokai, GitHub)
8
+ // that vary the background/foreground, not just the accent.
9
+ // `default` = no override, so the app's own app.css tokens show through.
10
+
11
+ /** The editable subset of theme tokens; apply-theme expands these to CSS vars. */
12
+ export interface ThemeTokens {
13
+ background: string;
14
+ card: string;
15
+ foreground: string;
16
+ mutedForeground: string;
17
+ border: string;
18
+ secondary: string;
19
+ accent: string;
20
+ primary: string;
21
+ primaryForeground: string;
22
+ }
23
+
24
+ export interface ThemePreset {
25
+ key: string;
26
+ label: string;
27
+ /** null = leave this mode on the app.css defaults (the `default` preset). */
28
+ light: ThemeTokens | null;
29
+ dark: ThemeTokens | null;
30
+ }
31
+
32
+ export const themePresets: ThemePreset[] = [
33
+ { key: "default", label: "Default", light: null, dark: null },
34
+
35
+ // --- shadcn neutral bases (accent = neutral, background stays white/near-black) ---
36
+ { key: "zinc", label: "Zinc", light: { background: "#ffffff", card: "#ffffff", foreground: "#09090b", mutedForeground: "#71717a", border: "#e4e4e7", secondary: "#f4f4f5", accent: "#f4f4f5", primary: "#18181b", primaryForeground: "#fafafa" }, dark: { background: "#09090b", card: "#09090b", foreground: "#fafafa", mutedForeground: "#a1a1aa", border: "#27272a", secondary: "#27272a", accent: "#27272a", primary: "#fafafa", primaryForeground: "#18181b" } },
37
+ { key: "slate", label: "Slate", light: { background: "#ffffff", card: "#ffffff", foreground: "#020817", mutedForeground: "#64748b", border: "#e2e8f0", secondary: "#f1f5f9", accent: "#f1f5f9", primary: "#0f172a", primaryForeground: "#f8fafc" }, dark: { background: "#020817", card: "#020817", foreground: "#f8fafc", mutedForeground: "#94a3b8", border: "#1e293b", secondary: "#1e293b", accent: "#1e293b", primary: "#f8fafc", primaryForeground: "#0f172a" } },
38
+ { key: "stone", label: "Stone", light: { background: "#ffffff", card: "#ffffff", foreground: "#0c0a09", mutedForeground: "#78716c", border: "#e7e5e4", secondary: "#f5f5f4", accent: "#f5f5f4", primary: "#1c1917", primaryForeground: "#fafaf9" }, dark: { background: "#0c0a09", card: "#0c0a09", foreground: "#fafaf9", mutedForeground: "#a8a29e", border: "#292524", secondary: "#292524", accent: "#292524", primary: "#fafaf9", primaryForeground: "#1c1917" } },
39
+ { key: "gray", label: "Gray", light: { background: "#ffffff", card: "#ffffff", foreground: "#030712", mutedForeground: "#6b7280", border: "#e5e7eb", secondary: "#f3f4f6", accent: "#f3f4f6", primary: "#111827", primaryForeground: "#f9fafb" }, dark: { background: "#030712", card: "#030712", foreground: "#f9fafb", mutedForeground: "#9ca3af", border: "#1f2937", secondary: "#1f2937", accent: "#1f2937", primary: "#f9fafb", primaryForeground: "#111827" } },
40
+ { key: "neutral", label: "Neutral", light: { background: "#ffffff", card: "#ffffff", foreground: "#0a0a0a", mutedForeground: "#737373", border: "#e5e5e5", secondary: "#f5f5f5", accent: "#f5f5f5", primary: "#171717", primaryForeground: "#fafafa" }, dark: { background: "#0a0a0a", card: "#0a0a0a", foreground: "#fafafa", mutedForeground: "#a3a3a3", border: "#262626", secondary: "#262626", accent: "#262626", primary: "#fafafa", primaryForeground: "#171717" } },
41
+
42
+ // --- shadcn accent bases (colored primary on a neutral background) ---
43
+ { key: "blue", label: "Blue", light: { background: "#ffffff", card: "#ffffff", foreground: "#020817", mutedForeground: "#64748b", border: "#e2e8f0", secondary: "#f1f5f9", accent: "#f1f5f9", primary: "#2563eb", primaryForeground: "#f8fafc" }, dark: { background: "#020817", card: "#020817", foreground: "#f8fafc", mutedForeground: "#94a3b8", border: "#1e293b", secondary: "#1e293b", accent: "#1e293b", primary: "#3b82f6", primaryForeground: "#0f172a" } },
44
+ { key: "green", label: "Green", light: { background: "#ffffff", card: "#ffffff", foreground: "#09090b", mutedForeground: "#71717a", border: "#e4e4e7", secondary: "#f4f4f5", accent: "#f4f4f5", primary: "#16a34a", primaryForeground: "#f0fdf4" }, dark: { background: "#0c0a09", card: "#1c1917", foreground: "#f2f2f2", mutedForeground: "#a1a1aa", border: "#27272a", secondary: "#27272a", accent: "#292524", primary: "#22c55e", primaryForeground: "#052e16" } },
45
+ { key: "violet", label: "Violet", light: { background: "#ffffff", card: "#ffffff", foreground: "#030712", mutedForeground: "#6b7280", border: "#e5e7eb", secondary: "#f3f4f6", accent: "#f3f4f6", primary: "#7c3aed", primaryForeground: "#f9fafb" }, dark: { background: "#030712", card: "#030712", foreground: "#f9fafb", mutedForeground: "#9ca3af", border: "#1f2937", secondary: "#1f2937", accent: "#1f2937", primary: "#8b5cf6", primaryForeground: "#f9fafb" } },
46
+ { key: "rose", label: "Rose", light: { background: "#ffffff", card: "#ffffff", foreground: "#09090b", mutedForeground: "#71717a", border: "#e4e4e7", secondary: "#f4f4f5", accent: "#f4f4f5", primary: "#e11d48", primaryForeground: "#fff1f2" }, dark: { background: "#0c0a09", card: "#1c1917", foreground: "#f2f2f2", mutedForeground: "#a1a1aa", border: "#27272a", secondary: "#27272a", accent: "#292524", primary: "#e11d48", primaryForeground: "#fff1f2" } },
47
+
48
+ // --- popular code-editor palettes (distinct background/foreground, not just accent) ---
49
+ { key: "dracula", label: "Dracula", light: { background: "#f8f8f2", card: "#ffffff", foreground: "#282a36", mutedForeground: "#6272a4", border: "#e3e0f0", secondary: "#eeecf7", accent: "#e8e5f5", primary: "#7c5cbf", primaryForeground: "#ffffff" }, dark: { background: "#282a36", card: "#343746", foreground: "#f8f8f2", mutedForeground: "#6272a4", border: "#44475a", secondary: "#44475a", accent: "#4d5066", primary: "#bd93f9", primaryForeground: "#282a36" } },
50
+ { key: "nord", label: "Nord", light: { background: "#eceff4", card: "#ffffff", foreground: "#2e3440", mutedForeground: "#4c566a", border: "#d8dee9", secondary: "#e5e9f0", accent: "#dfe4ee", primary: "#5e81ac", primaryForeground: "#eceff4" }, dark: { background: "#2e3440", card: "#3b4252", foreground: "#eceff4", mutedForeground: "#7b88a1", border: "#434c5e", secondary: "#3b4252", accent: "#434c5e", primary: "#88c0d0", primaryForeground: "#2e3440" } },
51
+ { key: "solarized", label: "Solarized", light: { background: "#fdf6e3", card: "#fbf0d6", foreground: "#586e75", mutedForeground: "#93a1a1", border: "#eee8d5", secondary: "#eee8d5", accent: "#e7e0c8", primary: "#268bd2", primaryForeground: "#fdf6e3" }, dark: { background: "#002b36", card: "#073642", foreground: "#93a1a1", mutedForeground: "#657b83", border: "#0e4b59", secondary: "#073642", accent: "#0e4b59", primary: "#268bd2", primaryForeground: "#fdf6e3" } },
52
+ { key: "gruvbox", label: "Gruvbox", light: { background: "#fbf1c7", card: "#f9f5d7", foreground: "#3c3836", mutedForeground: "#7c6f64", border: "#ebdbb2", secondary: "#ebdbb2", accent: "#e3d5aa", primary: "#b57614", primaryForeground: "#fbf1c7" }, dark: { background: "#282828", card: "#32302f", foreground: "#ebdbb2", mutedForeground: "#a89984", border: "#504945", secondary: "#3c3836", accent: "#504945", primary: "#fabd2f", primaryForeground: "#282828" } },
53
+ { key: "catppuccin", label: "Catppuccin", light: { background: "#eff1f5", card: "#ffffff", foreground: "#4c4f69", mutedForeground: "#6c6f85", border: "#dce0e8", secondary: "#e6e9ef", accent: "#ccd0da", primary: "#8839ef", primaryForeground: "#eff1f5" }, dark: { background: "#1e1e2e", card: "#313244", foreground: "#cdd6f4", mutedForeground: "#a6adc8", border: "#45475a", secondary: "#313244", accent: "#45475a", primary: "#cba6f7", primaryForeground: "#1e1e2e" } },
54
+ { key: "tokyo-night", label: "Tokyo Night", light: { background: "#e1e2e7", card: "#ffffff", foreground: "#3760bf", mutedForeground: "#848cb5", border: "#c4c8da", secondary: "#d0d5e3", accent: "#c8cde0", primary: "#2e7de9", primaryForeground: "#ffffff" }, dark: { background: "#1a1b26", card: "#24283b", foreground: "#c0caf5", mutedForeground: "#565f89", border: "#292e42", secondary: "#24283b", accent: "#2f344d", primary: "#7aa2f7", primaryForeground: "#1a1b26" } },
55
+ { key: "rose-pine", label: "Rosé Pine", light: { background: "#faf4ed", card: "#fffaf3", foreground: "#575279", mutedForeground: "#797593", border: "#f2e9e1", secondary: "#fffaf3", accent: "#f2e9e1", primary: "#907aa9", primaryForeground: "#faf4ed" }, dark: { background: "#191724", card: "#1f1d2e", foreground: "#e0def4", mutedForeground: "#908caa", border: "#26233a", secondary: "#1f1d2e", accent: "#26233a", primary: "#c4a7e7", primaryForeground: "#191724" } },
56
+ { key: "one-dark", label: "One Dark", light: { background: "#fafafa", card: "#ffffff", foreground: "#383a42", mutedForeground: "#a0a1a7", border: "#e5e5e6", secondary: "#f0f0f0", accent: "#eaeaeb", primary: "#4078f2", primaryForeground: "#ffffff" }, dark: { background: "#282c34", card: "#21252b", foreground: "#abb2bf", mutedForeground: "#5c6370", border: "#3b4048", secondary: "#2c313a", accent: "#3b4048", primary: "#61afef", primaryForeground: "#282c34" } },
57
+ { key: "everforest", label: "Everforest", light: { background: "#fdf6e3", card: "#f4f0d9", foreground: "#5c6a72", mutedForeground: "#939f91", border: "#eae4cf", secondary: "#f4f0d9", accent: "#eae4cf", primary: "#8da101", primaryForeground: "#fdf6e3" }, dark: { background: "#2d353b", card: "#343f44", foreground: "#d3c6aa", mutedForeground: "#859289", border: "#3d484d", secondary: "#343f44", accent: "#3d484d", primary: "#a7c080", primaryForeground: "#2d353b" } },
58
+ { key: "monokai", label: "Monokai", light: { background: "#fafafa", card: "#ffffff", foreground: "#272822", mutedForeground: "#75715e", border: "#e6e6e6", secondary: "#f0f0f0", accent: "#eaeaea", primary: "#e6187f", primaryForeground: "#ffffff" }, dark: { background: "#272822", card: "#2f302a", foreground: "#f8f8f2", mutedForeground: "#75715e", border: "#414339", secondary: "#3e3d32", accent: "#414339", primary: "#f92672", primaryForeground: "#ffffff" } },
59
+ { key: "github", label: "GitHub", light: { background: "#ffffff", card: "#ffffff", foreground: "#1f2328", mutedForeground: "#656d76", border: "#d0d7de", secondary: "#f6f8fa", accent: "#eaeef2", primary: "#1f66d0", primaryForeground: "#ffffff" }, dark: { background: "#0d1117", card: "#161b22", foreground: "#e6edf3", mutedForeground: "#8b949e", border: "#30363d", secondary: "#161b22", accent: "#21262d", primary: "#2f81f7", primaryForeground: "#ffffff" } },
60
+ ];
61
+
62
+ // Keep the first choice focused without removing any of the long-lived preset
63
+ // keys. The Appearance screen shows these up front and puts the rest behind a
64
+ // disclosure so a new user is not greeted by a 21-item wall of options.
65
+ export const FEATURED_THEME_PRESET_KEYS = ["default", "neutral", "slate", "blue", "green", "violet"] as const;
66
+
67
+ export const THEME_TOKEN_KEYS: readonly (keyof ThemeTokens)[] = [
68
+ "background",
69
+ "card",
70
+ "foreground",
71
+ "mutedForeground",
72
+ "border",
73
+ "secondary",
74
+ "accent",
75
+ "primary",
76
+ "primaryForeground",
77
+ ];
78
+
79
+ export function findPreset(key: string | null | undefined): ThemePreset | undefined {
80
+ return themePresets.find((p) => p.key === key);
81
+ }
@@ -7,6 +7,9 @@ export interface SiteSettings {
7
7
  supportEmail: string | null;
8
8
  footerText: string | null;
9
9
  brandColor: string | null;
10
+ themePreset: string | null;
11
+ themeRadius: string | null;
12
+ themeOverrides: string | null;
10
13
  termsUrl: string | null;
11
14
  privacyUrl: string | null;
12
15
  locale: string | null;
@@ -23,6 +26,9 @@ const empty: SiteSettings = {
23
26
  supportEmail: null,
24
27
  footerText: null,
25
28
  brandColor: null,
29
+ themePreset: null,
30
+ themeRadius: null,
31
+ themeOverrides: null,
26
32
  termsUrl: null,
27
33
  privacyUrl: null,
28
34
  locale: null,
@@ -1,3 +1,4 @@
1
+ import { redirect } from "@sveltejs/kit";
1
2
  import type { LayoutServerLoad } from "./$types";
2
3
  import type { Capabilities } from "@podosoft/podokit-api-client";
3
4
 
@@ -26,5 +27,21 @@ export const load: LayoutServerLoad = async ({ locals, fetch }) => {
26
27
  } catch {
27
28
  /* keep defaults */
28
29
  }
30
+
31
+ // Require-two-factor policy: a signed-in user who hasn't enrolled is sent to the
32
+ // enrolment page until they do (the API's TwoFactorRequiredGuard is the real
33
+ // boundary; this is the UX). /setup-2fa lives outside this layout, so no loop.
34
+ const user = locals.user as (App.Locals["user"] & { twoFactorEnabled?: boolean }) | null;
35
+ let mustEnrol = false;
36
+ if (user && !user.twoFactorEnabled) {
37
+ try {
38
+ const res = await fetch("/api/account/require-2fa");
39
+ mustEnrol = res.ok && ((await res.json()) as { require2fa?: boolean }).require2fa === true;
40
+ } catch {
41
+ /* don't block the app if the policy check fails */
42
+ }
43
+ }
44
+ if (mustEnrol) redirect(302, "/setup-2fa");
45
+
29
46
  return { user: locals.user, impersonating: !!locals.session?.impersonatedBy, capabilities };
30
47
  };