@commonpub/layer 0.61.0 → 0.63.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/nuxt.config.ts +3 -1
- package/package.json +8 -8
- package/server/api/public/v1/metrics/timeseries.get.ts +41 -0
- package/server/api/public/v1/openapi.json.get.ts +3 -0
- package/server/plugins/metrics-rollup.ts +54 -0
- package/server/utils/instanceTheme.ts +6 -2
- package/theme/stoa-dark.css +153 -0
- package/theme/stoa.css +361 -0
- package/utils/themeConfig.ts +5 -0
package/nuxt.config.ts
CHANGED
|
@@ -37,7 +37,7 @@ export default defineNuxtConfig({
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
rel: 'stylesheet',
|
|
40
|
-
href: 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Work+Sans:ital,wght@0,300..800;1,300..800&display=swap',
|
|
40
|
+
href: 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Newsreader:ital,opsz,wght@0,6..72,300..700;1,6..72,300..700&family=Work+Sans:ital,wght@0,300..800;1,300..800&display=swap',
|
|
41
41
|
},
|
|
42
42
|
],
|
|
43
43
|
},
|
|
@@ -48,6 +48,8 @@ export default defineNuxtConfig({
|
|
|
48
48
|
uiTheme('generics.css'),
|
|
49
49
|
uiTheme('agora.css'),
|
|
50
50
|
uiTheme('agora-dark.css'),
|
|
51
|
+
uiTheme('stoa.css'),
|
|
52
|
+
uiTheme('stoa-dark.css'),
|
|
51
53
|
uiTheme('components.css'),
|
|
52
54
|
uiTheme('prose.css'),
|
|
53
55
|
uiTheme('layouts.css'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
|
-
"@commonpub/editor": "0.7.11",
|
|
58
57
|
"@commonpub/config": "0.19.0",
|
|
59
|
-
"@commonpub/
|
|
58
|
+
"@commonpub/editor": "0.7.11",
|
|
59
|
+
"@commonpub/schema": "0.35.0",
|
|
60
|
+
"@commonpub/docs": "0.6.3",
|
|
60
61
|
"@commonpub/learning": "0.5.2",
|
|
61
|
-
"@commonpub/server": "2.
|
|
62
|
-
"@commonpub/ui": "0.
|
|
63
|
-
"@commonpub/
|
|
64
|
-
"@commonpub/
|
|
65
|
-
"@commonpub/docs": "0.6.3"
|
|
62
|
+
"@commonpub/server": "2.82.0",
|
|
63
|
+
"@commonpub/ui": "0.10.0",
|
|
64
|
+
"@commonpub/protocol": "0.13.0",
|
|
65
|
+
"@commonpub/explainer": "0.7.15"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getMetricsTimeseries, TIMESERIES_METRICS } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
const DAY_MS = 86_400_000;
|
|
6
|
+
|
|
7
|
+
const querySchema = z.object({
|
|
8
|
+
metric: z.enum(Object.keys(TIMESERIES_METRICS) as [string, ...string[]]),
|
|
9
|
+
interval: z.enum(['day', 'week', 'month']).default('day'),
|
|
10
|
+
from: z.string().regex(DAY).optional(),
|
|
11
|
+
to: z.string().regex(DAY).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/public/v1/metrics/timeseries
|
|
16
|
+
*
|
|
17
|
+
* Scope: read:analytics. Daily time-series from the `metrics_daily` rollups.
|
|
18
|
+
* `metric` is one of the registered series (users.total/new, content.total/new/
|
|
19
|
+
* views/likes/comments); `interval` buckets day|week|month. Defaults to the last
|
|
20
|
+
* 90 days. Engagement (views/likes/comments) series begin at the first rollup,
|
|
21
|
+
* surfaced via `since`.
|
|
22
|
+
*/
|
|
23
|
+
export default defineEventHandler(async (event) => {
|
|
24
|
+
requireApiScope(event, 'read:analytics');
|
|
25
|
+
const parsed = querySchema.safeParse(getQuery(event));
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
throw createError({ statusCode: 400, statusMessage: 'Invalid query parameters', data: parsed.error.flatten() });
|
|
28
|
+
}
|
|
29
|
+
const { metric, interval } = parsed.data;
|
|
30
|
+
|
|
31
|
+
// Default to the last 90 days; clamp the span to 2 years to bound the scan.
|
|
32
|
+
const todayMs = Date.now();
|
|
33
|
+
const to = parsed.data.to ?? new Date(todayMs).toISOString().slice(0, 10);
|
|
34
|
+
let from = parsed.data.from ?? new Date(todayMs - 90 * DAY_MS).toISOString().slice(0, 10);
|
|
35
|
+
if (from > to) from = to;
|
|
36
|
+
const minFrom = new Date(new Date(`${to}T00:00:00Z`).getTime() - 730 * DAY_MS).toISOString().slice(0, 10);
|
|
37
|
+
if (from < minFrom) from = minFrom;
|
|
38
|
+
|
|
39
|
+
const db = useDB();
|
|
40
|
+
return await getMetricsTimeseries(db, { metric, interval, from, to });
|
|
41
|
+
});
|
|
@@ -379,6 +379,9 @@ export default defineEventHandler((event) => {
|
|
|
379
379
|
'/metrics/engagement': {
|
|
380
380
|
get: { summary: 'Aggregate engagement ratios and funnels', security: [{ bearer: ['read:analytics'] }], responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object' } } } } } },
|
|
381
381
|
},
|
|
382
|
+
'/metrics/timeseries': {
|
|
383
|
+
get: { summary: 'Daily time-series from rollups', security: [{ bearer: ['read:analytics'] }], parameters: [{ name: 'metric', in: 'query', required: true, schema: { type: 'string', enum: ['users.total', 'users.new', 'content.total', 'content.new', 'content.views', 'content.likes', 'content.comments'] } }, { name: 'interval', in: 'query', schema: { type: 'string', enum: ['day', 'week', 'month'] } }, { name: 'from', in: 'query', schema: { type: 'string', format: 'date' } }, { name: 'to', in: 'query', schema: { type: 'string', format: 'date' } }], responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object' } } } } } },
|
|
384
|
+
},
|
|
382
385
|
'/metrics/federation': {
|
|
383
386
|
get: { summary: 'Federation reach (opt-in; read:federation)', security: [{ bearer: ['read:federation'] }], parameters: [{ name: 'limit', in: 'query', schema: { type: 'integer' } }], responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object' } } } }, '404': { description: 'Federation reach metrics not enabled', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } },
|
|
384
387
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily analytics rollup worker (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* When `features.publicApi` is on, snapshots aggregate metrics into
|
|
5
|
+
* `metrics_daily` so `GET /api/public/v1/metrics/timeseries` has history. On the
|
|
6
|
+
* first ever run (empty table) it backfills the deterministic count-based series
|
|
7
|
+
* from timestamps. Runs are idempotent (upsert per day), so the periodic
|
|
8
|
+
* interval just refreshes today's row. Aggregates only — no per-user data.
|
|
9
|
+
*/
|
|
10
|
+
import { runDailyRollup, backfillMetricsDaily } from '@commonpub/server';
|
|
11
|
+
import { metricsDaily } from '@commonpub/schema';
|
|
12
|
+
|
|
13
|
+
const ROLLUP_INTERVAL_MS = 6 * 60 * 60 * 1000; // every 6h; refreshes today's snapshot
|
|
14
|
+
|
|
15
|
+
export default defineNitroPlugin((nitro) => {
|
|
16
|
+
if (process.env.NODE_ENV === 'test') return;
|
|
17
|
+
|
|
18
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
19
|
+
|
|
20
|
+
const startupTimer = setTimeout(() => {
|
|
21
|
+
try {
|
|
22
|
+
const config = useConfig();
|
|
23
|
+
if (!config.features.publicApi) return; // no public API ⇒ no rollups needed
|
|
24
|
+
console.log(`[metrics-rollup] worker started (interval: ${ROLLUP_INTERVAL_MS}ms)`);
|
|
25
|
+
runRollup();
|
|
26
|
+
interval = setInterval(runRollup, ROLLUP_INTERVAL_MS);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('[metrics-rollup] failed to start:', err instanceof Error ? err.message : err);
|
|
29
|
+
}
|
|
30
|
+
}, 15_000); // stagger after federation/registry workers
|
|
31
|
+
|
|
32
|
+
async function runRollup(): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const db = useDB();
|
|
35
|
+
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD (UTC)
|
|
36
|
+
const [existing] = await db.select({ id: metricsDaily.id }).from(metricsDaily).limit(1);
|
|
37
|
+
if (!existing) {
|
|
38
|
+
const n = await backfillMetricsDaily(db);
|
|
39
|
+
console.log(`[metrics-rollup] backfilled ${n} historical rows`);
|
|
40
|
+
}
|
|
41
|
+
await runDailyRollup(db, today);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('[metrics-rollup] run error:', err instanceof Error ? err.message : err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
nitro.hooks.hook('close', () => {
|
|
48
|
+
clearTimeout(startupTimer);
|
|
49
|
+
if (interval) {
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
interval = null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -29,8 +29,12 @@ let cacheTime = 0;
|
|
|
29
29
|
async function loadThemeState(): Promise<CachedThemeState> {
|
|
30
30
|
const db = useDB();
|
|
31
31
|
|
|
32
|
-
// 1. Default theme ID
|
|
33
|
-
|
|
32
|
+
// 1. Default theme ID.
|
|
33
|
+
// Fallback is 'stoa' — the default CommonPub theme for fresh installs and any
|
|
34
|
+
// instance that has NOT explicitly set `theme.default` in the DB. Instances
|
|
35
|
+
// with an explicit setting (e.g. commonpub.io=agora-dark, branded instances)
|
|
36
|
+
// are unaffected; this only changes what an unconfigured instance shows.
|
|
37
|
+
let defaultTheme = 'stoa';
|
|
34
38
|
try {
|
|
35
39
|
const [row] = await db
|
|
36
40
|
.select({ value: instanceSettings.value })
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
@layer commonpub {
|
|
2
|
+
/* ===========================================
|
|
3
|
+
CommonPub Stoa Theme — Dark
|
|
4
|
+
"The same walk, by lamplight."
|
|
5
|
+
|
|
6
|
+
Deep warm-black paper, brighter moss accent,
|
|
7
|
+
the same Fraunces / Newsreader / Work Sans
|
|
8
|
+
type and soft rounded geometry as Stoa Light.
|
|
9
|
+
Component overrides are shared from stoa.css
|
|
10
|
+
(both data-theme values are targeted there).
|
|
11
|
+
=========================================== */
|
|
12
|
+
|
|
13
|
+
[data-theme="stoa-dark"] {
|
|
14
|
+
/* === SURFACES (warm near-black) === */
|
|
15
|
+
--bg: #15130d;
|
|
16
|
+
--surface: #1f1c14;
|
|
17
|
+
--surface2: #28241a;
|
|
18
|
+
--surface3: #332d20;
|
|
19
|
+
|
|
20
|
+
--color-surface: var(--surface);
|
|
21
|
+
--color-surface-alt: var(--surface2);
|
|
22
|
+
--color-surface-raised: var(--surface);
|
|
23
|
+
--color-surface-overlay: rgba(0, 0, 0, 0.6);
|
|
24
|
+
--color-surface-overlay-light: rgba(0, 0, 0, 0.45);
|
|
25
|
+
--color-surface-scrim: rgba(21, 19, 13, 0.8);
|
|
26
|
+
--color-surface-hover: var(--surface2);
|
|
27
|
+
--color-bg-subtle: var(--bg);
|
|
28
|
+
|
|
29
|
+
/* === TEXT (warm paper ink) === */
|
|
30
|
+
--text: #f1ebda;
|
|
31
|
+
--text-dim: #bcb4a2;
|
|
32
|
+
--text-faint: #8d8675;
|
|
33
|
+
|
|
34
|
+
--color-text: var(--text);
|
|
35
|
+
--color-text-secondary: var(--text-dim);
|
|
36
|
+
--color-text-muted: var(--text-faint);
|
|
37
|
+
--color-text-inverse: #15130d;
|
|
38
|
+
|
|
39
|
+
/* === BORDERS (warm hairlines) === */
|
|
40
|
+
--border: rgba(241, 235, 218, 0.24);
|
|
41
|
+
--border2: rgba(241, 235, 218, 0.13);
|
|
42
|
+
|
|
43
|
+
--color-border: var(--border2);
|
|
44
|
+
--color-border-strong: var(--border);
|
|
45
|
+
--color-border-focus: var(--accent);
|
|
46
|
+
|
|
47
|
+
/* === ACCENT (brighter moss for dark) === */
|
|
48
|
+
--accent: #5aa784;
|
|
49
|
+
--accent-bg: rgba(90, 167, 132, 0.14);
|
|
50
|
+
--accent-bg-strong: rgba(90, 167, 132, 0.24);
|
|
51
|
+
--accent-bg-heavy: rgba(90, 167, 132, 0.42);
|
|
52
|
+
--accent-bg-solid: rgba(90, 167, 132, 0.6);
|
|
53
|
+
--accent-border: rgba(90, 167, 132, 0.34);
|
|
54
|
+
--accent-focus-ring: 0 0 0 3px rgba(90, 167, 132, 0.2);
|
|
55
|
+
|
|
56
|
+
--color-primary: var(--accent);
|
|
57
|
+
--color-primary-hover: #79c4a0;
|
|
58
|
+
--color-primary-text: #0f140f;
|
|
59
|
+
--color-on-primary: #0f140f;
|
|
60
|
+
--color-accent: var(--accent);
|
|
61
|
+
--color-accent-hover: #79c4a0;
|
|
62
|
+
--color-accent-text: #0f140f;
|
|
63
|
+
--color-on-accent: #0f140f;
|
|
64
|
+
--color-accent-bg: var(--accent-bg);
|
|
65
|
+
--color-accent-border: var(--accent-border);
|
|
66
|
+
|
|
67
|
+
/* === SEMANTIC COLORS (dark warm) === */
|
|
68
|
+
--green: #5aa784;
|
|
69
|
+
--green-bg: rgba(90, 167, 132, 0.14);
|
|
70
|
+
--green-border: rgba(90, 167, 132, 0.32);
|
|
71
|
+
|
|
72
|
+
--yellow: #d6a259;
|
|
73
|
+
--yellow-bg: rgba(214, 162, 89, 0.13);
|
|
74
|
+
--yellow-border: rgba(214, 162, 89, 0.3);
|
|
75
|
+
|
|
76
|
+
--red: #d07c69;
|
|
77
|
+
--red-bg: rgba(208, 124, 105, 0.13);
|
|
78
|
+
--red-border: rgba(208, 124, 105, 0.3);
|
|
79
|
+
|
|
80
|
+
--purple: #a98cbe;
|
|
81
|
+
--purple-bg: rgba(169, 140, 190, 0.13);
|
|
82
|
+
--purple-border: rgba(169, 140, 190, 0.3);
|
|
83
|
+
|
|
84
|
+
--teal: #5fb3a6;
|
|
85
|
+
--teal-bg: rgba(95, 179, 166, 0.13);
|
|
86
|
+
--teal-border: rgba(95, 179, 166, 0.3);
|
|
87
|
+
|
|
88
|
+
--pink: #cd8aa0;
|
|
89
|
+
--pink-bg: rgba(205, 138, 160, 0.13);
|
|
90
|
+
--pink-border: rgba(205, 138, 160, 0.3);
|
|
91
|
+
|
|
92
|
+
--color-success: var(--green);
|
|
93
|
+
--color-warning: var(--yellow);
|
|
94
|
+
--color-error: var(--red);
|
|
95
|
+
--color-info: #82a2c0;
|
|
96
|
+
--color-success-bg: var(--green-bg);
|
|
97
|
+
--color-warning-bg: var(--yellow-bg);
|
|
98
|
+
--color-error-bg: var(--red-bg);
|
|
99
|
+
--color-info-bg: rgba(130, 162, 192, 0.13);
|
|
100
|
+
|
|
101
|
+
/* === OVERLAYS === */
|
|
102
|
+
--color-badge-overlay: rgba(241, 235, 218, 0.85);
|
|
103
|
+
|
|
104
|
+
/* === INTERACTIVE === */
|
|
105
|
+
--color-link: #79c4a0;
|
|
106
|
+
--color-link-hover: #9bd6bb;
|
|
107
|
+
|
|
108
|
+
/* === TYPOGRAPHY (same as light) === */
|
|
109
|
+
--font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
|
|
110
|
+
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
111
|
+
--font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
|
|
112
|
+
--font-read: 'Newsreader', Georgia, 'Times New Roman', serif;
|
|
113
|
+
|
|
114
|
+
--font-heading: var(--font-display);
|
|
115
|
+
--font-body: var(--font-sans);
|
|
116
|
+
|
|
117
|
+
--text-label: 0.6875rem;
|
|
118
|
+
|
|
119
|
+
--leading-tight: 1.16;
|
|
120
|
+
--leading-snug: 1.36;
|
|
121
|
+
--leading-normal: 1.62;
|
|
122
|
+
--leading-relaxed: 1.78;
|
|
123
|
+
|
|
124
|
+
--tracking-tight: -0.015em;
|
|
125
|
+
--tracking-normal: 0;
|
|
126
|
+
--tracking-wide: 0.04em;
|
|
127
|
+
--tracking-wider: 0.08em;
|
|
128
|
+
--tracking-widest: 0.14em;
|
|
129
|
+
|
|
130
|
+
/* === SHAPE (same rounded geometry) === */
|
|
131
|
+
--radius: 12px;
|
|
132
|
+
--radius-sm: 8px;
|
|
133
|
+
--radius-md: 14px;
|
|
134
|
+
--radius-lg: 20px;
|
|
135
|
+
--radius-xl: 28px;
|
|
136
|
+
--radius-2xl: 28px;
|
|
137
|
+
|
|
138
|
+
/* === SHADOWS (deeper for dark) === */
|
|
139
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
140
|
+
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.45), 0 10px 24px rgba(0, 0, 0, 0.32);
|
|
141
|
+
--shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.52), 0 4px 12px rgba(0, 0, 0, 0.36);
|
|
142
|
+
--shadow-xl: 0 26px 60px rgba(0, 0, 0, 0.6), 0 8px 18px rgba(0, 0, 0, 0.4);
|
|
143
|
+
--shadow-accent: 0 6px 20px rgba(90, 167, 132, 0.4);
|
|
144
|
+
|
|
145
|
+
/* === TRANSITIONS === */
|
|
146
|
+
--transition-fast: 140ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
147
|
+
--transition-default: 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
148
|
+
--transition-slow: 320ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
149
|
+
|
|
150
|
+
/* === FOCUS === */
|
|
151
|
+
--focus-ring: 0 0 0 3px rgba(90, 167, 132, 0.28);
|
|
152
|
+
}
|
|
153
|
+
} /* end @layer commonpub — token overrides only */
|
package/theme/stoa.css
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
@layer commonpub {
|
|
2
|
+
/* ===========================================
|
|
3
|
+
CommonPub Stoa Theme — Light
|
|
4
|
+
"A covered walk for thinking in public."
|
|
5
|
+
|
|
6
|
+
Warm paper grounds, moss-green accent,
|
|
7
|
+
Fraunces display + Newsreader reading serif,
|
|
8
|
+
Work Sans UI. Soft rounded corners, gentle
|
|
9
|
+
blurred lifts (not the brutalist offset
|
|
10
|
+
shadow), hairline warm borders. A calm,
|
|
11
|
+
bookish, daylight aesthetic.
|
|
12
|
+
=========================================== */
|
|
13
|
+
|
|
14
|
+
[data-theme="stoa"] {
|
|
15
|
+
/* === SURFACES (warm paper) === */
|
|
16
|
+
--bg: #f7f3ea;
|
|
17
|
+
--surface: #fffdf6;
|
|
18
|
+
--surface2: #eee8d9;
|
|
19
|
+
--surface3: #e4ddcb;
|
|
20
|
+
|
|
21
|
+
--color-surface: var(--surface);
|
|
22
|
+
--color-surface-alt: var(--surface2);
|
|
23
|
+
--color-surface-raised: var(--surface);
|
|
24
|
+
--color-surface-overlay: rgba(42, 38, 32, 0.5);
|
|
25
|
+
--color-surface-overlay-light: rgba(42, 38, 32, 0.4);
|
|
26
|
+
--color-surface-scrim: rgba(247, 243, 234, 0.78);
|
|
27
|
+
--color-surface-hover: var(--surface2);
|
|
28
|
+
--color-bg-subtle: var(--bg);
|
|
29
|
+
|
|
30
|
+
/* === TEXT (ink gradations) === */
|
|
31
|
+
--text: #2a2620;
|
|
32
|
+
--text-dim: #5a5247;
|
|
33
|
+
--text-faint: #8a8273;
|
|
34
|
+
|
|
35
|
+
--color-text: var(--text);
|
|
36
|
+
--color-text-secondary: var(--text-dim);
|
|
37
|
+
--color-text-muted: var(--text-faint);
|
|
38
|
+
--color-text-inverse: #fdfbf3;
|
|
39
|
+
|
|
40
|
+
/* === BORDERS (warm hairlines) === */
|
|
41
|
+
--border: rgba(42, 38, 32, 0.22);
|
|
42
|
+
--border2: rgba(42, 38, 32, 0.12);
|
|
43
|
+
|
|
44
|
+
--color-border: var(--border2);
|
|
45
|
+
--color-border-strong: var(--border);
|
|
46
|
+
--color-border-focus: var(--accent);
|
|
47
|
+
|
|
48
|
+
/* === ACCENT (moss green) === */
|
|
49
|
+
--accent: #3c8262;
|
|
50
|
+
--accent-bg: rgba(60, 130, 98, 0.12);
|
|
51
|
+
--accent-bg-strong: rgba(60, 130, 98, 0.2);
|
|
52
|
+
--accent-bg-heavy: rgba(60, 130, 98, 0.4);
|
|
53
|
+
--accent-bg-solid: rgba(60, 130, 98, 0.6);
|
|
54
|
+
--accent-border: rgba(60, 130, 98, 0.32);
|
|
55
|
+
--accent-focus-ring: 0 0 0 3px rgba(60, 130, 98, 0.18);
|
|
56
|
+
|
|
57
|
+
--color-primary: var(--accent);
|
|
58
|
+
--color-primary-hover: #2b6147;
|
|
59
|
+
--color-primary-text: #fdfbf3;
|
|
60
|
+
--color-on-primary: #fdfbf3;
|
|
61
|
+
--color-accent: var(--accent);
|
|
62
|
+
--color-accent-hover: #2b6147;
|
|
63
|
+
--color-accent-text: #fdfbf3;
|
|
64
|
+
--color-on-accent: #fdfbf3;
|
|
65
|
+
--color-accent-bg: var(--accent-bg);
|
|
66
|
+
--color-accent-border: var(--accent-border);
|
|
67
|
+
|
|
68
|
+
/* === SEMANTIC COLORS (warm) === */
|
|
69
|
+
--green: #3c8262;
|
|
70
|
+
--green-bg: rgba(60, 130, 98, 0.12);
|
|
71
|
+
--green-border: rgba(60, 130, 98, 0.3);
|
|
72
|
+
|
|
73
|
+
--yellow: #bd863a;
|
|
74
|
+
--yellow-bg: rgba(189, 134, 58, 0.1);
|
|
75
|
+
--yellow-border: rgba(189, 134, 58, 0.28);
|
|
76
|
+
|
|
77
|
+
--red: #b25a47;
|
|
78
|
+
--red-bg: rgba(178, 90, 71, 0.1);
|
|
79
|
+
--red-border: rgba(178, 90, 71, 0.28);
|
|
80
|
+
|
|
81
|
+
--purple: #7b6190;
|
|
82
|
+
--purple-bg: rgba(123, 97, 144, 0.1);
|
|
83
|
+
--purple-border: rgba(123, 97, 144, 0.28);
|
|
84
|
+
|
|
85
|
+
--teal: #3f8a80;
|
|
86
|
+
--teal-bg: rgba(63, 138, 128, 0.1);
|
|
87
|
+
--teal-border: rgba(63, 138, 128, 0.28);
|
|
88
|
+
|
|
89
|
+
--pink: #b0697f;
|
|
90
|
+
--pink-bg: rgba(176, 105, 127, 0.1);
|
|
91
|
+
--pink-border: rgba(176, 105, 127, 0.28);
|
|
92
|
+
|
|
93
|
+
/* Rank colors (warm-toned for contests) */
|
|
94
|
+
--gold: #c1923a;
|
|
95
|
+
--silver: #9a948a;
|
|
96
|
+
--bronze: #a8704a;
|
|
97
|
+
|
|
98
|
+
--color-success: var(--green);
|
|
99
|
+
--color-warning: var(--yellow);
|
|
100
|
+
--color-error: var(--red);
|
|
101
|
+
--color-info: #5d7e9c;
|
|
102
|
+
--color-success-bg: var(--green-bg);
|
|
103
|
+
--color-warning-bg: var(--yellow-bg);
|
|
104
|
+
--color-error-bg: var(--red-bg);
|
|
105
|
+
--color-info-bg: rgba(93, 126, 156, 0.1);
|
|
106
|
+
|
|
107
|
+
/* === OVERLAYS === */
|
|
108
|
+
--color-badge-overlay: rgba(42, 38, 32, 0.72);
|
|
109
|
+
|
|
110
|
+
/* === INTERACTIVE === */
|
|
111
|
+
--color-link: #2b6147;
|
|
112
|
+
--color-link-hover: #1f4a35;
|
|
113
|
+
|
|
114
|
+
/* === TYPOGRAPHY === */
|
|
115
|
+
--font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
|
|
116
|
+
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
117
|
+
--font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
|
|
118
|
+
--font-read: 'Newsreader', Georgia, 'Times New Roman', serif;
|
|
119
|
+
|
|
120
|
+
--font-heading: var(--font-display);
|
|
121
|
+
--font-body: var(--font-sans);
|
|
122
|
+
|
|
123
|
+
--text-label: 0.6875rem; /* 11px monospace UI labels */
|
|
124
|
+
|
|
125
|
+
--leading-tight: 1.16;
|
|
126
|
+
--leading-snug: 1.36;
|
|
127
|
+
--leading-normal: 1.62;
|
|
128
|
+
--leading-relaxed: 1.78;
|
|
129
|
+
|
|
130
|
+
--tracking-tight: -0.015em;
|
|
131
|
+
--tracking-normal: 0;
|
|
132
|
+
--tracking-wide: 0.04em;
|
|
133
|
+
--tracking-wider: 0.08em;
|
|
134
|
+
--tracking-widest: 0.14em;
|
|
135
|
+
|
|
136
|
+
/* === SHAPE (soft, rounded — Stoa's signature departure) === */
|
|
137
|
+
--radius: 12px;
|
|
138
|
+
--radius-sm: 8px;
|
|
139
|
+
--radius-md: 14px;
|
|
140
|
+
--radius-lg: 20px;
|
|
141
|
+
--radius-xl: 28px;
|
|
142
|
+
--radius-2xl: 28px;
|
|
143
|
+
|
|
144
|
+
/* === SHADOWS (soft blurred lifts, not offset) === */
|
|
145
|
+
--shadow-sm: 0 1px 2px rgba(38, 32, 24, 0.06), 0 1px 1px rgba(38, 32, 24, 0.04);
|
|
146
|
+
--shadow-md: 0 2px 6px rgba(38, 32, 24, 0.06), 0 8px 20px rgba(38, 32, 24, 0.06);
|
|
147
|
+
--shadow-lg: 0 12px 34px rgba(38, 32, 24, 0.11), 0 3px 10px rgba(38, 32, 24, 0.06);
|
|
148
|
+
--shadow-xl: 0 22px 50px rgba(38, 32, 24, 0.14), 0 6px 16px rgba(38, 32, 24, 0.08);
|
|
149
|
+
--shadow-accent: 0 6px 18px rgba(60, 130, 98, 0.28);
|
|
150
|
+
|
|
151
|
+
/* === TRANSITIONS (gentle, springy) === */
|
|
152
|
+
--transition-fast: 140ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
153
|
+
--transition-default: 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
154
|
+
--transition-slow: 320ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
155
|
+
|
|
156
|
+
/* === FOCUS (moss glow) === */
|
|
157
|
+
--focus-ring: 0 0 0 3px rgba(60, 130, 98, 0.22);
|
|
158
|
+
}
|
|
159
|
+
} /* end @layer commonpub — token overrides only */
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
/* ═══════════════════════════════════════════
|
|
163
|
+
COMPONENT OVERRIDES (outside @layer so they
|
|
164
|
+
beat Vue scoped styles). Shared by the light
|
|
165
|
+
AND dark variants — both data-theme values
|
|
166
|
+
are targeted so stoa-dark inherits them.
|
|
167
|
+
═══════════════════════════════════════════ */
|
|
168
|
+
|
|
169
|
+
/* Display serif for headings */
|
|
170
|
+
[data-theme="stoa"] h1,
|
|
171
|
+
[data-theme="stoa"] h2,
|
|
172
|
+
[data-theme="stoa"] h3,
|
|
173
|
+
[data-theme="stoa-dark"] h1,
|
|
174
|
+
[data-theme="stoa-dark"] h2,
|
|
175
|
+
[data-theme="stoa-dark"] h3,
|
|
176
|
+
[data-theme="stoa"] .cpub-section-title-lg,
|
|
177
|
+
[data-theme="stoa"] .cpub-section-title-sm,
|
|
178
|
+
[data-theme="stoa-dark"] .cpub-section-title-lg,
|
|
179
|
+
[data-theme="stoa-dark"] .cpub-section-title-sm,
|
|
180
|
+
[data-theme="stoa"] .admin-page-title,
|
|
181
|
+
[data-theme="stoa-dark"] .admin-page-title {
|
|
182
|
+
font-family: var(--font-display);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Wordmark + brand use the display serif */
|
|
186
|
+
[data-theme="stoa"] .cpub-logo-name,
|
|
187
|
+
[data-theme="stoa-dark"] .cpub-logo-name,
|
|
188
|
+
[data-theme="stoa"] .admin-brand,
|
|
189
|
+
[data-theme="stoa-dark"] .admin-brand {
|
|
190
|
+
font-family: var(--font-display);
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
letter-spacing: -0.01em;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
[data-theme="stoa"] .cpub-logo-bracket,
|
|
196
|
+
[data-theme="stoa-dark"] .cpub-logo-bracket {
|
|
197
|
+
color: var(--accent);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Focus: moss glow instead of a hard outline */
|
|
201
|
+
[data-theme="stoa"] :focus-visible,
|
|
202
|
+
[data-theme="stoa-dark"] :focus-visible {
|
|
203
|
+
outline: none;
|
|
204
|
+
box-shadow: var(--focus-ring);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Buttons: gentle lift on hover */
|
|
208
|
+
[data-theme="stoa"] .cpub-btn,
|
|
209
|
+
[data-theme="stoa-dark"] .cpub-btn {
|
|
210
|
+
transition: transform var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
[data-theme="stoa"] .cpub-btn:hover,
|
|
214
|
+
[data-theme="stoa-dark"] .cpub-btn:hover {
|
|
215
|
+
transform: translateY(-1px);
|
|
216
|
+
box-shadow: var(--shadow-sm);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[data-theme="stoa"] .cpub-btn:active,
|
|
220
|
+
[data-theme="stoa-dark"] .cpub-btn:active {
|
|
221
|
+
transform: translateY(0);
|
|
222
|
+
box-shadow: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[data-theme="stoa"] .cpub-btn-primary,
|
|
226
|
+
[data-theme="stoa-dark"] .cpub-btn-primary {
|
|
227
|
+
background: var(--accent);
|
|
228
|
+
border-color: var(--accent);
|
|
229
|
+
color: var(--color-on-accent);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
[data-theme="stoa"] .cpub-btn-primary:hover,
|
|
233
|
+
[data-theme="stoa-dark"] .cpub-btn-primary:hover {
|
|
234
|
+
background: var(--color-primary-hover);
|
|
235
|
+
box-shadow: var(--shadow-accent);
|
|
236
|
+
transform: translateY(-1px);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
[data-theme="stoa"] .cpub-btn-ghost,
|
|
240
|
+
[data-theme="stoa-dark"] .cpub-btn-ghost {
|
|
241
|
+
color: var(--accent);
|
|
242
|
+
border-color: transparent;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
[data-theme="stoa"] .cpub-btn-ghost:hover,
|
|
246
|
+
[data-theme="stoa-dark"] .cpub-btn-ghost:hover {
|
|
247
|
+
background: var(--accent-bg);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Cards: soft lift on hover */
|
|
251
|
+
[data-theme="stoa"] .cpub-content-card,
|
|
252
|
+
[data-theme="stoa-dark"] .cpub-content-card,
|
|
253
|
+
[data-theme="stoa"] .cpub-sb-card,
|
|
254
|
+
[data-theme="stoa-dark"] .cpub-sb-card,
|
|
255
|
+
[data-theme="stoa"] .cpub-stat-card,
|
|
256
|
+
[data-theme="stoa-dark"] .cpub-stat-card {
|
|
257
|
+
transition: transform var(--transition-fast), box-shadow var(--transition-fast), border-color var(--transition-fast);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
[data-theme="stoa"] .cpub-content-card:hover,
|
|
261
|
+
[data-theme="stoa-dark"] .cpub-content-card:hover {
|
|
262
|
+
transform: translateY(-2px);
|
|
263
|
+
box-shadow: var(--shadow-md);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Card titles use the display serif */
|
|
267
|
+
[data-theme="stoa"] .cpub-content-card h3,
|
|
268
|
+
[data-theme="stoa-dark"] .cpub-content-card h3,
|
|
269
|
+
[data-theme="stoa"] .cpub-card-title,
|
|
270
|
+
[data-theme="stoa-dark"] .cpub-card-title {
|
|
271
|
+
font-family: var(--font-display);
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Inputs: glow focus */
|
|
276
|
+
[data-theme="stoa"] .cpub-input:focus,
|
|
277
|
+
[data-theme="stoa-dark"] .cpub-input:focus,
|
|
278
|
+
[data-theme="stoa"] .cpub-textarea:focus,
|
|
279
|
+
[data-theme="stoa-dark"] .cpub-textarea:focus,
|
|
280
|
+
[data-theme="stoa"] .cpub-select:focus,
|
|
281
|
+
[data-theme="stoa-dark"] .cpub-select:focus {
|
|
282
|
+
border-color: var(--accent);
|
|
283
|
+
box-shadow: var(--focus-ring);
|
|
284
|
+
outline: none;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* Links: moss with a soft underline */
|
|
288
|
+
[data-theme="stoa"] a:not(.cpub-btn):not(.cpub-topbar-link):not(.cpub-footer-link):not(.admin-nav-link),
|
|
289
|
+
[data-theme="stoa-dark"] a:not(.cpub-btn):not(.cpub-topbar-link):not(.cpub-footer-link):not(.admin-nav-link) {
|
|
290
|
+
text-decoration-color: var(--accent-border);
|
|
291
|
+
text-underline-offset: 2px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
[data-theme="stoa"] .cpub-topbar-link.router-link-active,
|
|
295
|
+
[data-theme="stoa-dark"] .cpub-topbar-link.router-link-active {
|
|
296
|
+
color: var(--accent);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Prose: Newsreader reading serif, moss blockquote */
|
|
300
|
+
[data-theme="stoa"] .cpub-prose,
|
|
301
|
+
[data-theme="stoa-dark"] .cpub-prose {
|
|
302
|
+
font-family: var(--font-read);
|
|
303
|
+
font-size: 1.0625rem;
|
|
304
|
+
line-height: var(--leading-relaxed);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
[data-theme="stoa"] .cpub-prose h1,
|
|
308
|
+
[data-theme="stoa"] .cpub-prose h2,
|
|
309
|
+
[data-theme="stoa"] .cpub-prose h3,
|
|
310
|
+
[data-theme="stoa"] .cpub-prose h4,
|
|
311
|
+
[data-theme="stoa-dark"] .cpub-prose h1,
|
|
312
|
+
[data-theme="stoa-dark"] .cpub-prose h2,
|
|
313
|
+
[data-theme="stoa-dark"] .cpub-prose h3,
|
|
314
|
+
[data-theme="stoa-dark"] .cpub-prose h4 {
|
|
315
|
+
font-family: var(--font-display);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
[data-theme="stoa"] .cpub-prose blockquote,
|
|
319
|
+
[data-theme="stoa-dark"] .cpub-prose blockquote {
|
|
320
|
+
border-left-color: var(--accent);
|
|
321
|
+
background: var(--accent-bg);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
[data-theme="stoa"] .cpub-prose a,
|
|
325
|
+
[data-theme="stoa-dark"] .cpub-prose a {
|
|
326
|
+
color: var(--accent);
|
|
327
|
+
text-decoration-color: var(--accent-border);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Admin: active nav uses moss */
|
|
331
|
+
[data-theme="stoa"] .admin-nav-link.router-link-exact-active,
|
|
332
|
+
[data-theme="stoa-dark"] .admin-nav-link.router-link-exact-active {
|
|
333
|
+
color: var(--accent);
|
|
334
|
+
background: var(--accent-bg);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
[data-theme="stoa"] .cpub-footer-col-title,
|
|
338
|
+
[data-theme="stoa-dark"] .cpub-footer-col-title {
|
|
339
|
+
letter-spacing: var(--tracking-widest);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
/* ═══════════════════════════════════════════
|
|
344
|
+
LOGO SWITCH
|
|
345
|
+
Stoa shares Agora's Town Square mark (hide
|
|
346
|
+
the Classic logo, show the Agora wordmark +
|
|
347
|
+
hero aside).
|
|
348
|
+
═══════════════════════════════════════════ */
|
|
349
|
+
|
|
350
|
+
[data-theme="stoa"] .cpub-logo-classic,
|
|
351
|
+
[data-theme="stoa-dark"] .cpub-logo-classic { display: none !important; }
|
|
352
|
+
|
|
353
|
+
[data-theme="stoa"] .cpub-logo-agora,
|
|
354
|
+
[data-theme="stoa-dark"] .cpub-logo-agora { display: flex !important; }
|
|
355
|
+
|
|
356
|
+
[data-theme="stoa"] .cpub-hero-logo-aside,
|
|
357
|
+
[data-theme="stoa-dark"] .cpub-hero-logo-aside {
|
|
358
|
+
display: flex !important;
|
|
359
|
+
align-items: center;
|
|
360
|
+
justify-content: center;
|
|
361
|
+
}
|
package/utils/themeConfig.ts
CHANGED
|
@@ -17,6 +17,8 @@ export const THEME_TO_FAMILY: Record<string, string> = {
|
|
|
17
17
|
generics: 'generics',
|
|
18
18
|
agora: 'agora',
|
|
19
19
|
'agora-dark': 'agora',
|
|
20
|
+
stoa: 'stoa',
|
|
21
|
+
'stoa-dark': 'stoa',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
/** Light/dark variants for each family */
|
|
@@ -24,6 +26,7 @@ export const FAMILY_VARIANTS: Record<string, { light: string; dark: string }> =
|
|
|
24
26
|
classic: { light: 'base', dark: 'dark' },
|
|
25
27
|
agora: { light: 'agora', dark: 'agora-dark' },
|
|
26
28
|
generics: { light: 'generics', dark: 'generics' },
|
|
29
|
+
stoa: { light: 'stoa', dark: 'stoa-dark' },
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
/** Whether a theme ID is a dark theme */
|
|
@@ -33,6 +36,8 @@ export const IS_DARK: Record<string, boolean> = {
|
|
|
33
36
|
generics: true,
|
|
34
37
|
agora: false,
|
|
35
38
|
'agora-dark': true,
|
|
39
|
+
stoa: false,
|
|
40
|
+
'stoa-dark': true,
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
/** All valid theme IDs */
|