@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
- Finish to drop into the advanced editor with every token populated. You can re-open
335
- Studio any time, or fine-tune individual tokens by hand.
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.65.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/learning": "0.5.2",
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/editor": "0.7.11"
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: nextAvailableId(opts.id),
213
+ id,
213
214
  name: opts.name,
214
215
  description: '',
215
- family: 'custom',
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,