@commonpub/layer 0.65.0 → 0.66.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.
|
@@ -331,8 +331,9 @@ function back(): void {
|
|
|
331
331
|
</span>
|
|
332
332
|
</label>
|
|
333
333
|
<p class="cpub-studio-note">
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
Studio saves a matching light + dark pair, each tuned for its mode. Finish to drop
|
|
335
|
+
into the advanced editor with every token populated; re-open Studio any time, or
|
|
336
|
+
fine-tune individual tokens by hand.
|
|
336
337
|
</p>
|
|
337
338
|
</div>
|
|
338
339
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -55,15 +55,15 @@
|
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
57
|
"@commonpub/config": "0.20.0",
|
|
58
|
-
"@commonpub/
|
|
58
|
+
"@commonpub/docs": "0.6.3",
|
|
59
59
|
"@commonpub/protocol": "0.13.0",
|
|
60
|
+
"@commonpub/editor": "0.7.11",
|
|
60
61
|
"@commonpub/explainer": "0.7.15",
|
|
61
|
-
"@commonpub/docs": "0.6.3",
|
|
62
62
|
"@commonpub/schema": "0.36.0",
|
|
63
|
-
"@commonpub/theme-studio": "0.1.0",
|
|
64
63
|
"@commonpub/ui": "0.11.2",
|
|
65
64
|
"@commonpub/server": "2.83.0",
|
|
66
|
-
"@commonpub/
|
|
65
|
+
"@commonpub/theme-studio": "0.2.0",
|
|
66
|
+
"@commonpub/learning": "0.5.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
|
19
19
|
import { TOKEN_GROUP_LABELS, TOKEN_GROUP_ORDER, tokensByGroup } from '@commonpub/ui';
|
|
20
|
-
import { googleHref, type ThemeRecipe } from '@commonpub/theme-studio';
|
|
20
|
+
import { googleHref, recipeToTokens, type ThemeRecipe } from '@commonpub/theme-studio';
|
|
21
21
|
|
|
22
22
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
23
23
|
|
|
@@ -204,16 +204,61 @@ const pairCandidates = computed(() =>
|
|
|
204
204
|
|
|
205
205
|
// --- Save / cancel / export -----------------------------------------
|
|
206
206
|
|
|
207
|
+
/** The opposite-mode sibling's id for a paired Studio theme. */
|
|
208
|
+
function siblingIdFor(id: string, isDark: boolean): string {
|
|
209
|
+
const base = id.replace(/-(light|dark)$/, '');
|
|
210
|
+
return isDark ? `${base}-light` : `${base}-dark`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create/update the matching opposite-mode sibling of a Studio theme from the
|
|
215
|
+
* SAME recipe, so every Studio theme is a coherent light+dark pair (linked via
|
|
216
|
+
* pairId, sharing one family). The sibling is recipe-derived — it tracks the
|
|
217
|
+
* recipe, while per-token tweaks live on whichever variant you're editing.
|
|
218
|
+
*/
|
|
219
|
+
async function upsertSibling(recipe: ThemeRecipe, siblingId: string): Promise<void> {
|
|
220
|
+
const siblingDark = !draft.value.isDark;
|
|
221
|
+
const siblingRecipe: ThemeRecipe = { ...recipe, mode: siblingDark ? 'dark' : 'light' };
|
|
222
|
+
const gen = recipeToTokens(siblingRecipe);
|
|
223
|
+
const body = {
|
|
224
|
+
id: siblingId,
|
|
225
|
+
name: draft.value.name,
|
|
226
|
+
description: draft.value.description,
|
|
227
|
+
family: draft.value.family,
|
|
228
|
+
isDark: siblingDark,
|
|
229
|
+
pairId: draft.value.id,
|
|
230
|
+
parentTheme: gen.parentTheme,
|
|
231
|
+
tokens: gen.tokens,
|
|
232
|
+
recipe: siblingRecipe,
|
|
233
|
+
fonts: gen.fonts,
|
|
234
|
+
};
|
|
235
|
+
const put = $fetch as (url: string, opts: Record<string, unknown>) => Promise<unknown>;
|
|
236
|
+
if (themesApi.findCustom(siblingId)) {
|
|
237
|
+
await put(`/api/admin/themes/${siblingId}`, { method: 'PUT', body });
|
|
238
|
+
} else {
|
|
239
|
+
await $fetch('/api/admin/themes', { method: 'POST', body });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
207
243
|
/**
|
|
208
244
|
* Save the draft. If `apply` is true, ALSO set this theme as the
|
|
209
245
|
* instance default in the same await chain — must happen BEFORE the
|
|
210
246
|
* create-mode router.replace, otherwise the navigation could unmount
|
|
211
247
|
* the component mid-PUT and lose the apply.
|
|
248
|
+
*
|
|
249
|
+
* Studio (recipe-driven) themes are saved as a light+dark PAIR: the primary
|
|
250
|
+
* (this draft) plus its recipe-derived opposite-mode sibling, cross-linked
|
|
251
|
+
* via pairId in one family.
|
|
212
252
|
*/
|
|
213
253
|
async function save({ apply = false }: { apply?: boolean } = {}): Promise<void> {
|
|
214
254
|
saving.value = true;
|
|
215
255
|
error.value = null;
|
|
216
256
|
try {
|
|
257
|
+
// Pair bookkeeping: a Studio theme links to its opposite-mode sibling.
|
|
258
|
+
const recipe = draft.value.recipe;
|
|
259
|
+
const siblingId = recipe ? siblingIdFor(draft.value.id, draft.value.isDark) : null;
|
|
260
|
+
if (siblingId) draft.value.pairId = siblingId;
|
|
261
|
+
|
|
217
262
|
const payload = {
|
|
218
263
|
id: draft.value.id,
|
|
219
264
|
name: draft.value.name,
|
|
@@ -245,6 +290,16 @@ async function save({ apply = false }: { apply?: boolean } = {}): Promise<void>
|
|
|
245
290
|
savedId = draft.value.id;
|
|
246
291
|
}
|
|
247
292
|
|
|
293
|
+
// Create/update the matching opposite-mode sibling (recipe-driven pair).
|
|
294
|
+
// Soft-fail: the primary is already saved; a sibling hiccup shouldn't lose it.
|
|
295
|
+
if (recipe && siblingId) {
|
|
296
|
+
try {
|
|
297
|
+
await upsertSibling(recipe, siblingId);
|
|
298
|
+
} catch {
|
|
299
|
+
notify('Saved, but the matching light/dark variant could not sync', 'error');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
248
303
|
// Apply BEFORE refresh/navigation so the navigate doesn't unmount us
|
|
249
304
|
// mid-PUT (would lose the apply + the success toast).
|
|
250
305
|
if (apply) {
|
|
@@ -208,11 +208,15 @@ function createBlank(): void {
|
|
|
208
208
|
*/
|
|
209
209
|
function startFromRecipe(recipe: ThemeRecipe, opts: { id: string; name: string }): void {
|
|
210
210
|
const gen = recipeToTokens(recipe);
|
|
211
|
+
const id = nextAvailableId(opts.id);
|
|
211
212
|
const seed = {
|
|
212
|
-
id
|
|
213
|
+
id,
|
|
213
214
|
name: opts.name,
|
|
214
215
|
description: '',
|
|
215
|
-
family
|
|
216
|
+
// Unique family per theme (= the slug) so the picker keeps each Studio
|
|
217
|
+
// theme separate AND can group its light/dark pair together. (A shared
|
|
218
|
+
// 'custom' family would collapse every Studio theme into one card.)
|
|
219
|
+
family: id,
|
|
216
220
|
isDark: recipe.mode === 'dark',
|
|
217
221
|
parentTheme: gen.parentTheme,
|
|
218
222
|
tokens: gen.tokens,
|