@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.
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 +4 -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 +8 -8
  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 +13 -12
  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
@@ -595,7 +595,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
595
595
  :on-resize-start="resizeHandlerForSection(section)"
596
596
  />
597
597
  <!--
598
- Session 164 polish remove row × button.
598
+ Session 164 polish, remove row × button.
599
599
  Keyed child so <TransitionGroup> tracks it (TG requires keyed
600
600
  children). The button is position:absolute on the row corner
601
601
  so it doesn't take a grid column; FLIP doesn't move it because
@@ -603,7 +603,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
603
603
  when an onRemoveRow handler is wired (public path stays clean).
604
604
  -->
605
605
  <!--
606
- Phase 3e row select handle. Top-left corner so it doesn't collide
606
+ Phase 3e, row select handle. Top-left corner so it doesn't collide
607
607
  with the top-right remove button. Toggles row selection → the
608
608
  inspector swaps to the row-config form. Keyed for TransitionGroup;
609
609
  hidden on the public path (no onSelect handler).
@@ -614,7 +614,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
614
614
  type="button"
615
615
  class="cpub-layout-row-select"
616
616
  :class="{ 'cpub-layout-row-select--active': rowIsSelected }"
617
- :aria-label="rowIsSelected ? `Row in ${zone} selected activate to deselect` : `Select this row in ${zone}`"
617
+ :aria-label="rowIsSelected ? `Row in ${zone} selected, activate to deselect` : `Select this row in ${zone}`"
618
618
  :aria-pressed="rowIsSelected"
619
619
  :title="rowIsSelected ? 'Deselect row' : 'Select row'"
620
620
  @click.stop="handleRowSelectClick"
@@ -637,7 +637,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
637
637
  <i class="fa-solid fa-xmark" aria-hidden="true"></i>
638
638
  </button>
639
639
  <!--
640
- Phase 3c 12-col guideline overlay. Shown ONLY while a resize is
640
+ Phase 3c, 12-col guideline overlay. Shown ONLY while a resize is
641
641
  in flight AND it's resizing a section in THIS row. 12 vertical
642
642
  lines absolutely positioned across the row's inside; the line at
643
643
  `snapLineCol` (the resized section's right edge) bolds to opacity
@@ -704,7 +704,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
704
704
  empty rows but over-padded compact rows). :empty matches when the
705
705
  row has zero child elements, which happens when sections.length===0
706
706
  OR all sections are filtered out by sectionVisible. Both cases mean
707
- "no drop target without help" exactly when we need to enlarge it. */
707
+ "no drop target without help", exactly when we need to enlarge it. */
708
708
  .cpub-layout-row--editable:empty {
709
709
  min-height: 64px;
710
710
  }
@@ -774,7 +774,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
774
774
 
775
775
  /* Phase 3e — row select handle. Mirrors the remove button's reveal-on-
776
776
  hover/focus/selected behavior; positioned top-LEFT (remove is top-right)
777
- so both fit on a row corner without overlap. Accent (not red) it's a
777
+ so both fit on a row corner without overlap. Accent (not red), it's a
778
778
  selection affordance, not destructive. */
779
779
  .cpub-layout-row-select {
780
780
  position: absolute;
@@ -850,7 +850,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
850
850
  }
851
851
  /* When an item is leaving, its DOM stays for the duration of the
852
852
  leave transition. Take it out of the document flow so other items
853
- can FLIP into its space WITHOUT waiting for the leave to finish
853
+ can FLIP into its space WITHOUT waiting for the leave to finish -
854
854
  gives a visually-coherent reorder when a section is also being
855
855
  removed. */
856
856
  .cpub-flip-leave-active {
@@ -901,7 +901,7 @@ const isOver = computed<boolean>(() => isDragOver.value !== undefined);
901
901
  /* R1-7 audit fix: the overlay is a keyed child of the row's
902
902
  <TransitionGroup>, so it INHERITS the cpub-flip-enter/leave classes
903
903
  while mounting. Their opacity:0 + scale(0.96) prelude conflicts with
904
- the overlay's own fade-in animation for ~150ms the overlay would
904
+ the overlay's own fade-in animation, for ~150ms the overlay would
905
905
  pop to scale 0.96, then snap back. Override to neutralise the flip
906
906
  prelude on the overlay specifically; sections + the remove button
907
907
  keep their flip animations. */
@@ -477,7 +477,7 @@ function onHandlePointerDown(e: PointerEvent): void {
477
477
  <i class="fa-solid fa-chevron-down" aria-hidden="true"></i>
478
478
  </button>
479
479
  <!--
480
- Phase 3b/B "Move to zone…" disclosure. Renders only when the
480
+ Phase 3b/B, "Move to zone…" disclosure. Renders only when the
481
481
  parent provided a non-empty availableZones list (current zone
482
482
  excluded, zones with zero rows excluded). aria-haspopup='menu'
483
483
  + aria-expanded so screen readers announce the disclosure state;
@@ -525,8 +525,8 @@ function onHandlePointerDown(e: PointerEvent): void {
525
525
  </div>
526
526
 
527
527
  <!--
528
- Phase 3c right-edge resize handle.
529
- Renders only when the parent (LayoutRow) passes onResizeStart
528
+ Phase 3c, right-edge resize handle.
529
+ Renders only when the parent (LayoutRow) passes onResizeStart -
530
530
  that's the parent's signal that the section's registry def is
531
531
  `resizable: true` AND the row is wider than the mobile breakpoint
532
532
  (CSS further hides at < 768px defensively).
@@ -563,7 +563,7 @@ function onHandlePointerDown(e: PointerEvent): void {
563
563
  </button>
564
564
 
565
565
  <!--
566
- Phase 3c live span pill. Shown while the section is selected OR
566
+ Phase 3c, live span pill. Shown while the section is selected OR
567
567
  involved in an in-flight resize. Three-state visual:
568
568
  - selected only: subtle outline-style badge "8/12"
569
569
  - resizing (this section): accent-filled, follows live span
@@ -583,7 +583,7 @@ function onHandlePointerDown(e: PointerEvent): void {
583
583
  </div>
584
584
 
585
585
  <!--
586
- Phase 3c constraint snap label. Shown ONLY while THIS section is
586
+ Phase 3c, constraint snap label. Shown ONLY while THIS section is
587
587
  being resized AND a bound was hit. Provides the three independent
588
588
  signals plan §7.5 + WCAG 1.4.1 require: outline color change (the
589
589
  handle's --active state), lock icon ("🔒"), text ("min 3/12").
@@ -876,7 +876,7 @@ function onHandlePointerDown(e: PointerEvent): void {
876
876
  .cpub-layout-section-resize-handle {
877
877
  position: absolute;
878
878
  /* Centered on the section's right border. -2px so the 4px-wide handle
879
- sits half-in/half-out reads as "the border itself is the grip". */
879
+ sits half-in/half-out, reads as "the border itself is the grip". */
880
880
  top: 50%;
881
881
  right: -2px;
882
882
  transform: translateY(-50%);
@@ -912,7 +912,7 @@ function onHandlePointerDown(e: PointerEvent): void {
912
912
  pointer-events: none;
913
913
  }
914
914
  /* Reveal on the section's hover, selection, or focus-within (keyboard
915
- user tabbed to a child) the union covers all input modes. */
915
+ user tabbed to a child), the union covers all input modes. */
916
916
  .cpub-layout-section--editable:hover > .cpub-layout-section-resize-handle,
917
917
  .cpub-layout-section--selected > .cpub-layout-section-resize-handle,
918
918
  .cpub-layout-section--editable:focus-within > .cpub-layout-section-resize-handle,
@@ -947,7 +947,7 @@ function onHandlePointerDown(e: PointerEvent): void {
947
947
  .cpub-layout-section-resize-handle { transition: none; }
948
948
  }
949
949
  /* < 768px: hide the handle per plan §7.5. Colspan changes happen via
950
- the inspector slider on mobile (deferred to Phase 3e keyboard
950
+ the inspector slider on mobile (deferred to Phase 3e, keyboard
951
951
  path via Shift+Arrow still works in the meantime). */
952
952
  @media (max-width: 768px) {
953
953
  .cpub-layout-section-resize-handle { display: none; }
@@ -141,16 +141,16 @@ const zone = computed<LayoutZoneClient | null>(
141
141
  - no layout exists for this route
142
142
  - no zone of that slug in the layout
143
143
  - zone has zero rows
144
- All four are valid "absence" cases fall back to legacy rendering
144
+ All four are valid "absence" cases, fall back to legacy rendering
145
145
  via the page's v-if structure.
146
146
  -->
147
147
  <!--
148
148
  Phase 3b/A extraction: row + section rendering moved to <LayoutRow>
149
149
  so each row instance can own its own `makeDroppable` template ref
150
150
  (dnd-kit composables run per-component setup; one row instance per
151
- component is the natural fit). The HTML SHAPE is preserved same
151
+ component is the natural fit). The HTML SHAPE is preserved, same
152
152
  .cpub-layout-row + .cpub-layout-section classes, same data-* attrs
153
- so existing tests + selectors keep working unchanged.
153
+ , so existing tests + selectors keep working unchanged.
154
154
  -->
155
155
  <template v-if="zone && zone.rows.length > 0">
156
156
  <LayoutRow
@@ -101,7 +101,7 @@ async function remove(): Promise<void> {
101
101
 
102
102
  <p class="cpub-mm-sub">
103
103
  <span class="cpub-mm-dir">{{ isPull ? '↓ Pull (you receive their content)' : '↑ Push request' }}</span>
104
- one-directional: this instance receives content from {{ mirror.remoteDomain }}; they receive nothing from you.
104
+ , one-directional: this instance receives content from {{ mirror.remoteDomain }}; they receive nothing from you.
105
105
  </p>
106
106
 
107
107
  <!-- Facts -->
@@ -146,9 +146,9 @@ async function remove(): Promise<void> {
146
146
  {{ busy === 'backfill' ? 'Importing…' : 'Backfill' }}
147
147
  </button>
148
148
  </div>
149
- <p class="cpub-mm-hint">Crawls {{ mirror.remoteDomain }}'s outbox newest-first and stops at the chosen depth bounded so you don't pull an entire large instance at once.</p>
149
+ <p class="cpub-mm-hint">Crawls {{ mirror.remoteDomain }}'s outbox newest-first and stops at the chosen depth, bounded so you don't pull an entire large instance at once.</p>
150
150
  <div v-if="backfillResult" class="cpub-fed-result">
151
- Imported {{ backfillResult.processed }} item(s), {{ backfillResult.errors }} error(s), {{ backfillResult.pages }} page(s){{ backfillResult.complete ? ' complete.' : ' more available (run again).' }}
151
+ Imported {{ backfillResult.processed }} item(s), {{ backfillResult.errors }} error(s), {{ backfillResult.pages }} page(s){{ backfillResult.complete ? ', complete.' : ', more available (run again).' }}
152
152
  </div>
153
153
  </div>
154
154
 
@@ -19,7 +19,7 @@ useFocusTrap(contentRef, () => visible.value, () => emit('close'));
19
19
 
20
20
  // Same bounded depth choices as the create form — what history to pull when we approve.
21
21
  const DEPTH_OPTIONS: Array<{ label: string; body: Record<string, number> | null }> = [
22
- { label: 'None forward only (default)', body: null },
22
+ { label: 'None, forward only (default)', body: null },
23
23
  { label: 'Last 7 days', body: { sinceDays: 7 } },
24
24
  { label: 'Last 30 days', body: { sinceDays: 30 } },
25
25
  { label: 'Last 90 days', body: { sinceDays: 90 } },
@@ -50,7 +50,7 @@ async function approve(): Promise<void> {
50
50
  const url: string = `/api/admin/federation/mirror-requests/${props.request.id}/approve`;
51
51
  try {
52
52
  await $fetch(url, { method: 'POST', body });
53
- toast.success(`Approved now mirroring ${props.request.remoteDomain}`);
53
+ toast.success(`Approved, now mirroring ${props.request.remoteDomain}`);
54
54
  emit('changed');
55
55
  emit('close');
56
56
  } catch {
@@ -86,7 +86,7 @@ async function reject(): Promise<void> {
86
86
 
87
87
  <p class="cpub-mr-sub">
88
88
  <strong>{{ request.remoteDomain }}</strong> asked you to mirror your instance. Approving creates a
89
- <strong>pull mirror</strong> of them you'll receive their public content, with the depth and
89
+ <strong>pull mirror</strong> of them, you'll receive their public content, with the depth and
90
90
  filters you choose below. (One-directional: they still receive nothing from you.)
91
91
  </p>
92
92
 
@@ -53,7 +53,7 @@ async function vote(optionId: string): Promise<void> {
53
53
  :class="{ voted: data.userVote === option.id, clickable: !hasVoted && isAuthenticated }"
54
54
  :disabled="hasVoted || !isAuthenticated"
55
55
  :aria-pressed="data.userVote === option.id"
56
- :aria-label="`${option.label}${hasVoted ? ` ${percentage(option.voteCount)}%` : ''}`"
56
+ :aria-label="`${option.label}${hasVoted ? `, ${percentage(option.voteCount)}%` : ''}`"
57
57
  @click="vote(option.id)"
58
58
  >
59
59
  <div class="cpub-poll-bar" :style="{ width: hasVoted || !isAuthenticated ? `${percentage(option.voteCount)}%` : '0%' }" />
@@ -34,8 +34,8 @@ async function mirror(row: RegistryRow, direction: 'pull' | 'push'): Promise<voi
34
34
  body: { remoteDomain: row.domain, remoteActorUri: row.actorUri, direction },
35
35
  });
36
36
  toast.success(direction === 'pull'
37
- ? `Mirroring ${row.domain} their posts will arrive`
38
- : `Requested ${row.domain} to mirror you awaiting their approval`);
37
+ ? `Mirroring ${row.domain}, their posts will arrive`
38
+ : `Requested ${row.domain} to mirror you, awaiting their approval`);
39
39
  emit('changed');
40
40
  } catch {
41
41
  toast.error(direction === 'pull' ? 'Failed to add mirror' : 'Failed to send request');
@@ -220,7 +220,7 @@ function groupValue(field: AutoFormField): Record<string, unknown> {
220
220
  >
221
221
  <!-- Optional fields (no default) get a leading unset option so an
222
222
  undefined value reads as "default", not the first real choice. -->
223
- <option v-if="f.optional" value="">— Default —</option>
223
+ <option v-if="f.optional" value="">- Default -</option>
224
224
  <option v-for="opt in f.options" :key="String(opt.value)" :value="opt.value">
225
225
  {{ opt.label }}
226
226
  </option>
@@ -182,7 +182,7 @@ function findFirstRowInZone(zoneSlug: string): LayoutRowType | null {
182
182
  </div>
183
183
  <!--
184
184
  Consolidation Stage 2: the canvas previews the layout through the
185
- shared <PageFrame> the SAME frame production uses (full-width
185
+ shared <PageFrame>, the SAME frame production uses (full-width
186
186
  above; main + sidebar side-by-side; one max-width/sidebar-width).
187
187
  Previously zones were stacked as equal-width labeled boxes, which
188
188
  did NOT match what visitors see (broken WYSIWYG). Each zone keeps
@@ -217,7 +217,7 @@ function findFirstRowInZone(zoneSlug: string): LayoutRowType | null {
217
217
  <!--
218
218
  Session 164 polish (v1 blocker): "+ Add row". Without
219
219
  this, a fresh layout (or a layout with an empty zone)
220
- has no drop target admin is stuck. Click → editor
220
+ has no drop target, admin is stuck. Click → editor
221
221
  page mutates draft.zones[i].rows + records to history
222
222
  + narrates. Plan §7.2.
223
223
  Renders only when the parent provided onAddRow (so the
@@ -134,7 +134,7 @@ onBeforeUnmount(() => {
134
134
  <div id="cpub-admin-layouts-conflict-body" class="cpub-admin-layouts-conflict-body">
135
135
  <p>{{ message ?? 'Another admin saved this layout while you were editing.' }}</p>
136
136
  <p class="cpub-admin-layouts-conflict-body-hint">
137
- Reload their version (recommended) or keep your edits visible so you can copy what
137
+ Reload their version (recommended), or keep your edits visible so you can copy what
138
138
  you need before deciding. Overwriting their changes is destructive and final.
139
139
  </p>
140
140
  </div>
@@ -65,7 +65,7 @@ const groups: HotkeyGroup[] = [
65
65
  title: 'History',
66
66
  rows: [
67
67
  { chord: ['⌘', 'Z'], description: 'Undo the last change. Stack holds the most recent 50 operations.' },
68
- { chord: ['⌘', 'Shift', 'Z'], description: 'Redo. Cancelled by any new action Notion/Linear convention.' },
68
+ { chord: ['⌘', 'Shift', 'Z'], description: 'Redo. Cancelled by any new action, Notion/Linear convention.' },
69
69
  ],
70
70
  },
71
71
  // (Move group deliberately omitted — session 165 deep audit R1-A.
@@ -170,7 +170,7 @@ const id = (suffix: string): string => `cpub-inspector-page-${suffix}`;
170
170
  <option value="sidebar-right">sidebar-right</option>
171
171
  </select>
172
172
  <p id="frame-soon-hint" class="cpub-inspector-page-hint">
173
- Page chrome shape reserved for Phase 4. Currently has no effect; the renderer
173
+ Page chrome shape, reserved for Phase 4. Currently has no effect; the renderer
174
174
  always exposes the same three zones (full-width, main, sidebar).
175
175
  </p>
176
176
  </div>
@@ -163,7 +163,7 @@ const VIEWPORTS: Array<{ value: 'mobile' | 'tablet' | 'desktop'; icon: string; l
163
163
  </NuxtLink>
164
164
 
165
165
  <div class="cpub-admin-layouts-toolbar-title">
166
- <span class="cpub-admin-layouts-toolbar-name">{{ layoutName || '' }}</span>
166
+ <span class="cpub-admin-layouts-toolbar-name">{{ layoutName || '-' }}</span>
167
167
  <span
168
168
  class="cpub-admin-layouts-toolbar-state"
169
169
  :data-state="effectiveState"
@@ -173,7 +173,7 @@ const VIEWPORTS: Array<{ value: 'mobile' | 'tablet' | 'desktop'; icon: string; l
173
173
  <!--
174
174
  Session 164 polish: palette/inspector toggles MOVED to edge tabs on the
175
175
  panels themselves (see pages/admin/layouts/[id].vue body). The toolbar
176
- previously hosted these buttons, but the placement was non-obvious
176
+ previously hosted these buttons, but the placement was non-obvious -
177
177
  collapsing made it unclear where to re-open. The edge tabs at the
178
178
  panel/canvas boundary follow the Notion/Linear convention: when
179
179
  expanded they sit at the panel's outer edge; when collapsed they sit
@@ -200,7 +200,7 @@ const VIEWPORTS: Array<{ value: 'mobile' | 'tablet' | 'desktop'; icon: string; l
200
200
  </div>
201
201
 
202
202
  <!--
203
- Phase 3b/B undo / redo. Plan §7.12 toolbar mockup shows '⤺ ⤻'
203
+ Phase 3b/B, undo / redo. Plan §7.12 toolbar mockup shows '⤺ ⤻'
204
204
  between viewport and save indicator. Tooltip carries the next
205
205
  command's specific label ("Undo: move hero") so the discoverable
206
206
  affordance answers "what will Cmd+Z do?" without taking action.
@@ -250,7 +250,7 @@ const VIEWPORTS: Array<{ value: 'mobile' | 'tablet' | 'desktop'; icon: string; l
250
250
  <!-- R4 audit P2 fix: Discard button wires useLayoutEditor.discard().
251
251
  Enabled only when dirty; emits 'discard' for the parent page to
252
252
  confirm + invoke. Previously discard() was implemented but
253
- unwired admin's only revert path was page refresh. -->
253
+ unwired, admin's only revert path was page refresh. -->
254
254
  <button
255
255
  type="button"
256
256
  class="cpub-admin-layouts-toolbar-btn"
@@ -338,7 +338,7 @@ const VIEWPORTS: Array<{ value: 'mobile' | 'tablet' | 'desktop'; icon: string; l
338
338
  }
339
339
  /* "modified" pill: yellow border + tint background, but theme-safe
340
340
  text color (var(--text)) for WCAG contrast. The raw --yellow token
341
- (#f59e0b) is 2.07:1 on white fails both AA text (4.5:1) and
341
+ (#f59e0b) is 2.07:1 on white, fails both AA text (4.5:1) and
342
342
  non-text UI (3:1). Pairing border+tint with --text gives the visual
343
343
  signal (warning) without the contrast failure. Per session 160
344
344
  audit catch. */
@@ -24,9 +24,9 @@
24
24
  <code class="scene-inline-code">var(--text-base)</code>. The quick brown fox
25
25
  jumps over the lazy dog.
26
26
  </p>
27
- <p class="scene-muted">Secondary text note the contrast against background.</p>
28
- <p class="scene-faint">Tertiary text used for placeholders and faint metadata.</p>
29
- <p class="scene-mono-label">Mono label uppercase letter-spaced</p>
27
+ <p class="scene-muted">Secondary text, note the contrast against background.</p>
28
+ <p class="scene-faint">Tertiary text, used for placeholders and faint metadata.</p>
29
+ <p class="scene-mono-label">Mono label, uppercase letter-spaced</p>
30
30
  </div>
31
31
  </section>
32
32
 
@@ -13,7 +13,7 @@
13
13
  <h1 class="scene-prose-title">Building a federated maker community without a platform</h1>
14
14
  <p class="scene-prose-deck">
15
15
  How CommonPub instances stay sovereign while still talking to Mastodon, Lemmy,
16
- and each other and what we learned shipping the first three live sites.
16
+ and each other, and what we learned shipping the first three live sites.
17
17
  </p>
18
18
  <div class="scene-prose-byline">
19
19
  <span class="scene-prose-avatar">M</span>
@@ -27,7 +27,7 @@
27
27
  Most maker communities live on someone else's platform. The platform owns the
28
28
  identity, the content, the moderation, and the moment the platform changes
29
29
  direction, the community goes with it. That's the failure mode CommonPub is
30
- built around every instance is a complete site, federation is opt-in, and
30
+ built around, every instance is a complete site, federation is opt-in, and
31
31
  moving your community is a database export, not a migration ticket.
32
32
  </p>
33
33
 
@@ -41,7 +41,7 @@
41
41
  </p>
42
42
 
43
43
  <blockquote class="scene-prose-quote">
44
- The schema is the work everything else follows from it.
44
+ The schema is the work, everything else follows from it.
45
45
  </blockquote>
46
46
 
47
47
  <h3 class="scene-prose-h3">What the layer ships</h3>
@@ -132,7 +132,7 @@ const WEIGHTS = ['100', '200', '300', '400', '500', '600', '700', '800', '900'];
132
132
  @change="(e) => commitLengthParts(lengthParts.num, (e.target as HTMLSelectElement).value as never)"
133
133
  >
134
134
  <option v-for="u in NUMBER_UNITS" :key="u" :value="u">{{ u }}</option>
135
- <option value="">—</option>
135
+ <option value="">-</option>
136
136
  </select>
137
137
  <input
138
138
  class="token-input token-input-raw"
@@ -159,7 +159,7 @@ pre.hljs .hljs-title.class_ { color: var(--hljs-variable); }
159
159
  /* Reset the universal `*,::before,::after{border-radius:var(--radius)}`
160
160
  rule from base.css. Themes that override --radius to non-zero (e.g.
161
161
  deveco --radius:6px) leave the inner header + body with their own
162
- rounded corners the rounded edges curve AWAY from each other inside
162
+ rounded corners, the rounded edges curve AWAY from each other inside
163
163
  the outer rounded container, leaving wedges of empty page-bg between
164
164
  them. Sharp inner edges tile flush inside the outer overflow:hidden
165
165
  rounded box. (deveco.io report, 2026-05-21) */
@@ -212,7 +212,7 @@ pre.hljs .hljs-title.class_ { color: var(--hljs-variable); }
212
212
  /* Reset the global `.cpub-prose pre` rule from prose.css that adds
213
213
  border + 16px top/bottom margin to every <pre> inside prose. Inside
214
214
  a BlockCodeView the container already owns the border + the header
215
- sits directly above the body the global rule's margin and extra
215
+ sits directly above the body, the global rule's margin and extra
216
216
  border created a visible "floating bar with gap then code block"
217
217
  effect (heatsynclabs.io report, 2026-05-21). */
218
218
  border: 0 !important;
@@ -40,7 +40,7 @@ const spacingY = computed(() => {
40
40
  .cpub-block-divider {
41
41
  border: none;
42
42
  border-top: var(--border-width-default) solid var(--border);
43
- margin: 36px 0; /* default preserved for existing block callers */
43
+ margin: 36px 0; /* default, preserved for existing block callers */
44
44
  }
45
45
 
46
46
  /* variants */
@@ -47,7 +47,7 @@ const totalPrice = computed(() => {
47
47
  <span v-else>{{ part.name || 'Unknown' }}</span>
48
48
  </td>
49
49
  <td class="cpub-col-qty">{{ (part.qty ?? part.quantity) ?? 1 }}</td>
50
- <td class="cpub-part-notes">{{ part.notes || '' }}</td>
50
+ <td class="cpub-part-notes">{{ part.notes || '-' }}</td>
51
51
  </tr>
52
52
  </tbody>
53
53
  </table>
@@ -79,7 +79,7 @@ function optionClass(idx: number): string {
79
79
 
80
80
  <div v-if="answered" class="cpub-quiz-feedback" :class="isCorrect ? 'correct' : 'wrong'" role="status" aria-live="polite" aria-atomic="true">
81
81
  <i :class="isCorrect ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark'"></i>
82
- <span>{{ isCorrect ? 'Correct!' : 'Not quite the correct answer is highlighted above.' }}</span>
82
+ <span>{{ isCorrect ? 'Correct!' : 'Not quite, the correct answer is highlighted above.' }}</span>
83
83
  </div>
84
84
  </div>
85
85
  </div>
@@ -10,7 +10,7 @@ const attribution = computed(() => (props.content.attribution as string) || '');
10
10
  <template>
11
11
  <blockquote class="cpub-block-quote">
12
12
  <div class="cpub-quote-text" v-html="html" />
13
- <footer v-if="attribution" class="cpub-quote-attr">— {{ attribution }}</footer>
13
+ <footer v-if="attribution" class="cpub-quote-attr">- {{ attribution }}</footer>
14
14
  </blockquote>
15
15
  </template>
16
16
 
@@ -113,7 +113,7 @@ const dateRange = computed<string>(() => {
113
113
  new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', ...(withYear ? { year: 'numeric' } : {}) });
114
114
  const start = c.value?.startDate ? fmt(c.value.startDate) : '';
115
115
  const end = c.value?.endDate ? fmt(c.value.endDate, true) : '';
116
- if (start && end) return `${start} ${end}`;
116
+ if (start && end) return `${start}, ${end}`;
117
117
  return start || end;
118
118
  });
119
119
  </script>
@@ -207,7 +207,7 @@ const dateRange = computed<string>(() => {
207
207
  </div>
208
208
  <div v-else-if="isDraft" class="cpub-countdown-ended">
209
209
  <i class="fa-solid fa-pen-ruler"></i>
210
- <span>Draft not launched</span>
210
+ <span>Draft, not launched</span>
211
211
  </div>
212
212
  <div v-else-if="dateNote" class="cpub-countdown-ended">
213
213
  <i class="fa-regular fa-calendar"></i>
@@ -189,7 +189,7 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
189
189
  :value="stage.description ?? ''"
190
190
  type="text"
191
191
  class="cpub-form-input"
192
- placeholder="What happens or what to submit/refine this stage"
192
+ placeholder="What happens, or what to submit/refine, this stage"
193
193
  @input="setField(i, { description: ($event.target as HTMLInputElement).value || undefined })"
194
194
  />
195
195
  </div>
@@ -198,13 +198,13 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
198
198
  <div v-if="stage.kind === 'review'" class="cpub-stage-criteria">
199
199
  <div class="cpub-form-field" style="margin-bottom: 10px;">
200
200
  <label class="cpub-form-label">Advance the top N to the next stage</label>
201
- <input :value="stage.advanceCount ?? ''" type="number" min="1" class="cpub-form-input cpub-stage-advn" placeholder="e.g. 50 leave blank to decide at advance time" @input="advanceCountInput(i, $event)" />
201
+ <input :value="stage.advanceCount ?? ''" type="number" min="1" class="cpub-form-input cpub-stage-advn" placeholder="e.g. 50, leave blank to decide at advance time" @input="advanceCountInput(i, $event)" />
202
202
  </div>
203
203
  <div class="cpub-stage-criteria-head">
204
- <span class="cpub-form-label" style="margin: 0;">Judging criteria this round</span>
204
+ <span class="cpub-form-label" style="margin: 0;">Judging criteria, this round</span>
205
205
  <button type="button" class="cpub-btn cpub-btn-sm" @click="addCriterion(i)"><i class="fa-solid fa-plus"></i> Add</button>
206
206
  </div>
207
- <p class="cpub-form-hint" style="margin: 4px 0;">Optional leave empty to use the contest’s default criteria. Set per-round criteria for multi-round contests (e.g. judge proposals on Feasibility, prototypes on Deployment readiness).</p>
207
+ <p class="cpub-form-hint" style="margin: 4px 0;">Optional, leave empty to use the contest’s default criteria. Set per-round criteria for multi-round contests (e.g. judge proposals on Feasibility, prototypes on Deployment readiness).</p>
208
208
  <div v-for="(crit, ci) in (stage.criteria ?? [])" :key="ci" class="cpub-stage-crit-row">
209
209
  <input :value="crit.label" type="text" class="cpub-form-input" placeholder="Criterion (e.g. Community impact)" @input="setCriterion(i, ci, { label: ($event.target as HTMLInputElement).value })" />
210
210
  <input :value="crit.weight ?? ''" type="number" min="0" max="100" class="cpub-form-input cpub-stage-crit-pts" placeholder="pts" @input="critWeightInput(i, ci, $event)" />
@@ -337,7 +337,7 @@ const canvasMaxWidth = computed(() => {
337
337
  <label class="cpub-ae-assets-drop" :class="{ 'cpub-ae-assets-uploading': uploading }">
338
338
  <i :class="uploading ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-cloud-arrow-up'"></i>
339
339
  <div class="cpub-ae-assets-drop-label">{{ uploading ? 'Uploading...' : 'Drop files here' }}</div>
340
- <div class="cpub-ae-assets-drop-sub">JPG, PNG, GIF, SVG, PDF max {{ MAX_CONTENT_UPLOAD_MB }} MB</div>
340
+ <div class="cpub-ae-assets-drop-sub">JPG, PNG, GIF, SVG, PDF, max {{ MAX_CONTENT_UPLOAD_MB }} MB</div>
341
341
  <input type="file" class="cpub-sr-only" :disabled="uploading" @change="onAssetUpload">
342
342
  </label>
343
343
  <div v-if="uploadError" class="cpub-ae-assets-error">
@@ -48,7 +48,7 @@ const blockTypes: BlockTypeGroup[] = [
48
48
  {
49
49
  name: 'Structure',
50
50
  blocks: [
51
- { type: 'sectionHeader', label: 'Section Header', icon: 'fa-heading', description: 'Tag + title + intro starts a section' },
51
+ { type: 'sectionHeader', label: 'Section Header', icon: 'fa-heading', description: 'Tag + title + intro, starts a section' },
52
52
  { type: 'horizontal_rule', label: 'Section Divider', icon: 'fa-minus', description: 'Visual break' },
53
53
  ],
54
54
  },
@@ -86,7 +86,7 @@ const isEmpty = computed(() => !pending.value && items.value.length === 0);
86
86
  Using <img> rather than background-image: (a) Vue auto-escapes
87
87
  attribute bindings so a path.coverImageUrl containing `");
88
88
  evil(` can't escape the url(...) context (modern browsers
89
- ignore JS in CSS URLs but still defence in depth), and (b)
89
+ ignore JS in CSS URLs but still, defence in depth), and (b)
90
90
  the cover IS semantically information when present, so giving
91
91
  it an `alt` of the path title is better a11y than `role=
92
92
  presentation`. Empty alt would also be fine here; the title
@@ -209,7 +209,7 @@ useJsonLd({
209
209
  </NuxtLink>
210
210
  <div v-else class="cpub-series-nav-btn cpub-prev cpub-disabled">
211
211
  <div class="cpub-series-nav-dir"><i class="fa-solid fa-chevron-left"></i> Previous</div>
212
- <div class="cpub-series-nav-ep">—</div>
212
+ <div class="cpub-series-nav-ep">-</div>
213
213
  </div>
214
214
  <NuxtLink v-if="content.seriesNext" :to="content.seriesNext.url || '#'" class="cpub-series-nav-btn cpub-next">
215
215
  <div class="cpub-series-nav-dir">Next <i class="fa-solid fa-chevron-right"></i></div>
@@ -440,7 +440,7 @@ useJsonLd({
440
440
  height: var(--cpub-av-size);
441
441
  /* Hard-lock to a square. Without min/max clamps, a global img reset or a
442
442
  dropped dimension lets the <img> fall back to its intrinsic aspect ratio,
443
- so a portrait photo renders as a tall oval (the deveco blog-avatar bug
443
+ so a portrait photo renders as a tall oval (the deveco blog-avatar bug -
444
444
  visible even on wide viewports, so it's not flex compression). min/max on
445
445
  BOTH axes clamp the used size regardless of what sets width/height. */
446
446
  min-width: var(--cpub-av-size);
@@ -494,7 +494,7 @@ async function handleBuild(): Promise<void> {
494
494
  <tr v-for="(part, idx) in partsFromBlocks" :key="idx">
495
495
  <td class="cpub-part-name">{{ part.name }}</td>
496
496
  <td class="cpub-part-qty">{{ part.quantity }}</td>
497
- <td class="cpub-part-notes">{{ part.notes || '' }}</td>
497
+ <td class="cpub-part-notes">{{ part.notes || '-' }}</td>
498
498
  </tr>
499
499
  </tbody>
500
500
  </table>
@@ -587,7 +587,7 @@ async function handleBuild(): Promise<void> {
587
587
  </div>
588
588
  <div class="cpub-bom-summary-row">
589
589
  <span class="cpub-bom-label">Total Cost</span>
590
- <span class="cpub-bom-val cpub-bom-green">{{ content.estimatedCost || '' }}</span>
590
+ <span class="cpub-bom-val cpub-bom-green">{{ content.estimatedCost || '-' }}</span>
591
591
  </div>
592
592
  <!-- Linked products from catalog -->
593
593
  <template v-if="bomProducts?.length">
@@ -983,7 +983,7 @@ img.cpub-av {
983
983
  .cpub-has-sidebar → content + sidebar
984
984
  .cpub-has-toc.cpub-has-sidebar → TOC + content + sidebar
985
985
  The sidebar 260px column is reserved ONLY when there's sidebar
986
- content to put in it (BOM/parts OR community hub) otherwise the
986
+ content to put in it (BOM/parts OR community hub), otherwise the
987
987
  content column gets the freed width. */
988
988
  .cpub-project-grid {
989
989
  display: grid;
@@ -38,8 +38,8 @@
38
38
  const COOKIE_KEY = 'cpub-admin-sidebar-collapsed';
39
39
 
40
40
  const EDITOR_ROUTE_PATTERNS: RegExp[] = [
41
- /^\/admin\/layouts\/[^/]+$/, // /admin/layouts/[id] Phase 3a layout editor
42
- /^\/admin\/theme\/edit\/[^/]+$/, // /admin/theme/edit/[id] session 154+156 theme editor
41
+ /^\/admin\/layouts\/[^/]+$/, // /admin/layouts/[id], Phase 3a layout editor
42
+ /^\/admin\/theme\/edit\/[^/]+$/, // /admin/theme/edit/[id], session 154+156 theme editor
43
43
  ];
44
44
 
45
45
  export interface AdminSidebarApi {
@@ -68,7 +68,7 @@ export function useAdminSidebar(): AdminSidebarApi {
68
68
  // emit Set-Cookie for unchanged default values).
69
69
  const userPref = useCookie<boolean>(COOKIE_KEY, {
70
70
  default: () => false,
71
- maxAge: 60 * 60 * 24 * 365, // 1 year sidebar pref is "forever"
71
+ maxAge: 60 * 60 * 24 * 365, // 1 year, sidebar pref is "forever"
72
72
  path: '/',
73
73
  sameSite: 'lax',
74
74
  });
@@ -385,7 +385,7 @@ export function useLayoutEditor(id: string): LayoutEditorState {
385
385
  // the beacon path can't carry this payload. Beforeunload still
386
386
  // catches the user's intent to leave; the auto-save's pre-hide
387
387
  // visibility-flush handles smaller payloads (no keepalive cap).
388
- const BEACON_BODY_MAX_BYTES = 60 * 1024; // 60KB 4KB headroom under the 64KB browser cap
388
+ const BEACON_BODY_MAX_BYTES = 60 * 1024; // 60KB, 4KB headroom under the 64KB browser cap
389
389
  if (body.length > BEACON_BODY_MAX_BYTES) {
390
390
  return false;
391
391
  }
@@ -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.