@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', family.light?.id ?? family.dark!.id)"
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', family.light?.id ?? family.dark!.id)"
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', family.light?.id ?? family.dark!.id)"
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', family.light?.id ?? family.dark!.id)"
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 previewMode = ref<'light' | 'dark'>(props.isDark ? 'dark' : 'light');
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.0",
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/ui": "0.12.0",
66
- "@commonpub/theme-studio": "0.3.0"
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="draft.tokens"
594
- :parent-theme="draft.parentTheme"
595
- :is-dark="draft.isDark"
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
- if (!confirm(`Delete custom theme "${customId}"? This cannot be undone.`)) return;
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
- const res = await $fetch<{ ok: true; resetDefault: boolean }>(`/api/admin/themes/${customId}`, {
153
- method: 'DELETE',
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(res.resetDefault ? 'Theme deleted, default reset to Classic' : 'Theme deleted', 'success');
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 */