@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.
@@ -0,0 +1,168 @@
1
+ <script setup lang="ts">
2
+ useSeoMeta({
3
+ title: `Terms of Service — ${useSiteName()}`,
4
+ description: 'Terms and conditions for using this platform.',
5
+ });
6
+
7
+ const siteName = useSiteName();
8
+ </script>
9
+
10
+ <template>
11
+ <div class="cpub-legal">
12
+ <div class="cpub-legal-header">
13
+ <h1 class="cpub-legal-title">Terms of Service</h1>
14
+ <p class="cpub-legal-updated">Last updated: April 2026</p>
15
+ </div>
16
+
17
+ <div class="cpub-legal-body">
18
+ <section class="cpub-legal-section">
19
+ <h2>1. Acceptance</h2>
20
+ <p>By creating an account or using {{ siteName }}, you agree to these Terms of Service and our <NuxtLink to="/privacy">Privacy Policy</NuxtLink>. If you do not agree, do not use the service.</p>
21
+ </section>
22
+
23
+ <section class="cpub-legal-section">
24
+ <h2>2. Your Account</h2>
25
+ <ul>
26
+ <li>You must provide accurate information when creating an account.</li>
27
+ <li>You are responsible for keeping your login credentials secure.</li>
28
+ <li>You must be at least 16 years old to create an account (or the minimum age required by your jurisdiction).</li>
29
+ <li>One person, one account. Automated or bot accounts require prior approval from the instance administrator.</li>
30
+ </ul>
31
+ </section>
32
+
33
+ <section class="cpub-legal-section">
34
+ <h2>3. Your Content</h2>
35
+ <p>You retain ownership of content you create on {{ siteName }}. By publishing content, you grant us a license to display, distribute, and federate it as part of operating the platform.</p>
36
+ <ul>
37
+ <li>You must have the right to publish any content you upload (no plagiarism, no copyright infringement).</li>
38
+ <li>Content you import must be your own original work.</li>
39
+ <li>You may delete your content at any time. Federated copies on remote instances are outside our control.</li>
40
+ <li>We may remove content that violates these terms or applicable law.</li>
41
+ </ul>
42
+ </section>
43
+
44
+ <section class="cpub-legal-section">
45
+ <h2>4. Acceptable Use</h2>
46
+ <p>You agree not to:</p>
47
+ <ul>
48
+ <li>Post illegal content or content that infringes others' rights</li>
49
+ <li>Harass, threaten, or abuse other users</li>
50
+ <li>Spam, phish, or distribute malware</li>
51
+ <li>Attempt to gain unauthorized access to the platform or other users' accounts</li>
52
+ <li>Scrape, crawl, or automatically collect data from the platform beyond what public APIs allow</li>
53
+ <li>Impersonate another person or entity</li>
54
+ <li>Use the platform for commercial advertising without permission</li>
55
+ </ul>
56
+ </section>
57
+
58
+ <section class="cpub-legal-section">
59
+ <h2>5. Moderation</h2>
60
+ <p>The instance administrator may, at their discretion:</p>
61
+ <ul>
62
+ <li>Remove content that violates these terms</li>
63
+ <li>Suspend or delete accounts that violate these terms</li>
64
+ <li>Block federation with other instances</li>
65
+ </ul>
66
+ <p>We aim to handle moderation decisions transparently and fairly.</p>
67
+ </section>
68
+
69
+ <section class="cpub-legal-section">
70
+ <h2>6. Availability and Changes</h2>
71
+ <p>We provide {{ siteName }} on an "as is" basis. We do not guarantee uninterrupted availability. We may modify or discontinue the service at any time.</p>
72
+ <p>These terms may be updated. Continued use after changes constitutes acceptance.</p>
73
+ </section>
74
+
75
+ <section class="cpub-legal-section">
76
+ <h2>7. Limitation of Liability</h2>
77
+ <p>To the fullest extent permitted by law, the instance operator is not liable for any indirect, incidental, or consequential damages arising from your use of the platform. This includes data loss, service interruptions, or actions of other users or federated instances.</p>
78
+ </section>
79
+
80
+ <section class="cpub-legal-section">
81
+ <h2>8. Account Deletion</h2>
82
+ <p>You may delete your account at any time from your <NuxtLink to="/settings/account">account settings</NuxtLink>. Account deletion is permanent and removes all your data, content, and activity from this instance. See our <NuxtLink to="/privacy">Privacy Policy</NuxtLink> for details on data retention and federation.</p>
83
+ </section>
84
+
85
+ <section class="cpub-legal-section">
86
+ <h2>9. Contact</h2>
87
+ <p>For questions about these terms, contact the administrator of this {{ siteName }} instance.</p>
88
+ </section>
89
+ </div>
90
+ </div>
91
+ </template>
92
+
93
+ <style scoped>
94
+ .cpub-legal {
95
+ max-width: 740px;
96
+ margin: 0 auto;
97
+ padding: 48px 24px 80px;
98
+ }
99
+
100
+ .cpub-legal-header {
101
+ margin-bottom: 40px;
102
+ }
103
+
104
+ .cpub-legal-title {
105
+ font-size: 28px;
106
+ font-weight: 700;
107
+ margin-bottom: 8px;
108
+ }
109
+
110
+ .cpub-legal-updated {
111
+ font-size: 12px;
112
+ color: var(--text-faint);
113
+ font-family: var(--font-mono);
114
+ }
115
+
116
+ .cpub-legal-body {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 32px;
120
+ }
121
+
122
+ .cpub-legal-section h2 {
123
+ font-size: 16px;
124
+ font-weight: 600;
125
+ margin-bottom: 12px;
126
+ }
127
+
128
+ .cpub-legal-section p {
129
+ font-size: 14px;
130
+ line-height: 1.7;
131
+ color: var(--text-dim);
132
+ margin-bottom: 8px;
133
+ }
134
+
135
+ .cpub-legal-section ul {
136
+ padding-left: 20px;
137
+ margin: 8px 0;
138
+ }
139
+
140
+ .cpub-legal-section li {
141
+ font-size: 14px;
142
+ line-height: 1.7;
143
+ color: var(--text-dim);
144
+ margin-bottom: 4px;
145
+ }
146
+
147
+ .cpub-legal-section strong {
148
+ color: var(--text);
149
+ }
150
+
151
+ .cpub-legal-section a {
152
+ color: var(--accent);
153
+ text-decoration: none;
154
+ }
155
+
156
+ .cpub-legal-section a:hover {
157
+ text-decoration: underline;
158
+ }
159
+
160
+ @media (max-width: 640px) {
161
+ .cpub-legal {
162
+ padding: 24px 16px 60px;
163
+ }
164
+ .cpub-legal-title {
165
+ font-size: 22px;
166
+ }
167
+ }
168
+ </style>
@@ -0,0 +1,32 @@
1
+ // Theme plugin — resolves theme on server (zero flash) and hydrates on client.
2
+ //
3
+ // The admin picks the instance theme (family + default mode).
4
+ // Users only toggle light/dark. Server middleware resolves the final theme ID.
5
+ // This plugin reads that resolved value and sets data-theme on <html>.
6
+
7
+ export default defineNuxtPlugin(() => {
8
+ const themeId = useState<string>('cpub-theme', () => 'base');
9
+ const instanceTheme = useState<string>('cpub-instance-theme', () => 'base');
10
+ const isDark = useState<boolean>('cpub-dark-mode', () => false);
11
+
12
+ if (import.meta.server) {
13
+ const event = useRequestEvent();
14
+ if (event?.context) {
15
+ themeId.value = event.context.resolvedTheme ?? 'base';
16
+ instanceTheme.value = event.context.instanceTheme ?? 'base';
17
+ isDark.value = event.context.isDarkMode ?? false;
18
+ }
19
+ }
20
+
21
+ if (import.meta.client) {
22
+ // One-time migration: clear old localStorage theme (replaced by server-side resolution)
23
+ localStorage.removeItem('cpub-theme');
24
+ }
25
+
26
+ // Set data-theme on <html> during SSR — first paint has the correct theme
27
+ if (themeId.value && themeId.value !== 'base') {
28
+ useHead({
29
+ htmlAttrs: { 'data-theme': themeId.value },
30
+ });
31
+ }
32
+ });
@@ -6,11 +6,15 @@ export default defineEventHandler(async (event) => {
6
6
  const db = useDB();
7
7
  const config = useConfig();
8
8
 
9
- // Get DB-stored settings
9
+ // Get DB-stored settings (returns Map — convert to plain object)
10
10
  const dbSettings = await getInstanceSettings(db);
11
+ const stored: Record<string, unknown> = {};
12
+ for (const [key, value] of dbSettings) {
13
+ stored[key] = value;
14
+ }
11
15
 
12
16
  // Merge with running config defaults so the UI shows actual values
13
- const defaults: Record<string, string> = {
17
+ const defaults: Record<string, unknown> = {
14
18
  'instance.name': config.instance.name,
15
19
  'instance.description': config.instance.description,
16
20
  'instance.registrationOpen': 'true',
@@ -18,5 +22,5 @@ export default defineEventHandler(async (event) => {
18
22
  'instance.contactEmail': config.instance.contactEmail ?? '',
19
23
  };
20
24
 
21
- return { ...defaults, ...dbSettings };
25
+ return { ...defaults, ...stored };
22
26
  });
@@ -7,5 +7,10 @@ export default defineEventHandler(async (event): Promise<void> => {
7
7
  const db = useDB();
8
8
  const input = await parseBody(event, adminSettingSchema);
9
9
 
10
- return setInstanceSetting(db, input.key, input.value, admin.id);
10
+ await setInstanceSetting(db, input.key, input.value, admin.id);
11
+
12
+ // Invalidate server-side theme cache when the default changes
13
+ if (input.key === 'theme.default') {
14
+ invalidateThemeCache();
15
+ }
11
16
  });
@@ -0,0 +1,55 @@
1
+ import { deleteUser, federateDelete } from '@commonpub/server';
2
+ import { contentItems } from '@commonpub/schema';
3
+ import { eq, and } from 'drizzle-orm';
4
+
5
+ export default defineEventHandler(async (event): Promise<{ success: true }> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const config = useConfig();
9
+
10
+ // Prevent deleting the last admin
11
+ if (user.role === 'admin') {
12
+ const { users } = await import('@commonpub/schema');
13
+ const admins = await db
14
+ .select({ id: users.id })
15
+ .from(users)
16
+ .where(eq(users.role, 'admin'))
17
+ .limit(2);
18
+ if (admins.length <= 1) {
19
+ throw createError({
20
+ statusCode: 400,
21
+ statusMessage: 'Cannot delete the only admin account',
22
+ });
23
+ }
24
+ }
25
+
26
+ // Federation cleanup: send Delete activities for published content
27
+ if (config.features.federation) {
28
+ const domain = config.instance.domain;
29
+ if (domain) {
30
+ const published = await db
31
+ .select({ id: contentItems.id })
32
+ .from(contentItems)
33
+ .where(and(
34
+ eq(contentItems.authorId, user.id),
35
+ eq(contentItems.status, 'published'),
36
+ ));
37
+
38
+ for (const item of published) {
39
+ try {
40
+ await federateDelete(db, item.id, domain, user.username);
41
+ } catch {
42
+ // Best-effort — don't block deletion if federation fails
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ // Delete the user (cascades to all related data)
49
+ await deleteUser(db, user.id, user.id);
50
+
51
+ // Clear the session cookie
52
+ deleteCookie(event, 'better-auth.session_token', { path: '/' });
53
+
54
+ return { success: true };
55
+ });
@@ -0,0 +1,15 @@
1
+ import { exportUserData } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+
7
+ const data = await exportUserData(db, user.id);
8
+
9
+ const filename = `commonpub-export-${user.username}-${new Date().toISOString().split('T')[0]}.json`;
10
+
11
+ setHeader(event, 'Content-Type', 'application/json');
12
+ setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`);
13
+
14
+ return data;
15
+ });
@@ -0,0 +1,34 @@
1
+ // Resolves the active theme for SSR and injects it into the event context.
2
+ // Runs before page rendering so the theme plugin can set data-theme on <html>.
3
+ //
4
+ // Admin picks instance theme → user only toggles light/dark → server resolves.
5
+
6
+ declare module 'h3' {
7
+ interface H3EventContext {
8
+ /** Final resolved theme ID for this request */
9
+ resolvedTheme: string;
10
+ /** The admin-configured instance default theme */
11
+ instanceTheme: string;
12
+ /** Whether the resolved theme is dark */
13
+ isDarkMode: boolean;
14
+ }
15
+ }
16
+
17
+ export default defineEventHandler(async (event) => {
18
+ // Only resolve theme for SSR page requests — skip API, assets, and internal routes
19
+ const path = getRequestURL(event).pathname;
20
+ if (path.startsWith('/api') || path.startsWith('/_nuxt') || path.startsWith('/__nuxt')) return;
21
+
22
+ const instanceTheme = await getInstanceDefaultTheme();
23
+ event.context.instanceTheme = instanceTheme;
24
+
25
+ // Read user's light/dark preference from cookie
26
+ const schemeCookie = getCookie(event, 'cpub-color-scheme');
27
+ const userScheme = schemeCookie === 'light' || schemeCookie === 'dark'
28
+ ? schemeCookie
29
+ : null;
30
+
31
+ const resolved = resolveThemeForUser(instanceTheme, userScheme);
32
+ event.context.resolvedTheme = resolved;
33
+ event.context.isDarkMode = isDefaultDark(resolved);
34
+ });
@@ -0,0 +1,67 @@
1
+ // Cached instance-level theme resolution for SSR.
2
+ // The admin sets a theme (determines family + default mode).
3
+ // Users only toggle light/dark within that family.
4
+
5
+ import { eq } from 'drizzle-orm';
6
+ import { instanceSettings } from '@commonpub/schema';
7
+ import { THEME_TO_FAMILY, FAMILY_VARIANTS, IS_DARK, VALID_THEME_IDS } from '../../utils/themeConfig';
8
+
9
+ const CACHE_TTL = 60_000; // 1 minute
10
+
11
+ let cached: string | null = null;
12
+ let cacheTime = 0;
13
+
14
+ export async function getInstanceDefaultTheme(): Promise<string> {
15
+ const now = Date.now();
16
+ if (cached !== null && now - cacheTime < CACHE_TTL) return cached;
17
+
18
+ try {
19
+ const db = useDB();
20
+ const [row] = await db
21
+ .select({ value: instanceSettings.value })
22
+ .from(instanceSettings)
23
+ .where(eq(instanceSettings.key, 'theme.default'));
24
+
25
+ if (row?.value && typeof row.value === 'string' && VALID_THEME_IDS.has(row.value)) {
26
+ cached = row.value;
27
+ } else {
28
+ cached = 'base';
29
+ }
30
+ } catch (err) {
31
+ console.warn('[theme] Failed to read instance theme from DB, using fallback:', err instanceof Error ? err.message : String(err));
32
+ cached = 'base';
33
+ }
34
+
35
+ cacheTime = now;
36
+ return cached;
37
+ }
38
+
39
+ /**
40
+ * Resolve the final theme ID given the admin's chosen default and the
41
+ * user's light/dark preference.
42
+ */
43
+ export function resolveThemeForUser(
44
+ adminTheme: string,
45
+ userScheme: 'light' | 'dark' | null,
46
+ ): string {
47
+ const family = THEME_TO_FAMILY[adminTheme] ?? 'classic';
48
+ const variants = FAMILY_VARIANTS[family] ?? FAMILY_VARIANTS.classic!;
49
+
50
+ if (userScheme === null) return adminTheme;
51
+ return userScheme === 'dark' ? variants.dark : variants.light;
52
+ }
53
+
54
+ /** Whether a given theme ID is dark */
55
+ export function isDefaultDark(themeId: string): boolean {
56
+ return IS_DARK[themeId] ?? false;
57
+ }
58
+
59
+ /** Call after admin changes the instance default theme */
60
+ export function invalidateThemeCache(): void {
61
+ cached = null;
62
+ cacheTime = 0;
63
+ }
64
+
65
+ export function isServerValidThemeId(id: string): boolean {
66
+ return VALID_THEME_IDS.has(id);
67
+ }
@@ -0,0 +1,157 @@
1
+ @layer commonpub {
2
+ /* ===========================================
3
+ CommonPub Agora Theme — Dark (Grove)
4
+ Warm dark surfaces with green-tinted blacks,
5
+ green accent, serif display font.
6
+ =========================================== */
7
+
8
+ [data-theme="agora-dark"] {
9
+ /* === SURFACES (grove-tinted warm darks) === */
10
+ --bg: #0d1a12;
11
+ --surface: #141f17;
12
+ --surface2: #1a261e;
13
+ --surface3: #213028;
14
+
15
+ --color-surface: var(--surface);
16
+ --color-surface-alt: var(--surface2);
17
+ --color-surface-raised: var(--surface2);
18
+ --color-surface-overlay: rgba(0, 0, 0, 0.7);
19
+ --color-surface-overlay-light: rgba(0, 0, 0, 0.6);
20
+ --color-surface-scrim: rgba(13, 26, 18, 0.85);
21
+ --color-surface-hover: var(--surface3);
22
+ --color-bg-subtle: var(--bg);
23
+
24
+ /* === TEXT (warm light on dark) === */
25
+ --text: #e8e8e2;
26
+ --text-dim: #b0b0a6;
27
+ --text-faint: #7a7a72;
28
+
29
+ --color-text: var(--text);
30
+ --color-text-secondary: var(--text-dim);
31
+ --color-text-muted: var(--text-faint);
32
+ --color-text-inverse: #0d1a12;
33
+ --color-on-accent: #ffffff;
34
+ --color-on-primary: #ffffff;
35
+ --color-primary-hover: #4aa06e;
36
+
37
+ /* === BORDERS (grove-tinted) === */
38
+ --border: #3a4f40;
39
+ --border2: #2a3a2f;
40
+
41
+ --color-border: var(--border2);
42
+ --color-border-strong: var(--border);
43
+ --color-border-focus: var(--accent);
44
+
45
+ /* === ACCENT (commons green, brighter for dark bg) === */
46
+ --accent: #4aa06e;
47
+ --accent-bg: rgba(74, 160, 110, 0.12);
48
+ --accent-bg-strong: rgba(74, 160, 110, 0.25);
49
+ --accent-bg-heavy: rgba(74, 160, 110, 0.45);
50
+ --accent-bg-solid: rgba(74, 160, 110, 0.65);
51
+ --accent-border: rgba(74, 160, 110, 0.3);
52
+ --accent-focus-ring: 0 0 0 3px rgba(74, 160, 110, 0.2);
53
+ --shadow-accent: 4px 4px 0 var(--accent);
54
+
55
+ --color-primary: var(--accent);
56
+ --color-accent: var(--accent);
57
+ --color-accent-hover: #5ab87e;
58
+ --color-accent-text: #0d1a12;
59
+ --color-primary-text: #ffffff;
60
+ --color-accent-bg: var(--accent-bg);
61
+ --color-accent-border: var(--accent-border);
62
+
63
+ /* === SEMANTIC COLORS (brighter for dark bg) === */
64
+ --green: #4aa06e;
65
+ --green-bg: rgba(74, 160, 110, 0.12);
66
+ --green-border: rgba(74, 160, 110, 0.3);
67
+
68
+ --yellow: #daa040;
69
+ --yellow-bg: rgba(218, 160, 64, 0.12);
70
+ --yellow-border: rgba(218, 160, 64, 0.3);
71
+
72
+ --red: #d85e55;
73
+ --red-bg: rgba(216, 94, 85, 0.12);
74
+ --red-border: rgba(216, 94, 85, 0.3);
75
+
76
+ --purple: #9a7ec4;
77
+ --purple-bg: rgba(154, 126, 196, 0.12);
78
+ --purple-border: rgba(154, 126, 196, 0.3);
79
+
80
+ --teal: #3dbcab;
81
+ --teal-bg: rgba(61, 188, 171, 0.12);
82
+ --teal-border: rgba(61, 188, 171, 0.3);
83
+
84
+ --pink: #d07090;
85
+ --pink-bg: rgba(208, 112, 144, 0.12);
86
+ --pink-border: rgba(208, 112, 144, 0.3);
87
+
88
+ --color-success: var(--green);
89
+ --color-warning: var(--yellow);
90
+ --color-error: var(--red);
91
+ --color-info: #6a9fd0;
92
+ --color-success-bg: var(--green-bg);
93
+ --color-warning-bg: var(--yellow-bg);
94
+ --color-error-bg: var(--red-bg);
95
+ --color-info-bg: rgba(106, 159, 208, 0.12);
96
+
97
+ /* === OVERLAYS === */
98
+ --color-badge-overlay: rgba(0, 0, 0, 0.75);
99
+
100
+ /* === INTERACTIVE === */
101
+ --color-link: var(--accent);
102
+ --color-link-hover: #5ab87e;
103
+
104
+ /* === TYPOGRAPHY (warm, distinctive) === */
105
+ --font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
106
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
107
+ --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
108
+
109
+ --font-heading: var(--font-display);
110
+ --font-body: var(--font-sans);
111
+
112
+ /* Font Sizes (Agora scale) */
113
+ --text-xs: 0.6875rem;
114
+ --text-sm: 0.8125rem;
115
+ --text-base: 0.9375rem;
116
+ --text-md: 1.0625rem;
117
+ --text-lg: 1.25rem;
118
+ --text-xl: 1.5rem;
119
+ --text-2xl: 1.875rem;
120
+ --text-3xl: 2.375rem;
121
+ --text-4xl: 3rem;
122
+ --text-5xl: 4rem;
123
+ --text-label: 0.625rem;
124
+
125
+ /* Line Heights */
126
+ --leading-tight: 1.15;
127
+ --leading-snug: 1.35;
128
+ --leading-normal: 1.7;
129
+ --leading-relaxed: 1.9;
130
+
131
+ /* Letter Spacing */
132
+ --tracking-tight: -0.02em;
133
+ --tracking-normal: 0;
134
+ --tracking-wide: 0.06em;
135
+ --tracking-wider: 0.1em;
136
+ --tracking-widest: 0.16em;
137
+
138
+ /* === SHADOWS (softer for dark mode) === */
139
+ --shadow-sm: 2px 2px 0 rgba(0, 0, 0, 0.4);
140
+ --shadow-md: 4px 4px 0 rgba(0, 0, 0, 0.4);
141
+ --shadow-lg: 6px 6px 0 rgba(0, 0, 0, 0.4);
142
+ --shadow-xl: 8px 8px 0 rgba(0, 0, 0, 0.4);
143
+
144
+ /* === FOCUS === */
145
+ --focus-ring: var(--shadow-accent);
146
+
147
+ color-scheme: dark;
148
+ }
149
+
150
+ /* === CONTENT TYPE BADGE COLORS (Agora dark palette) === */
151
+ [data-theme="agora-dark"] [data-content-type="article"] { --badge-color: #6a9fd0; --badge-bg: rgba(106, 159, 208, 0.12); }
152
+ [data-theme="agora-dark"] [data-content-type="blog"] { --badge-color: #4aa06e; --badge-bg: rgba(74, 160, 110, 0.12); }
153
+ [data-theme="agora-dark"] [data-content-type="project"] { --badge-color: #9a7ec4; --badge-bg: rgba(154, 126, 196, 0.12); }
154
+ [data-theme="agora-dark"] [data-content-type="explainer"] { --badge-color: #3dbcab; --badge-bg: rgba(61, 188, 171, 0.12); }
155
+ [data-theme="agora-dark"] [data-content-type="video"] { --badge-color: #d85e55; --badge-bg: rgba(216, 94, 85, 0.12); }
156
+ [data-theme="agora-dark"] [data-content-type="tutorial"] { --badge-color: #daa040; --badge-bg: rgba(218, 160, 64, 0.12); }
157
+ }