@commonpub/layer 0.56.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.
Files changed (132) hide show
  1. package/components/LayoutRow.vue +8 -8
  2. package/components/LayoutSection.vue +8 -8
  3. package/components/LayoutSlot.vue +3 -3
  4. package/components/MirrorDetailModal.vue +3 -3
  5. package/components/MirrorRequestApproveModal.vue +3 -3
  6. package/components/PollDisplay.vue +1 -1
  7. package/components/RegistryDirectory.vue +2 -2
  8. package/components/admin/layouts/AdminLayoutsAutoForm.vue +1 -1
  9. package/components/admin/layouts/AdminLayoutsCanvas.vue +2 -2
  10. package/components/admin/layouts/AdminLayoutsConflictModal.vue +1 -1
  11. package/components/admin/layouts/AdminLayoutsHelpOverlay.vue +1 -1
  12. package/components/admin/layouts/AdminLayoutsInspectorPage.vue +1 -1
  13. package/components/admin/layouts/AdminLayoutsToolbar.vue +5 -5
  14. package/components/admin/theme/AdminThemeSceneGallery.vue +3 -3
  15. package/components/admin/theme/AdminThemeSceneProse.vue +3 -3
  16. package/components/admin/theme/AdminThemeTokenInput.vue +1 -1
  17. package/components/blocks/BlockCodeView.vue +2 -2
  18. package/components/blocks/BlockDividerView.vue +1 -1
  19. package/components/blocks/BlockPartsListView.vue +1 -1
  20. package/components/blocks/BlockQuizView.vue +1 -1
  21. package/components/blocks/BlockQuoteView.vue +1 -1
  22. package/components/contest/ContestHero.vue +2 -2
  23. package/components/contest/ContestStagesEditor.vue +13 -4
  24. package/components/editors/ArticleEditor.vue +1 -1
  25. package/components/editors/ExplainerEditor.vue +1 -1
  26. package/components/sections/SectionLearning.vue +1 -1
  27. package/components/views/ArticleView.vue +2 -2
  28. package/components/views/ProjectView.vue +3 -3
  29. package/composables/useAdminSidebar.ts +3 -3
  30. package/composables/useLayoutEditor.ts +1 -1
  31. package/composables/useLayoutHotkeys.ts +1 -1
  32. package/composables/useLayoutResize.ts +1 -1
  33. package/composables/usePublishValidation.ts +1 -1
  34. package/composables/useThemeAdmin.ts +2 -2
  35. package/error.vue +1 -1
  36. package/layouts/admin.vue +2 -2
  37. package/layouts/default.vue +2 -2
  38. package/package.json +7 -7
  39. package/pages/[type]/index.vue +1 -1
  40. package/pages/about.vue +3 -3
  41. package/pages/admin/api-keys.vue +5 -5
  42. package/pages/admin/audit.vue +2 -2
  43. package/pages/admin/categories.vue +1 -1
  44. package/pages/admin/content.vue +2 -2
  45. package/pages/admin/features.vue +1 -1
  46. package/pages/admin/federation.vue +9 -9
  47. package/pages/admin/homepage.vue +4 -4
  48. package/pages/admin/index.vue +1 -1
  49. package/pages/admin/layouts/[id].vue +18 -18
  50. package/pages/admin/layouts/index.vue +4 -4
  51. package/pages/admin/navigation.vue +1 -1
  52. package/pages/admin/reports.vue +1 -1
  53. package/pages/admin/settings.vue +2 -2
  54. package/pages/admin/theme/edit/[id].vue +2 -2
  55. package/pages/admin/theme/index.vue +5 -5
  56. package/pages/admin/users.vue +1 -1
  57. package/pages/auth/forgot-password.vue +1 -1
  58. package/pages/auth/login.vue +3 -3
  59. package/pages/auth/register.vue +1 -1
  60. package/pages/auth/reset-password.vue +1 -1
  61. package/pages/auth/verify-email.vue +1 -1
  62. package/pages/cert/[code].vue +1 -1
  63. package/pages/contests/[slug]/edit.vue +20 -14
  64. package/pages/contests/[slug]/index.vue +7 -7
  65. package/pages/contests/[slug]/judge.vue +15 -3
  66. package/pages/contests/[slug]/results.vue +5 -5
  67. package/pages/contests/create.vue +15 -15
  68. package/pages/contests/index.vue +2 -2
  69. package/pages/cookies.vue +1 -1
  70. package/pages/create.vue +2 -2
  71. package/pages/dashboard.vue +1 -1
  72. package/pages/docs/[siteSlug]/[...pagePath].vue +1 -1
  73. package/pages/docs/[siteSlug]/edit.vue +1 -1
  74. package/pages/docs/[siteSlug]/index.vue +1 -1
  75. package/pages/docs/create.vue +1 -1
  76. package/pages/docs/index.vue +1 -1
  77. package/pages/events/[slug]/edit.vue +1 -1
  78. package/pages/events/[slug]/index.vue +2 -2
  79. package/pages/events/create.vue +1 -1
  80. package/pages/events/index.vue +1 -1
  81. package/pages/explore.vue +1 -1
  82. package/pages/federated-hubs/[id]/index.vue +3 -3
  83. package/pages/federated-hubs/[id]/posts/[postId].vue +1 -1
  84. package/pages/federation/search.vue +1 -1
  85. package/pages/feed.vue +1 -1
  86. package/pages/hubs/[slug]/members.vue +1 -1
  87. package/pages/hubs/[slug]/posts/[postId].vue +1 -1
  88. package/pages/hubs/[slug]/settings.vue +5 -5
  89. package/pages/hubs/create.vue +6 -6
  90. package/pages/hubs/index.vue +1 -1
  91. package/pages/index.vue +2 -2
  92. package/pages/learn/[slug]/[lessonSlug]/edit.vue +1 -1
  93. package/pages/learn/[slug]/[lessonSlug]/index.vue +4 -4
  94. package/pages/learn/[slug]/edit.vue +1 -1
  95. package/pages/learn/[slug]/index.vue +1 -1
  96. package/pages/learn/create.vue +1 -1
  97. package/pages/learn/index.vue +2 -2
  98. package/pages/messages/[conversationId].vue +1 -1
  99. package/pages/messages/index.vue +1 -1
  100. package/pages/notifications.vue +1 -1
  101. package/pages/privacy.vue +5 -5
  102. package/pages/products/[slug].vue +1 -1
  103. package/pages/products/index.vue +1 -1
  104. package/pages/search.vue +1 -1
  105. package/pages/settings/profile.vue +1 -1
  106. package/pages/settings.vue +1 -1
  107. package/pages/tags/[slug].vue +1 -1
  108. package/pages/tags/index.vue +1 -1
  109. package/pages/terms.vue +1 -1
  110. package/pages/u/[username]/[type]/[slug]/edit.vue +3 -3
  111. package/pages/u/[username]/[type]/[slug]/index.vue +1 -1
  112. package/pages/u/[username]/followers.vue +1 -1
  113. package/pages/u/[username]/following.vue +1 -1
  114. package/pages/u/[username]/index.vue +3 -3
  115. package/pages/videos/[id].vue +1 -1
  116. package/pages/videos/index.vue +1 -1
  117. package/pages/videos/submit.vue +2 -2
  118. package/sections/builtin/hero.ts +1 -1
  119. package/sections/builtin/markdown.ts +1 -1
  120. package/server/api/admin/homepage/sections.put.ts +1 -1
  121. package/server/api/admin/layouts/[id].put.ts +1 -1
  122. package/server/api/contests/[slug]/entries.post.ts +3 -3
  123. package/server/api/hubs/[slug]/feed.xml.get.ts +1 -1
  124. package/server/api/users/[username]/feed.xml.get.ts +1 -1
  125. package/server/middleware/content-redirect.ts +1 -1
  126. package/server/plugins/federation-delivery.ts +1 -1
  127. package/server/plugins/federation-hub-sync.ts +1 -1
  128. package/server/plugins/registry-heartbeat.ts +3 -3
  129. package/server/plugins/search-index.ts +1 -1
  130. package/server/utils/email.ts +3 -3
  131. package/server/utils/instanceTheme.ts +1 -1
  132. package/utils/contestStages.ts +3 -3
@@ -253,7 +253,7 @@ export function useLayoutHotkeys(opts: UseLayoutHotkeysOptions): UseLayoutHotkey
253
253
  const draft = opts.getDraft();
254
254
  if (!draft) return;
255
255
  const loc = findSectionLocation(draft, sel.id);
256
- if (!loc) return; // stale selection section vanished mid-keydown
256
+ if (!loc) return; // stale selection, section vanished mid-keydown
257
257
 
258
258
  // --- Keyboard resize (Phase 3c) ---
259
259
  // Run BEFORE Backspace/Cmd+D so Shift+ArrowRight doesn't fall through.
@@ -186,7 +186,7 @@ export function clampResize(params: {
186
186
  if (neighbourStart === null) {
187
187
  return {
188
188
  newColSpan,
189
- newNeighbourColSpan: 0, // sentinel caller knows there's no neighbour
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 add at least one block');
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 the original CommonPub look' },
20
- agora: { name: 'Agora', description: 'Warm parchment tones, green accent, serif display font institutional warmth' },
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} CommonPub` });
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" the icon alone has no accessible name.
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 session 160 audit. -->
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>
@@ -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
- height, radius, shadow, position, padding not just its colors, without forking
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 that one
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.56.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
58
  "@commonpub/explainer": "0.7.15",
61
- "@commonpub/schema": "0.31.0",
62
- "@commonpub/server": "2.78.0",
59
+ "@commonpub/learning": "0.5.2",
60
+ "@commonpub/schema": "0.33.0",
61
+ "@commonpub/auth": "0.8.0",
62
+ "@commonpub/server": "2.80.0",
63
63
  "@commonpub/protocol": "0.13.0",
64
- "@commonpub/ui": "0.9.2",
65
- "@commonpub/learning": "0.5.2"
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",
@@ -16,7 +16,7 @@ if (!isTypeEnabled(contentType.value as ContentType)) {
16
16
  }
17
17
 
18
18
  useSeoMeta({
19
- title: () => `${contentType.value} ${siteName}`,
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 ${useSiteName()}`,
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 everything you need to share how you built it.</p>
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 all in one place.</p>
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>
@@ -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 Admin ${useSiteName()}` });
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 copy it now.</strong>
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 the server only keeps a hash.
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 ?? '' }}</td>
331
+ <td>{{ e.p95LatencyMs ?? '-' }}</td>
332
332
  </tr>
333
333
  </tbody>
334
334
  </table>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
3
 
4
- useSeoMeta({ title: `Audit Log Admin ${useSiteName()}` });
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 || '' }}</td>
44
+ <td class="audit-id">{{ log.targetId || '-' }}</td>
45
45
  <td>{{ new Date(log.createdAt).toLocaleString() }}</td>
46
46
  </tr>
47
47
  </tbody>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
- useSeoMeta({ title: `Categories Admin ${useSiteName()}` });
3
+ useSeoMeta({ title: `Categories, Admin, ${useSiteName()}` });
4
4
 
5
5
  const toast = useToast();
6
6
 
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
- useSeoMeta({ title: `Content Management Admin ${useSiteName()}` });
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">—</option>
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>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
- useSeoMeta({ title: `Feature Flags Admin ${useSiteName()}` });
3
+ useSeoMeta({ title: `Feature Flags, Admin, ${useSiteName()}` });
4
4
 
5
5
  const toast = useToast();
6
6
 
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
- useSeoMeta({ title: `Federation Admin ${useSiteName()}` });
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 forward only (default)', body: null as Record<string, number> | null },
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} they must approve before they mirror you`);
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 imported ${r?.processed ?? 0} item(s)`);
186
+ toast.success(`Mirror added, imported ${r?.processed ?? 0} item(s)`);
187
187
  } catch {
188
- toast.error('Mirror added, but history import failed use Backfill in its details to retry.');
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 new posts will arrive as they publish');
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> you receive their posts; they receive nothing from
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 they pull your public content. (One-directional: you don't pull them unless you add a mirror above.)</p>
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 choose how far back.</p>
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>
@@ -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 Admin ${useSiteName()}` });
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 it
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 use the new visual editor for live changes.</p>
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 links to
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 {
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
- useSeoMeta({ title: `Admin Dashboard ${useSiteName()}` });
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'} Admin ${useSiteName()}`,
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 the saved draft is durable.',
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 but the editor view is stale. Reload to sync.',
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 server state loaded');
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 auto-save is now paused so the
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) your edits will
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" same neutral level, different semantic
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 matches docs/plans/layout-and-pages.md §7.7.
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 comment matches.
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 a singleton aria-live region
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 scroll position, focused field across hide/show. -->
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 the toggles
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 base.css line 70-71 + all variants) that other "attention"
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) admin sees the editing surface
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 the chevron centers inside. */
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 reads as "the boundary
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 the desktop
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 Admin ${useSiteName()}` });
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 opening it');
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 single icon + headline + one-line description +
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 hero, feed, blocks into reusable page templates.
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 Admin ${useSiteName()}` });
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');
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
- useSeoMeta({ title: `Reports Admin ${useSiteName()}` });
3
+ useSeoMeta({ title: `Reports, Admin, ${useSiteName()}` });
4
4
 
5
5
  const toast = useToast();
6
6
  const statusFilter = ref<string>('pending');
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: 'admin', middleware: 'auth' });
3
3
 
4
- useSeoMeta({ title: `Settings Admin ${useSiteName()}` });
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] ?? '' }}</span>
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">— none —</option>
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 shown on the theme list (optional)"
370
+ placeholder="Description, shown on the theme list (optional)"
371
371
  rows="2"
372
372
  @input="onMetaChange"
373
373
  />