@commonpub/layer 0.21.22 → 0.22.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.
@@ -0,0 +1,277 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * One row in the admin theme picker. Displays:
4
+ * • light + dark swatches (each clickable to pick that variant directly)
5
+ * • family name + description
6
+ * • active / current pill
7
+ * • action buttons: Edit (custom only), Duplicate, Export, Delete
8
+ *
9
+ * Stays presentational — all wiring (select / edit / duplicate / delete /
10
+ * export) is emitted up so the page owns the state machine.
11
+ */
12
+ import type { ThemeFamilyView } from '../../../types/theme';
13
+
14
+ defineProps<{
15
+ family: ThemeFamilyView;
16
+ active: boolean;
17
+ saving: boolean;
18
+ }>();
19
+
20
+ const emit = defineEmits<{
21
+ select: [themeId: string];
22
+ edit: [themeId: string];
23
+ duplicate: [themeId: string];
24
+ exportTheme: [themeId: string];
25
+ remove: [themeId: string];
26
+ }>();
27
+
28
+ function variantBoxStyle(v: { bg: string; surface: string; accent: string; text: string; border: string }) {
29
+ return {
30
+ backgroundColor: v.bg,
31
+ borderColor: v.border,
32
+ };
33
+ }
34
+
35
+ function variantInnerStyle(v: { bg: string; surface: string; accent: string; text: string; border: string }) {
36
+ return {
37
+ backgroundColor: v.surface,
38
+ borderColor: v.border,
39
+ boxShadow: `3px 3px 0 ${v.border}`,
40
+ };
41
+ }
42
+
43
+ function badge(family: ThemeFamilyView): { label: string; tone: 'builtin' | 'registered' | 'custom' } {
44
+ if (family.source === 'custom') return { label: 'Custom', tone: 'custom' };
45
+ if (family.source === 'registered') return { label: 'From code', tone: 'registered' };
46
+ return { label: 'Built-in', tone: 'builtin' };
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <article class="theme-family-card" :class="{ active }">
52
+ <div class="theme-family-previews">
53
+ <button
54
+ v-if="family.light"
55
+ type="button"
56
+ class="theme-family-preview"
57
+ :style="variantBoxStyle(family.preview.light)"
58
+ :disabled="saving"
59
+ :aria-label="`Select ${family.name} light`"
60
+ @click="emit('select', family.light.id)"
61
+ >
62
+ <div class="theme-family-preview-card" :style="variantInnerStyle(family.preview.light)">
63
+ <div class="theme-family-preview-heading" :style="{ backgroundColor: family.preview.light.text, opacity: 0.85 }" />
64
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.light.text, opacity: 0.35 }" />
65
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.light.text, opacity: 0.35, width: '60%' }" />
66
+ <div class="theme-family-preview-accent" :style="{ backgroundColor: family.preview.light.accent }" />
67
+ </div>
68
+ <span class="theme-family-mode-label">Light</span>
69
+ </button>
70
+
71
+ <button
72
+ v-if="family.dark"
73
+ type="button"
74
+ class="theme-family-preview theme-family-preview-dark"
75
+ :style="variantBoxStyle(family.preview.dark)"
76
+ :disabled="saving"
77
+ :aria-label="`Select ${family.name} dark`"
78
+ @click="emit('select', family.dark.id)"
79
+ >
80
+ <div class="theme-family-preview-card" :style="variantInnerStyle(family.preview.dark)">
81
+ <div class="theme-family-preview-heading" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.85 }" />
82
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.35 }" />
83
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.35, width: '60%' }" />
84
+ <div class="theme-family-preview-accent" :style="{ backgroundColor: family.preview.dark.accent }" />
85
+ </div>
86
+ <span class="theme-family-mode-label">Dark</span>
87
+ </button>
88
+ </div>
89
+
90
+ <div class="theme-family-meta">
91
+ <div class="theme-family-meta-head">
92
+ <h3 class="theme-family-name">{{ family.name }}</h3>
93
+ <span class="theme-family-tag" :class="`tag-${badge(family).tone}`">{{ badge(family).label }}</span>
94
+ <span v-if="active" class="theme-family-active">
95
+ <i class="fa-solid fa-check" aria-hidden="true" /> Active
96
+ </span>
97
+ </div>
98
+ <p class="theme-family-desc">{{ family.description }}</p>
99
+
100
+ <div class="theme-family-actions">
101
+ <button
102
+ v-if="family.source === 'custom'"
103
+ type="button"
104
+ class="cpub-btn cpub-btn-sm"
105
+ @click="emit('edit', family.light?.id ?? family.dark!.id)"
106
+ >
107
+ <i class="fa-solid fa-pen-to-square" aria-hidden="true" /> Edit
108
+ </button>
109
+ <button
110
+ type="button"
111
+ class="cpub-btn cpub-btn-sm"
112
+ :title="family.source === 'custom' ? 'Duplicate this theme' : 'Fork to a new editable custom theme'"
113
+ @click="emit('duplicate', family.light?.id ?? family.dark!.id)"
114
+ >
115
+ <i class="fa-solid fa-copy" aria-hidden="true" /> {{ family.source === 'custom' ? 'Duplicate' : 'Fork' }}
116
+ </button>
117
+ <button
118
+ v-if="family.source === 'custom'"
119
+ type="button"
120
+ class="cpub-btn cpub-btn-sm"
121
+ @click="emit('exportTheme', family.light?.id ?? family.dark!.id)"
122
+ >
123
+ <i class="fa-solid fa-file-export" aria-hidden="true" /> Export
124
+ </button>
125
+ <button
126
+ v-if="family.source === 'custom'"
127
+ type="button"
128
+ class="cpub-btn cpub-btn-sm theme-family-action-danger"
129
+ @click="emit('remove', family.light?.id ?? family.dark!.id)"
130
+ >
131
+ <i class="fa-solid fa-trash" aria-hidden="true" /> Delete
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </article>
136
+ </template>
137
+
138
+ <style scoped>
139
+ .theme-family-card {
140
+ display: grid;
141
+ grid-template-columns: auto 1fr;
142
+ border: var(--border-width-default) solid var(--border2);
143
+ background: var(--surface);
144
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
145
+ }
146
+
147
+ .theme-family-card.active {
148
+ border-color: var(--accent);
149
+ box-shadow: var(--shadow-accent);
150
+ }
151
+
152
+ .theme-family-card:hover { border-color: var(--border); }
153
+
154
+ .theme-family-previews {
155
+ display: flex;
156
+ border-right: var(--border-width-default) solid var(--border2);
157
+ }
158
+
159
+ .theme-family-preview {
160
+ position: relative;
161
+ width: 130px;
162
+ height: 110px;
163
+ padding: var(--space-3);
164
+ display: flex;
165
+ flex-direction: column;
166
+ align-items: center;
167
+ justify-content: center;
168
+ border: 0;
169
+ border-right: var(--border-width-default) solid var(--border2);
170
+ cursor: pointer;
171
+ border-radius: 0;
172
+ }
173
+
174
+ .theme-family-preview:last-child { border-right: 0; }
175
+ .theme-family-preview:hover { filter: brightness(1.05); }
176
+ .theme-family-preview:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
177
+ .theme-family-preview:disabled { cursor: wait; opacity: 0.6; }
178
+
179
+ .theme-family-preview-card {
180
+ width: 80%;
181
+ padding: var(--space-2) var(--space-3);
182
+ border-width: 2px;
183
+ border-style: solid;
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 4px;
187
+ border-radius: 0;
188
+ }
189
+
190
+ .theme-family-preview-heading { height: 5px; width: 55%; border-radius: 0; }
191
+ .theme-family-preview-text { height: 3px; width: 85%; border-radius: 0; }
192
+ .theme-family-preview-accent { height: 10px; width: 40%; margin-top: 4px; border-radius: 0; }
193
+
194
+ .theme-family-mode-label {
195
+ position: absolute;
196
+ bottom: 4px;
197
+ right: 6px;
198
+ font-family: var(--font-mono);
199
+ font-size: 9px;
200
+ letter-spacing: var(--tracking-wide);
201
+ text-transform: uppercase;
202
+ color: var(--text-faint);
203
+ opacity: 0.7;
204
+ }
205
+
206
+ .theme-family-meta {
207
+ padding: var(--space-4);
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: var(--space-2);
211
+ min-width: 0;
212
+ }
213
+
214
+ .theme-family-meta-head {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: var(--space-2);
218
+ flex-wrap: wrap;
219
+ }
220
+
221
+ .theme-family-name {
222
+ font-size: var(--text-md);
223
+ font-weight: var(--font-weight-bold);
224
+ margin: 0;
225
+ }
226
+
227
+ .theme-family-tag {
228
+ font-family: var(--font-mono);
229
+ font-size: 10px;
230
+ letter-spacing: var(--tracking-wide);
231
+ text-transform: uppercase;
232
+ padding: 2px 6px;
233
+ border: var(--border-width-thin) solid var(--border2);
234
+ color: var(--text-dim);
235
+ }
236
+ .tag-custom { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
237
+ .tag-registered { color: var(--purple); border-color: var(--purple-border); background: var(--purple-bg); }
238
+
239
+ .theme-family-active {
240
+ margin-left: auto;
241
+ display: inline-flex;
242
+ align-items: center;
243
+ gap: 4px;
244
+ font-family: var(--font-mono);
245
+ font-size: 10px;
246
+ letter-spacing: var(--tracking-wide);
247
+ text-transform: uppercase;
248
+ color: var(--accent);
249
+ font-weight: var(--font-weight-semibold);
250
+ }
251
+
252
+ .theme-family-desc {
253
+ font-size: var(--text-sm);
254
+ color: var(--text-dim);
255
+ margin: 0;
256
+ line-height: var(--leading-snug);
257
+ }
258
+
259
+ .theme-family-actions {
260
+ display: flex;
261
+ gap: var(--space-2);
262
+ flex-wrap: wrap;
263
+ margin-top: auto;
264
+ }
265
+
266
+ .theme-family-action-danger {
267
+ color: var(--red);
268
+ border-color: var(--red-border);
269
+ }
270
+ .theme-family-action-danger:hover { background: var(--red-bg); }
271
+
272
+ @media (max-width: 640px) {
273
+ .theme-family-card { grid-template-columns: 1fr; }
274
+ .theme-family-previews { border-right: 0; border-bottom: var(--border-width-default) solid var(--border2); }
275
+ .theme-family-preview { flex: 1; }
276
+ }
277
+ </style>
@@ -0,0 +1,222 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Legacy "token overrides" panel — applies on top of whichever theme is
4
+ * active. Useful for one-off tweaks ("flip the accent for our anniversary
5
+ * week") without authoring a full custom theme. For real edits, the
6
+ * editor at `/admin/theme/edit/[id]` is the better tool.
7
+ *
8
+ * Self-contained: takes the initial map from a prop, owns the in-progress
9
+ * draft, emits `save` with the final map. The parent persists via
10
+ * `/api/admin/settings` and decides when to refresh.
11
+ */
12
+ import { computed, ref, watch } from 'vue';
13
+
14
+ const props = defineProps<{
15
+ /** Initial map from `instance_settings.theme.token_overrides`. */
16
+ initial: Record<string, string>;
17
+ /** Disables save buttons while a parent-driven request is in flight. */
18
+ saving: boolean;
19
+ }>();
20
+
21
+ const emit = defineEmits<{
22
+ save: [overrides: Record<string, string>];
23
+ }>();
24
+
25
+ const draft = ref<Record<string, string>>({ ...props.initial });
26
+ const newKey = ref('');
27
+ const newValue = ref('');
28
+
29
+ watch(() => props.initial, (next) => {
30
+ // Reset draft when the parent reloads settings (e.g. after a save round-trip)
31
+ draft.value = { ...next };
32
+ });
33
+
34
+ const count = computed(() => Object.keys(draft.value).length);
35
+ const dirty = computed(() => JSON.stringify(draft.value) !== JSON.stringify(props.initial));
36
+
37
+ function addOverride(): void {
38
+ const k = newKey.value.trim().replace(/^--/, '');
39
+ const v = newValue.value.trim();
40
+ if (!k || !v) return;
41
+ draft.value = { ...draft.value, [k]: v };
42
+ newKey.value = '';
43
+ newValue.value = '';
44
+ }
45
+
46
+ function removeOverride(key: string): void {
47
+ const next = { ...draft.value };
48
+ delete next[key];
49
+ draft.value = next;
50
+ }
51
+
52
+ function looksLikeColor(value: string): boolean {
53
+ return value.startsWith('#') || value.startsWith('rgb');
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <details class="admin-theme-overrides">
59
+ <summary class="admin-theme-overrides-summary">
60
+ <i class="fa-solid fa-sliders" aria-hidden="true" />
61
+ <span>Token overrides</span>
62
+ <span v-if="count > 0" class="admin-theme-overrides-count">{{ count }} active</span>
63
+ </summary>
64
+ <div class="admin-theme-overrides-body">
65
+ <p class="admin-theme-overrides-desc">
66
+ Ad-hoc overrides applied on top of whichever theme is active. Useful for
67
+ one-off tweaks without authoring a full custom theme. For real edits,
68
+ create or fork a theme above.
69
+ </p>
70
+
71
+ <div v-if="count > 0" class="admin-overrides-list">
72
+ <div v-for="(value, key) in draft" :key="key" class="admin-override-row">
73
+ <code class="admin-override-key">--{{ key }}</code>
74
+ <span class="admin-override-value">
75
+ <span
76
+ v-if="looksLikeColor(String(value))"
77
+ class="admin-override-swatch"
78
+ :style="{ backgroundColor: String(value) }"
79
+ />
80
+ {{ value }}
81
+ </span>
82
+ <button
83
+ class="cpub-btn cpub-btn-sm admin-override-remove"
84
+ :aria-label="`Remove override for ${key}`"
85
+ @click="removeOverride(key as string)"
86
+ >
87
+ <i class="fa-solid fa-xmark" aria-hidden="true" />
88
+ </button>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="admin-override-add">
93
+ <input v-model="newKey" class="admin-override-input" placeholder="Token name (e.g. accent)" @keyup.enter="addOverride" />
94
+ <input v-model="newValue" class="admin-override-input" placeholder="Value (e.g. #ff6600)" @keyup.enter="addOverride" />
95
+ <button class="cpub-btn cpub-btn-sm" :disabled="!newKey.trim() || !newValue.trim()" @click="addOverride">Add</button>
96
+ </div>
97
+
98
+ <div class="admin-override-actions">
99
+ <button class="cpub-btn cpub-btn-primary" :disabled="saving || !dirty" @click="emit('save', draft)">
100
+ <i class="fa-solid fa-floppy-disk" aria-hidden="true" /> Save overrides
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </details>
105
+ </template>
106
+
107
+ <style scoped>
108
+ .admin-theme-overrides {
109
+ margin-top: var(--space-6);
110
+ border-top: var(--border-width-default) solid var(--border);
111
+ padding-top: var(--space-4);
112
+ }
113
+
114
+ .admin-theme-overrides-summary {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--space-2);
118
+ font-size: var(--text-md);
119
+ font-weight: var(--font-weight-semibold);
120
+ color: var(--text);
121
+ cursor: pointer;
122
+ padding: var(--space-2);
123
+ list-style: none;
124
+ }
125
+ .admin-theme-overrides-summary::-webkit-details-marker { display: none; }
126
+ .admin-theme-overrides-summary::before {
127
+ content: '\f054'; /* fa-chevron-right */
128
+ font-family: 'Font Awesome 6 Free';
129
+ font-weight: 900;
130
+ font-size: 12px;
131
+ color: var(--text-dim);
132
+ transition: transform var(--transition-fast);
133
+ }
134
+ [open] > .admin-theme-overrides-summary::before { transform: rotate(90deg); }
135
+
136
+ .admin-theme-overrides-count {
137
+ font-family: var(--font-mono);
138
+ font-size: 10px;
139
+ letter-spacing: var(--tracking-wide);
140
+ text-transform: uppercase;
141
+ padding: 1px 6px;
142
+ background: var(--accent-bg);
143
+ color: var(--accent);
144
+ border: var(--border-width-thin) solid var(--accent-border);
145
+ }
146
+
147
+ .admin-theme-overrides-body { padding: var(--space-3) 0; }
148
+ .admin-theme-overrides-desc {
149
+ font-size: var(--text-sm);
150
+ color: var(--text-dim);
151
+ margin: 0 0 var(--space-3);
152
+ max-width: 560px;
153
+ line-height: var(--leading-snug);
154
+ }
155
+
156
+ .admin-overrides-list {
157
+ border: var(--border-width-default) solid var(--border);
158
+ background: var(--surface);
159
+ margin-bottom: var(--space-3);
160
+ }
161
+ .admin-override-row {
162
+ display: flex;
163
+ align-items: center;
164
+ padding: var(--space-2) var(--space-3);
165
+ border-bottom: var(--border-width-thin) solid var(--border2);
166
+ gap: var(--space-3);
167
+ }
168
+ .admin-override-row:last-child { border-bottom: 0; }
169
+ .admin-override-key {
170
+ font-family: var(--font-mono);
171
+ font-size: var(--text-sm);
172
+ color: var(--accent);
173
+ flex-shrink: 0;
174
+ }
175
+ .admin-override-value {
176
+ font-family: var(--font-mono);
177
+ font-size: var(--text-sm);
178
+ color: var(--text-dim);
179
+ display: flex;
180
+ align-items: center;
181
+ gap: var(--space-2);
182
+ flex: 1;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+ .admin-override-swatch {
189
+ display: inline-block;
190
+ width: 14px;
191
+ height: 14px;
192
+ border: var(--border-width-thin) solid var(--border2);
193
+ flex-shrink: 0;
194
+ }
195
+ .admin-override-remove { flex-shrink: 0; padding: var(--space-1); color: var(--text-faint); }
196
+ .admin-override-remove:hover { color: var(--red); }
197
+
198
+ .admin-override-add {
199
+ display: flex;
200
+ gap: var(--space-2);
201
+ padding: var(--space-3);
202
+ border: 2px dashed var(--border2);
203
+ background: var(--surface);
204
+ margin-bottom: var(--space-3);
205
+ }
206
+ .admin-override-input {
207
+ font-size: var(--text-sm);
208
+ padding: var(--space-1) var(--space-2);
209
+ border: var(--border-width-default) solid var(--border);
210
+ background: var(--surface2);
211
+ color: var(--text);
212
+ font-family: var(--font-mono);
213
+ flex: 1;
214
+ min-width: 0;
215
+ }
216
+
217
+ .admin-override-actions { margin-top: var(--space-3); }
218
+
219
+ @media (max-width: 640px) {
220
+ .admin-override-add { flex-direction: column; }
221
+ }
222
+ </style>
@@ -0,0 +1,187 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Live preview pane for the theme editor. Hosts the scene picker, the
4
+ * light/dark mode toggle, and the scrollable scene surface that renders
5
+ * the in-progress theme tokens applied to representative components.
6
+ *
7
+ * The scene system is pluggable: each scene is a Vue component rendered
8
+ * inside the token-scoped wrapper. Add scenes by registering them in
9
+ * PREVIEW_SCENES and dropping an `AdminThemeScene*.vue` in this directory.
10
+ *
11
+ * Future scenes the architecture is built to absorb:
12
+ * - 'iframe-route' — render an actual site route with the in-progress theme
13
+ * - 'page-layout' — full landing-page mockup with editable section list
14
+ * - 'layout-builder' — drag-and-drop section composer
15
+ */
16
+ import { computed, ref } from 'vue';
17
+
18
+ const props = defineProps<{
19
+ tokens: Record<string, string>;
20
+ /** The base theme whose CSS file provides inherited defaults (via data-theme). */
21
+ parentTheme: string;
22
+ isDark: boolean;
23
+ }>();
24
+
25
+ interface SceneOption {
26
+ id: 'gallery' | 'prose' | 'admin';
27
+ label: string;
28
+ description: string;
29
+ icon: string;
30
+ }
31
+
32
+ const PREVIEW_SCENES: SceneOption[] = [
33
+ { id: 'gallery', label: 'Components', description: 'Buttons, cards, forms, badges, prose, code', icon: 'fa-th-large' },
34
+ { id: 'prose', label: 'Article', description: 'Headings, paragraphs, quote, code block, list', icon: 'fa-file-lines' },
35
+ { id: 'admin', label: 'Admin shell', description: 'Topbar, sidebar, table, stat cards', icon: 'fa-gauge' },
36
+ ];
37
+
38
+ const activeScene = ref<SceneOption['id']>('gallery');
39
+ const previewMode = ref<'light' | 'dark'>(props.isDark ? 'dark' : 'light');
40
+
41
+ /**
42
+ * Build the inline style string scoped to the preview surface. We apply
43
+ * tokens to the wrapper element only — that scopes the in-progress theme
44
+ * to the preview and avoids leaking it into the surrounding admin UI.
45
+ */
46
+ const previewStyle = computed(() => {
47
+ const lines: string[] = [];
48
+ for (const [k, v] of Object.entries(props.tokens)) {
49
+ if (typeof v !== 'string') continue;
50
+ const safeKey = k.replace(/[^a-zA-Z0-9_-]/g, '');
51
+ const safeVal = v.replace(/[\r\n;]/g, ' ');
52
+ if (!safeKey) continue;
53
+ lines.push(`--${safeKey}: ${safeVal}`);
54
+ }
55
+ return lines.join('; ');
56
+ });
57
+ </script>
58
+
59
+ <template>
60
+ <div class="theme-preview-pane">
61
+ <header class="theme-preview-header">
62
+ <div class="theme-preview-scene-picker" role="tablist" aria-label="Preview scene">
63
+ <button
64
+ v-for="scene in PREVIEW_SCENES"
65
+ :key="scene.id"
66
+ type="button"
67
+ role="tab"
68
+ :aria-selected="activeScene === scene.id"
69
+ class="theme-preview-scene-tab"
70
+ :class="{ active: activeScene === scene.id }"
71
+ :title="scene.description"
72
+ @click="activeScene = scene.id"
73
+ >
74
+ <i :class="['fa-solid', scene.icon]" aria-hidden="true" />
75
+ <span>{{ scene.label }}</span>
76
+ </button>
77
+ </div>
78
+
79
+ <div class="theme-preview-mode-toggle" role="radiogroup" aria-label="Preview mode">
80
+ <button
81
+ type="button"
82
+ role="radio"
83
+ :aria-checked="previewMode === 'light'"
84
+ class="theme-preview-mode-btn"
85
+ :class="{ active: previewMode === 'light' }"
86
+ @click="previewMode = 'light'"
87
+ >
88
+ <i class="fa-solid fa-sun" aria-hidden="true" /> Light
89
+ </button>
90
+ <button
91
+ type="button"
92
+ role="radio"
93
+ :aria-checked="previewMode === 'dark'"
94
+ class="theme-preview-mode-btn"
95
+ :class="{ active: previewMode === 'dark' }"
96
+ @click="previewMode = 'dark'"
97
+ >
98
+ <i class="fa-solid fa-moon" aria-hidden="true" /> Dark
99
+ </button>
100
+ </div>
101
+ </header>
102
+
103
+ <div
104
+ class="theme-preview-surface"
105
+ :data-theme="parentTheme"
106
+ :style="previewStyle"
107
+ :data-preview-mode="previewMode"
108
+ >
109
+ <AdminThemeSceneGallery v-if="activeScene === 'gallery'" />
110
+ <AdminThemeSceneProse v-else-if="activeScene === 'prose'" />
111
+ <AdminThemeSceneAdmin v-else-if="activeScene === 'admin'" />
112
+ </div>
113
+ </div>
114
+ </template>
115
+
116
+ <style scoped>
117
+ .theme-preview-pane {
118
+ display: flex;
119
+ flex-direction: column;
120
+ height: 100%;
121
+ min-height: 0;
122
+ background: var(--surface2);
123
+ border-left: var(--border-width-default) solid var(--border);
124
+ }
125
+
126
+ .theme-preview-header {
127
+ display: flex;
128
+ justify-content: space-between;
129
+ align-items: center;
130
+ padding: var(--space-2) var(--space-3);
131
+ background: var(--surface);
132
+ border-bottom: var(--border-width-default) solid var(--border);
133
+ flex-wrap: wrap;
134
+ gap: var(--space-2);
135
+ }
136
+
137
+ .theme-preview-scene-picker,
138
+ .theme-preview-mode-toggle {
139
+ display: flex;
140
+ gap: 2px;
141
+ background: var(--surface2);
142
+ padding: 2px;
143
+ border: var(--border-width-thin) solid var(--border2);
144
+ }
145
+
146
+ .theme-preview-scene-tab,
147
+ .theme-preview-mode-btn {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ gap: 6px;
151
+ padding: 6px 10px;
152
+ background: none;
153
+ border: 0;
154
+ font-family: var(--font-mono);
155
+ font-size: 11px;
156
+ letter-spacing: var(--tracking-wide);
157
+ text-transform: uppercase;
158
+ color: var(--text-dim);
159
+ cursor: pointer;
160
+ border-radius: 0;
161
+ }
162
+ .theme-preview-scene-tab:hover,
163
+ .theme-preview-mode-btn:hover { color: var(--text); }
164
+ .theme-preview-scene-tab.active {
165
+ background: var(--surface);
166
+ color: var(--text);
167
+ box-shadow: inset 0 -2px 0 var(--accent);
168
+ }
169
+ .theme-preview-mode-btn.active {
170
+ background: var(--surface);
171
+ color: var(--accent);
172
+ }
173
+ .theme-preview-scene-tab i { font-size: 10px; }
174
+
175
+ .theme-preview-surface {
176
+ flex: 1;
177
+ overflow: auto;
178
+ padding: var(--space-4);
179
+ background-color: var(--bg);
180
+ color: var(--text);
181
+ font-family: var(--font-body);
182
+ /* Token re-application happens via inline style on this element. The
183
+ `data-theme` attr seeds inherited defaults; inline style overrides
184
+ each token the editor has changed. */
185
+ min-height: 0;
186
+ }
187
+ </style>