@commonpub/layer 0.22.0 → 0.22.1
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.
|
@@ -38,6 +38,37 @@ const PREVIEW_SCENES: SceneOption[] = [
|
|
|
38
38
|
const activeScene = ref<SceneOption['id']>('gallery');
|
|
39
39
|
const previewMode = ref<'light' | 'dark'>(props.isDark ? 'dark' : 'light');
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Map every parent-theme id to its family's light + dark variant. Mirrors
|
|
43
|
+
* `layers/base/utils/themeConfig.ts` THEME_TO_FAMILY + FAMILY_VARIANTS,
|
|
44
|
+
* inlined here so the preview pane doesn't need to import the SSR-side
|
|
45
|
+
* utils. Custom-theme parents (`cpub-custom-*`) and any unknown id fall
|
|
46
|
+
* back to the classic family — the user's tokens override on top regardless.
|
|
47
|
+
*/
|
|
48
|
+
const FAMILY_VARIANT_OF: Record<string, { light: string; dark: string }> = {
|
|
49
|
+
base: { light: 'base', dark: 'dark' },
|
|
50
|
+
dark: { light: 'base', dark: 'dark' },
|
|
51
|
+
agora: { light: 'agora', dark: 'agora-dark' },
|
|
52
|
+
'agora-dark': { light: 'agora', dark: 'agora-dark' },
|
|
53
|
+
generics: { light: 'generics', dark: 'generics' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The actual `data-theme` attribute applied to the preview surface,
|
|
58
|
+
* resolved from `parentTheme` + `previewMode`. Returns `undefined` for the
|
|
59
|
+
* base/light case (no attribute = `:root` rules apply natively, matching
|
|
60
|
+
* the convention used by `applyThemeToElement` elsewhere).
|
|
61
|
+
*
|
|
62
|
+
* **Bug fix from 0.22.0**: previously `:data-theme="parentTheme"` was
|
|
63
|
+
* hardcoded, so the Light/Dark toggle updated a ref but never re-rendered
|
|
64
|
+
* the preview. Now the toggle actually swaps the rendered theme.
|
|
65
|
+
*/
|
|
66
|
+
const effectiveDataTheme = computed<string | undefined>(() => {
|
|
67
|
+
const variants = FAMILY_VARIANT_OF[props.parentTheme] ?? FAMILY_VARIANT_OF.base!;
|
|
68
|
+
const v = previewMode.value === 'dark' ? variants.dark : variants.light;
|
|
69
|
+
return v === 'base' ? undefined : v;
|
|
70
|
+
});
|
|
71
|
+
|
|
41
72
|
/**
|
|
42
73
|
* Build the inline style string scoped to the preview surface. We apply
|
|
43
74
|
* tokens to the wrapper element only — that scopes the in-progress theme
|
|
@@ -102,7 +133,7 @@ const previewStyle = computed(() => {
|
|
|
102
133
|
|
|
103
134
|
<div
|
|
104
135
|
class="theme-preview-surface"
|
|
105
|
-
:data-theme="
|
|
136
|
+
:data-theme="effectiveDataTheme"
|
|
106
137
|
:style="previewStyle"
|
|
107
138
|
:data-preview-mode="previewMode"
|
|
108
139
|
>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
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/
|
|
53
|
+
"@commonpub/auth": "0.6.0",
|
|
54
54
|
"@commonpub/docs": "0.6.3",
|
|
55
|
-
"@commonpub/learning": "0.5.2",
|
|
56
55
|
"@commonpub/config": "0.14.0",
|
|
57
|
-
"@commonpub/
|
|
58
|
-
"@commonpub/
|
|
56
|
+
"@commonpub/editor": "0.7.11",
|
|
57
|
+
"@commonpub/explainer": "0.7.15",
|
|
58
|
+
"@commonpub/learning": "0.5.2",
|
|
59
59
|
"@commonpub/schema": "0.17.0",
|
|
60
60
|
"@commonpub/protocol": "0.12.0",
|
|
61
61
|
"@commonpub/ui": "0.9.0",
|
|
62
|
-
"@commonpub/
|
|
62
|
+
"@commonpub/server": "2.56.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -149,7 +149,13 @@ const pairCandidates = computed(() =>
|
|
|
149
149
|
|
|
150
150
|
// --- Save / cancel / export -----------------------------------------
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Save the draft. If `apply` is true, ALSO set this theme as the
|
|
154
|
+
* instance default in the same await chain — must happen BEFORE the
|
|
155
|
+
* create-mode router.replace, otherwise the navigation could unmount
|
|
156
|
+
* the component mid-PUT and lose the apply.
|
|
157
|
+
*/
|
|
158
|
+
async function save({ apply = false }: { apply?: boolean } = {}): Promise<void> {
|
|
153
159
|
saving.value = true;
|
|
154
160
|
error.value = null;
|
|
155
161
|
try {
|
|
@@ -163,15 +169,14 @@ async function save(): Promise<void> {
|
|
|
163
169
|
parentTheme: draft.value.parentTheme,
|
|
164
170
|
tokens: draft.value.tokens,
|
|
165
171
|
};
|
|
172
|
+
|
|
173
|
+
let savedId: string;
|
|
166
174
|
if (isCreating) {
|
|
167
175
|
const created = await $fetch('/api/admin/themes', {
|
|
168
176
|
method: 'POST',
|
|
169
177
|
body: payload,
|
|
170
178
|
});
|
|
171
|
-
|
|
172
|
-
dirty.value = false;
|
|
173
|
-
await themesApi.refresh();
|
|
174
|
-
router.replace(`/admin/theme/edit/${(created as { id: string }).id}`);
|
|
179
|
+
savedId = (created as { id: string }).id;
|
|
175
180
|
} else {
|
|
176
181
|
// Cast: Nuxt's typed-route inference for dynamic URLs picks the
|
|
177
182
|
// narrowest method overload (GET) — same workaround used in
|
|
@@ -180,9 +185,25 @@ async function save(): Promise<void> {
|
|
|
180
185
|
`/api/admin/themes/${draft.value.id}`,
|
|
181
186
|
{ method: 'PUT', body: payload },
|
|
182
187
|
);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
savedId = draft.value.id;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Apply BEFORE refresh/navigation so the navigate doesn't unmount us
|
|
192
|
+
// mid-PUT (would lose the apply + the success toast).
|
|
193
|
+
if (apply) {
|
|
194
|
+
await $fetch('/api/admin/settings', {
|
|
195
|
+
method: 'PUT',
|
|
196
|
+
body: { key: 'theme.default', value: `cpub-custom-${savedId}` },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
notify(apply ? 'Saved & applied' : (isCreating ? 'Theme created' : 'Saved'), 'success');
|
|
201
|
+
dirty.value = false;
|
|
202
|
+
await themesApi.refresh();
|
|
203
|
+
|
|
204
|
+
// Navigate LAST so all the awaits above have observable effects.
|
|
205
|
+
if (isCreating) {
|
|
206
|
+
router.replace(`/admin/theme/edit/${savedId}`);
|
|
186
207
|
}
|
|
187
208
|
} catch (err) {
|
|
188
209
|
const msg = err instanceof Error ? err.message : 'Save failed';
|
|
@@ -194,13 +215,7 @@ async function save(): Promise<void> {
|
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
async function applyAndSave(): Promise<void> {
|
|
197
|
-
await save();
|
|
198
|
-
if (error.value) return;
|
|
199
|
-
await $fetch('/api/admin/settings', {
|
|
200
|
-
method: 'PUT',
|
|
201
|
-
body: { key: 'theme.default', value: `cpub-custom-${draft.value.id}` },
|
|
202
|
-
});
|
|
203
|
-
notify('Saved and applied instance-wide', 'success');
|
|
218
|
+
await save({ apply: true });
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
function exportTheme(): void {
|
|
@@ -248,9 +263,14 @@ onBeforeUnmount(() => {
|
|
|
248
263
|
<template>
|
|
249
264
|
<div class="theme-editor">
|
|
250
265
|
<header class="theme-editor-toolbar">
|
|
251
|
-
<button
|
|
266
|
+
<button
|
|
267
|
+
class="cpub-btn cpub-btn-sm theme-editor-back"
|
|
268
|
+
:title="dirty ? 'You have unsaved changes' : 'Back to themes list'"
|
|
269
|
+
@click="cancel"
|
|
270
|
+
>
|
|
252
271
|
<i class="fa-solid fa-arrow-left" aria-hidden="true" />
|
|
253
272
|
<span>Themes</span>
|
|
273
|
+
<span v-if="dirty" class="theme-editor-dirty-dot" aria-label="unsaved changes"></span>
|
|
254
274
|
</button>
|
|
255
275
|
|
|
256
276
|
<div class="theme-editor-meta">
|
|
@@ -329,11 +349,13 @@ onBeforeUnmount(() => {
|
|
|
329
349
|
<button class="cpub-btn cpub-btn-sm" @click="exportTheme" title="Download .cpub-theme.json">
|
|
330
350
|
<i class="fa-solid fa-file-export" aria-hidden="true" /> Export
|
|
331
351
|
</button>
|
|
332
|
-
<button class="cpub-btn cpub-btn-sm" :disabled="saving || !dirty" @click="save">
|
|
333
|
-
<i class="fa-solid fa-floppy-disk" aria-hidden="true" />
|
|
352
|
+
<button class="cpub-btn cpub-btn-sm" :disabled="saving || !dirty" @click="() => save()">
|
|
353
|
+
<i :class="['fa-solid', saving ? 'fa-circle-notch fa-spin' : 'fa-floppy-disk']" aria-hidden="true" />
|
|
354
|
+
{{ saving ? 'Saving…' : 'Save' }}
|
|
334
355
|
</button>
|
|
335
356
|
<button class="cpub-btn cpub-btn-sm cpub-btn-primary" :disabled="saving" @click="applyAndSave">
|
|
336
|
-
<i class="fa-solid fa-rocket" aria-hidden="true" />
|
|
357
|
+
<i :class="['fa-solid', saving ? 'fa-circle-notch fa-spin' : 'fa-rocket']" aria-hidden="true" />
|
|
358
|
+
{{ saving ? 'Applying…' : 'Save & apply' }}
|
|
337
359
|
</button>
|
|
338
360
|
</div>
|
|
339
361
|
</header>
|
|
@@ -406,7 +428,33 @@ onBeforeUnmount(() => {
|
|
|
406
428
|
flex-wrap: wrap;
|
|
407
429
|
}
|
|
408
430
|
|
|
409
|
-
.theme-editor-back {
|
|
431
|
+
.theme-editor-back {
|
|
432
|
+
flex-shrink: 0;
|
|
433
|
+
position: relative;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.theme-editor-dirty-dot {
|
|
437
|
+
display: inline-block;
|
|
438
|
+
width: 6px;
|
|
439
|
+
height: 6px;
|
|
440
|
+
background: var(--accent);
|
|
441
|
+
border-radius: var(--radius-full);
|
|
442
|
+
margin-left: 4px;
|
|
443
|
+
/* Subtle pulse so it draws the eye without being noisy */
|
|
444
|
+
animation: theme-editor-dirty-pulse 2s ease-in-out infinite;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@keyframes theme-editor-dirty-pulse {
|
|
448
|
+
0%, 100% { opacity: 1; }
|
|
449
|
+
50% { opacity: 0.4; }
|
|
450
|
+
}
|
|
451
|
+
@media (prefers-reduced-motion: reduce) {
|
|
452
|
+
.theme-editor-dirty-dot { animation: none; }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.theme-editor-input-name {
|
|
456
|
+
font-weight: var(--font-weight-semibold);
|
|
457
|
+
}
|
|
410
458
|
|
|
411
459
|
.theme-editor-meta {
|
|
412
460
|
display: flex;
|
|
@@ -244,6 +244,28 @@ function recheckDiscovery(): void {
|
|
|
244
244
|
discovery.value = detectAppliedOverrides();
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Only show the "your site has a custom theme" banner when the detected
|
|
249
|
+
* overrides are LIKELY from a CSS file shipped by the layer app — NOT from
|
|
250
|
+
* a custom theme the admin has already saved (which would also appear as
|
|
251
|
+
* :root token overrides because the SSR middleware injects them there).
|
|
252
|
+
*
|
|
253
|
+
* Gating rules:
|
|
254
|
+
* - hide when the active default is already a cpub-custom-* theme
|
|
255
|
+
* - hide when instance-wide token overrides are set (those tokens explain
|
|
256
|
+
* the diff; the banner would confuse the admin into re-capturing them)
|
|
257
|
+
* - hide when no overrides were detected
|
|
258
|
+
*
|
|
259
|
+
* If admins want to re-capture from a fresh :root state, they can revert
|
|
260
|
+
* to the base theme, clear overrides, then the banner will reappear.
|
|
261
|
+
*/
|
|
262
|
+
const showDiscoveryBanner = computed<boolean>(() => {
|
|
263
|
+
if (discovery.value.count === 0) return false;
|
|
264
|
+
if (instanceDefault.value.startsWith('cpub-custom-')) return false;
|
|
265
|
+
if (Object.keys(initialOverrides.value).length > 0) return false;
|
|
266
|
+
return true;
|
|
267
|
+
});
|
|
268
|
+
|
|
247
269
|
// --- Token overrides (legacy / quick tweaks) ---
|
|
248
270
|
// State + UI live in <AdminThemeOverridesPanel>; this page only persists
|
|
249
271
|
// what the panel emits.
|
|
@@ -304,9 +326,12 @@ async function saveOverrides(overrides: Record<string, string>): Promise<void> {
|
|
|
304
326
|
{{ toast.msg }}
|
|
305
327
|
</div>
|
|
306
328
|
|
|
307
|
-
<!-- Discovery banner
|
|
329
|
+
<!-- Discovery banner — only when the overrides are from CSS (not from
|
|
330
|
+
a custom theme this admin already saved or instance-wide overrides).
|
|
331
|
+
Without this gate, the banner would re-appear after capture since
|
|
332
|
+
the custom theme it created now appears as a token override on :root. -->
|
|
308
333
|
<section
|
|
309
|
-
v-if="
|
|
334
|
+
v-if="showDiscoveryBanner"
|
|
310
335
|
class="admin-theme-discovery"
|
|
311
336
|
role="region"
|
|
312
337
|
aria-label="Discovered theme tokens"
|