@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
|
@@ -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>
|