@commonpub/layer 0.68.0 → 0.68.2
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.
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* Stays presentational — all wiring (select / edit / duplicate / delete /
|
|
10
10
|
* export) is emitted up so the page owns the state machine.
|
|
11
11
|
*/
|
|
12
|
+
import { computed } from 'vue';
|
|
12
13
|
import type { ThemeFamilyView } from '../../../types/theme';
|
|
13
14
|
|
|
14
|
-
defineProps<{
|
|
15
|
+
const props = defineProps<{
|
|
15
16
|
family: ThemeFamilyView;
|
|
16
17
|
active: boolean;
|
|
17
18
|
saving: boolean;
|
|
@@ -25,6 +26,18 @@ const emit = defineEmits<{
|
|
|
25
26
|
remove: [themeId: string];
|
|
26
27
|
}>();
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The "primary" record of the family for edit/duplicate/export/delete. For a
|
|
31
|
+
* Studio light/dark PAIR the primary is the variant WITHOUT a `-light`/`-dark`
|
|
32
|
+
* suffix (the original slug); its sibling is the suffixed one. Targeting the
|
|
33
|
+
* primary fixes a bug where dark-primary pairs edited/deleted the wrong
|
|
34
|
+
* (auto-generated) record. Falls back to whichever variant exists.
|
|
35
|
+
*/
|
|
36
|
+
const primaryId = computed<string>(() => {
|
|
37
|
+
const ids = [props.family.light?.id, props.family.dark?.id].filter((x): x is string => Boolean(x));
|
|
38
|
+
return ids.find((id) => !/-(?:light|dark)$/.test(id)) ?? ids[0]!;
|
|
39
|
+
});
|
|
40
|
+
|
|
28
41
|
function variantBoxStyle(v: { bg: string; surface: string; accent: string; text: string; border: string }) {
|
|
29
42
|
return {
|
|
30
43
|
backgroundColor: v.bg,
|
|
@@ -102,7 +115,7 @@ function badge(family: ThemeFamilyView): { label: string; tone: 'builtin' | 'reg
|
|
|
102
115
|
v-if="family.source === 'custom'"
|
|
103
116
|
type="button"
|
|
104
117
|
class="cpub-btn cpub-btn-sm"
|
|
105
|
-
@click="emit('edit',
|
|
118
|
+
@click="emit('edit', primaryId)"
|
|
106
119
|
>
|
|
107
120
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true" /> Edit
|
|
108
121
|
</button>
|
|
@@ -110,7 +123,7 @@ function badge(family: ThemeFamilyView): { label: string; tone: 'builtin' | 'reg
|
|
|
110
123
|
type="button"
|
|
111
124
|
class="cpub-btn cpub-btn-sm"
|
|
112
125
|
:title="family.source === 'custom' ? 'Duplicate this theme' : 'Fork to a new editable custom theme'"
|
|
113
|
-
@click="emit('duplicate',
|
|
126
|
+
@click="emit('duplicate', primaryId)"
|
|
114
127
|
>
|
|
115
128
|
<i class="fa-solid fa-copy" aria-hidden="true" /> {{ family.source === 'custom' ? 'Duplicate' : 'Fork' }}
|
|
116
129
|
</button>
|
|
@@ -118,7 +131,7 @@ function badge(family: ThemeFamilyView): { label: string; tone: 'builtin' | 'reg
|
|
|
118
131
|
v-if="family.source === 'custom'"
|
|
119
132
|
type="button"
|
|
120
133
|
class="cpub-btn cpub-btn-sm"
|
|
121
|
-
@click="emit('exportTheme',
|
|
134
|
+
@click="emit('exportTheme', primaryId)"
|
|
122
135
|
>
|
|
123
136
|
<i class="fa-solid fa-file-export" aria-hidden="true" /> Export
|
|
124
137
|
</button>
|
|
@@ -126,7 +139,7 @@ function badge(family: ThemeFamilyView): { label: string; tone: 'builtin' | 'reg
|
|
|
126
139
|
v-if="family.source === 'custom'"
|
|
127
140
|
type="button"
|
|
128
141
|
class="cpub-btn cpub-btn-sm theme-family-action-danger"
|
|
129
|
-
@click="emit('remove',
|
|
142
|
+
@click="emit('remove', primaryId)"
|
|
130
143
|
>
|
|
131
144
|
<i class="fa-solid fa-trash" aria-hidden="true" /> Delete
|
|
132
145
|
</button>
|
|
@@ -20,8 +20,14 @@ const props = defineProps<{
|
|
|
20
20
|
/** The base theme whose CSS file provides inherited defaults (via data-theme). */
|
|
21
21
|
parentTheme: string;
|
|
22
22
|
isDark: boolean;
|
|
23
|
+
/** Optional controlled preview mode. When provided, the Light/Dark toggle
|
|
24
|
+
* emits `update:mode` and the parent owns the state (so it can swap the
|
|
25
|
+
* previewed variant's tokens). Uncontrolled (internal) when omitted. */
|
|
26
|
+
mode?: 'light' | 'dark';
|
|
23
27
|
}>();
|
|
24
28
|
|
|
29
|
+
const emit = defineEmits<{ 'update:mode': ['light' | 'dark'] }>();
|
|
30
|
+
|
|
25
31
|
interface SceneOption {
|
|
26
32
|
id: 'gallery' | 'prose' | 'admin' | 'sheet';
|
|
27
33
|
label: string;
|
|
@@ -37,7 +43,14 @@ const PREVIEW_SCENES: SceneOption[] = [
|
|
|
37
43
|
];
|
|
38
44
|
|
|
39
45
|
const activeScene = ref<SceneOption['id']>('gallery');
|
|
40
|
-
const
|
|
46
|
+
const internalMode = ref<'light' | 'dark'>(props.isDark ? 'dark' : 'light');
|
|
47
|
+
const previewMode = computed<'light' | 'dark'>({
|
|
48
|
+
get: () => props.mode ?? internalMode.value,
|
|
49
|
+
set: (v) => {
|
|
50
|
+
internalMode.value = v;
|
|
51
|
+
emit('update:mode', v);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
56
|
* Map every parent-theme id to its family's light + dark variant. Mirrors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.68.
|
|
3
|
+
"version": "0.68.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
57
|
"@commonpub/config": "0.20.0",
|
|
58
58
|
"@commonpub/docs": "0.6.3",
|
|
59
|
-
"@commonpub/explainer": "0.7.15",
|
|
60
59
|
"@commonpub/editor": "0.7.11",
|
|
60
|
+
"@commonpub/explainer": "0.7.15",
|
|
61
|
+
"@commonpub/protocol": "0.13.0",
|
|
61
62
|
"@commonpub/learning": "0.5.2",
|
|
62
63
|
"@commonpub/schema": "0.37.0",
|
|
63
|
-
"@commonpub/protocol": "0.13.0",
|
|
64
64
|
"@commonpub/server": "2.83.0",
|
|
65
|
-
"@commonpub/
|
|
66
|
-
"@commonpub/
|
|
65
|
+
"@commonpub/theme-studio": "0.3.0",
|
|
66
|
+
"@commonpub/ui": "0.12.1"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -156,6 +156,25 @@ function resetToken(key: string): void {
|
|
|
156
156
|
|
|
157
157
|
const modifiedTotal = computed(() => Object.keys(draft.value.tokens).length);
|
|
158
158
|
|
|
159
|
+
// --- Preview light/dark ------------------------------------------------
|
|
160
|
+
// The preview pane's Light/Dark toggle is controlled here so a Studio theme
|
|
161
|
+
// shows its ACTUAL light + dark variants (regenerated from the recipe per
|
|
162
|
+
// mode) — not the primary variant's tokens in both positions. (Fixes "the
|
|
163
|
+
// editor light/dark buttons don't do anything".)
|
|
164
|
+
const previewMode = ref<'light' | 'dark'>('light');
|
|
165
|
+
const previewTokens = computed<Record<string, string>>(() => {
|
|
166
|
+
if (!draft.value.recipe) return draft.value.tokens;
|
|
167
|
+
// The variant being edited (primary) shows the LIVE draft tokens (so hand
|
|
168
|
+
// tweaks appear); the other variant shows the recipe-derived sibling (what
|
|
169
|
+
// will be saved for it).
|
|
170
|
+
const primaryMode = draft.value.isDark ? 'dark' : 'light';
|
|
171
|
+
if (previewMode.value === primaryMode) return draft.value.tokens;
|
|
172
|
+
return recipeToTokens({ ...draft.value.recipe, mode: previewMode.value }).tokens;
|
|
173
|
+
});
|
|
174
|
+
const previewParent = computed<string>(() =>
|
|
175
|
+
draft.value.recipe ? (previewMode.value === 'dark' ? 'dark' : 'base') : draft.value.parentTheme,
|
|
176
|
+
);
|
|
177
|
+
|
|
159
178
|
// --- Studio (guided generator) ---------------------------------------
|
|
160
179
|
|
|
161
180
|
/** Studio regenerated the whole token set — replace the draft's tokens. */
|
|
@@ -590,9 +609,11 @@ onBeforeUnmount(() => {
|
|
|
590
609
|
|
|
591
610
|
<AdminThemePreviewPane
|
|
592
611
|
class="theme-editor-preview"
|
|
593
|
-
:tokens="
|
|
594
|
-
:parent-theme="
|
|
595
|
-
:is-dark="
|
|
612
|
+
:tokens="previewTokens"
|
|
613
|
+
:parent-theme="previewParent"
|
|
614
|
+
:is-dark="previewMode === 'dark'"
|
|
615
|
+
:mode="previewMode"
|
|
616
|
+
@update:mode="previewMode = $event"
|
|
596
617
|
/>
|
|
597
618
|
</div>
|
|
598
619
|
|
|
@@ -146,14 +146,25 @@ async function duplicateTheme(themeId: string): Promise<void> {
|
|
|
146
146
|
async function removeTheme(themeId: string): Promise<void> {
|
|
147
147
|
const customId = parseCustomThemeId(themeId);
|
|
148
148
|
if (!customId) return;
|
|
149
|
-
|
|
149
|
+
// Delete the whole pair: the theme + its linked light/dark sibling, so a
|
|
150
|
+
// Studio pair (one card) goes away cleanly instead of orphaning one record.
|
|
151
|
+
const src = themesApi.data.value?.custom.find((t) => t.id === customId);
|
|
152
|
+
const ids = [customId];
|
|
153
|
+
if (src?.pairId && src.pairId !== customId) ids.push(src.pairId);
|
|
154
|
+
const label = ids.length > 1 ? `theme "${customId}" and its light/dark pair` : `custom theme "${customId}"`;
|
|
155
|
+
if (!confirm(`Delete ${label}? This cannot be undone.`)) return;
|
|
150
156
|
saving.value = true;
|
|
151
157
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
let resetDefault = false;
|
|
159
|
+
for (const id of ids) {
|
|
160
|
+
const res = await ($fetch as (u: string, o: Record<string, unknown>) => Promise<{ resetDefault?: boolean }>)(
|
|
161
|
+
`/api/admin/themes/${id}`,
|
|
162
|
+
{ method: 'DELETE' },
|
|
163
|
+
);
|
|
164
|
+
if (res?.resetDefault) resetDefault = true;
|
|
165
|
+
}
|
|
155
166
|
await Promise.all([themesApi.refresh(), refreshSettings()]);
|
|
156
|
-
notify(
|
|
167
|
+
notify(resetDefault ? 'Theme deleted, default reset to Classic' : 'Theme deleted', 'success');
|
|
157
168
|
} catch (err) {
|
|
158
169
|
notify(err instanceof Error ? err.message : 'Failed to delete', 'error');
|
|
159
170
|
} finally {
|
package/theme/base.css
CHANGED
|
@@ -300,9 +300,35 @@ body {
|
|
|
300
300
|
*::before,
|
|
301
301
|
*::after {
|
|
302
302
|
box-sizing: border-box;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Surface radius. Real UI surfaces (buttons, cards, inputs, tags, panels)
|
|
306
|
+
* inherit the theme's --radius. Structural, media, and decorative elements are
|
|
307
|
+
* reset to 0 below so a rounded theme (e.g. Stoa, --radius:12px) does NOT put
|
|
308
|
+
* radii on line breaks, dividers, rules, icons, images, table cells, or
|
|
309
|
+
* pseudo-element decorations — the "line breaks carry rounded corners" bug.
|
|
310
|
+
* (Elements that genuinely want rounding set it explicitly, e.g. avatars use
|
|
311
|
+
* --radius-full; an explicit class-level border-radius outranks these resets.) */
|
|
312
|
+
* {
|
|
303
313
|
border-radius: var(--radius);
|
|
304
314
|
}
|
|
305
315
|
|
|
316
|
+
hr,
|
|
317
|
+
svg,
|
|
318
|
+
img,
|
|
319
|
+
picture,
|
|
320
|
+
video,
|
|
321
|
+
canvas,
|
|
322
|
+
iframe,
|
|
323
|
+
table, thead, tbody, tfoot, tr, td, th, caption, col, colgroup,
|
|
324
|
+
::before,
|
|
325
|
+
::after,
|
|
326
|
+
::marker,
|
|
327
|
+
::placeholder,
|
|
328
|
+
.cpub-divider {
|
|
329
|
+
border-radius: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
306
332
|
/* === ACCESSIBILITY === */
|
|
307
333
|
|
|
308
334
|
/* Skip-to-content link */
|