@commonpub/layer 0.3.37 → 0.4.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.
- package/components/CookieConsent.vue +91 -0
- package/composables/useCookieConsent.ts +117 -0
- package/composables/useTheme.ts +45 -24
- package/layouts/admin.vue +1 -0
- package/layouts/default.vue +6 -1
- package/nuxt.config.ts +17 -0
- package/package.json +7 -6
- package/pages/admin/theme.vue +500 -0
- package/pages/auth/register.vue +22 -0
- package/pages/cookies.vue +186 -0
- package/pages/privacy.vue +207 -0
- package/pages/settings/account.vue +9 -0
- package/pages/settings/appearance.vue +143 -38
- package/pages/terms.vue +168 -0
- package/plugins/theme.ts +32 -0
- package/server/api/admin/settings.get.ts +7 -3
- package/server/api/admin/settings.put.ts +6 -1
- package/server/api/auth/delete-user.post.ts +55 -0
- package/server/api/auth/export-data.get.ts +15 -0
- package/server/middleware/theme.ts +34 -0
- package/server/utils/instanceTheme.ts +67 -0
- package/theme/agora-dark.css +157 -0
- package/theme/agora.css +156 -0
- package/utils/themeConfig.ts +39 -0
|
@@ -0,0 +1,500 @@
|
|
|
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, 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
|
+
<!-- Theme Families -->
|
|
160
|
+
<section class="admin-theme-families">
|
|
161
|
+
<div v-for="family in families" :key="family.id" class="admin-family-card" :class="{ active: activeFamily === family.id }" >
|
|
162
|
+
<button
|
|
163
|
+
class="admin-family-select"
|
|
164
|
+
:disabled="saving"
|
|
165
|
+
@click="selectFamily(family)"
|
|
166
|
+
>
|
|
167
|
+
<div class="admin-family-previews">
|
|
168
|
+
<!-- Light preview -->
|
|
169
|
+
<div
|
|
170
|
+
v-if="family.light"
|
|
171
|
+
class="admin-family-preview"
|
|
172
|
+
:style="{ backgroundColor: family.preview.light.bg, borderColor: family.preview.light.border }"
|
|
173
|
+
>
|
|
174
|
+
<div
|
|
175
|
+
class="admin-preview-card"
|
|
176
|
+
:style="{
|
|
177
|
+
backgroundColor: family.preview.light.surface,
|
|
178
|
+
borderColor: family.preview.light.border,
|
|
179
|
+
boxShadow: `3px 3px 0 ${family.preview.light.border}`,
|
|
180
|
+
}"
|
|
181
|
+
>
|
|
182
|
+
<div class="admin-preview-heading" :style="{ backgroundColor: family.preview.light.text, opacity: 0.8 }"></div>
|
|
183
|
+
<div class="admin-preview-text" :style="{ backgroundColor: family.preview.light.text, opacity: 0.3 }"></div>
|
|
184
|
+
<div class="admin-preview-accent" :style="{ backgroundColor: family.preview.light.accent }"></div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<!-- Dark preview -->
|
|
188
|
+
<div
|
|
189
|
+
v-if="family.dark"
|
|
190
|
+
class="admin-family-preview"
|
|
191
|
+
:style="{ backgroundColor: family.preview.dark.bg, borderColor: family.preview.dark.border }"
|
|
192
|
+
>
|
|
193
|
+
<div
|
|
194
|
+
class="admin-preview-card"
|
|
195
|
+
:style="{
|
|
196
|
+
backgroundColor: family.preview.dark.surface,
|
|
197
|
+
borderColor: family.preview.dark.border,
|
|
198
|
+
boxShadow: `3px 3px 0 ${family.preview.dark.border}`,
|
|
199
|
+
}"
|
|
200
|
+
>
|
|
201
|
+
<div class="admin-preview-heading" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.8 }"></div>
|
|
202
|
+
<div class="admin-preview-text" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.3 }"></div>
|
|
203
|
+
<div class="admin-preview-accent" :style="{ backgroundColor: family.preview.dark.accent }"></div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div class="admin-family-meta">
|
|
209
|
+
<span class="admin-family-name">{{ family.name }}</span>
|
|
210
|
+
<span class="admin-family-desc">{{ family.description }}</span>
|
|
211
|
+
<span v-if="activeFamily === family.id" class="admin-family-active">
|
|
212
|
+
<i class="fa-solid fa-check"></i> Active
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
|
|
219
|
+
<!-- Token Overrides -->
|
|
220
|
+
<section class="admin-theme-overrides">
|
|
221
|
+
<h2 class="admin-section-title">Token Overrides</h2>
|
|
222
|
+
<p class="admin-section-desc">
|
|
223
|
+
Override individual CSS tokens instance-wide. These apply on top of the selected theme.
|
|
224
|
+
Use CSS values (colors, font families, sizes).
|
|
225
|
+
</p>
|
|
226
|
+
|
|
227
|
+
<div class="admin-overrides-list" v-if="Object.keys(tokenOverrides).length > 0">
|
|
228
|
+
<div v-for="(value, key) in tokenOverrides" :key="key" class="admin-override-row">
|
|
229
|
+
<code class="admin-override-key">--{{ key }}</code>
|
|
230
|
+
<span class="admin-override-value">
|
|
231
|
+
<span
|
|
232
|
+
v-if="String(value).startsWith('#') || String(value).startsWith('rgb')"
|
|
233
|
+
class="admin-override-swatch"
|
|
234
|
+
:style="{ backgroundColor: String(value) }"
|
|
235
|
+
></span>
|
|
236
|
+
{{ value }}
|
|
237
|
+
</span>
|
|
238
|
+
<button
|
|
239
|
+
class="cpub-btn cpub-btn-sm admin-override-remove"
|
|
240
|
+
aria-label="Remove override"
|
|
241
|
+
@click="removeTokenOverride(key as string)"
|
|
242
|
+
>
|
|
243
|
+
<i class="fa-solid fa-xmark"></i>
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div class="admin-override-add">
|
|
249
|
+
<input
|
|
250
|
+
v-model="newTokenKey"
|
|
251
|
+
class="admin-override-input"
|
|
252
|
+
placeholder="Token name (e.g. accent)"
|
|
253
|
+
@keyup.enter="addTokenOverride"
|
|
254
|
+
/>
|
|
255
|
+
<input
|
|
256
|
+
v-model="newTokenValue"
|
|
257
|
+
class="admin-override-input"
|
|
258
|
+
placeholder="Value (e.g. #ff6600)"
|
|
259
|
+
@keyup.enter="addTokenOverride"
|
|
260
|
+
/>
|
|
261
|
+
<button class="cpub-btn cpub-btn-sm" :disabled="!newTokenKey.trim() || !newTokenValue.trim()" @click="addTokenOverride">
|
|
262
|
+
Add
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="admin-override-actions">
|
|
267
|
+
<button class="cpub-btn cpub-btn-primary" :disabled="saving" @click="saveTokenOverrides">
|
|
268
|
+
<i class="fa-solid fa-floppy-disk"></i> Save Overrides
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
</section>
|
|
272
|
+
</div>
|
|
273
|
+
</template>
|
|
274
|
+
|
|
275
|
+
<style scoped>
|
|
276
|
+
.admin-theme { max-width: 900px; }
|
|
277
|
+
|
|
278
|
+
.admin-theme-header { margin-bottom: var(--space-6); }
|
|
279
|
+
.admin-page-title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); margin-bottom: var(--space-2); }
|
|
280
|
+
.admin-page-desc { font-size: var(--text-sm); color: var(--text-dim); }
|
|
281
|
+
|
|
282
|
+
.admin-theme-toast {
|
|
283
|
+
position: fixed;
|
|
284
|
+
top: calc(var(--nav-height) + var(--space-4));
|
|
285
|
+
right: var(--space-4);
|
|
286
|
+
padding: var(--space-2) var(--space-4);
|
|
287
|
+
background: var(--green);
|
|
288
|
+
color: #fff;
|
|
289
|
+
font-size: var(--text-sm);
|
|
290
|
+
font-weight: var(--font-weight-semibold);
|
|
291
|
+
z-index: var(--z-toast);
|
|
292
|
+
border: var(--border-width-default) solid var(--border);
|
|
293
|
+
box-shadow: var(--shadow-md);
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: var(--space-2);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.admin-section-title {
|
|
300
|
+
font-size: var(--text-lg);
|
|
301
|
+
font-weight: var(--font-weight-bold);
|
|
302
|
+
margin-bottom: var(--space-2);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.admin-section-desc {
|
|
306
|
+
font-size: var(--text-sm);
|
|
307
|
+
color: var(--text-dim);
|
|
308
|
+
margin-bottom: var(--space-4);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Theme families */
|
|
312
|
+
.admin-theme-families {
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-direction: column;
|
|
315
|
+
gap: var(--space-4);
|
|
316
|
+
margin-bottom: var(--space-8);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.admin-family-card {
|
|
320
|
+
border: var(--border-width-default) solid var(--border2);
|
|
321
|
+
background: var(--surface);
|
|
322
|
+
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.admin-family-card.active {
|
|
326
|
+
border-color: var(--accent);
|
|
327
|
+
box-shadow: var(--shadow-accent);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.admin-family-card:hover {
|
|
331
|
+
border-color: var(--border);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.admin-family-select {
|
|
335
|
+
display: flex;
|
|
336
|
+
width: 100%;
|
|
337
|
+
text-align: left;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
background: none;
|
|
340
|
+
border: none;
|
|
341
|
+
padding: 0;
|
|
342
|
+
color: inherit;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.admin-family-select:disabled {
|
|
346
|
+
opacity: 0.6;
|
|
347
|
+
cursor: wait;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.admin-family-previews {
|
|
351
|
+
display: flex;
|
|
352
|
+
flex-shrink: 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.admin-family-preview {
|
|
356
|
+
width: 140px;
|
|
357
|
+
height: 100px;
|
|
358
|
+
padding: var(--space-3);
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
justify-content: center;
|
|
362
|
+
border-right: var(--border-width-default) solid var(--border2);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.admin-preview-card {
|
|
366
|
+
width: 80%;
|
|
367
|
+
padding: var(--space-2) var(--space-3);
|
|
368
|
+
border-width: 2px;
|
|
369
|
+
border-style: solid;
|
|
370
|
+
display: flex;
|
|
371
|
+
flex-direction: column;
|
|
372
|
+
gap: 4px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.admin-preview-heading { height: 5px; width: 55%; }
|
|
376
|
+
.admin-preview-text { height: 3px; width: 85%; }
|
|
377
|
+
.admin-preview-accent { height: 12px; width: 40%; margin-top: 4px; }
|
|
378
|
+
|
|
379
|
+
.admin-family-meta {
|
|
380
|
+
padding: var(--space-4);
|
|
381
|
+
display: flex;
|
|
382
|
+
flex-direction: column;
|
|
383
|
+
justify-content: center;
|
|
384
|
+
gap: 2px;
|
|
385
|
+
min-width: 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.admin-family-name {
|
|
389
|
+
font-size: var(--text-md);
|
|
390
|
+
font-weight: var(--font-weight-bold);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.admin-family-desc {
|
|
394
|
+
font-size: var(--text-sm);
|
|
395
|
+
color: var(--text-dim);
|
|
396
|
+
line-height: var(--leading-snug);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.admin-family-active {
|
|
400
|
+
display: inline-flex;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: 4px;
|
|
403
|
+
font-size: var(--text-xs);
|
|
404
|
+
font-family: var(--font-mono);
|
|
405
|
+
font-weight: var(--font-weight-semibold);
|
|
406
|
+
text-transform: uppercase;
|
|
407
|
+
letter-spacing: var(--tracking-wide);
|
|
408
|
+
color: var(--accent);
|
|
409
|
+
margin-top: var(--space-1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Token overrides */
|
|
413
|
+
.admin-theme-overrides {
|
|
414
|
+
border-top: var(--border-width-default) solid var(--border);
|
|
415
|
+
padding-top: var(--space-6);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.admin-overrides-list {
|
|
419
|
+
border: var(--border-width-default) solid var(--border);
|
|
420
|
+
background: var(--surface);
|
|
421
|
+
margin-bottom: var(--space-4);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.admin-override-row {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
padding: var(--space-2) var(--space-3);
|
|
428
|
+
border-bottom: var(--border-width-default) solid var(--border2);
|
|
429
|
+
gap: var(--space-3);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.admin-override-row:last-child { border-bottom: none; }
|
|
433
|
+
|
|
434
|
+
.admin-override-key {
|
|
435
|
+
font-family: var(--font-mono);
|
|
436
|
+
font-size: var(--text-sm);
|
|
437
|
+
color: var(--accent);
|
|
438
|
+
flex-shrink: 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.admin-override-value {
|
|
442
|
+
font-family: var(--font-mono);
|
|
443
|
+
font-size: var(--text-sm);
|
|
444
|
+
color: var(--text-dim);
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: var(--space-2);
|
|
448
|
+
flex: 1;
|
|
449
|
+
min-width: 0;
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
text-overflow: ellipsis;
|
|
452
|
+
white-space: nowrap;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.admin-override-swatch {
|
|
456
|
+
display: inline-block;
|
|
457
|
+
width: 14px;
|
|
458
|
+
height: 14px;
|
|
459
|
+
border: 1px solid var(--border2);
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.admin-override-remove {
|
|
464
|
+
flex-shrink: 0;
|
|
465
|
+
padding: var(--space-1);
|
|
466
|
+
color: var(--text-faint);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.admin-override-remove:hover { color: var(--red); }
|
|
470
|
+
|
|
471
|
+
.admin-override-add {
|
|
472
|
+
display: flex;
|
|
473
|
+
gap: var(--space-2);
|
|
474
|
+
padding: var(--space-3);
|
|
475
|
+
border: 2px dashed var(--border2);
|
|
476
|
+
background: var(--surface);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.admin-override-input {
|
|
480
|
+
font-size: var(--text-sm);
|
|
481
|
+
padding: var(--space-1) var(--space-2);
|
|
482
|
+
border: var(--border-width-default) solid var(--border);
|
|
483
|
+
background: var(--surface2);
|
|
484
|
+
color: var(--text);
|
|
485
|
+
font-family: var(--font-mono);
|
|
486
|
+
flex: 1;
|
|
487
|
+
min-width: 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.admin-override-actions {
|
|
491
|
+
margin-top: var(--space-4);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@media (max-width: 640px) {
|
|
495
|
+
.admin-family-select { flex-direction: column; }
|
|
496
|
+
.admin-family-previews { width: 100%; }
|
|
497
|
+
.admin-family-preview { flex: 1; border-right: none; border-bottom: var(--border-width-default) solid var(--border2); }
|
|
498
|
+
.admin-override-add { flex-direction: column; }
|
|
499
|
+
}
|
|
500
|
+
</style>
|
package/pages/auth/register.vue
CHANGED
|
@@ -101,6 +101,13 @@ async function handleSubmit(): Promise<void> {
|
|
|
101
101
|
/>
|
|
102
102
|
</div>
|
|
103
103
|
|
|
104
|
+
<p class="register-legal">
|
|
105
|
+
By creating an account, you agree to our
|
|
106
|
+
<NuxtLink to="/terms">Terms of Service</NuxtLink>
|
|
107
|
+
and acknowledge our
|
|
108
|
+
<NuxtLink to="/privacy">Privacy Policy</NuxtLink>.
|
|
109
|
+
</p>
|
|
110
|
+
|
|
104
111
|
<button type="submit" class="submit-btn" :disabled="loading">
|
|
105
112
|
{{ loading ? 'Creating...' : 'Create account' }}
|
|
106
113
|
</button>
|
|
@@ -206,6 +213,21 @@ async function handleSubmit(): Promise<void> {
|
|
|
206
213
|
cursor: not-allowed;
|
|
207
214
|
}
|
|
208
215
|
|
|
216
|
+
.register-legal {
|
|
217
|
+
font-size: 11px;
|
|
218
|
+
color: var(--text-faint);
|
|
219
|
+
line-height: 1.5;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.register-legal a {
|
|
223
|
+
color: var(--accent);
|
|
224
|
+
text-decoration: none;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.register-legal a:hover {
|
|
228
|
+
text-decoration: underline;
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
.register-footer {
|
|
210
232
|
text-align: center;
|
|
211
233
|
font-size: 12px;
|