@commonpub/layer 0.57.0 → 0.58.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/LayoutRow.vue +8 -8
- package/components/LayoutSection.vue +8 -8
- package/components/LayoutSlot.vue +3 -3
- package/components/MirrorDetailModal.vue +3 -3
- package/components/MirrorRequestApproveModal.vue +3 -3
- package/components/PollDisplay.vue +1 -1
- package/components/RegistryDirectory.vue +2 -2
- package/components/admin/layouts/AdminLayoutsAutoForm.vue +1 -1
- package/components/admin/layouts/AdminLayoutsCanvas.vue +2 -2
- package/components/admin/layouts/AdminLayoutsConflictModal.vue +1 -1
- package/components/admin/layouts/AdminLayoutsHelpOverlay.vue +1 -1
- package/components/admin/layouts/AdminLayoutsInspectorPage.vue +1 -1
- package/components/admin/layouts/AdminLayoutsToolbar.vue +5 -5
- package/components/admin/theme/AdminThemeSceneGallery.vue +3 -3
- package/components/admin/theme/AdminThemeSceneProse.vue +3 -3
- package/components/admin/theme/AdminThemeTokenInput.vue +1 -1
- package/components/blocks/BlockCodeView.vue +2 -2
- package/components/blocks/BlockDividerView.vue +1 -1
- package/components/blocks/BlockPartsListView.vue +1 -1
- package/components/blocks/BlockQuizView.vue +1 -1
- package/components/blocks/BlockQuoteView.vue +1 -1
- package/components/contest/ContestHero.vue +2 -2
- package/components/contest/ContestStagesEditor.vue +4 -4
- package/components/editors/ArticleEditor.vue +1 -1
- package/components/editors/ExplainerEditor.vue +1 -1
- package/components/sections/SectionLearning.vue +1 -1
- package/components/views/ArticleView.vue +2 -2
- package/components/views/ProjectView.vue +3 -3
- package/composables/useAdminSidebar.ts +3 -3
- package/composables/useLayoutEditor.ts +1 -1
- package/composables/useLayoutHotkeys.ts +1 -1
- package/composables/useLayoutResize.ts +1 -1
- package/composables/usePublishValidation.ts +1 -1
- package/composables/useThemeAdmin.ts +2 -2
- package/error.vue +1 -1
- package/layouts/admin.vue +2 -2
- package/layouts/default.vue +2 -2
- package/package.json +8 -8
- package/pages/[type]/index.vue +1 -1
- package/pages/about.vue +3 -3
- package/pages/admin/api-keys.vue +5 -5
- package/pages/admin/audit.vue +2 -2
- package/pages/admin/categories.vue +1 -1
- package/pages/admin/content.vue +2 -2
- package/pages/admin/features.vue +1 -1
- package/pages/admin/federation.vue +9 -9
- package/pages/admin/homepage.vue +4 -4
- package/pages/admin/index.vue +1 -1
- package/pages/admin/layouts/[id].vue +18 -18
- package/pages/admin/layouts/index.vue +4 -4
- package/pages/admin/navigation.vue +1 -1
- package/pages/admin/reports.vue +1 -1
- package/pages/admin/settings.vue +2 -2
- package/pages/admin/theme/edit/[id].vue +2 -2
- package/pages/admin/theme/index.vue +5 -5
- package/pages/admin/users.vue +1 -1
- package/pages/auth/forgot-password.vue +1 -1
- package/pages/auth/login.vue +3 -3
- package/pages/auth/register.vue +1 -1
- package/pages/auth/reset-password.vue +1 -1
- package/pages/auth/verify-email.vue +1 -1
- package/pages/cert/[code].vue +1 -1
- package/pages/contests/[slug]/edit.vue +13 -12
- package/pages/contests/[slug]/index.vue +7 -7
- package/pages/contests/[slug]/judge.vue +15 -3
- package/pages/contests/[slug]/results.vue +5 -5
- package/pages/contests/create.vue +15 -15
- package/pages/contests/index.vue +2 -2
- package/pages/cookies.vue +1 -1
- package/pages/create.vue +2 -2
- package/pages/dashboard.vue +1 -1
- package/pages/docs/[siteSlug]/[...pagePath].vue +1 -1
- package/pages/docs/[siteSlug]/edit.vue +1 -1
- package/pages/docs/[siteSlug]/index.vue +1 -1
- package/pages/docs/create.vue +1 -1
- package/pages/docs/index.vue +1 -1
- package/pages/events/[slug]/edit.vue +1 -1
- package/pages/events/[slug]/index.vue +2 -2
- package/pages/events/create.vue +1 -1
- package/pages/events/index.vue +1 -1
- package/pages/explore.vue +1 -1
- package/pages/federated-hubs/[id]/index.vue +3 -3
- package/pages/federated-hubs/[id]/posts/[postId].vue +1 -1
- package/pages/federation/search.vue +1 -1
- package/pages/feed.vue +1 -1
- package/pages/hubs/[slug]/members.vue +1 -1
- package/pages/hubs/[slug]/posts/[postId].vue +1 -1
- package/pages/hubs/[slug]/settings.vue +5 -5
- package/pages/hubs/create.vue +6 -6
- package/pages/hubs/index.vue +1 -1
- package/pages/index.vue +2 -2
- package/pages/learn/[slug]/[lessonSlug]/edit.vue +1 -1
- package/pages/learn/[slug]/[lessonSlug]/index.vue +4 -4
- package/pages/learn/[slug]/edit.vue +1 -1
- package/pages/learn/[slug]/index.vue +1 -1
- package/pages/learn/create.vue +1 -1
- package/pages/learn/index.vue +2 -2
- package/pages/messages/[conversationId].vue +1 -1
- package/pages/messages/index.vue +1 -1
- package/pages/notifications.vue +1 -1
- package/pages/privacy.vue +5 -5
- package/pages/products/[slug].vue +1 -1
- package/pages/products/index.vue +1 -1
- package/pages/search.vue +1 -1
- package/pages/settings/profile.vue +1 -1
- package/pages/settings.vue +1 -1
- package/pages/tags/[slug].vue +1 -1
- package/pages/tags/index.vue +1 -1
- package/pages/terms.vue +1 -1
- package/pages/u/[username]/[type]/[slug]/edit.vue +3 -3
- package/pages/u/[username]/[type]/[slug]/index.vue +1 -1
- package/pages/u/[username]/followers.vue +1 -1
- package/pages/u/[username]/following.vue +1 -1
- package/pages/u/[username]/index.vue +3 -3
- package/pages/videos/[id].vue +1 -1
- package/pages/videos/index.vue +1 -1
- package/pages/videos/submit.vue +2 -2
- package/sections/builtin/hero.ts +1 -1
- package/sections/builtin/markdown.ts +1 -1
- package/server/api/admin/homepage/sections.put.ts +1 -1
- package/server/api/admin/layouts/[id].put.ts +1 -1
- package/server/api/contests/[slug]/entries.post.ts +3 -3
- package/server/api/hubs/[slug]/feed.xml.get.ts +1 -1
- package/server/api/users/[username]/feed.xml.get.ts +1 -1
- package/server/middleware/content-redirect.ts +1 -1
- package/server/plugins/federation-delivery.ts +1 -1
- package/server/plugins/federation-hub-sync.ts +1 -1
- package/server/plugins/registry-heartbeat.ts +3 -3
- package/server/plugins/search-index.ts +1 -1
- package/server/utils/email.ts +3 -3
- package/server/utils/instanceTheme.ts +1 -1
- package/utils/contestStages.ts +3 -3
|
@@ -186,7 +186,7 @@ export function clampResize(params: {
|
|
|
186
186
|
if (neighbourStart === null) {
|
|
187
187
|
return {
|
|
188
188
|
newColSpan,
|
|
189
|
-
newNeighbourColSpan: 0, // sentinel
|
|
189
|
+
newNeighbourColSpan: 0, // sentinel, caller knows there's no neighbour
|
|
190
190
|
constraintHit,
|
|
191
191
|
constraintBound,
|
|
192
192
|
};
|
|
@@ -49,7 +49,7 @@ export function usePublishValidation(opts: PublishValidationOptions): PublishVal
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
if (required.includes('content') && opts.getBlockTuples().length === 0) {
|
|
52
|
-
errs.push('Content is empty
|
|
52
|
+
errs.push('Content is empty, add at least one block');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
errors.value = errs;
|
|
@@ -16,8 +16,8 @@ import type { CustomThemeRecord, ThemesPayload, ThemeFamilyView } from '../types
|
|
|
16
16
|
// ---- Family display metadata for built-in themes ------------------------
|
|
17
17
|
|
|
18
18
|
const BUILT_IN_FAMILY_META: Record<string, { name: string; description: string }> = {
|
|
19
|
-
classic: { name: 'Classic', description: 'Sharp corners, offset shadows, blue accent
|
|
20
|
-
agora: { name: 'Agora', description: 'Warm parchment tones, green accent, serif display font
|
|
19
|
+
classic: { name: 'Classic', description: 'Sharp corners, offset shadows, blue accent, the original CommonPub look' },
|
|
20
|
+
agora: { name: 'Agora', description: 'Warm parchment tones, green accent, serif display font, institutional warmth' },
|
|
21
21
|
generics: { name: 'Generics', description: 'Minimal dark aesthetic with soft glow shadows' },
|
|
22
22
|
};
|
|
23
23
|
|
package/error.vue
CHANGED
|
@@ -7,7 +7,7 @@ const props = defineProps<{
|
|
|
7
7
|
};
|
|
8
8
|
}>();
|
|
9
9
|
|
|
10
|
-
useSeoMeta({ title: `${props.error.statusCode}
|
|
10
|
+
useSeoMeta({ title: `${props.error.statusCode}, CommonPub` });
|
|
11
11
|
|
|
12
12
|
// Error pages render outside app.vue's NuxtLayout tree during SSR, so the theme
|
|
13
13
|
// plugin's useHead doesn't propagate here. Re-apply BOTH the data-theme attribute
|
package/layouts/admin.vue
CHANGED
|
@@ -54,7 +54,7 @@ const { desktopCollapsed, mobileOpen, toggleDesktop, toggleMobile, closeMobile }
|
|
|
54
54
|
<!--
|
|
55
55
|
Nav link pattern: icon + visible label. When collapsed, label text
|
|
56
56
|
stays in the DOM (clip-path) so screen readers still announce
|
|
57
|
-
"Dashboard, link"
|
|
57
|
+
"Dashboard, link", the icon alone has no accessible name.
|
|
58
58
|
`title` attr only set when collapsed → visual tooltip on hover.
|
|
59
59
|
-->
|
|
60
60
|
<NuxtLink to="/admin" class="admin-nav-link" :title="desktopCollapsed ? 'Dashboard' : undefined" @click="closeMobile">
|
|
@@ -83,7 +83,7 @@ const { desktopCollapsed, mobileOpen, toggleDesktop, toggleMobile, closeMobile }
|
|
|
83
83
|
</NuxtLink>
|
|
84
84
|
<!-- Layouts editor — gated on layoutEngine feature flag (CLAUDE.md rule #2).
|
|
85
85
|
Stays invisible until the operator flips the flag, then appears between
|
|
86
|
-
the legacy /admin/homepage editor and Navigation. Phase 3a
|
|
86
|
+
the legacy /admin/homepage editor and Navigation. Phase 3a, session 160 audit. -->
|
|
87
87
|
<NuxtLink v-if="layoutEngine" to="/admin/layouts" class="admin-nav-link" :title="desktopCollapsed ? 'Layouts' : undefined" @click="closeMobile">
|
|
88
88
|
<i class="fa-solid fa-table-cells-large"></i><span class="admin-nav-label">Layouts</span>
|
|
89
89
|
</NuxtLink>
|
package/layouts/default.vue
CHANGED
|
@@ -268,10 +268,10 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
268
268
|
|
|
269
269
|
/* ═══ TOPBAR ═══
|
|
270
270
|
Structure is token-driven (--cpub-topbar-*) so a theme can change the bar's SHAPE
|
|
271
|
-
|
|
271
|
+
, height, radius, shadow, position, padding, not just its colors, without forking
|
|
272
272
|
this layout. Every default reproduces the current flat 48px bar exactly.
|
|
273
273
|
(Centering the bar's CONTENT at a max width while keeping a full-bleed background
|
|
274
|
-
needs an inner wrapper element, which the base markup doesn't have
|
|
274
|
+
needs an inner wrapper element, which the base markup doesn't have, that one
|
|
275
275
|
aspect stays a structural choice, not a token.) */
|
|
276
276
|
.cpub-topbar {
|
|
277
277
|
position: var(--cpub-topbar-position, fixed); top: 0; left: 0; right: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -53,16 +53,16 @@
|
|
|
53
53
|
"vue": "^3.4.0",
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
|
-
"@commonpub/auth": "0.8.0",
|
|
57
56
|
"@commonpub/config": "0.18.0",
|
|
58
|
-
"@commonpub/docs": "0.6.3",
|
|
59
57
|
"@commonpub/editor": "0.7.11",
|
|
60
|
-
"@commonpub/
|
|
58
|
+
"@commonpub/explainer": "0.7.15",
|
|
61
59
|
"@commonpub/learning": "0.5.2",
|
|
62
|
-
"@commonpub/
|
|
63
|
-
"@commonpub/
|
|
64
|
-
"@commonpub/
|
|
65
|
-
"@commonpub/
|
|
60
|
+
"@commonpub/schema": "0.33.0",
|
|
61
|
+
"@commonpub/auth": "0.8.0",
|
|
62
|
+
"@commonpub/server": "2.80.0",
|
|
63
|
+
"@commonpub/protocol": "0.13.0",
|
|
64
|
+
"@commonpub/docs": "0.6.3",
|
|
65
|
+
"@commonpub/ui": "0.9.2"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
package/pages/[type]/index.vue
CHANGED
|
@@ -16,7 +16,7 @@ if (!isTypeEnabled(contentType.value as ContentType)) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
useSeoMeta({
|
|
19
|
-
title: () => `${contentType.value}
|
|
19
|
+
title: () => `${contentType.value}, ${siteName}`,
|
|
20
20
|
description: () => `Browse ${contentType.value} on ${siteName}.`,
|
|
21
21
|
});
|
|
22
22
|
|
package/pages/about.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
useSeoMeta({
|
|
3
|
-
title: `About
|
|
3
|
+
title: `About, ${useSiteName()}`,
|
|
4
4
|
description: 'CommonPub is an open-source, federated platform for maker communities.',
|
|
5
5
|
});
|
|
6
6
|
|
|
@@ -23,12 +23,12 @@ const { hubs: hubsEnabled, learning: learningEnabled, contests: contestsEnabled,
|
|
|
23
23
|
<div class="cpub-about-card">
|
|
24
24
|
<div class="cpub-about-card-icon"><i class="fa-solid fa-microchip"></i></div>
|
|
25
25
|
<h3>For Makers</h3>
|
|
26
|
-
<p>Document your builds with rich block editors. Parts lists, wiring diagrams, build steps, and code blocks
|
|
26
|
+
<p>Document your builds with rich block editors. Parts lists, wiring diagrams, build steps, and code blocks, everything you need to share how you built it.</p>
|
|
27
27
|
</div>
|
|
28
28
|
<div v-if="hubsEnabled" class="cpub-about-card">
|
|
29
29
|
<div class="cpub-about-card-icon"><i class="fa-solid fa-users"></i></div>
|
|
30
30
|
<h3>Community Hubs</h3>
|
|
31
|
-
<p>Create spaces for your community, product, or company. Discussions, content galleries, learning paths, and contests
|
|
31
|
+
<p>Create spaces for your community, product, or company. Discussions, content galleries, learning paths, and contests, all in one place.</p>
|
|
32
32
|
</div>
|
|
33
33
|
<div v-if="federationEnabled" class="cpub-about-card">
|
|
34
34
|
<div class="cpub-about-card-icon"><i class="fa-solid fa-globe"></i></div>
|
package/pages/admin/api-keys.vue
CHANGED
|
@@ -4,7 +4,7 @@ import { PUBLIC_API_SCOPES } from '@commonpub/schema';
|
|
|
4
4
|
|
|
5
5
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
6
6
|
|
|
7
|
-
useSeoMeta({ title: `API Keys
|
|
7
|
+
useSeoMeta({ title: `API Keys, Admin, ${useSiteName()}` });
|
|
8
8
|
|
|
9
9
|
interface KeyListResponse {
|
|
10
10
|
items: AdminApiKeyView[];
|
|
@@ -116,7 +116,7 @@ function dismissCreated(): void {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function fmtDate(iso: string | null): string {
|
|
119
|
-
if (!iso) return '
|
|
119
|
+
if (!iso) return '-';
|
|
120
120
|
return new Date(iso).toLocaleString();
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -167,14 +167,14 @@ function fmtErrorRate(rate: number): string {
|
|
|
167
167
|
<!-- One-time token reveal -->
|
|
168
168
|
<div v-if="createdKey" class="cpub-key-reveal" role="alert">
|
|
169
169
|
<div class="cpub-key-reveal-head">
|
|
170
|
-
<strong>Key created
|
|
170
|
+
<strong>Key created, copy it now.</strong>
|
|
171
171
|
<button class="cpub-btn-link" aria-label="Close" @click="dismissCreated">
|
|
172
172
|
<i class="fa-solid fa-xmark"></i>
|
|
173
173
|
</button>
|
|
174
174
|
</div>
|
|
175
175
|
<p class="cpub-key-reveal-warn">
|
|
176
176
|
This is the only time the full token will be displayed. Store it somewhere safe before
|
|
177
|
-
leaving this page
|
|
177
|
+
leaving this page, the server only keeps a hash.
|
|
178
178
|
</p>
|
|
179
179
|
<div class="cpub-key-reveal-value">
|
|
180
180
|
<code>{{ createdKey.token }}</code>
|
|
@@ -328,7 +328,7 @@ function fmtErrorRate(rate: number): string {
|
|
|
328
328
|
<tr v-for="e in (usageCache[k.id] as ApiKeyUsageStats).topEndpoints" :key="e.endpoint">
|
|
329
329
|
<td><code>{{ e.endpoint }}</code></td>
|
|
330
330
|
<td>{{ e.count }}</td>
|
|
331
|
-
<td>{{ e.p95LatencyMs ?? '
|
|
331
|
+
<td>{{ e.p95LatencyMs ?? '-' }}</td>
|
|
332
332
|
</tr>
|
|
333
333
|
</tbody>
|
|
334
334
|
</table>
|
package/pages/admin/audit.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
3
|
|
|
4
|
-
useSeoMeta({ title: `Audit Log
|
|
4
|
+
useSeoMeta({ title: `Audit Log, Admin, ${useSiteName()}` });
|
|
5
5
|
|
|
6
6
|
const { data: logsData, pending } = await useFetch('/api/admin/audit');
|
|
7
7
|
|
|
@@ -41,7 +41,7 @@ const logs = computed<AuditEntry[]>(() => {
|
|
|
41
41
|
<tr v-for="log in logs" :key="log.id">
|
|
42
42
|
<td class="audit-action">{{ log.action }}</td>
|
|
43
43
|
<td class="audit-id">{{ log.actorId }}</td>
|
|
44
|
-
<td class="audit-id">{{ log.targetId || '
|
|
44
|
+
<td class="audit-id">{{ log.targetId || '-' }}</td>
|
|
45
45
|
<td>{{ new Date(log.createdAt).toLocaleString() }}</td>
|
|
46
46
|
</tr>
|
|
47
47
|
</tbody>
|
package/pages/admin/content.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
|
-
useSeoMeta({ title: `Content Management
|
|
3
|
+
useSeoMeta({ title: `Content Management, Admin, ${useSiteName()}` });
|
|
4
4
|
|
|
5
5
|
const toast = useToast();
|
|
6
6
|
|
|
@@ -153,7 +153,7 @@ async function setCategory(id: string, categoryId: string | null): Promise<void>
|
|
|
153
153
|
<button class="cpub-btn cpub-btn-sm" @click="bulkAction('uneditorial')"><i class="fa-regular fa-pen-to-square"></i> Unpick</button>
|
|
154
154
|
<select class="cpub-bulk-cat-select" @change="(e) => bulkSetCategory((e.target as HTMLSelectElement).value || null)" aria-label="Set category">
|
|
155
155
|
<option value="">Set Category...</option>
|
|
156
|
-
<option :value="''" v-if="false"
|
|
156
|
+
<option :value="''" v-if="false">-</option>
|
|
157
157
|
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
|
|
158
158
|
<option value="">Remove Category</option>
|
|
159
159
|
</select>
|
package/pages/admin/features.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
|
-
useSeoMeta({ title: `Federation
|
|
3
|
+
useSeoMeta({ title: `Federation, Admin, ${useSiteName()}` });
|
|
4
4
|
|
|
5
5
|
const activeTab = ref<'activity' | 'mirrors' | 'registry' | 'clients' | 'trusted' | 'tools'>('activity');
|
|
6
6
|
|
|
@@ -115,7 +115,7 @@ function onRegistrySearch(value: string): void {
|
|
|
115
115
|
const FEDERATABLE_TYPES = ['project', 'blog', 'explainer'] as const;
|
|
116
116
|
// Bounded "how far back" choices for the optional history import on create.
|
|
117
117
|
const DEPTH_OPTIONS = [
|
|
118
|
-
{ label: 'None
|
|
118
|
+
{ label: 'None, forward only (default)', body: null as Record<string, number> | null },
|
|
119
119
|
{ label: 'Last 7 days', body: { sinceDays: 7 } },
|
|
120
120
|
{ label: 'Last 30 days', body: { sinceDays: 30 } },
|
|
121
121
|
{ label: 'Last 90 days', body: { sinceDays: 90 } },
|
|
@@ -157,7 +157,7 @@ async function createMirror(): Promise<void> {
|
|
|
157
157
|
direction: 'push',
|
|
158
158
|
},
|
|
159
159
|
});
|
|
160
|
-
toast.success(`Request sent to ${domain}
|
|
160
|
+
toast.success(`Request sent to ${domain}, they must approve before they mirror you`);
|
|
161
161
|
resetMirrorForm();
|
|
162
162
|
newMirrorDirection.value = 'pull';
|
|
163
163
|
await refreshRequests();
|
|
@@ -183,12 +183,12 @@ async function createMirror(): Promise<void> {
|
|
|
183
183
|
const backfillUrl: string = `/api/admin/federation/mirrors/${created.id}/backfill`;
|
|
184
184
|
try {
|
|
185
185
|
const r = await $fetch<{ processed: number }>(backfillUrl, { method: 'POST', body: depth });
|
|
186
|
-
toast.success(`Mirror added
|
|
186
|
+
toast.success(`Mirror added, imported ${r?.processed ?? 0} item(s)`);
|
|
187
187
|
} catch {
|
|
188
|
-
toast.error('Mirror added, but history import failed
|
|
188
|
+
toast.error('Mirror added, but history import failed, use Backfill in its details to retry.');
|
|
189
189
|
}
|
|
190
190
|
} else {
|
|
191
|
-
toast.success('Mirror added
|
|
191
|
+
toast.success('Mirror added, new posts will arrive as they publish');
|
|
192
192
|
}
|
|
193
193
|
resetMirrorForm();
|
|
194
194
|
await refreshMirrors();
|
|
@@ -400,7 +400,7 @@ async function refederate(): Promise<void> {
|
|
|
400
400
|
<div v-if="activeTab === 'mirrors'">
|
|
401
401
|
<p class="cpub-fed-explain">
|
|
402
402
|
A <strong>mirror</strong> pulls another instance's public content into your federated feed.
|
|
403
|
-
It's <strong>one-directional</strong
|
|
403
|
+
It's <strong>one-directional</strong>, you receive their posts; they receive nothing from
|
|
404
404
|
you and need do nothing. New posts arrive automatically once added; use <strong>Import
|
|
405
405
|
history</strong> to also pull older posts (bounded, so you don't ingest an entire large
|
|
406
406
|
instance at once).
|
|
@@ -467,7 +467,7 @@ async function refederate(): Promise<void> {
|
|
|
467
467
|
|
|
468
468
|
<!-- Instances mirroring you -->
|
|
469
469
|
<h3 class="cpub-fed-subhead">Instances mirroring you</h3>
|
|
470
|
-
<p class="cpub-fed-info-text" style="margin-bottom: 8px;">Remote instances following your instance actor
|
|
470
|
+
<p class="cpub-fed-info-text" style="margin-bottom: 8px;">Remote instances following your instance actor, they pull your public content. (One-directional: you don't pull them unless you add a mirror above.)</p>
|
|
471
471
|
<div class="cpub-fed-activity-list">
|
|
472
472
|
<div v-if="!followersData?.length" class="cpub-fed-empty">No instances are mirroring you yet.</div>
|
|
473
473
|
<div v-for="f in followersData" :key="f.actorUri" class="cpub-fed-activity-row">
|
|
@@ -605,7 +605,7 @@ async function refederate(): Promise<void> {
|
|
|
605
605
|
<!-- Re-federate Content + Hub Posts -->
|
|
606
606
|
<div class="cpub-fed-tool-card">
|
|
607
607
|
<h3 class="cpub-fed-tool-title"><i class="fa-solid fa-rotate"></i> Re-federate</h3>
|
|
608
|
-
<p class="cpub-fed-tool-desc">Re-queue your published content (Create) and hub posts (Announce) for delivery to your current followers. Idempotent. <strong>Bounded by default</strong> so you don't blast every follower with thousands of activities
|
|
608
|
+
<p class="cpub-fed-tool-desc">Re-queue your published content (Create) and hub posts (Announce) for delivery to your current followers. Idempotent. <strong>Bounded by default</strong> so you don't blast every follower with thousands of activities, choose how far back.</p>
|
|
609
609
|
<div class="cpub-fed-form">
|
|
610
610
|
<select v-model="refederateScope" class="cpub-fed-input" style="flex:0 0 auto;width:auto;" aria-label="Re-federate scope">
|
|
611
611
|
<option value="7">Last 7 days</option>
|
package/pages/admin/homepage.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { HomepageSection } from '@commonpub/server';
|
|
3
3
|
|
|
4
4
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
5
|
-
useSeoMeta({ title: `Homepage
|
|
5
|
+
useSeoMeta({ title: `Homepage, Admin, ${useSiteName()}` });
|
|
6
6
|
|
|
7
7
|
const toast = useToast();
|
|
8
8
|
const { data, refresh } = await useFetch<HomepageSection[]>('/api/admin/homepage/sections');
|
|
@@ -115,12 +115,12 @@ const { layoutEngine } = useFeatures();
|
|
|
115
115
|
<!-- R4 audit (session 160): deprecation banner when layoutEngine is on.
|
|
116
116
|
The new visual editor at /admin/layouts is the canonical surface;
|
|
117
117
|
this legacy page still saves its JSON but the live homepage now
|
|
118
|
-
renders via the layouts table. Auto-sync is non-destructive
|
|
118
|
+
renders via the layouts table. Auto-sync is non-destructive, it
|
|
119
119
|
only creates the layout if one doesn't yet exist. -->
|
|
120
120
|
<div v-if="layoutEngine" class="cpub-admin-homepage-deprecation" role="status">
|
|
121
121
|
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
|
|
122
122
|
<div>
|
|
123
|
-
<p><strong>This is the legacy homepage editor.</strong> The Layout Engine is active on this instance
|
|
123
|
+
<p><strong>This is the legacy homepage editor.</strong> The Layout Engine is active on this instance, use the new visual editor for live changes.</p>
|
|
124
124
|
<NuxtLink to="/admin/layouts" class="cpub-admin-homepage-deprecation-link">
|
|
125
125
|
Open Layouts editor <i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
|
|
126
126
|
</NuxtLink>
|
|
@@ -312,7 +312,7 @@ const { layoutEngine } = useFeatures();
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/* R4 audit (session 160): deprecation banner for the legacy editor
|
|
315
|
-
when layoutEngine is on. Direct, friendly, non-blocking
|
|
315
|
+
when layoutEngine is on. Direct, friendly, non-blocking, links to
|
|
316
316
|
the new editor without removing access to this page (which still
|
|
317
317
|
saves the JSON for backward compat). */
|
|
318
318
|
.cpub-admin-homepage-deprecation {
|
package/pages/admin/index.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
|
-
useSeoMeta({ title: `Admin Dashboard
|
|
3
|
+
useSeoMeta({ title: `Admin Dashboard, ${useSiteName()}` });
|
|
4
4
|
|
|
5
5
|
const { data: stats, pending } = await useFetch('/api/admin/stats');
|
|
6
6
|
|
|
@@ -234,7 +234,7 @@ if (initial.value) {
|
|
|
234
234
|
history.clear();
|
|
235
235
|
|
|
236
236
|
useSeoMeta({
|
|
237
|
-
title: () => `Edit: ${editor.draft.value?.name ?? 'Layout'}
|
|
237
|
+
title: () => `Edit: ${editor.draft.value?.name ?? 'Layout'}, Admin, ${useSiteName()}`,
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
// Viewport preview state — purely UI; doesn't mutate the layout.
|
|
@@ -493,7 +493,7 @@ async function onPublish(): Promise<void> {
|
|
|
493
493
|
case 'publish':
|
|
494
494
|
toast.error(
|
|
495
495
|
'Your changes are saved as a draft, but publish failed. ' +
|
|
496
|
-
'Try Publish again
|
|
496
|
+
'Try Publish again, the saved draft is durable.',
|
|
497
497
|
);
|
|
498
498
|
return;
|
|
499
499
|
case 'refresh':
|
|
@@ -501,7 +501,7 @@ async function onPublish(): Promise<void> {
|
|
|
501
501
|
// is stale. The next save / publish picks up correctly; a
|
|
502
502
|
// reload syncs immediately.
|
|
503
503
|
toast.show(
|
|
504
|
-
'Published
|
|
504
|
+
'Published, but the editor view is stale. Reload to sync.',
|
|
505
505
|
);
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
@@ -519,7 +519,7 @@ async function onConflictRefresh(): Promise<void> {
|
|
|
519
519
|
// Clear the throttle so auto-save resumes; if cascade really
|
|
520
520
|
// persists, the rolling-window will trip again on its own.
|
|
521
521
|
editor.clearConflictHistory();
|
|
522
|
-
toast.success('Refreshed
|
|
522
|
+
toast.success('Refreshed, server state loaded');
|
|
523
523
|
} catch (err) {
|
|
524
524
|
const e = err as { statusMessage?: string };
|
|
525
525
|
toast.error(e.statusMessage ?? 'Refresh failed');
|
|
@@ -571,7 +571,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
571
571
|
|
|
572
572
|
<!--
|
|
573
573
|
Session 162 P2.5: conflict-thrash banner. Shows when 3+ saves
|
|
574
|
-
have 409'd within the last 60s
|
|
574
|
+
have 409'd within the last 60s, auto-save is now paused so the
|
|
575
575
|
page stops banging the server while the admin reconciles. The
|
|
576
576
|
existing AdminLayoutsConflictModal handles the per-conflict UX;
|
|
577
577
|
this banner is the layer above, addressing the cascade pattern.
|
|
@@ -595,7 +595,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
595
595
|
<strong>Auto-save paused</strong>
|
|
596
596
|
<span>
|
|
597
597
|
Three of your recent saves collided with another admin's
|
|
598
|
-
edits. Reload their version (recommended)
|
|
598
|
+
edits. Reload their version (recommended), your edits will
|
|
599
599
|
be lost. Overwriting their changes is destructive and final.
|
|
600
600
|
</span>
|
|
601
601
|
</div>
|
|
@@ -606,7 +606,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
606
606
|
option, danger red = destructive action LAST in tab order so
|
|
607
607
|
keyboard users don't land on it.
|
|
608
608
|
Banner-specific: "Resume auto-save" replaces the modal's
|
|
609
|
-
"Keep editing here"
|
|
609
|
+
"Keep editing here", same neutral level, different semantic
|
|
610
610
|
(banner's middle option turns auto-save back on without
|
|
611
611
|
reconciliation; modal's middle option closes the modal).
|
|
612
612
|
-->
|
|
@@ -642,9 +642,9 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
642
642
|
<!--
|
|
643
643
|
Round-3 audit fix: phone (≤640px) sees a single banner instead
|
|
644
644
|
of the editor. Drag-drop on a 375px viewport is user-hostile
|
|
645
|
-
regardless of how well-designed
|
|
645
|
+
regardless of how well-designed, matches docs/plans/layout-and-pages.md §7.7.
|
|
646
646
|
Note: the @media rule uses `max-width: 640px` (inclusive), so
|
|
647
|
-
a viewport at exactly 640px sees the banner
|
|
647
|
+
a viewport at exactly 640px sees the banner, comment matches.
|
|
648
648
|
-->
|
|
649
649
|
<div class="cpub-admin-layouts-editor-phone-only">
|
|
650
650
|
<i class="fa-solid fa-display cpub-admin-layouts-editor-phone-icon" aria-hidden="true"></i>
|
|
@@ -668,7 +668,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
668
668
|
falls back to the page-meta form per §7.9 dispatch pattern).
|
|
669
669
|
-->
|
|
670
670
|
<!--
|
|
671
|
-
Phase 3b/A: SR narration channel
|
|
671
|
+
Phase 3b/A: SR narration channel, a singleton aria-live region
|
|
672
672
|
that <LayoutSection> + <LayoutRow> mirror drag/drop + Move
|
|
673
673
|
Up/Down events into. dnd-kit ships no announcer OOTB; this
|
|
674
674
|
closes the WCAG 2.1.1 gap. Mounted ONCE outside the
|
|
@@ -693,7 +693,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
693
693
|
(Pre-audit ordering put palette first → admin had to scroll
|
|
694
694
|
past 17 tiles to reach the canvas.)
|
|
695
695
|
v-show on palette + inspector (not v-if) preserves component
|
|
696
|
-
state
|
|
696
|
+
state, scroll position, focused field, across hide/show. -->
|
|
697
697
|
<AdminLayoutsCanvas
|
|
698
698
|
:layout="editor.draft.value"
|
|
699
699
|
:viewport="viewport"
|
|
@@ -726,7 +726,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
726
726
|
are always visible in editable mode.
|
|
727
727
|
|
|
728
728
|
Hidden on mobile/tablet (< 1024px) where the body falls
|
|
729
|
-
back to a single column DOM-order stack
|
|
729
|
+
back to a single column DOM-order stack, the toggles
|
|
730
730
|
would float over content with no panel to collapse.
|
|
731
731
|
-->
|
|
732
732
|
<button
|
|
@@ -799,7 +799,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
799
799
|
--warning token didn't exist in the theme system → fell back to
|
|
800
800
|
surface2 which read as a neutral box, not alert. Now uses the
|
|
801
801
|
established --yellow-bg / --yellow-border tokens (defined on every
|
|
802
|
-
theme
|
|
802
|
+
theme, base.css line 70-71 + all variants) that other "attention"
|
|
803
803
|
surfaces in the layer use. Sits between toolbar + body so it's
|
|
804
804
|
visible regardless of canvas scroll. */
|
|
805
805
|
.cpub-admin-layouts-editor-thrash {
|
|
@@ -944,7 +944,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
944
944
|
|
|
945
945
|
@media (max-width: 1024px) {
|
|
946
946
|
/* On tablet, fall back to DOM-order single column (canvas first,
|
|
947
|
-
palette next, inspector last)
|
|
947
|
+
palette next, inspector last), admin sees the editing surface
|
|
948
948
|
immediately without scrolling past the palette. v1 doesn't ship
|
|
949
949
|
bottom-sheet behavior (Phase 6a). */
|
|
950
950
|
.cpub-admin-layouts-editor-body {
|
|
@@ -992,7 +992,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
992
992
|
z-index: 5;
|
|
993
993
|
transition: left 200ms ease-out, right 200ms ease-out, background var(--transition-default), color var(--transition-default);
|
|
994
994
|
/* Compact icon size matches the slim handle silhouette. The 28px
|
|
995
|
-
touch surface is what WCAG cares about
|
|
995
|
+
touch surface is what WCAG cares about, the chevron centers inside. */
|
|
996
996
|
font-size: 10px;
|
|
997
997
|
}
|
|
998
998
|
.cpub-admin-layouts-editor-edge-tab:hover {
|
|
@@ -1009,7 +1009,7 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
1009
1009
|
.cpub-admin-layouts-editor-edge-tab--left {
|
|
1010
1010
|
/* Sit at the right edge of the palette (which is 280px wide). The
|
|
1011
1011
|
-14px offset centers the 28px-wide tab ON the boundary so half is
|
|
1012
|
-
in the palette + half in the canvas
|
|
1012
|
+
in the palette + half in the canvas, reads as "the boundary
|
|
1013
1013
|
itself is the toggle". (Was -9px when the tab was 18px wide.) */
|
|
1014
1014
|
left: calc(280px - 14px);
|
|
1015
1015
|
}
|
|
@@ -1042,14 +1042,14 @@ async function onConflictForceSave(): Promise<void> {
|
|
|
1042
1042
|
.cpub-admin-layouts-editor-edge-tab { display: none; }
|
|
1043
1043
|
/* Session 164 audit R3-3: force panels visible regardless of the
|
|
1044
1044
|
cookie-persisted desktop-collapse state. At tablet/phone the body
|
|
1045
|
-
falls back to a DOM-order single-column stack
|
|
1045
|
+
falls back to a DOM-order single-column stack, the desktop
|
|
1046
1046
|
'collapsed' state has no useful meaning when there's no grid column
|
|
1047
1047
|
to remove, but `chrome.paletteHidden` / `chrome.inspectorHidden`
|
|
1048
1048
|
still drive v-show on the panel components, leaving an admin who
|
|
1049
1049
|
collapsed on desktop with NO way to re-show on tablet (the edge
|
|
1050
1050
|
tabs are hidden by the rule above; the toolbar toggles were
|
|
1051
1051
|
removed in the 164 polish). Override v-show's inline display:none
|
|
1052
|
-
with `flex !important` (panels natively use display:flex column
|
|
1052
|
+
with `flex !important` (panels natively use display:flex column -
|
|
1053
1053
|
'block' would break their internal layout). Scoped :deep() because
|
|
1054
1054
|
the .cpub-admin-layouts-{palette,inspector} root classes live in
|
|
1055
1055
|
child components. */
|
|
@@ -18,7 +18,7 @@ definePageMeta({
|
|
|
18
18
|
layout: 'admin',
|
|
19
19
|
middleware: ['auth', 'admin-layouts'],
|
|
20
20
|
});
|
|
21
|
-
useSeoMeta({ title: `Layouts
|
|
21
|
+
useSeoMeta({ title: `Layouts, Admin, ${useSiteName()}` });
|
|
22
22
|
|
|
23
23
|
const toast = useToast();
|
|
24
24
|
const { data: layouts, refresh, pending } = await useFetch<LayoutRecord[]>(
|
|
@@ -59,7 +59,7 @@ async function migrateHomepage(): Promise<void> {
|
|
|
59
59
|
toast.success('Homepage migrated to layout engine');
|
|
60
60
|
await refresh();
|
|
61
61
|
} else if (result.reason === 'layout-already-exists') {
|
|
62
|
-
toast.show('A homepage layout already exists
|
|
62
|
+
toast.show('A homepage layout already exists, opening it');
|
|
63
63
|
await refresh();
|
|
64
64
|
} else {
|
|
65
65
|
toast.show(result.reason ?? 'Migration finished');
|
|
@@ -141,7 +141,7 @@ const sortedLayouts = computed<LayoutRecord[]>(() => {
|
|
|
141
141
|
|
|
142
142
|
<template v-else-if="sortedLayouts.length === 0">
|
|
143
143
|
<!--
|
|
144
|
-
Empty state
|
|
144
|
+
Empty state, single icon + headline + one-line description +
|
|
145
145
|
single primary action, per Carbon + Mobbin SaaS empty-state
|
|
146
146
|
synthesis. Skipping illustration on purpose: the sharp-corner +
|
|
147
147
|
mono UI label aesthetic reads as intentional with just text.
|
|
@@ -154,7 +154,7 @@ const sortedLayouts = computed<LayoutRecord[]>(() => {
|
|
|
154
154
|
<i class="fa-regular fa-folder-open cpub-admin-layouts-empty-icon" aria-hidden="true"></i>
|
|
155
155
|
<h2 class="cpub-admin-layouts-empty-text">No layouts yet</h2>
|
|
156
156
|
<p class="cpub-admin-layouts-empty-hint">
|
|
157
|
-
Layouts arrange sections
|
|
157
|
+
Layouts arrange sections, hero, feed, blocks, into reusable page templates.
|
|
158
158
|
Start by migrating your existing homepage from the legacy editor.
|
|
159
159
|
</p>
|
|
160
160
|
<button
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { NavItem } from '@commonpub/server';
|
|
3
3
|
|
|
4
4
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
5
|
-
useSeoMeta({ title: `Navigation
|
|
5
|
+
useSeoMeta({ title: `Navigation, Admin, ${useSiteName()}` });
|
|
6
6
|
|
|
7
7
|
const toast = useToast();
|
|
8
8
|
const { data, refresh } = await useFetch<NavItem[]>('/api/admin/navigation/items');
|
package/pages/admin/reports.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
|
-
useSeoMeta({ title: `Reports
|
|
3
|
+
useSeoMeta({ title: `Reports, Admin, ${useSiteName()}` });
|
|
4
4
|
|
|
5
5
|
const toast = useToast();
|
|
6
6
|
const statusFilter = ref<string>('pending');
|
package/pages/admin/settings.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ layout: 'admin', middleware: 'auth' });
|
|
3
3
|
|
|
4
|
-
useSeoMeta({ title: `Settings
|
|
4
|
+
useSeoMeta({ title: `Settings, Admin, ${useSiteName()}` });
|
|
5
5
|
|
|
6
6
|
const { data: settings, pending, refresh } = await useFetch<Record<string, string>>('/api/admin/settings');
|
|
7
7
|
|
|
@@ -98,7 +98,7 @@ async function backfillCdn(dryRun: boolean): Promise<void> {
|
|
|
98
98
|
<button class="cpub-btn cpub-btn-sm" @click="cancelEdit">Cancel</button>
|
|
99
99
|
</template>
|
|
100
100
|
<template v-else>
|
|
101
|
-
<span class="settings-value">{{ (settings as Record<string, string>)[item.key] ?? '
|
|
101
|
+
<span class="settings-value">{{ (settings as Record<string, string>)[item.key] ?? '-' }}</span>
|
|
102
102
|
<button class="cpub-btn cpub-btn-sm" @click="startEdit(item.key, (settings as Record<string, string>)[item.key] ?? '')">Edit</button>
|
|
103
103
|
</template>
|
|
104
104
|
</div>
|
|
@@ -336,7 +336,7 @@ onBeforeUnmount(() => {
|
|
|
336
336
|
<label v-if="pairCandidates.length" class="theme-editor-field">
|
|
337
337
|
<span class="theme-editor-field-label">Pair with</span>
|
|
338
338
|
<select v-model="draft.pairId" class="theme-editor-input" @change="onMetaChange">
|
|
339
|
-
<option :value="undefined"
|
|
339
|
+
<option :value="undefined">- none -</option>
|
|
340
340
|
<option v-for="p in pairCandidates" :key="p.id" :value="p.id">{{ p.name }}</option>
|
|
341
341
|
</select>
|
|
342
342
|
</label>
|
|
@@ -367,7 +367,7 @@ onBeforeUnmount(() => {
|
|
|
367
367
|
<textarea
|
|
368
368
|
v-model="draft.description"
|
|
369
369
|
class="theme-editor-description"
|
|
370
|
-
placeholder="Description
|
|
370
|
+
placeholder="Description, shown on the theme list (optional)"
|
|
371
371
|
rows="2"
|
|
372
372
|
@input="onMetaChange"
|
|
373
373
|
/>
|