@commonpub/layer 0.21.22 → 0.22.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/components/admin/theme/AdminThemeFamilyCard.vue +277 -0
- package/components/admin/theme/AdminThemeOverridesPanel.vue +222 -0
- package/components/admin/theme/AdminThemePreviewPane.vue +187 -0
- package/components/admin/theme/AdminThemeSceneAdmin.vue +189 -0
- package/components/admin/theme/AdminThemeSceneGallery.vue +353 -0
- package/components/admin/theme/AdminThemeSceneProse.vue +140 -0
- package/components/admin/theme/AdminThemeTokenGroup.vue +98 -0
- package/components/admin/theme/AdminThemeTokenInput.vue +278 -0
- package/composables/useTheme.ts +24 -14
- package/composables/useThemeAdmin.ts +167 -0
- package/package.json +7 -7
- package/pages/admin/theme/edit/[id].vue +547 -0
- package/pages/admin/theme/index.vue +424 -0
- package/plugins/theme.ts +25 -7
- package/server/api/admin/themes/[id].delete.ts +40 -0
- package/server/api/admin/themes/[id].get.ts +20 -0
- package/server/api/admin/themes/[id].put.ts +45 -0
- package/server/api/admin/themes/discover.get.ts +22 -0
- package/server/api/admin/themes/index.get.ts +40 -0
- package/server/api/admin/themes/index.post.ts +46 -0
- package/server/api/profile/theme.put.ts +2 -1
- package/server/middleware/theme.ts +23 -9
- package/server/utils/instanceTheme.ts +145 -25
- package/types/theme.ts +54 -0
- package/utils/themeDiscovery.ts +67 -0
- package/utils/themeIO.ts +79 -0
- package/utils/themeIds.ts +25 -0
- package/pages/admin/theme.vue +0 -502
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client-side state for the admin theme system.
|
|
3
|
+
*
|
|
4
|
+
* Singleton — both the list page (`/admin/theme`) and the editor
|
|
5
|
+
* (`/admin/theme/edit/[id]`) read the same `data`/`families` refs so
|
|
6
|
+
* a save in one place propagates to the other without refetching.
|
|
7
|
+
*
|
|
8
|
+
* Discovery helpers live in `utils/themeDiscovery.ts`; import/export
|
|
9
|
+
* in `utils/themeIO.ts`; id helpers in `utils/themeIds.ts`; types in
|
|
10
|
+
* `types/theme.ts`. This file orchestrates them into one composable.
|
|
11
|
+
*/
|
|
12
|
+
import { computed, ref } from 'vue';
|
|
13
|
+
import { BUILT_IN_THEMES, previewFromTokens } from '@commonpub/ui';
|
|
14
|
+
import type { CustomThemeRecord, ThemesPayload, ThemeFamilyView } from '../types/theme';
|
|
15
|
+
|
|
16
|
+
// ---- Family display metadata for built-in themes ------------------------
|
|
17
|
+
|
|
18
|
+
const BUILT_IN_FAMILY_META: Record<string, { name: string; description: string }> = {
|
|
19
|
+
classic: { name: 'Classic', description: 'Sharp corners, offset shadows, blue accent — the original CommonPub look' },
|
|
20
|
+
agora: { name: 'Agora', description: 'Warm parchment tones, green accent, serif display font — institutional warmth' },
|
|
21
|
+
generics: { name: 'Generics', description: 'Minimal dark aesthetic with soft glow shadows' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const BUILT_IN_PREVIEWS: Record<string, { bg: string; surface: string; accent: string; text: string; border: string }> = {
|
|
25
|
+
base: { bg: '#fafaf9', surface: '#ffffff', accent: '#5b9cf6', text: '#1a1a1a', border: '#1a1a1a' },
|
|
26
|
+
dark: { bg: '#111111', surface: '#1a1a1a', accent: '#5b9cf6', text: '#e5e5e3', border: '#444440' },
|
|
27
|
+
generics: { bg: '#0c0c0b', surface: '#141413', accent: '#5b9cf6', text: '#d8d5cf', border: '#272725' },
|
|
28
|
+
agora: { bg: '#f7f4ed', surface: '#faf8f3', accent: '#3d8b5e', text: '#1a1a1a', border: '#1a1a1a' },
|
|
29
|
+
'agora-dark': { bg: '#0d1a12', surface: '#141f17', accent: '#4aa06e', text: '#e8e8e2', border: '#3a4f40' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---- Singleton state ----------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const data = ref<ThemesPayload | null>(null);
|
|
35
|
+
const loading = ref(false);
|
|
36
|
+
const error = ref<string | null>(null);
|
|
37
|
+
|
|
38
|
+
async function refresh(): Promise<void> {
|
|
39
|
+
loading.value = true;
|
|
40
|
+
error.value = null;
|
|
41
|
+
try {
|
|
42
|
+
data.value = await $fetch<ThemesPayload>('/api/admin/themes');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
error.value = err instanceof Error ? err.message : 'Failed to load themes';
|
|
45
|
+
} finally {
|
|
46
|
+
loading.value = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- Family view-model builder ------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Merge built-in + registered + custom themes into one family-grouped
|
|
54
|
+
* list. The same family slug across sources collapses into a single
|
|
55
|
+
* entry; later sources overwrite earlier display metadata (custom >
|
|
56
|
+
* registered > built-in).
|
|
57
|
+
*/
|
|
58
|
+
function buildFamilies(payload: ThemesPayload): ThemeFamilyView[] {
|
|
59
|
+
const map = new Map<string, ThemeFamilyView>();
|
|
60
|
+
|
|
61
|
+
// Built-in first
|
|
62
|
+
for (const t of payload.builtIn) {
|
|
63
|
+
const meta = BUILT_IN_FAMILY_META[t.family] ?? { name: t.family, description: '' };
|
|
64
|
+
const fam = ensureFamily(map, t.family, {
|
|
65
|
+
name: meta.name,
|
|
66
|
+
description: meta.description,
|
|
67
|
+
source: 'builtin',
|
|
68
|
+
});
|
|
69
|
+
const preview = BUILT_IN_PREVIEWS[t.id] ?? (t.isDark ? BUILT_IN_PREVIEWS.dark! : BUILT_IN_PREVIEWS.base!);
|
|
70
|
+
placeVariant(fam, t.id, t.name, t.isDark, preview);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Registered themes — promote source if family was previously built-in
|
|
74
|
+
for (const t of payload.registered) {
|
|
75
|
+
const fallback = t.isDark ? BUILT_IN_PREVIEWS.dark! : BUILT_IN_PREVIEWS.base!;
|
|
76
|
+
const preview = {
|
|
77
|
+
bg: t.preview?.bg ?? fallback.bg,
|
|
78
|
+
surface: t.preview?.surface ?? fallback.surface,
|
|
79
|
+
accent: t.preview?.accent ?? fallback.accent,
|
|
80
|
+
text: t.preview?.text ?? fallback.text,
|
|
81
|
+
border: t.preview?.border ?? fallback.border,
|
|
82
|
+
};
|
|
83
|
+
const fam = ensureFamily(map, t.family, {
|
|
84
|
+
name: t.name,
|
|
85
|
+
description: t.description ?? `Code-registered theme from this app's commonpub.config.ts`,
|
|
86
|
+
source: 'registered',
|
|
87
|
+
});
|
|
88
|
+
if (fam.source === 'builtin') fam.source = 'registered';
|
|
89
|
+
placeVariant(fam, t.id, t.name, t.isDark, preview);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Custom (DB-stored) — these win the meta tug-of-war
|
|
93
|
+
for (const t of payload.custom) {
|
|
94
|
+
const dataAttr = `cpub-custom-${t.id}`;
|
|
95
|
+
const preview = previewFromTokens(t.tokens, t.isDark);
|
|
96
|
+
const fam = ensureFamily(map, t.family, {
|
|
97
|
+
name: t.name,
|
|
98
|
+
description: t.description || 'Custom theme',
|
|
99
|
+
source: 'custom',
|
|
100
|
+
});
|
|
101
|
+
fam.source = 'custom';
|
|
102
|
+
fam.name = t.name;
|
|
103
|
+
if (t.description) fam.description = t.description;
|
|
104
|
+
placeVariant(fam, dataAttr, t.name, t.isDark, preview);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [...map.values()];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureFamily(
|
|
111
|
+
map: Map<string, ThemeFamilyView>,
|
|
112
|
+
id: string,
|
|
113
|
+
init: { name: string; description: string; source: ThemeFamilyView['source'] },
|
|
114
|
+
): ThemeFamilyView {
|
|
115
|
+
let fam = map.get(id);
|
|
116
|
+
if (!fam) {
|
|
117
|
+
fam = {
|
|
118
|
+
id,
|
|
119
|
+
name: init.name,
|
|
120
|
+
description: init.description,
|
|
121
|
+
source: init.source,
|
|
122
|
+
light: null,
|
|
123
|
+
dark: null,
|
|
124
|
+
preview: { light: BUILT_IN_PREVIEWS.base!, dark: BUILT_IN_PREVIEWS.dark! },
|
|
125
|
+
};
|
|
126
|
+
map.set(id, fam);
|
|
127
|
+
}
|
|
128
|
+
return fam;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function placeVariant(
|
|
132
|
+
fam: ThemeFamilyView,
|
|
133
|
+
themeId: string,
|
|
134
|
+
themeName: string,
|
|
135
|
+
isDark: boolean,
|
|
136
|
+
preview: { bg: string; surface: string; accent: string; text: string; border: string },
|
|
137
|
+
): void {
|
|
138
|
+
if (isDark) {
|
|
139
|
+
fam.dark = { id: themeId, name: themeName };
|
|
140
|
+
fam.preview.dark = preview;
|
|
141
|
+
} else {
|
|
142
|
+
fam.light = { id: themeId, name: themeName };
|
|
143
|
+
fam.preview.light = preview;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- Composable surface -------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export function useThemeAdmin(): {
|
|
150
|
+
data: typeof data;
|
|
151
|
+
loading: typeof loading;
|
|
152
|
+
error: typeof error;
|
|
153
|
+
families: import('vue').ComputedRef<ThemeFamilyView[]>;
|
|
154
|
+
refresh: typeof refresh;
|
|
155
|
+
/** Find a custom theme by id in the current payload. Returns null if absent. */
|
|
156
|
+
findCustom: (id: string) => CustomThemeRecord | null;
|
|
157
|
+
} {
|
|
158
|
+
const families = computed<ThemeFamilyView[]>(() => (data.value ? buildFamilies(data.value) : []));
|
|
159
|
+
return {
|
|
160
|
+
data,
|
|
161
|
+
loading,
|
|
162
|
+
error,
|
|
163
|
+
families,
|
|
164
|
+
refresh,
|
|
165
|
+
findCustom: (id: string) => data.value?.custom.find((t) => t.id === id) ?? null,
|
|
166
|
+
};
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"vue": "^3.4.0",
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@commonpub/config": "0.13.0",
|
|
54
|
-
"@commonpub/auth": "0.6.0",
|
|
55
|
-
"@commonpub/explainer": "0.7.15",
|
|
56
53
|
"@commonpub/editor": "0.7.11",
|
|
57
54
|
"@commonpub/docs": "0.6.3",
|
|
58
55
|
"@commonpub/learning": "0.5.2",
|
|
59
|
-
"@commonpub/
|
|
60
|
-
"@commonpub/server": "2.
|
|
56
|
+
"@commonpub/config": "0.14.0",
|
|
57
|
+
"@commonpub/server": "2.56.0",
|
|
58
|
+
"@commonpub/auth": "0.6.0",
|
|
59
|
+
"@commonpub/schema": "0.17.0",
|
|
61
60
|
"@commonpub/protocol": "0.12.0",
|
|
62
|
-
"@commonpub/
|
|
61
|
+
"@commonpub/ui": "0.9.0",
|
|
62
|
+
"@commonpub/explainer": "0.7.15"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|