@commonpub/layer 0.3.38 → 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 +4 -0
- package/nuxt.config.ts +17 -0
- package/package.json +7 -6
- package/pages/admin/theme.vue +500 -0
- package/pages/cookies.vue +186 -0
- package/pages/privacy.vue +6 -5
- package/pages/settings/appearance.vue +143 -38
- 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 +1 -1
- 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>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const runtimeConfig = useRuntimeConfig();
|
|
3
|
+
const siteName = computed(() => (runtimeConfig.public.siteName as string) || 'CommonPub');
|
|
4
|
+
|
|
5
|
+
useSeoMeta({ title: `Cookie Policy — ${siteName.value}` });
|
|
6
|
+
|
|
7
|
+
const { cookies, consentLevel, acceptAll, acceptEssential, resetConsent, hasConsented } = useCookieConsent();
|
|
8
|
+
|
|
9
|
+
const essentialCookies = computed(() => cookies.value.filter((c) => c.category === 'essential'));
|
|
10
|
+
const functionalCookies = computed(() => cookies.value.filter((c) => c.category === 'functional'));
|
|
11
|
+
const analyticsCookies = computed(() => cookies.value.filter((c) => c.category === 'analytics'));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="cpub-legal">
|
|
16
|
+
<div class="cpub-legal-header">
|
|
17
|
+
<h1 class="cpub-legal-title">Cookie Policy</h1>
|
|
18
|
+
<p class="cpub-legal-updated">Last updated: {{ new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="cpub-legal-body">
|
|
22
|
+
<section class="cpub-legal-section">
|
|
23
|
+
<h2>What are cookies?</h2>
|
|
24
|
+
<p>Cookies are small text files stored on your device when you visit a website. They help the site remember your preferences and provide essential functionality.</p>
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
<section class="cpub-legal-section">
|
|
28
|
+
<h2>Your consent</h2>
|
|
29
|
+
<p v-if="hasConsented">
|
|
30
|
+
You have accepted <strong>{{ consentLevel === 'all' ? 'all cookies' : 'essential cookies only' }}</strong>.
|
|
31
|
+
You can change this at any time.
|
|
32
|
+
</p>
|
|
33
|
+
<p v-else>You have not yet made a cookie choice.</p>
|
|
34
|
+
<div class="cpub-cookie-consent-actions">
|
|
35
|
+
<button class="cpub-btn cpub-btn-sm" @click="acceptEssential">Essential only</button>
|
|
36
|
+
<button class="cpub-btn cpub-btn-sm cpub-btn-primary" @click="acceptAll">Accept all</button>
|
|
37
|
+
<button v-if="hasConsented" class="cpub-btn cpub-btn-sm" @click="resetConsent">Reset choice</button>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<!-- Essential Cookies -->
|
|
42
|
+
<section class="cpub-legal-section">
|
|
43
|
+
<h2>Essential cookies</h2>
|
|
44
|
+
<p>These are strictly necessary for the site to function. They cannot be disabled.</p>
|
|
45
|
+
<table class="cpub-cookie-table" v-if="essentialCookies.length > 0">
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>Cookie</th>
|
|
49
|
+
<th>Purpose</th>
|
|
50
|
+
<th>Duration</th>
|
|
51
|
+
<th v-if="essentialCookies.some(c => c.provider)">Provider</th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
<tr v-for="cookie in essentialCookies" :key="cookie.name">
|
|
56
|
+
<td><code>{{ cookie.name }}</code></td>
|
|
57
|
+
<td>{{ cookie.description }}</td>
|
|
58
|
+
<td>{{ cookie.duration }}</td>
|
|
59
|
+
<td v-if="essentialCookies.some(c => c.provider)">{{ cookie.provider ?? siteName }}</td>
|
|
60
|
+
</tr>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</section>
|
|
64
|
+
|
|
65
|
+
<!-- Functional Cookies -->
|
|
66
|
+
<section v-if="functionalCookies.length > 0" class="cpub-legal-section">
|
|
67
|
+
<h2>Functional cookies</h2>
|
|
68
|
+
<p>These remember your preferences (like dark mode) to improve your experience. They are set only with your consent.</p>
|
|
69
|
+
<table class="cpub-cookie-table">
|
|
70
|
+
<thead>
|
|
71
|
+
<tr>
|
|
72
|
+
<th>Cookie</th>
|
|
73
|
+
<th>Purpose</th>
|
|
74
|
+
<th>Duration</th>
|
|
75
|
+
<th v-if="functionalCookies.some(c => c.provider)">Provider</th>
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody>
|
|
79
|
+
<tr v-for="cookie in functionalCookies" :key="cookie.name">
|
|
80
|
+
<td><code>{{ cookie.name }}</code></td>
|
|
81
|
+
<td>{{ cookie.description }}</td>
|
|
82
|
+
<td>{{ cookie.duration }}</td>
|
|
83
|
+
<td v-if="functionalCookies.some(c => c.provider)">{{ cookie.provider ?? siteName }}</td>
|
|
84
|
+
</tr>
|
|
85
|
+
</tbody>
|
|
86
|
+
</table>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
89
|
+
<!-- Analytics Cookies -->
|
|
90
|
+
<section v-if="analyticsCookies.length > 0" class="cpub-legal-section">
|
|
91
|
+
<h2>Analytics cookies</h2>
|
|
92
|
+
<p>These help the instance operator understand how the site is used. They are set only with your consent.</p>
|
|
93
|
+
<table class="cpub-cookie-table">
|
|
94
|
+
<thead>
|
|
95
|
+
<tr>
|
|
96
|
+
<th>Cookie</th>
|
|
97
|
+
<th>Purpose</th>
|
|
98
|
+
<th>Duration</th>
|
|
99
|
+
<th>Provider</th>
|
|
100
|
+
</tr>
|
|
101
|
+
</thead>
|
|
102
|
+
<tbody>
|
|
103
|
+
<tr v-for="cookie in analyticsCookies" :key="cookie.name">
|
|
104
|
+
<td><code>{{ cookie.name }}</code></td>
|
|
105
|
+
<td>{{ cookie.description }}</td>
|
|
106
|
+
<td>{{ cookie.duration }}</td>
|
|
107
|
+
<td>{{ cookie.provider ?? siteName }}</td>
|
|
108
|
+
</tr>
|
|
109
|
+
</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
</section>
|
|
112
|
+
|
|
113
|
+
<section class="cpub-legal-section">
|
|
114
|
+
<h2>Managing cookies</h2>
|
|
115
|
+
<p>You can change your cookie preferences at any time using the buttons above or by clearing your browser cookies. Most browsers also allow you to control cookies through their settings.</p>
|
|
116
|
+
<p>For more information about how we handle your data, see our <NuxtLink to="/privacy">Privacy Policy</NuxtLink>.</p>
|
|
117
|
+
</section>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<style scoped>
|
|
123
|
+
.cpub-legal {
|
|
124
|
+
max-width: 740px;
|
|
125
|
+
margin: 0 auto;
|
|
126
|
+
padding: var(--space-12) var(--space-6) var(--space-20);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.cpub-legal-header { margin-bottom: var(--space-10); }
|
|
130
|
+
.cpub-legal-title { font-size: var(--text-3xl); font-weight: var(--font-weight-bold); margin-bottom: var(--space-2); }
|
|
131
|
+
.cpub-legal-updated { font-size: var(--text-sm); color: var(--text-faint); }
|
|
132
|
+
|
|
133
|
+
.cpub-legal-section { margin-bottom: var(--space-8); }
|
|
134
|
+
.cpub-legal-section h2 { font-size: var(--text-lg); font-weight: var(--font-weight-bold); margin-bottom: var(--space-3); }
|
|
135
|
+
.cpub-legal-section p { font-size: var(--text-base); line-height: var(--leading-normal); color: var(--text-dim); margin-bottom: var(--space-3); }
|
|
136
|
+
.cpub-legal-section a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
137
|
+
|
|
138
|
+
.cpub-cookie-consent-actions {
|
|
139
|
+
display: flex;
|
|
140
|
+
gap: var(--space-2);
|
|
141
|
+
margin-top: var(--space-3);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.cpub-cookie-table {
|
|
145
|
+
width: 100%;
|
|
146
|
+
border-collapse: collapse;
|
|
147
|
+
font-size: var(--text-sm);
|
|
148
|
+
border: var(--border-width-default) solid var(--border);
|
|
149
|
+
margin-top: var(--space-3);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.cpub-cookie-table th,
|
|
153
|
+
.cpub-cookie-table td {
|
|
154
|
+
padding: var(--space-2) var(--space-3);
|
|
155
|
+
text-align: left;
|
|
156
|
+
border-bottom: var(--border-width-default) solid var(--border2);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.cpub-cookie-table th {
|
|
160
|
+
font-family: var(--font-mono);
|
|
161
|
+
font-size: var(--text-label);
|
|
162
|
+
text-transform: uppercase;
|
|
163
|
+
letter-spacing: var(--tracking-wide);
|
|
164
|
+
color: var(--text-faint);
|
|
165
|
+
background: var(--surface2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.cpub-cookie-table td {
|
|
169
|
+
color: var(--text-dim);
|
|
170
|
+
line-height: var(--leading-snug);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cpub-cookie-table code {
|
|
174
|
+
font-family: var(--font-mono);
|
|
175
|
+
font-size: var(--text-xs);
|
|
176
|
+
color: var(--accent);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.cpub-cookie-table tr:last-child td { border-bottom: none; }
|
|
180
|
+
|
|
181
|
+
@media (max-width: 640px) {
|
|
182
|
+
.cpub-legal { padding: var(--space-6) var(--space-4) var(--space-12); }
|
|
183
|
+
.cpub-cookie-table { font-size: var(--text-xs); }
|
|
184
|
+
.cpub-cookie-table th, .cpub-cookie-table td { padding: var(--space-1) var(--space-2); }
|
|
185
|
+
}
|
|
186
|
+
</style>
|