@commonpub/layer 0.21.22 → 0.22.1
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.
- package/components/admin/theme/AdminThemeFamilyCard.vue +277 -0
- package/components/admin/theme/AdminThemeOverridesPanel.vue +222 -0
- package/components/admin/theme/AdminThemePreviewPane.vue +218 -0
- package/components/admin/theme/AdminThemeSceneAdmin.vue +189 -0
- package/components/admin/theme/AdminThemeSceneGallery.vue +353 -0
- package/components/admin/theme/AdminThemeSceneProse.vue +140 -0
- package/components/admin/theme/AdminThemeTokenGroup.vue +98 -0
- package/components/admin/theme/AdminThemeTokenInput.vue +278 -0
- package/composables/useTheme.ts +24 -14
- package/composables/useThemeAdmin.ts +167 -0
- package/package.json +7 -7
- package/pages/admin/theme/edit/[id].vue +595 -0
- package/pages/admin/theme/index.vue +449 -0
- package/plugins/theme.ts +25 -7
- package/server/api/admin/themes/[id].delete.ts +40 -0
- package/server/api/admin/themes/[id].get.ts +20 -0
- package/server/api/admin/themes/[id].put.ts +45 -0
- package/server/api/admin/themes/discover.get.ts +22 -0
- package/server/api/admin/themes/index.get.ts +40 -0
- package/server/api/admin/themes/index.post.ts +46 -0
- package/server/api/profile/theme.put.ts +2 -1
- package/server/middleware/theme.ts +23 -9
- package/server/utils/instanceTheme.ts +145 -25
- package/types/theme.ts +54 -0
- package/utils/themeDiscovery.ts +67 -0
- package/utils/themeIO.ts +79 -0
- package/utils/themeIds.ts +25 -0
- package/pages/admin/theme.vue +0 -502
package/pages/admin/theme.vue
DELETED
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { BUILT_IN_THEMES } from '@commonpub/ui';
|
|
3
|
-
|
|
4
|
-
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
5
|
-
useSeoMeta({ title: `Theme — Admin — ${useSiteName()}` });
|
|
6
|
-
|
|
7
|
-
const { data: settings, pending, refresh } = await useFetch<Record<string, unknown>>('/api/admin/settings');
|
|
8
|
-
|
|
9
|
-
const saving = ref(false);
|
|
10
|
-
const saveSuccess = ref(false);
|
|
11
|
-
|
|
12
|
-
const instanceDefault = computed(() => {
|
|
13
|
-
const val = settings.value?.['theme.default'];
|
|
14
|
-
return typeof val === 'string' ? val : 'base';
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// Build families from BUILT_IN_THEMES
|
|
18
|
-
interface ThemeFamily {
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
description: string;
|
|
22
|
-
light: { id: string; name: string } | null;
|
|
23
|
-
dark: { id: string; name: string } | null;
|
|
24
|
-
preview: {
|
|
25
|
-
light: { bg: string; surface: string; accent: string; text: string; border: string };
|
|
26
|
-
dark: { bg: string; surface: string; accent: string; text: string; border: string };
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const PREVIEW_COLORS: Record<string, { bg: string; surface: string; accent: string; text: string; border: string }> = {
|
|
31
|
-
base: { bg: '#fafaf9', surface: '#ffffff', accent: '#5b9cf6', text: '#1a1a1a', border: '#1a1a1a' },
|
|
32
|
-
dark: { bg: '#111111', surface: '#1a1a1a', accent: '#5b9cf6', text: '#e5e5e3', border: '#444440' },
|
|
33
|
-
generics: { bg: '#0c0c0b', surface: '#141413', accent: '#5b9cf6', text: '#d8d5cf', border: '#272725' },
|
|
34
|
-
agora: { bg: '#f7f4ed', surface: '#faf8f3', accent: '#3d8b5e', text: '#1a1a1a', border: '#1a1a1a' },
|
|
35
|
-
'agora-dark': { bg: '#0d1a12', surface: '#141f17', accent: '#4aa06e', text: '#e8e8e2', border: '#3a4f40' },
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const FAMILY_META: Record<string, { name: string; description: string }> = {
|
|
39
|
-
classic: { name: 'Classic', description: 'Sharp corners, offset shadows, blue accent — the original CommonPub look' },
|
|
40
|
-
agora: { name: 'Agora', description: 'Warm parchment tones, green accent, Fraunces serif — institutional warmth' },
|
|
41
|
-
generics: { name: 'Generics', description: 'Minimal dark aesthetic with soft glow shadows' },
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const families = computed<ThemeFamily[]>(() => {
|
|
45
|
-
const map = new Map<string, ThemeFamily>();
|
|
46
|
-
|
|
47
|
-
for (const theme of BUILT_IN_THEMES) {
|
|
48
|
-
if (!map.has(theme.family)) {
|
|
49
|
-
const meta = FAMILY_META[theme.family] ?? { name: theme.family, description: '' };
|
|
50
|
-
map.set(theme.family, {
|
|
51
|
-
id: theme.family,
|
|
52
|
-
name: meta.name,
|
|
53
|
-
description: meta.description,
|
|
54
|
-
light: null,
|
|
55
|
-
dark: null,
|
|
56
|
-
preview: {
|
|
57
|
-
light: PREVIEW_COLORS.base!,
|
|
58
|
-
dark: PREVIEW_COLORS.dark!,
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
const fam = map.get(theme.family)!;
|
|
63
|
-
if (theme.isDark) {
|
|
64
|
-
fam.dark = { id: theme.id, name: theme.name };
|
|
65
|
-
fam.preview.dark = PREVIEW_COLORS[theme.id] ?? PREVIEW_COLORS.dark!;
|
|
66
|
-
} else {
|
|
67
|
-
fam.light = { id: theme.id, name: theme.name };
|
|
68
|
-
fam.preview.light = PREVIEW_COLORS[theme.id] ?? PREVIEW_COLORS.base!;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return [...map.values()];
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
/** Which family is currently active? */
|
|
76
|
-
const THEME_TO_FAMILY: Record<string, string> = {
|
|
77
|
-
base: 'classic', dark: 'classic', generics: 'generics',
|
|
78
|
-
agora: 'agora', 'agora-dark': 'agora',
|
|
79
|
-
};
|
|
80
|
-
const activeFamily = computed(() => THEME_TO_FAMILY[instanceDefault.value] ?? 'classic');
|
|
81
|
-
|
|
82
|
-
async function selectFamily(family: ThemeFamily): Promise<void> {
|
|
83
|
-
// Set the light variant as default (users toggle dark mode themselves)
|
|
84
|
-
const themeId = family.light?.id ?? family.dark?.id ?? 'base';
|
|
85
|
-
saving.value = true;
|
|
86
|
-
saveSuccess.value = false;
|
|
87
|
-
try {
|
|
88
|
-
await $fetch('/api/admin/settings', {
|
|
89
|
-
method: 'PUT',
|
|
90
|
-
body: { key: 'theme.default', value: themeId },
|
|
91
|
-
});
|
|
92
|
-
await refresh();
|
|
93
|
-
saveSuccess.value = true;
|
|
94
|
-
setTimeout(() => { saveSuccess.value = false; }, 2000);
|
|
95
|
-
} finally {
|
|
96
|
-
saving.value = false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Token overrides
|
|
101
|
-
const tokenOverrides = ref<Record<string, string>>({});
|
|
102
|
-
const newTokenKey = ref('');
|
|
103
|
-
const newTokenValue = ref('');
|
|
104
|
-
|
|
105
|
-
watchEffect(() => {
|
|
106
|
-
const raw = settings.value?.['theme.token_overrides'];
|
|
107
|
-
if (raw && typeof raw === 'object' && raw !== null) {
|
|
108
|
-
tokenOverrides.value = { ...(raw as Record<string, string>) };
|
|
109
|
-
} else {
|
|
110
|
-
tokenOverrides.value = {};
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
async function saveTokenOverrides(): Promise<void> {
|
|
115
|
-
saving.value = true;
|
|
116
|
-
saveSuccess.value = false;
|
|
117
|
-
try {
|
|
118
|
-
await $fetch('/api/admin/settings', {
|
|
119
|
-
method: 'PUT',
|
|
120
|
-
body: { key: 'theme.token_overrides', value: tokenOverrides.value },
|
|
121
|
-
});
|
|
122
|
-
await refresh();
|
|
123
|
-
saveSuccess.value = true;
|
|
124
|
-
setTimeout(() => { saveSuccess.value = false; }, 2000);
|
|
125
|
-
} finally {
|
|
126
|
-
saving.value = false;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function addTokenOverride(): void {
|
|
131
|
-
const key = newTokenKey.value.trim();
|
|
132
|
-
const value = newTokenValue.value.trim();
|
|
133
|
-
if (!key || !value) return;
|
|
134
|
-
tokenOverrides.value[key] = value;
|
|
135
|
-
newTokenKey.value = '';
|
|
136
|
-
newTokenValue.value = '';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function removeTokenOverride(key: string): void {
|
|
140
|
-
const next = { ...tokenOverrides.value };
|
|
141
|
-
delete next[key];
|
|
142
|
-
tokenOverrides.value = next;
|
|
143
|
-
}
|
|
144
|
-
</script>
|
|
145
|
-
|
|
146
|
-
<template>
|
|
147
|
-
<div class="admin-theme">
|
|
148
|
-
<div class="admin-theme-header">
|
|
149
|
-
<h1 class="admin-page-title">Theme</h1>
|
|
150
|
-
<p class="admin-page-desc">
|
|
151
|
-
Set the instance theme. This applies to all users. Individual users can toggle between light and dark mode.
|
|
152
|
-
</p>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<div v-if="saveSuccess" class="admin-theme-toast">
|
|
156
|
-
<i class="fa-solid fa-check"></i> Saved
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<p v-if="pending" class="admin-empty"><i class="fa-solid fa-circle-notch fa-spin"></i> Loading theme settings...</p>
|
|
160
|
-
|
|
161
|
-
<!-- Theme Families -->
|
|
162
|
-
<section v-else class="admin-theme-families">
|
|
163
|
-
<div v-for="family in families" :key="family.id" class="admin-family-card" :class="{ active: activeFamily === family.id }" >
|
|
164
|
-
<button
|
|
165
|
-
class="admin-family-select"
|
|
166
|
-
:disabled="saving"
|
|
167
|
-
@click="selectFamily(family)"
|
|
168
|
-
>
|
|
169
|
-
<div class="admin-family-previews">
|
|
170
|
-
<!-- Light preview -->
|
|
171
|
-
<div
|
|
172
|
-
v-if="family.light"
|
|
173
|
-
class="admin-family-preview"
|
|
174
|
-
:style="{ backgroundColor: family.preview.light.bg, borderColor: family.preview.light.border }"
|
|
175
|
-
>
|
|
176
|
-
<div
|
|
177
|
-
class="admin-preview-card"
|
|
178
|
-
:style="{
|
|
179
|
-
backgroundColor: family.preview.light.surface,
|
|
180
|
-
borderColor: family.preview.light.border,
|
|
181
|
-
boxShadow: `3px 3px 0 ${family.preview.light.border}`,
|
|
182
|
-
}"
|
|
183
|
-
>
|
|
184
|
-
<div class="admin-preview-heading" :style="{ backgroundColor: family.preview.light.text, opacity: 0.8 }"></div>
|
|
185
|
-
<div class="admin-preview-text" :style="{ backgroundColor: family.preview.light.text, opacity: 0.3 }"></div>
|
|
186
|
-
<div class="admin-preview-accent" :style="{ backgroundColor: family.preview.light.accent }"></div>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
<!-- Dark preview -->
|
|
190
|
-
<div
|
|
191
|
-
v-if="family.dark"
|
|
192
|
-
class="admin-family-preview"
|
|
193
|
-
:style="{ backgroundColor: family.preview.dark.bg, borderColor: family.preview.dark.border }"
|
|
194
|
-
>
|
|
195
|
-
<div
|
|
196
|
-
class="admin-preview-card"
|
|
197
|
-
:style="{
|
|
198
|
-
backgroundColor: family.preview.dark.surface,
|
|
199
|
-
borderColor: family.preview.dark.border,
|
|
200
|
-
boxShadow: `3px 3px 0 ${family.preview.dark.border}`,
|
|
201
|
-
}"
|
|
202
|
-
>
|
|
203
|
-
<div class="admin-preview-heading" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.8 }"></div>
|
|
204
|
-
<div class="admin-preview-text" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.3 }"></div>
|
|
205
|
-
<div class="admin-preview-accent" :style="{ backgroundColor: family.preview.dark.accent }"></div>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<div class="admin-family-meta">
|
|
211
|
-
<span class="admin-family-name">{{ family.name }}</span>
|
|
212
|
-
<span class="admin-family-desc">{{ family.description }}</span>
|
|
213
|
-
<span v-if="activeFamily === family.id" class="admin-family-active">
|
|
214
|
-
<i class="fa-solid fa-check"></i> Active
|
|
215
|
-
</span>
|
|
216
|
-
</div>
|
|
217
|
-
</button>
|
|
218
|
-
</div>
|
|
219
|
-
</section>
|
|
220
|
-
|
|
221
|
-
<!-- Token Overrides -->
|
|
222
|
-
<section class="admin-theme-overrides">
|
|
223
|
-
<h2 class="admin-section-title">Token Overrides</h2>
|
|
224
|
-
<p class="admin-section-desc">
|
|
225
|
-
Override individual CSS tokens instance-wide. These apply on top of the selected theme.
|
|
226
|
-
Use CSS values (colors, font families, sizes).
|
|
227
|
-
</p>
|
|
228
|
-
|
|
229
|
-
<div class="admin-overrides-list" v-if="Object.keys(tokenOverrides).length > 0">
|
|
230
|
-
<div v-for="(value, key) in tokenOverrides" :key="key" class="admin-override-row">
|
|
231
|
-
<code class="admin-override-key">--{{ key }}</code>
|
|
232
|
-
<span class="admin-override-value">
|
|
233
|
-
<span
|
|
234
|
-
v-if="String(value).startsWith('#') || String(value).startsWith('rgb')"
|
|
235
|
-
class="admin-override-swatch"
|
|
236
|
-
:style="{ backgroundColor: String(value) }"
|
|
237
|
-
></span>
|
|
238
|
-
{{ value }}
|
|
239
|
-
</span>
|
|
240
|
-
<button
|
|
241
|
-
class="cpub-btn cpub-btn-sm admin-override-remove"
|
|
242
|
-
aria-label="Remove override"
|
|
243
|
-
@click="removeTokenOverride(key as string)"
|
|
244
|
-
>
|
|
245
|
-
<i class="fa-solid fa-xmark"></i>
|
|
246
|
-
</button>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<div class="admin-override-add">
|
|
251
|
-
<input
|
|
252
|
-
v-model="newTokenKey"
|
|
253
|
-
class="admin-override-input"
|
|
254
|
-
placeholder="Token name (e.g. accent)"
|
|
255
|
-
@keyup.enter="addTokenOverride"
|
|
256
|
-
/>
|
|
257
|
-
<input
|
|
258
|
-
v-model="newTokenValue"
|
|
259
|
-
class="admin-override-input"
|
|
260
|
-
placeholder="Value (e.g. #ff6600)"
|
|
261
|
-
@keyup.enter="addTokenOverride"
|
|
262
|
-
/>
|
|
263
|
-
<button class="cpub-btn cpub-btn-sm" :disabled="!newTokenKey.trim() || !newTokenValue.trim()" @click="addTokenOverride">
|
|
264
|
-
Add
|
|
265
|
-
</button>
|
|
266
|
-
</div>
|
|
267
|
-
|
|
268
|
-
<div class="admin-override-actions">
|
|
269
|
-
<button class="cpub-btn cpub-btn-primary" :disabled="saving" @click="saveTokenOverrides">
|
|
270
|
-
<i class="fa-solid fa-floppy-disk"></i> Save Overrides
|
|
271
|
-
</button>
|
|
272
|
-
</div>
|
|
273
|
-
</section>
|
|
274
|
-
</div>
|
|
275
|
-
</template>
|
|
276
|
-
|
|
277
|
-
<style scoped>
|
|
278
|
-
.admin-theme { max-width: 900px; }
|
|
279
|
-
|
|
280
|
-
.admin-theme-header { margin-bottom: var(--space-6); }
|
|
281
|
-
.admin-page-title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); margin-bottom: var(--space-2); }
|
|
282
|
-
.admin-page-desc { font-size: var(--text-sm); color: var(--text-dim); }
|
|
283
|
-
|
|
284
|
-
.admin-theme-toast {
|
|
285
|
-
position: fixed;
|
|
286
|
-
top: calc(var(--nav-height) + var(--space-4));
|
|
287
|
-
right: var(--space-4);
|
|
288
|
-
padding: var(--space-2) var(--space-4);
|
|
289
|
-
background: var(--green);
|
|
290
|
-
color: var(--color-text-inverse);
|
|
291
|
-
font-size: var(--text-sm);
|
|
292
|
-
font-weight: var(--font-weight-semibold);
|
|
293
|
-
z-index: var(--z-toast);
|
|
294
|
-
border: var(--border-width-default) solid var(--border);
|
|
295
|
-
box-shadow: var(--shadow-md);
|
|
296
|
-
display: flex;
|
|
297
|
-
align-items: center;
|
|
298
|
-
gap: var(--space-2);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
.admin-section-title {
|
|
302
|
-
font-size: var(--text-lg);
|
|
303
|
-
font-weight: var(--font-weight-bold);
|
|
304
|
-
margin-bottom: var(--space-2);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
.admin-section-desc {
|
|
308
|
-
font-size: var(--text-sm);
|
|
309
|
-
color: var(--text-dim);
|
|
310
|
-
margin-bottom: var(--space-4);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/* Theme families */
|
|
314
|
-
.admin-theme-families {
|
|
315
|
-
display: flex;
|
|
316
|
-
flex-direction: column;
|
|
317
|
-
gap: var(--space-4);
|
|
318
|
-
margin-bottom: var(--space-8);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
.admin-family-card {
|
|
322
|
-
border: var(--border-width-default) solid var(--border2);
|
|
323
|
-
background: var(--surface);
|
|
324
|
-
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
.admin-family-card.active {
|
|
328
|
-
border-color: var(--accent);
|
|
329
|
-
box-shadow: var(--shadow-accent);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
.admin-family-card:hover {
|
|
333
|
-
border-color: var(--border);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.admin-family-select {
|
|
337
|
-
display: flex;
|
|
338
|
-
width: 100%;
|
|
339
|
-
text-align: left;
|
|
340
|
-
cursor: pointer;
|
|
341
|
-
background: none;
|
|
342
|
-
border: none;
|
|
343
|
-
padding: 0;
|
|
344
|
-
color: inherit;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.admin-family-select:disabled {
|
|
348
|
-
opacity: 0.6;
|
|
349
|
-
cursor: wait;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
.admin-family-previews {
|
|
353
|
-
display: flex;
|
|
354
|
-
flex-shrink: 0;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
.admin-family-preview {
|
|
358
|
-
width: 140px;
|
|
359
|
-
height: 100px;
|
|
360
|
-
padding: var(--space-3);
|
|
361
|
-
display: flex;
|
|
362
|
-
align-items: center;
|
|
363
|
-
justify-content: center;
|
|
364
|
-
border-right: var(--border-width-default) solid var(--border2);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
.admin-preview-card {
|
|
368
|
-
width: 80%;
|
|
369
|
-
padding: var(--space-2) var(--space-3);
|
|
370
|
-
border-width: 2px;
|
|
371
|
-
border-style: solid;
|
|
372
|
-
display: flex;
|
|
373
|
-
flex-direction: column;
|
|
374
|
-
gap: 4px;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
.admin-preview-heading { height: 5px; width: 55%; }
|
|
378
|
-
.admin-preview-text { height: 3px; width: 85%; }
|
|
379
|
-
.admin-preview-accent { height: 12px; width: 40%; margin-top: 4px; }
|
|
380
|
-
|
|
381
|
-
.admin-family-meta {
|
|
382
|
-
padding: var(--space-4);
|
|
383
|
-
display: flex;
|
|
384
|
-
flex-direction: column;
|
|
385
|
-
justify-content: center;
|
|
386
|
-
gap: 2px;
|
|
387
|
-
min-width: 0;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
.admin-family-name {
|
|
391
|
-
font-size: var(--text-md);
|
|
392
|
-
font-weight: var(--font-weight-bold);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
.admin-family-desc {
|
|
396
|
-
font-size: var(--text-sm);
|
|
397
|
-
color: var(--text-dim);
|
|
398
|
-
line-height: var(--leading-snug);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.admin-family-active {
|
|
402
|
-
display: inline-flex;
|
|
403
|
-
align-items: center;
|
|
404
|
-
gap: 4px;
|
|
405
|
-
font-size: var(--text-xs);
|
|
406
|
-
font-family: var(--font-mono);
|
|
407
|
-
font-weight: var(--font-weight-semibold);
|
|
408
|
-
text-transform: uppercase;
|
|
409
|
-
letter-spacing: var(--tracking-wide);
|
|
410
|
-
color: var(--accent);
|
|
411
|
-
margin-top: var(--space-1);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/* Token overrides */
|
|
415
|
-
.admin-theme-overrides {
|
|
416
|
-
border-top: var(--border-width-default) solid var(--border);
|
|
417
|
-
padding-top: var(--space-6);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
.admin-overrides-list {
|
|
421
|
-
border: var(--border-width-default) solid var(--border);
|
|
422
|
-
background: var(--surface);
|
|
423
|
-
margin-bottom: var(--space-4);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
.admin-override-row {
|
|
427
|
-
display: flex;
|
|
428
|
-
align-items: center;
|
|
429
|
-
padding: var(--space-2) var(--space-3);
|
|
430
|
-
border-bottom: var(--border-width-default) solid var(--border2);
|
|
431
|
-
gap: var(--space-3);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
.admin-override-row:last-child { border-bottom: none; }
|
|
435
|
-
|
|
436
|
-
.admin-override-key {
|
|
437
|
-
font-family: var(--font-mono);
|
|
438
|
-
font-size: var(--text-sm);
|
|
439
|
-
color: var(--accent);
|
|
440
|
-
flex-shrink: 0;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
.admin-override-value {
|
|
444
|
-
font-family: var(--font-mono);
|
|
445
|
-
font-size: var(--text-sm);
|
|
446
|
-
color: var(--text-dim);
|
|
447
|
-
display: flex;
|
|
448
|
-
align-items: center;
|
|
449
|
-
gap: var(--space-2);
|
|
450
|
-
flex: 1;
|
|
451
|
-
min-width: 0;
|
|
452
|
-
overflow: hidden;
|
|
453
|
-
text-overflow: ellipsis;
|
|
454
|
-
white-space: nowrap;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.admin-override-swatch {
|
|
458
|
-
display: inline-block;
|
|
459
|
-
width: 14px;
|
|
460
|
-
height: 14px;
|
|
461
|
-
border: var(--border-width-default) solid var(--border2);
|
|
462
|
-
flex-shrink: 0;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
.admin-override-remove {
|
|
466
|
-
flex-shrink: 0;
|
|
467
|
-
padding: var(--space-1);
|
|
468
|
-
color: var(--text-faint);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
.admin-override-remove:hover { color: var(--red); }
|
|
472
|
-
|
|
473
|
-
.admin-override-add {
|
|
474
|
-
display: flex;
|
|
475
|
-
gap: var(--space-2);
|
|
476
|
-
padding: var(--space-3);
|
|
477
|
-
border: 2px dashed var(--border2);
|
|
478
|
-
background: var(--surface);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
.admin-override-input {
|
|
482
|
-
font-size: var(--text-sm);
|
|
483
|
-
padding: var(--space-1) var(--space-2);
|
|
484
|
-
border: var(--border-width-default) solid var(--border);
|
|
485
|
-
background: var(--surface2);
|
|
486
|
-
color: var(--text);
|
|
487
|
-
font-family: var(--font-mono);
|
|
488
|
-
flex: 1;
|
|
489
|
-
min-width: 0;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
.admin-override-actions {
|
|
493
|
-
margin-top: var(--space-4);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
@media (max-width: 640px) {
|
|
497
|
-
.admin-family-select { flex-direction: column; }
|
|
498
|
-
.admin-family-previews { width: 100%; }
|
|
499
|
-
.admin-family-preview { flex: 1; border-right: none; border-bottom: var(--border-width-default) solid var(--border2); }
|
|
500
|
-
.admin-override-add { flex-direction: column; }
|
|
501
|
-
}
|
|
502
|
-
</style>
|