@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,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>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
useSeoMeta({
|
|
3
|
+
title: `Privacy Policy — ${useSiteName()}`,
|
|
4
|
+
description: 'How we collect, use, and protect your personal data.',
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const siteName = useSiteName();
|
|
8
|
+
const { federation: federationEnabled } = useFeatures();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="cpub-legal">
|
|
13
|
+
<div class="cpub-legal-header">
|
|
14
|
+
<h1 class="cpub-legal-title">Privacy Policy</h1>
|
|
15
|
+
<p class="cpub-legal-updated">Last updated: April 2026</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="cpub-legal-body">
|
|
19
|
+
<section class="cpub-legal-section">
|
|
20
|
+
<h2>1. Who We Are</h2>
|
|
21
|
+
<p>
|
|
22
|
+
This {{ siteName }} instance is operated by its administrator (the "data controller").
|
|
23
|
+
{{ siteName }} is powered by <a href="https://commonpub.io" target="_blank" rel="noopener">CommonPub</a>,
|
|
24
|
+
an open-source, self-hosted platform. Each instance is independently operated and responsible for its own data processing.
|
|
25
|
+
</p>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section class="cpub-legal-section">
|
|
29
|
+
<h2>2. What Data We Collect</h2>
|
|
30
|
+
<p>When you create an account, we collect:</p>
|
|
31
|
+
<ul>
|
|
32
|
+
<li><strong>Account data:</strong> email address, username, password (stored as a secure hash)</li>
|
|
33
|
+
<li><strong>Profile data:</strong> display name, bio, headline, location, website, avatar, banner image, social links, skills, pronouns, timezone (all optional)</li>
|
|
34
|
+
<li><strong>Content:</strong> projects, articles, blog posts, comments, and other content you create</li>
|
|
35
|
+
<li><strong>Activity data:</strong> likes, follows, bookmarks, hub memberships, learning path enrollments</li>
|
|
36
|
+
<li><strong>Messages:</strong> direct messages you send to other users on this instance</li>
|
|
37
|
+
</ul>
|
|
38
|
+
<p>We also automatically collect:</p>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><strong>Session data:</strong> IP address and browser user agent when you log in, stored for the duration of your session (up to 7 days)</li>
|
|
41
|
+
<li><strong>Theme preference:</strong> your light/dark mode choice, stored in your browser's local storage</li>
|
|
42
|
+
</ul>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section class="cpub-legal-section">
|
|
46
|
+
<h2>3. How We Use Your Data</h2>
|
|
47
|
+
<ul>
|
|
48
|
+
<li><strong>Providing the service:</strong> displaying your profile, publishing your content, delivering notifications</li>
|
|
49
|
+
<li><strong>Authentication:</strong> verifying your identity when you log in</li>
|
|
50
|
+
<li><strong>Security:</strong> protecting against unauthorized access, abuse, and spam</li>
|
|
51
|
+
<li><strong>Email notifications:</strong> sending notification digests and alerts you've opted into (configurable in settings)</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section class="cpub-legal-section">
|
|
56
|
+
<h2>4. Legal Basis for Processing</h2>
|
|
57
|
+
<p>We process your data under the following legal bases (GDPR Article 6):</p>
|
|
58
|
+
<ul>
|
|
59
|
+
<li><strong>Contract performance (Art. 6(1)(b)):</strong> processing necessary to provide you with the service you signed up for</li>
|
|
60
|
+
<li><strong>Legitimate interest (Art. 6(1)(f)):</strong> session security, rate limiting, and preventing abuse</li>
|
|
61
|
+
</ul>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section class="cpub-legal-section">
|
|
65
|
+
<h2>5. Cookies</h2>
|
|
66
|
+
<p>We use a small number of cookies to provide and improve the service:</p>
|
|
67
|
+
<ul>
|
|
68
|
+
<li><strong>Session cookie</strong> (<code>better-auth.session_token</code>): strictly necessary — authenticates your login session. HttpOnly, secure, 7-day expiry.</li>
|
|
69
|
+
<li><strong>Consent cookie</strong> (<code>cpub-consent</code>): strictly necessary — stores your cookie consent choice.</li>
|
|
70
|
+
<li><strong>Color scheme</strong> (<code>cpub-color-scheme</code>): functional — remembers your light/dark mode preference. Set only with your consent.</li>
|
|
71
|
+
</ul>
|
|
72
|
+
<p>We do not use any advertising or tracking cookies. Your instance operator may add analytics cookies — these require your explicit consent. For the full list of cookies and to manage your preferences, visit our <NuxtLink to="/cookies">Cookie Policy</NuxtLink>.</p>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
75
|
+
<section v-if="federationEnabled" class="cpub-legal-section">
|
|
76
|
+
<h2>6. Federation and ActivityPub</h2>
|
|
77
|
+
<p>This instance participates in the <a href="https://activitypub.rocks" target="_blank" rel="noopener">ActivityPub</a> federation protocol. When you publish content or interact publicly, the following data may be shared with remote instances:</p>
|
|
78
|
+
<ul>
|
|
79
|
+
<li>Your username, display name, avatar, and bio</li>
|
|
80
|
+
<li>Your published content (projects, articles, blog posts)</li>
|
|
81
|
+
<li>Your public interactions (likes, follows, comments on federated content)</li>
|
|
82
|
+
</ul>
|
|
83
|
+
<p>Your email address, location, social links, timezone, and other private profile fields are <strong>never</strong> shared via federation.</p>
|
|
84
|
+
<p><strong>Important:</strong> Once your data is federated to remote instances, this instance cannot guarantee its deletion on those servers. Remote instances operate independently and may retain cached copies of your public data even after you delete your account here.</p>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section class="cpub-legal-section">
|
|
88
|
+
<h2>{{ federationEnabled ? '7' : '6' }}. Third-Party Services</h2>
|
|
89
|
+
<p>We load icon fonts from <strong>Font Awesome</strong> via the Cloudflare CDN (<code>cdnjs.cloudflare.com</code>). This means your browser makes requests to Cloudflare's servers, which are subject to <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener">Cloudflare's privacy policy</a>.</p>
|
|
90
|
+
<p>We do not use any analytics services, advertising networks, or tracking technologies.</p>
|
|
91
|
+
</section>
|
|
92
|
+
|
|
93
|
+
<section class="cpub-legal-section">
|
|
94
|
+
<h2>{{ federationEnabled ? '8' : '7' }}. Data Retention</h2>
|
|
95
|
+
<ul>
|
|
96
|
+
<li><strong>Account data:</strong> retained until you delete your account</li>
|
|
97
|
+
<li><strong>Session data:</strong> automatically expires after 7 days of inactivity</li>
|
|
98
|
+
<li><strong>Content:</strong> retained until you delete it or delete your account</li>
|
|
99
|
+
<li><strong>Audit logs:</strong> retained per the instance operator's policy</li>
|
|
100
|
+
</ul>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<section class="cpub-legal-section">
|
|
104
|
+
<h2>{{ federationEnabled ? '9' : '8' }}. Your Rights</h2>
|
|
105
|
+
<p>Under the GDPR and similar data protection laws, you have the right to:</p>
|
|
106
|
+
<ul>
|
|
107
|
+
<li><strong>Access:</strong> view the data we hold about you (via your profile and settings)</li>
|
|
108
|
+
<li><strong>Rectification:</strong> update or correct your data (via your profile settings)</li>
|
|
109
|
+
<li><strong>Erasure:</strong> delete your account and all associated data (via account settings)</li>
|
|
110
|
+
<li><strong>Portability:</strong> download your data in a machine-readable format (via account settings)</li>
|
|
111
|
+
<li><strong>Restriction and objection:</strong> contact the instance administrator</li>
|
|
112
|
+
</ul>
|
|
113
|
+
<p>To exercise these rights, visit your <NuxtLink to="/settings/account">account settings</NuxtLink> or contact the instance administrator.</p>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<section class="cpub-legal-section">
|
|
117
|
+
<h2>{{ federationEnabled ? '10' : '9' }}. Contact</h2>
|
|
118
|
+
<p>For privacy-related inquiries, contact the administrator of this {{ siteName }} instance.</p>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<style scoped>
|
|
125
|
+
.cpub-legal {
|
|
126
|
+
max-width: 740px;
|
|
127
|
+
margin: 0 auto;
|
|
128
|
+
padding: 48px 24px 80px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.cpub-legal-header {
|
|
132
|
+
margin-bottom: 40px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.cpub-legal-title {
|
|
136
|
+
font-size: 28px;
|
|
137
|
+
font-weight: 700;
|
|
138
|
+
margin-bottom: 8px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.cpub-legal-updated {
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
color: var(--text-faint);
|
|
144
|
+
font-family: var(--font-mono);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.cpub-legal-body {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 32px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.cpub-legal-section h2 {
|
|
154
|
+
font-size: 16px;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
margin-bottom: 12px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.cpub-legal-section p {
|
|
160
|
+
font-size: 14px;
|
|
161
|
+
line-height: 1.7;
|
|
162
|
+
color: var(--text-dim);
|
|
163
|
+
margin-bottom: 8px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.cpub-legal-section ul {
|
|
167
|
+
padding-left: 20px;
|
|
168
|
+
margin: 8px 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.cpub-legal-section li {
|
|
172
|
+
font-size: 14px;
|
|
173
|
+
line-height: 1.7;
|
|
174
|
+
color: var(--text-dim);
|
|
175
|
+
margin-bottom: 4px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.cpub-legal-section strong {
|
|
179
|
+
color: var(--text);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.cpub-legal-section code {
|
|
183
|
+
font-family: var(--font-mono);
|
|
184
|
+
font-size: 12px;
|
|
185
|
+
padding: 1px 5px;
|
|
186
|
+
background: var(--surface2);
|
|
187
|
+
border: var(--border-width-default) solid var(--border);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.cpub-legal-section a {
|
|
191
|
+
color: var(--accent);
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.cpub-legal-section a:hover {
|
|
196
|
+
text-decoration: underline;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@media (max-width: 640px) {
|
|
200
|
+
.cpub-legal {
|
|
201
|
+
padding: 24px 16px 60px;
|
|
202
|
+
}
|
|
203
|
+
.cpub-legal-title {
|
|
204
|
+
font-size: 22px;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
@@ -86,6 +86,14 @@ async function handleDeleteAccount(): Promise<void> {
|
|
|
86
86
|
</button>
|
|
87
87
|
</form>
|
|
88
88
|
|
|
89
|
+
<div class="cpub-form-group">
|
|
90
|
+
<label class="cpub-form-label">Your Data</label>
|
|
91
|
+
<p class="cpub-form-hint cpub-mb-2">Download a copy of all your data in JSON format.</p>
|
|
92
|
+
<a href="/api/auth/export-data" download class="cpub-btn cpub-btn-sm">
|
|
93
|
+
<i class="fa-solid fa-download"></i> Download My Data
|
|
94
|
+
</a>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
89
97
|
<hr class="cpub-danger-divider" />
|
|
90
98
|
|
|
91
99
|
<div>
|
|
@@ -126,6 +134,7 @@ async function handleDeleteAccount(): Promise<void> {
|
|
|
126
134
|
|
|
127
135
|
<style scoped>
|
|
128
136
|
.cpub-mt-2 { margin-top: var(--space-2); }
|
|
137
|
+
.cpub-mb-2 { margin-bottom: var(--space-2); }
|
|
129
138
|
|
|
130
139
|
.cpub-danger-divider {
|
|
131
140
|
border: none;
|
|
@@ -1,32 +1,112 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ middleware: 'auth' });
|
|
3
3
|
|
|
4
|
-
const { themeId
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const { themeId, isDark, instanceDefault, setDarkMode } = useTheme();
|
|
5
|
+
|
|
6
|
+
const familyLabels: Record<string, string> = {
|
|
7
|
+
base: 'Classic',
|
|
8
|
+
dark: 'Classic',
|
|
9
|
+
generics: 'Generics',
|
|
10
|
+
agora: 'Agora',
|
|
11
|
+
'agora-dark': 'Agora',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const currentFamily = computed(() => familyLabels[instanceDefault.value] ?? 'Classic');
|
|
15
|
+
|
|
16
|
+
function previewColors(dark: boolean): { bg: string; surface: string; accent: string; text: string; border: string } {
|
|
17
|
+
// Resolve what theme ID would be for this mode
|
|
18
|
+
const families: Record<string, { light: string; dark: string }> = {
|
|
19
|
+
classic: { light: 'base', dark: 'dark' },
|
|
20
|
+
agora: { light: 'agora', dark: 'agora-dark' },
|
|
21
|
+
generics: { light: 'generics', dark: 'generics' },
|
|
22
|
+
};
|
|
23
|
+
const familyMap: Record<string, string> = {
|
|
24
|
+
base: 'classic', dark: 'classic', generics: 'generics',
|
|
25
|
+
agora: 'agora', 'agora-dark': 'agora',
|
|
26
|
+
};
|
|
27
|
+
const family = familyMap[instanceDefault.value] ?? 'classic';
|
|
28
|
+
const id = dark ? families[family]!.dark : families[family]!.light;
|
|
29
|
+
|
|
30
|
+
const palette: 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
|
+
return palette[id] ?? palette.base!;
|
|
38
|
+
}
|
|
10
39
|
</script>
|
|
11
40
|
|
|
12
41
|
<template>
|
|
13
42
|
<div>
|
|
14
43
|
<h2 class="cpub-section-title-lg">Appearance</h2>
|
|
15
44
|
|
|
16
|
-
<
|
|
45
|
+
<p class="cpub-appearance-note">
|
|
46
|
+
Your instance uses the <strong>{{ currentFamily }}</strong> theme.
|
|
47
|
+
Choose your preferred color scheme.
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<div class="cpub-scheme-grid">
|
|
51
|
+
<button
|
|
52
|
+
class="cpub-scheme-card"
|
|
53
|
+
:class="{ active: !isDark }"
|
|
54
|
+
@click="setDarkMode(false)"
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
class="cpub-scheme-preview"
|
|
58
|
+
:style="{
|
|
59
|
+
backgroundColor: previewColors(false).bg,
|
|
60
|
+
borderColor: previewColors(false).border,
|
|
61
|
+
}"
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
class="cpub-scheme-preview-card"
|
|
65
|
+
:style="{
|
|
66
|
+
backgroundColor: previewColors(false).surface,
|
|
67
|
+
borderColor: previewColors(false).border,
|
|
68
|
+
boxShadow: `3px 3px 0 ${previewColors(false).border}`,
|
|
69
|
+
}"
|
|
70
|
+
>
|
|
71
|
+
<div class="cpub-preview-heading" :style="{ backgroundColor: previewColors(false).text, opacity: 0.8 }"></div>
|
|
72
|
+
<div class="cpub-preview-line" :style="{ backgroundColor: previewColors(false).text, opacity: 0.3 }"></div>
|
|
73
|
+
<div class="cpub-preview-line short" :style="{ backgroundColor: previewColors(false).text, opacity: 0.2 }"></div>
|
|
74
|
+
<div class="cpub-preview-btn" :style="{ backgroundColor: previewColors(false).accent }"></div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="cpub-scheme-info">
|
|
78
|
+
<span class="cpub-scheme-name">Light</span>
|
|
79
|
+
</div>
|
|
80
|
+
</button>
|
|
81
|
+
|
|
17
82
|
<button
|
|
18
|
-
|
|
19
|
-
:
|
|
20
|
-
|
|
21
|
-
:class="{ active: theme === t.id }"
|
|
22
|
-
@click="setTheme(t.id)"
|
|
83
|
+
class="cpub-scheme-card"
|
|
84
|
+
:class="{ active: isDark }"
|
|
85
|
+
@click="setDarkMode(true)"
|
|
23
86
|
>
|
|
24
|
-
<div
|
|
25
|
-
|
|
87
|
+
<div
|
|
88
|
+
class="cpub-scheme-preview"
|
|
89
|
+
:style="{
|
|
90
|
+
backgroundColor: previewColors(true).bg,
|
|
91
|
+
borderColor: previewColors(true).border,
|
|
92
|
+
}"
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
class="cpub-scheme-preview-card"
|
|
96
|
+
:style="{
|
|
97
|
+
backgroundColor: previewColors(true).surface,
|
|
98
|
+
borderColor: previewColors(true).border,
|
|
99
|
+
boxShadow: `3px 3px 0 ${previewColors(true).border}`,
|
|
100
|
+
}"
|
|
101
|
+
>
|
|
102
|
+
<div class="cpub-preview-heading" :style="{ backgroundColor: previewColors(true).text, opacity: 0.8 }"></div>
|
|
103
|
+
<div class="cpub-preview-line" :style="{ backgroundColor: previewColors(true).text, opacity: 0.3 }"></div>
|
|
104
|
+
<div class="cpub-preview-line short" :style="{ backgroundColor: previewColors(true).text, opacity: 0.2 }"></div>
|
|
105
|
+
<div class="cpub-preview-btn" :style="{ backgroundColor: previewColors(true).accent }"></div>
|
|
106
|
+
</div>
|
|
26
107
|
</div>
|
|
27
|
-
<div class="cpub-
|
|
28
|
-
<span class="cpub-
|
|
29
|
-
<span class="cpub-theme-desc">{{ t.desc }}</span>
|
|
108
|
+
<div class="cpub-scheme-info">
|
|
109
|
+
<span class="cpub-scheme-name">Dark</span>
|
|
30
110
|
</div>
|
|
31
111
|
</button>
|
|
32
112
|
</div>
|
|
@@ -34,48 +114,73 @@ const themes = [
|
|
|
34
114
|
</template>
|
|
35
115
|
|
|
36
116
|
<style scoped>
|
|
37
|
-
.cpub-
|
|
117
|
+
.cpub-appearance-note {
|
|
118
|
+
font-size: var(--text-sm);
|
|
119
|
+
color: var(--text-dim);
|
|
120
|
+
margin-bottom: var(--space-4);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.cpub-scheme-grid {
|
|
38
124
|
display: grid;
|
|
39
|
-
grid-template-columns: repeat(
|
|
40
|
-
gap:
|
|
125
|
+
grid-template-columns: repeat(2, 1fr);
|
|
126
|
+
gap: var(--space-3);
|
|
127
|
+
max-width: 440px;
|
|
41
128
|
}
|
|
42
129
|
|
|
43
|
-
.cpub-
|
|
130
|
+
.cpub-scheme-card {
|
|
44
131
|
background: var(--surface);
|
|
45
132
|
border: var(--border-width-default) solid var(--border2);
|
|
46
|
-
padding:
|
|
133
|
+
padding: 0;
|
|
47
134
|
cursor: pointer;
|
|
48
135
|
text-align: left;
|
|
136
|
+
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
|
49
137
|
}
|
|
50
138
|
|
|
51
|
-
.cpub-
|
|
139
|
+
.cpub-scheme-card.active {
|
|
52
140
|
border-color: var(--accent);
|
|
53
141
|
box-shadow: var(--shadow-accent);
|
|
54
142
|
}
|
|
55
143
|
|
|
56
|
-
.cpub-
|
|
144
|
+
.cpub-scheme-card:hover {
|
|
57
145
|
border-color: var(--border);
|
|
146
|
+
transform: translate(-1px, -1px);
|
|
147
|
+
box-shadow: var(--shadow-sm);
|
|
58
148
|
}
|
|
59
149
|
|
|
60
|
-
.cpub-
|
|
61
|
-
height:
|
|
62
|
-
|
|
63
|
-
border: var(--border-width-default) solid var(--border2);
|
|
64
|
-
|
|
150
|
+
.cpub-scheme-preview {
|
|
151
|
+
height: 80px;
|
|
152
|
+
padding: var(--space-3);
|
|
153
|
+
border-bottom: var(--border-width-default) solid var(--border2);
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: center;
|
|
65
157
|
}
|
|
66
158
|
|
|
67
|
-
.cpub-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
159
|
+
.cpub-scheme-preview-card {
|
|
160
|
+
width: 75%;
|
|
161
|
+
padding: var(--space-2);
|
|
162
|
+
border-width: 2px;
|
|
163
|
+
border-style: solid;
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 3px;
|
|
71
167
|
}
|
|
72
168
|
|
|
73
|
-
.cpub-
|
|
74
|
-
|
|
75
|
-
|
|
169
|
+
.cpub-preview-heading { height: 5px; width: 55%; }
|
|
170
|
+
.cpub-preview-line { height: 3px; width: 85%; }
|
|
171
|
+
.cpub-preview-line.short { width: 45%; }
|
|
172
|
+
.cpub-preview-btn { height: 12px; width: 35%; margin-top: 3px; }
|
|
173
|
+
|
|
174
|
+
.cpub-scheme-info {
|
|
175
|
+
padding: var(--space-3);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.cpub-scheme-name {
|
|
179
|
+
font-size: var(--text-sm);
|
|
180
|
+
font-weight: var(--font-weight-semibold);
|
|
76
181
|
}
|
|
77
182
|
|
|
78
|
-
@media (max-width:
|
|
79
|
-
.cpub-
|
|
183
|
+
@media (max-width: 480px) {
|
|
184
|
+
.cpub-scheme-grid { grid-template-columns: 1fr; }
|
|
80
185
|
}
|
|
81
186
|
</style>
|