@commonpub/layer 0.57.0 → 0.59.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 +6 -6
- 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 +78 -19
- 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
package/components/LayoutRow.vue
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ? '
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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}
|
|
38
|
-
: `Requested ${row.domain} to mirror you
|
|
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=""
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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 || '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
28
|
-
<p class="scene-faint">Tertiary text
|
|
29
|
-
<p class="scene-mono-label">Mono label
|
|
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
|
|
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
|
|
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
|
|
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=""
|
|
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
|
|
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
|
|
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
|
|
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 || '
|
|
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
|
|
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"
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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 || '
|
|
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 || '
|
|
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)
|
|
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]
|
|
42
|
-
/^\/admin\/theme\/edit\/[^/]+$/, // /admin/theme/edit/[id]
|
|
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
|
|
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
|
|
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
|
|
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.
|