smoodly 0.0.14 → 0.0.16
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/README.md +94 -15
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/editor/CanvasOverlay.d.ts +5 -1
- package/dist/admin/editor/CanvasOverlay.js +5 -5
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +251 -23
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/SharedInspector.d.ts +23 -0
- package/dist/admin/editor/SharedInspector.js +13 -0
- package/dist/admin/editor/shared-edit.d.ts +39 -0
- package/dist/admin/editor/shared-edit.js +124 -0
- package/dist/admin/editor/useSharedEdit.d.ts +6 -0
- package/dist/admin/editor/useSharedEdit.js +18 -0
- package/dist/admin/forms/tabs.d.ts +9 -3
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +198 -23
- package/dist/admin/ops.d.ts +32 -0
- package/dist/admin/serialize.d.ts +0 -1
- package/dist/admin/serialize.js +0 -1
- package/dist/admin/shared-items.js +14 -0
- package/dist/admin/shell/AdminApp.js +1 -1
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/IconRail.js +3 -3
- package/dist/admin/shell/PagesList.js +3 -2
- package/dist/admin/shell/SharedFields.d.ts +21 -0
- package/dist/admin/shell/SharedFields.js +15 -0
- package/dist/admin/shell/SharedForm.js +166 -52
- package/dist/admin/shell/SharedList.js +3 -2
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +13 -0
- package/dist/admin/ui/primitives.js +18 -2
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +46 -6
- package/dist/collections.d.ts +0 -6
- package/dist/collections.js +0 -1
- package/dist/entry-store.d.ts +38 -13
- package/dist/entry-store.js +90 -23
- package/dist/next/page-renderer.js +8 -4
- package/dist/shared.d.ts +8 -1
- package/dist/shared.js +10 -1
- package/dist/site.d.ts +2 -0
- package/dist/site.js +17 -7
- package/dist/sql.js +18 -15
- package/dist/store.d.ts +38 -13
- package/dist/store.js +82 -12
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +101 -25
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +88 -19
- package/package.json +1 -1
|
@@ -106,11 +106,11 @@ export function Segmented({ options, value, onChange, stretch, disabled, label,
|
|
|
106
106
|
}) }));
|
|
107
107
|
}
|
|
108
108
|
/* ── data ────────────────────────────────────────────────────────────── */
|
|
109
|
+
// Keyed by the product labels of ui/status-label.ts (lowercased by StatusBadge).
|
|
109
110
|
const STATUS_TONE = {
|
|
110
111
|
published: "published",
|
|
112
|
+
"unpublished changes": "changes",
|
|
111
113
|
draft: "draft",
|
|
112
|
-
"draft edits": "scheduled",
|
|
113
|
-
scheduled: "scheduled",
|
|
114
114
|
};
|
|
115
115
|
export function StatusBadge({ status }) {
|
|
116
116
|
const tone = STATUS_TONE[status.toLowerCase()] ?? "draft";
|
|
@@ -134,9 +134,25 @@ export function SectionLabel({ children, action, mono, style }) {
|
|
|
134
134
|
export function Divider({ bleed = 0 }) {
|
|
135
135
|
return _jsx("div", { className: "sm-divider", style: { margin: `0 -${bleed}px` } });
|
|
136
136
|
}
|
|
137
|
+
/** The lines under a publish-validation banner: one per field, each focusing its widget
|
|
138
|
+
* when the screen can reveal it (spec 2026-09-14 §2). */
|
|
139
|
+
export function FieldErrorList({ fields, onReveal }) {
|
|
140
|
+
if (fields.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return (_jsx("ul", { className: "sm-fielderr", children: fields.map((f) => (_jsxs("li", { children: [onReveal
|
|
143
|
+
? _jsx("button", { type: "button", className: "sm-fielderr__field", onClick: () => onReveal(f.field), children: f.field })
|
|
144
|
+
: f.field, ": ", f.message] }, f.field))) }));
|
|
145
|
+
}
|
|
137
146
|
export function Banner({ tone, children, onDismiss }) {
|
|
138
147
|
return (_jsxs("div", { className: `sm-banner sm-banner--${tone}`, role: tone === "error" ? "alert" : undefined, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: children }), onDismiss && (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": "Dismiss", style: { background: "transparent", border: 0, color: "inherit", cursor: "pointer", font: "inherit", padding: 0 }, children: "\u00D7" }))] }));
|
|
139
148
|
}
|
|
140
149
|
export function cx(...parts) {
|
|
141
150
|
return parts.filter(Boolean).join(" ");
|
|
142
151
|
}
|
|
152
|
+
/** One line of a form's info block: a muted label, the value right-aligned and clipped. */
|
|
153
|
+
export function InfoRow({ label, value, mono }) {
|
|
154
|
+
return (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 10, fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: [_jsx("span", { children: label }), _jsx("span", { style: {
|
|
155
|
+
color: "var(--text)", fontFamily: mono ? "var(--font-mono)" : undefined,
|
|
156
|
+
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
|
157
|
+
}, children: value })] }));
|
|
158
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LocaleStatus } from "../shell/pages-tree.ts";
|
|
2
|
+
export type StatusLabel = "Published" | "Unpublished changes" | "Draft";
|
|
3
|
+
export declare function statusLabel(status: LocaleStatus): StatusLabel;
|
|
4
|
+
/** Which of the three actions apply (spec §2). `pending`: the editor holds a change
|
|
5
|
+
* the store has not confirmed yet — Publish wakes on the first keystroke, not on the ack. */
|
|
6
|
+
export declare function publishActions(status: LocaleStatus | null, pending: boolean): {
|
|
7
|
+
publish: boolean;
|
|
8
|
+
discard: boolean;
|
|
9
|
+
unpublish: boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function statusLabel(status) {
|
|
2
|
+
return status === "published" ? "Published" : status === "draft edits" ? "Unpublished changes" : "Draft";
|
|
3
|
+
}
|
|
4
|
+
/** Which of the three actions apply (spec §2). `pending`: the editor holds a change
|
|
5
|
+
* the store has not confirmed yet — Publish wakes on the first keystroke, not on the ack. */
|
|
6
|
+
export function publishActions(status, pending) {
|
|
7
|
+
if (status === null)
|
|
8
|
+
return { publish: false, discard: false, unpublish: false };
|
|
9
|
+
return {
|
|
10
|
+
publish: status !== "published" || pending,
|
|
11
|
+
discard: status === "draft edits",
|
|
12
|
+
unpublish: status === "published" || status === "draft edits",
|
|
13
|
+
};
|
|
14
|
+
}
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -25,6 +25,7 @@ const TOKENS_LIGHT = `
|
|
|
25
25
|
--line:#E8E3DA; --line-dashed:#D8D1C4; --line-soft:#EFEBE3;
|
|
26
26
|
--text:#1F1D1A; --text-secondary:#4E4942; --text-muted:#7A746A; --text-subtle:#A39C90; --text-disabled:#C4BDB1;
|
|
27
27
|
--selection:var(--accent); --nav-active-bg:var(--accent-soft); --nav-active-fg:var(--accent);
|
|
28
|
+
--selection-shared:#C4620C;
|
|
28
29
|
--hover:rgba(31,29,26,.05); --press:rgba(31,29,26,.09); --control-hover:#F8F6F1;
|
|
29
30
|
--btn-secondary-bg:var(--surface); --btn-secondary-hover:#F8F6F1; --tile-hover:var(--surface);
|
|
30
31
|
--bar-bg:transparent; --bar-border:0; --menu-border:0; --card-divider:0;
|
|
@@ -40,7 +41,8 @@ const TOKENS_LIGHT = `
|
|
|
40
41
|
--logo-color:var(--accent);
|
|
41
42
|
|
|
42
43
|
--status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
|
|
43
|
-
--status-
|
|
44
|
+
--status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
|
|
45
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--surface-inset);
|
|
44
46
|
--status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);
|
|
45
47
|
--status-saved-dot:var(--green-500);
|
|
46
48
|
--warn:var(--amber-700); --warn-soft:var(--amber-50);
|
|
@@ -61,6 +63,7 @@ const TOKENS_LIGHT = `
|
|
|
61
63
|
`;
|
|
62
64
|
const TOKENS_DARK = `
|
|
63
65
|
--accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --accent-line:#6CC4A6; --on-accent:#0B1F18;
|
|
66
|
+
--selection-shared:#F09A5C;
|
|
64
67
|
--bg:#1C1A17; --canvas:#141210; --surface:#27241F; --surface-form:#27241F; --surface-inset:#1C1A17;
|
|
65
68
|
--input-bg:#211E1A; --input-hover-bg:#211E1A; --input-focus-bg:#211E1A;
|
|
66
69
|
--line:#3A3630; --line-dashed:#4A453D; --line-soft:#3A3630;
|
|
@@ -70,7 +73,8 @@ const TOKENS_DARK = `
|
|
|
70
73
|
--toolbar-bg:#0F0E0C; --toolbar-fg:#F1EDE6;
|
|
71
74
|
|
|
72
75
|
--status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);
|
|
73
|
-
--status-
|
|
76
|
+
--status-changes-fg:var(--amber-300); --status-changes-bg:rgba(232,184,90,.14);
|
|
77
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--press);
|
|
74
78
|
--warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);
|
|
75
79
|
--danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);
|
|
76
80
|
|
|
@@ -106,6 +110,7 @@ const SCALE = `
|
|
|
106
110
|
--table-pad:8px 12px; --table-top:80px; --canvas-pad:24px 40px 0; --card-pad:16px 18px; --card-gap:12px; --form-pad:32px 40px;
|
|
107
111
|
--border:1px solid var(--line);
|
|
108
112
|
--outline-selection:2px solid var(--selection);
|
|
113
|
+
--outline-selection-shared:2px solid var(--selection-shared);
|
|
109
114
|
`;
|
|
110
115
|
/* ── TEMPORARY: the original look, kept for side-by-side comparison while
|
|
111
116
|
the warm restyle lands. Frozen copies of the three strings above as they
|
|
@@ -128,11 +133,13 @@ const TECHNICAL_LIGHT = `
|
|
|
128
133
|
--text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);
|
|
129
134
|
--accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);
|
|
130
135
|
--selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);
|
|
136
|
+
--selection-shared:#D2610A;
|
|
131
137
|
--hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);
|
|
132
138
|
--control-hover:var(--gray-100);
|
|
133
139
|
|
|
134
140
|
--status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
|
|
135
|
-
--status-
|
|
141
|
+
--status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
|
|
142
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--surface-inset);
|
|
136
143
|
--status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);
|
|
137
144
|
--status-saved-dot:var(--green-500);
|
|
138
145
|
--warn:var(--amber-700); --warn-soft:var(--amber-50);
|
|
@@ -155,12 +162,14 @@ const TECHNICAL_DARK = `
|
|
|
155
162
|
--line:#2A2F3B; --line-dashed:#2A2F3B;
|
|
156
163
|
--text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;
|
|
157
164
|
--accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);
|
|
165
|
+
--selection-shared:#F2A35F;
|
|
158
166
|
--nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);
|
|
159
167
|
--hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);
|
|
160
168
|
--control-hover:var(--input-bg);
|
|
161
169
|
|
|
162
170
|
--status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);
|
|
163
|
-
--status-
|
|
171
|
+
--status-changes-fg:var(--amber-300); --status-changes-bg:rgba(232,184,90,.14);
|
|
172
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--press);
|
|
164
173
|
--warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);
|
|
165
174
|
--danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);
|
|
166
175
|
|
|
@@ -181,6 +190,7 @@ const TECHNICAL_SCALE = `
|
|
|
181
190
|
--nav-w:72px; --inspector-w:380px;
|
|
182
191
|
--border:1px solid var(--line);
|
|
183
192
|
--outline-selection:1.5px solid var(--selection);
|
|
193
|
+
--outline-selection-shared:1.5px solid var(--selection-shared);
|
|
184
194
|
`;
|
|
185
195
|
/* The tokens the warm restyle added, with the values the technical look
|
|
186
196
|
implied, so the frozen block above renders the old shapes too. */
|
|
@@ -293,7 +303,7 @@ body:has(.sm-admin){margin:0}
|
|
|
293
303
|
.sm-rail__btn[aria-current="page"],.sm-rail__btn[aria-expanded="true"]{color:var(--nav-active-fg);font-weight:600}
|
|
294
304
|
.sm-rail__btn[aria-current="page"]:hover,.sm-rail__btn[aria-expanded="true"]:hover{color:var(--nav-active-fg)}
|
|
295
305
|
.sm-rail__btn[aria-current="page"] .sm-rail__tile,.sm-rail__btn[aria-expanded="true"] .sm-rail__tile{background:var(--nav-active-bg)}
|
|
296
|
-
.sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:
|
|
306
|
+
.sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
297
307
|
.sm-rail__chev{position:absolute;right:4px;bottom:3px;width:8px;height:8px}
|
|
298
308
|
.sm-rail__chev svg{width:8px;height:8px;stroke-width:3}
|
|
299
309
|
.sm-rail__label{letter-spacing:.01em;white-space:nowrap}
|
|
@@ -592,6 +602,15 @@ body:has(.sm-admin){margin:0}
|
|
|
592
602
|
border-radius:var(--radius-sm);line-height:1;height:24px;
|
|
593
603
|
}
|
|
594
604
|
.sm-rowmenu:hover{background:var(--surface-inset);color:var(--text)}
|
|
605
|
+
.sm-dotmenu{
|
|
606
|
+
width:var(--button-h);height:var(--button-h);flex:none;border-radius:50%;border:0;padding:0;
|
|
607
|
+
background:var(--btn-secondary-bg);color:var(--text-muted);font:inherit;font-weight:700;letter-spacing:1px;
|
|
608
|
+
display:grid;place-items:center;cursor:pointer;
|
|
609
|
+
}
|
|
610
|
+
.sm-dotmenu:hover{background:var(--btn-secondary-hover);color:var(--text)}
|
|
611
|
+
.sm-dotmenu:disabled{opacity:.4;cursor:default}
|
|
612
|
+
.sm-dotmenu:disabled:hover{background:var(--btn-secondary-bg);color:var(--text-muted)}
|
|
613
|
+
.sm-picker--menu{position:absolute;top:calc(100% + 6px);right:0;width:220px;z-index:60}
|
|
595
614
|
|
|
596
615
|
/* Status pill: a dot before the word in the warm look (none in the technical). */
|
|
597
616
|
.sm-badge{
|
|
@@ -664,6 +683,8 @@ body:has(.sm-admin){margin:0}
|
|
|
664
683
|
.sm-banner--error{background:var(--danger-soft);color:var(--danger)}
|
|
665
684
|
.sm-banner--ok{background:var(--status-published-bg);color:var(--status-published-fg)}
|
|
666
685
|
.sm-banner--warn{background:var(--warn-soft);color:var(--warn)}
|
|
686
|
+
.sm-fielderr{margin:6px 0 0;padding-left:18px;font:inherit;font-size:var(--text-sm);font-family:var(--font-mono)}
|
|
687
|
+
.sm-fielderr__field{background:none;border:0;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
|
|
667
688
|
|
|
668
689
|
/* ── cards ─────────────────────────────────────────────────────────── */
|
|
669
690
|
/* The inspector column (editor, entry and shared forms) is a stack of
|
|
@@ -732,6 +753,7 @@ body:has(.sm-admin){margin:0}
|
|
|
732
753
|
letter-spacing:var(--blocktag-tracking);text-transform:var(--blocktag-transform);padding:3px 10px;
|
|
733
754
|
border-radius:var(--blocktag-radius);white-space:nowrap;
|
|
734
755
|
}
|
|
756
|
+
.sm-blocktag--shared{background:var(--selection-shared)}
|
|
735
757
|
.sm-blocktoolbar{
|
|
736
758
|
display:flex;gap:2px;padding:3px;background:var(--toolbar-bg);border-radius:var(--radius-btn);
|
|
737
759
|
box-shadow:var(--shadow-float);
|
|
@@ -785,6 +807,10 @@ body:has(.sm-admin){margin:0}
|
|
|
785
807
|
border:0;border-radius:var(--radius-sm);padding:8px 10px;cursor:pointer;color:var(--text);font:inherit;
|
|
786
808
|
}
|
|
787
809
|
.sm-picker__item:hover{background:var(--nav-active-bg)}
|
|
810
|
+
/* after the base item rule: same specificity, so source order decides (ActionMenu states) */
|
|
811
|
+
.sm-picker__item:disabled{opacity:.4;cursor:default}
|
|
812
|
+
.sm-picker__item:disabled:hover{background:transparent}
|
|
813
|
+
.sm-picker__item--danger{color:var(--danger)}
|
|
788
814
|
.sm-picker__icon{
|
|
789
815
|
width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);
|
|
790
816
|
color:var(--accent);display:grid;place-items:center;font-size:13px;
|
|
@@ -796,7 +822,9 @@ body:has(.sm-admin){margin:0}
|
|
|
796
822
|
}
|
|
797
823
|
|
|
798
824
|
/* ── media ─────────────────────────────────────────────────────────── */
|
|
799
|
-
|
|
825
|
+
/* The single track is minmax(0,1fr), not auto: an auto track grows to the panel's own width, so the
|
|
826
|
+
panel's max-width:100% would cap at nothing and a 1200px History panel ran past a laptop viewport. */
|
|
827
|
+
.sm-modal{position:fixed;inset:0;z-index:70;background:rgba(0,0,0,.45);display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr);place-items:center;padding:24px}
|
|
800
828
|
.sm-modal__panel{
|
|
801
829
|
max-width:100%;max-height:100%;display:flex;flex-direction:column;background:var(--surface);
|
|
802
830
|
border:var(--menu-border);border-radius:var(--radius-card);box-shadow:var(--shadow-pop);overflow:hidden;
|
|
@@ -804,6 +832,18 @@ body:has(.sm-admin){margin:0}
|
|
|
804
832
|
.sm-modal__head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;border-bottom:var(--card-divider)}
|
|
805
833
|
.sm-modal__body{padding:12px 16px;overflow:auto;display:flex;flex-direction:column;gap:10px;min-height:0}
|
|
806
834
|
.sm-modal__foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px 14px;border-top:var(--card-divider)}
|
|
835
|
+
.sm-modal__body--flush{padding:0;gap:0;flex:1}
|
|
836
|
+
/* ── history (spec 2026-09-13) ─────────────────────────────────── */
|
|
837
|
+
.sm-history{display:flex;flex:1;min-height:0}
|
|
838
|
+
.sm-history__list{width:260px;flex:none;overflow:auto;display:flex;flex-direction:column;border-right:var(--card-divider)}
|
|
839
|
+
.sm-history__row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;border:0;border-bottom:var(--card-divider);background:none;color:inherit;font:inherit;font-size:var(--text-sm);text-align:left;cursor:pointer}
|
|
840
|
+
.sm-history__row:hover{background:var(--input-bg)}
|
|
841
|
+
.sm-history__row--active,.sm-history__row--active:hover{background:var(--input-bg);font-weight:600}
|
|
842
|
+
.sm-history__tag{font-size:var(--text-sm);color:var(--text-muted);padding:1px 8px;border-radius:999px;border:1px solid var(--line-dashed)}
|
|
843
|
+
.sm-history__tag--live{color:var(--status-published-fg);background:var(--status-published-bg);border-color:transparent}
|
|
844
|
+
.sm-history__main{flex:1;min-width:0;display:flex;overflow:auto}
|
|
845
|
+
.sm-history__frame{flex:1;width:100%;height:100%;border:0;background:#fff}
|
|
846
|
+
.sm-history__form{flex:1;min-width:0;margin:0;padding:16px;border:0;pointer-events:none;display:flex;flex-direction:column;gap:14px}
|
|
807
847
|
.sm-picker-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
808
848
|
.sm-dropzone{
|
|
809
849
|
border:1px dashed var(--line-dashed);border-radius:var(--radius-lg);background:var(--input-bg);color:var(--text-muted);
|
package/dist/collections.d.ts
CHANGED
|
@@ -29,11 +29,6 @@ export type CollectionSchema<F extends BuilderMap = BuilderMap, N extends string
|
|
|
29
29
|
admin?: {
|
|
30
30
|
listColumns?: string[];
|
|
31
31
|
};
|
|
32
|
-
/** Per-locale version history for this collection's entries (spec
|
|
33
|
-
* 2026-09-06 §7): every save becomes an `entry_versions` snapshot and
|
|
34
|
-
* publish moves a pointer, as on pages. Off by default — a People list
|
|
35
|
-
* does not want history; articles do. */
|
|
36
|
-
versions?: boolean;
|
|
37
32
|
/** phantom: the entry value shape, used by f.ref target inference */
|
|
38
33
|
readonly __entry?: NamespaceValues<F>;
|
|
39
34
|
};
|
|
@@ -52,5 +47,4 @@ export declare function collection<F extends BuilderMap, const N extends string
|
|
|
52
47
|
admin?: {
|
|
53
48
|
listColumns?: string[];
|
|
54
49
|
};
|
|
55
|
-
versions?: boolean;
|
|
56
50
|
}): CollectionSchema<F, N>;
|
package/dist/collections.js
CHANGED
|
@@ -43,7 +43,6 @@ export function collection(input) {
|
|
|
43
43
|
...(input.path !== undefined ? { path: input.path } : {}),
|
|
44
44
|
...(input.tree !== undefined ? { tree: input.tree } : {}),
|
|
45
45
|
...(input.order !== undefined ? { order: input.order } : {}),
|
|
46
|
-
...(input.versions ? { versions: true } : {}),
|
|
47
46
|
admin: input.admin,
|
|
48
47
|
};
|
|
49
48
|
}
|
package/dist/entry-store.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type EntryLocale = {
|
|
|
13
13
|
draftVersionId: string | null;
|
|
14
14
|
publishedVersionId: string | null;
|
|
15
15
|
updatedAt: number;
|
|
16
|
+
/** The editor's email behind the last write that touched this row; null for the seed and scripts (spec 2026-09-13 history §2). */
|
|
17
|
+
updatedBy: string | null;
|
|
16
18
|
};
|
|
17
19
|
export type EntryRecord = {
|
|
18
20
|
id: string;
|
|
@@ -32,7 +34,8 @@ export type EntryVersion = {
|
|
|
32
34
|
locale: string;
|
|
33
35
|
/** The MERGED fields (shared + localized + slug) at that save. */
|
|
34
36
|
fields: Record<string, unknown>;
|
|
35
|
-
|
|
37
|
+
/** When the content last changed: the unpublished draft row is rewritten in place (spec 2026-09-13 history §1). */
|
|
38
|
+
savedAt: number;
|
|
36
39
|
};
|
|
37
40
|
export type EntryUsage = {
|
|
38
41
|
sourceKind: string;
|
|
@@ -87,7 +90,7 @@ export interface EntryStore {
|
|
|
87
90
|
/** Creates the node and ONE locale row from the merged `fields`: shared
|
|
88
91
|
* keys go to the node, .localized() keys and `slug` to the row. The
|
|
89
92
|
* parent (if any) must exist in that locale. */
|
|
90
|
-
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions): Promise<EntryRecord>;
|
|
93
|
+
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions, by?: string): Promise<EntryRecord>;
|
|
91
94
|
get(collection: string, id: string): Promise<EntryRecord | null>;
|
|
92
95
|
list(collection: string, options?: ListOptions): Promise<EntryRecord[]>;
|
|
93
96
|
/** Batched read for the resolver: the MERGED fields in `locale`, keyed
|
|
@@ -101,17 +104,29 @@ export interface EntryStore {
|
|
|
101
104
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
102
105
|
/** Replaces the merged fields for `locale`: shared keys on the node
|
|
103
106
|
* (every locale sees them), localized keys and the slug on the row. */
|
|
104
|
-
update(collection: string, id: string, locale: string, fields: Record<string, unknown
|
|
105
|
-
setStatus(collection: string, id: string, locale: string, status: "draft" | "published"): Promise<EntryRecord>;
|
|
107
|
+
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string): Promise<EntryRecord>;
|
|
108
|
+
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string): Promise<EntryRecord>;
|
|
109
|
+
/** The live row goes back to what is published: the published snapshot's fields
|
|
110
|
+
* (shared and localized, split as on save — the slug is never versioned) written over
|
|
111
|
+
* the row, the draft pointer set to the published one, the abandoned draft row deleted
|
|
112
|
+
* (spec 2026-09-13 history §1). Refused when the locale has no published version (a
|
|
113
|
+
* non-versioned collection never has one). */
|
|
114
|
+
discardDraft(collection: string, id: string, locale: string, by?: string): Promise<EntryRecord>;
|
|
106
115
|
/** Copies `from`'s localized fields and slug as the new row (shared
|
|
107
116
|
* fields need no copy); refused when the entry has the locale or its
|
|
108
117
|
* parent lacks it. */
|
|
109
|
-
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions): Promise<EntryRecord>;
|
|
118
|
+
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
110
119
|
/** Deletes the row, its versions and its paths rows; refused for the
|
|
111
120
|
* last locale and while a child has it. */
|
|
112
121
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
113
|
-
/** Newest first
|
|
122
|
+
/** Newest first (every collection is versioned, spec 2026-09-14 §1). */
|
|
114
123
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
124
|
+
/** The snapshot's shared and localized fields become the draft (the slug is never
|
|
125
|
+
* versioned) through the draft rule; the live version restores as a discard. Refused
|
|
126
|
+
* when the version is not this entry's in this locale (spec 2026-09-13 history §6). */
|
|
127
|
+
restore(collection: string, id: string, locale: string, versionId: string, by?: string): Promise<EntryRecord>;
|
|
128
|
+
/** One stored version by id — callers check `entryId` and `locale`. */
|
|
129
|
+
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
|
115
130
|
/** Blocked while anything references the entry (the safe-delete guard),
|
|
116
131
|
* and while it still has child entries. Removes every locale. */
|
|
117
132
|
delete(collection: string, id: string): Promise<void>;
|
|
@@ -144,6 +159,7 @@ export declare const isSharedSchema: (schema: CollectionSchema<any>) => boolean;
|
|
|
144
159
|
export declare const DEFAULT_ENTRY_LOCALES: LocaleSet;
|
|
145
160
|
export declare const entryErrors: {
|
|
146
161
|
notFound: (collection: string, id: string) => Error;
|
|
162
|
+
nothingPublished: (collection: string, id: string, locale: string) => Error;
|
|
147
163
|
hasChildren: (collection: string, id: string) => Error;
|
|
148
164
|
cycle: () => Error;
|
|
149
165
|
foreignParent: () => Error;
|
|
@@ -162,6 +178,7 @@ export declare const entryErrors: {
|
|
|
162
178
|
owned: (name: string, verb: string) => Error;
|
|
163
179
|
noSlug: (name: string) => Error;
|
|
164
180
|
everyLocale: (name: string) => Error;
|
|
181
|
+
foreignVersion: (collection: string, id: string, locale: string, versionId: string) => Error;
|
|
165
182
|
};
|
|
166
183
|
/** The keys a locale row owns: every field with .localized(), except
|
|
167
184
|
* `slug`, which is the row's own column (a segment is an address, per
|
|
@@ -204,7 +221,7 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
204
221
|
private refs;
|
|
205
222
|
private entries;
|
|
206
223
|
private versions;
|
|
207
|
-
/** Insertion order per version — `
|
|
224
|
+
/** Insertion order per version — `savedAt` alone can tie within a millisecond. */
|
|
208
225
|
private versionSeq;
|
|
209
226
|
private seq;
|
|
210
227
|
private order;
|
|
@@ -221,9 +238,14 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
221
238
|
private subtree;
|
|
222
239
|
/** An edit bumps the node and the edited row; `move` and `reorder` never call this. */
|
|
223
240
|
private bump;
|
|
224
|
-
/**
|
|
225
|
-
*
|
|
241
|
+
/** The draft rule (spec 2026-09-13 history §1) for every collection (spec 2026-09-14 §1):
|
|
242
|
+
* the unpublished draft row is rewritten in place and a new one starts only when the
|
|
243
|
+
* draft IS the published one. */
|
|
226
244
|
private recordVersion;
|
|
245
|
+
/** A published row from before every collection was versioned has no snapshot; give it
|
|
246
|
+
* one from what is live BEFORE a write changes the fields, so the site keeps serving it
|
|
247
|
+
* and the write lands in a new draft row (spec 2026-09-14 §1). */
|
|
248
|
+
private ensureLiveSnapshot;
|
|
227
249
|
private dropVersions;
|
|
228
250
|
/** Every field set the site can render for this entry: each present
|
|
229
251
|
* locale's live view, plus a published snapshot the draft has moved
|
|
@@ -240,17 +262,20 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
240
262
|
private assertParent;
|
|
241
263
|
/** The parent rule: a locale can only be added where the parent has it. */
|
|
242
264
|
private assertParentHasLocale;
|
|
243
|
-
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions): Promise<EntryRecord>;
|
|
265
|
+
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions, by?: string): Promise<EntryRecord>;
|
|
244
266
|
get(collection: string, id: string): Promise<EntryRecord | null>;
|
|
245
267
|
list(collection: string, options?: ListOptions): Promise<EntryRecord[]>;
|
|
246
268
|
getMany(collection: string, ids: string[], locale: string, options?: {
|
|
247
269
|
status?: "published";
|
|
248
270
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
249
|
-
update(collection: string, id: string, locale: string, fields: Record<string, unknown
|
|
250
|
-
setStatus(collection: string, id: string, locale: string, status: "draft" | "published"): Promise<EntryRecord>;
|
|
251
|
-
|
|
271
|
+
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string): Promise<EntryRecord>;
|
|
272
|
+
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string): Promise<EntryRecord>;
|
|
273
|
+
discardDraft(collection: string, id: string, locale: string, by?: string): Promise<EntryRecord>;
|
|
274
|
+
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
252
275
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
253
276
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
277
|
+
restore(collection: string, id: string, locale: string, versionId: string, by?: string): Promise<EntryRecord>;
|
|
278
|
+
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
|
254
279
|
move(collection: string, id: string, to: {
|
|
255
280
|
parentId: string | null;
|
|
256
281
|
index?: number;
|
package/dist/entry-store.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SHARED fields) with one row per locale it exists in (spec 2026-09-06
|
|
4
4
|
// §7): the locale row owns its slug, its publish state and the
|
|
5
5
|
// .localized() fields, and — for a collection registered with
|
|
6
|
-
//
|
|
6
|
+
// every collection — a history of merged snapshots, one row per publish. Nothing falls back
|
|
7
7
|
// to another locale. Writes always flow through this API (never raw
|
|
8
8
|
// table access), which is what lets the refs index stay correct: every
|
|
9
9
|
// save rewrites the entry's outgoing edges over EVERY locale's live
|
|
@@ -47,6 +47,7 @@ const uid = (prefix) => `${prefix}-${++counter}-${Date.now().toString(36)}`;
|
|
|
47
47
|
export const DEFAULT_ENTRY_LOCALES = { default: "en", supported: ["en"] };
|
|
48
48
|
export const entryErrors = {
|
|
49
49
|
notFound: (collection, id) => new Error(`smoodly: no entry "${id}" in "${collection}".`),
|
|
50
|
+
nothingPublished: (collection, id, locale) => new Error(`smoodly: entry "${id}" in "${collection}" has nothing published in locale "${locale}" — there is no version to restore.`),
|
|
50
51
|
hasChildren: (collection, id) => new Error(`smoodly: "${collection}/${id}" has child entries — move or delete them first.`),
|
|
51
52
|
cycle: () => new Error("smoodly: an entry cannot be moved under itself."),
|
|
52
53
|
foreignParent: () => new Error("smoodly: the parent of an entry must be in the same collection."),
|
|
@@ -66,6 +67,7 @@ export const entryErrors = {
|
|
|
66
67
|
owned: (name, verb) => new Error(`smoodly: shared item "${name}" is owned by code — ${verb}.`),
|
|
67
68
|
noSlug: (name) => new Error(`smoodly: shared item "${name}" has no URL — it cannot take a slug.`),
|
|
68
69
|
everyLocale: (name) => new Error(`smoodly: shared item "${name}" exists in every locale — it cannot be removed from one.`),
|
|
70
|
+
foreignVersion: (collection, id, locale, versionId) => new Error(`smoodly: "${versionId}" is not a version of entry "${id}" in "${collection}", locale "${locale}".`),
|
|
69
71
|
};
|
|
70
72
|
/** The keys a locale row owns: every field with .localized(), except
|
|
71
73
|
* `slug`, which is the row's own column (a segment is an address, per
|
|
@@ -158,7 +160,7 @@ export class MemoryEntryStore {
|
|
|
158
160
|
this.refs = refs;
|
|
159
161
|
this.entries = new Map();
|
|
160
162
|
this.versions = new Map();
|
|
161
|
-
/** Insertion order per version — `
|
|
163
|
+
/** Insertion order per version — `savedAt` alone can tie within a millisecond. */
|
|
162
164
|
this.versionSeq = new Map();
|
|
163
165
|
this.seq = 0;
|
|
164
166
|
this.order = new Map();
|
|
@@ -197,23 +199,44 @@ export class MemoryEntryStore {
|
|
|
197
199
|
return out;
|
|
198
200
|
}
|
|
199
201
|
/** An edit bumps the node and the edited row; `move` and `reorder` never call this. */
|
|
200
|
-
bump(entry, row) {
|
|
202
|
+
bump(entry, row, by) {
|
|
201
203
|
const now = Date.now();
|
|
202
204
|
entry.updatedAt = now > entry.updatedAt ? now : entry.updatedAt + 1;
|
|
203
|
-
if (row)
|
|
205
|
+
if (row) {
|
|
204
206
|
row.updatedAt = entry.updatedAt;
|
|
207
|
+
row.updatedBy = by ?? null;
|
|
208
|
+
}
|
|
205
209
|
}
|
|
206
|
-
/**
|
|
207
|
-
*
|
|
210
|
+
/** The draft rule (spec 2026-09-13 history §1) for every collection (spec 2026-09-14 §1):
|
|
211
|
+
* the unpublished draft row is rewritten in place and a new one starts only when the
|
|
212
|
+
* draft IS the published one. */
|
|
208
213
|
recordVersion(entry, locale) {
|
|
209
|
-
|
|
214
|
+
const row = entry.locales[locale];
|
|
215
|
+
const fields = entryFieldsIn(entry, locale);
|
|
216
|
+
const current = row.draftVersionId ? this.versions.get(row.draftVersionId) : undefined;
|
|
217
|
+
if (current && row.draftVersionId !== row.publishedVersionId) {
|
|
218
|
+
current.fields = fields;
|
|
219
|
+
current.savedAt = Date.now();
|
|
220
|
+
this.versionSeq.set(current.id, ++this.seq); // newest again
|
|
210
221
|
return;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
222
|
+
}
|
|
223
|
+
const version = { id: uid("version"), entryId: entry.id, locale, fields, savedAt: Date.now() };
|
|
214
224
|
this.versions.set(version.id, version);
|
|
215
225
|
this.versionSeq.set(version.id, ++this.seq);
|
|
216
|
-
|
|
226
|
+
row.draftVersionId = version.id;
|
|
227
|
+
}
|
|
228
|
+
/** A published row from before every collection was versioned has no snapshot; give it
|
|
229
|
+
* one from what is live BEFORE a write changes the fields, so the site keeps serving it
|
|
230
|
+
* and the write lands in a new draft row (spec 2026-09-14 §1). */
|
|
231
|
+
ensureLiveSnapshot(entry, locale) {
|
|
232
|
+
const row = entry.locales[locale];
|
|
233
|
+
if (row.status !== "published" || row.publishedVersionId !== null)
|
|
234
|
+
return;
|
|
235
|
+
const version = { id: uid("version"), entryId: entry.id, locale, fields: entryFieldsIn(entry, locale), savedAt: Date.now() };
|
|
236
|
+
this.versions.set(version.id, version);
|
|
237
|
+
this.versionSeq.set(version.id, ++this.seq);
|
|
238
|
+
row.draftVersionId = version.id;
|
|
239
|
+
row.publishedVersionId = version.id;
|
|
217
240
|
}
|
|
218
241
|
dropVersions(entryId, locale) {
|
|
219
242
|
for (const [id, v] of this.versions) {
|
|
@@ -277,7 +300,7 @@ export class MemoryEntryStore {
|
|
|
277
300
|
if (parent && !parent.locales[locale])
|
|
278
301
|
throw entryErrors.parentNotInLocale(locale);
|
|
279
302
|
}
|
|
280
|
-
async create(collection, fields, options) {
|
|
303
|
+
async create(collection, fields, options, by) {
|
|
281
304
|
const schema = this.schema(collection);
|
|
282
305
|
assertSupportedLocale(options.locale, this.locales);
|
|
283
306
|
const parentId = options.parentId ?? null;
|
|
@@ -299,7 +322,9 @@ export class MemoryEntryStore {
|
|
|
299
322
|
id: isSharedSchema(schema) ? await sharedEntryId(DEFAULT_SPACE_ID, collection) : uid("entry"),
|
|
300
323
|
collection, parentId, fields: shared,
|
|
301
324
|
locales: {
|
|
302
|
-
[options.locale]: {
|
|
325
|
+
[options.locale]: {
|
|
326
|
+
slug, status: "draft", fields: localized, draftVersionId: null, publishedVersionId: null, updatedAt: now, updatedBy: by ?? null,
|
|
327
|
+
},
|
|
303
328
|
},
|
|
304
329
|
sort: null, createdAt: now, updatedAt: now,
|
|
305
330
|
};
|
|
@@ -362,10 +387,11 @@ export class MemoryEntryStore {
|
|
|
362
387
|
}
|
|
363
388
|
return out;
|
|
364
389
|
}
|
|
365
|
-
async update(collection, id, locale, fields) {
|
|
390
|
+
async update(collection, id, locale, fields, by) {
|
|
366
391
|
const schema = this.schema(collection);
|
|
367
392
|
const entry = this.must(collection, id);
|
|
368
393
|
const row = entryLocaleRow(entry, locale);
|
|
394
|
+
this.ensureLiveSnapshot(entry, locale);
|
|
369
395
|
const { shared, localized, slug } = splitFields(fields, schema);
|
|
370
396
|
if (isSharedSchema(schema) && slug !== null)
|
|
371
397
|
throw entryErrors.noSlug(collection);
|
|
@@ -383,27 +409,46 @@ export class MemoryEntryStore {
|
|
|
383
409
|
throw e;
|
|
384
410
|
}
|
|
385
411
|
this.recordVersion(entry, locale);
|
|
386
|
-
this.bump(entry, row);
|
|
412
|
+
this.bump(entry, row, by);
|
|
387
413
|
this.writeRefs(entry);
|
|
388
414
|
return this.snapshot(entry);
|
|
389
415
|
}
|
|
390
|
-
async setStatus(collection, id, locale, status) {
|
|
416
|
+
async setStatus(collection, id, locale, status, by) {
|
|
391
417
|
const entry = this.must(collection, id);
|
|
392
418
|
const row = entryLocaleRow(entry, locale);
|
|
393
419
|
if (status === "published") {
|
|
394
|
-
// Publish points at the current draft; a
|
|
395
|
-
|
|
396
|
-
if (this.schema(collection).versions && !row.draftVersionId)
|
|
420
|
+
// Publish points at the current draft; a row always has one after its first write, but never trust that from here.
|
|
421
|
+
if (!row.draftVersionId)
|
|
397
422
|
this.recordVersion(entry, locale);
|
|
398
423
|
row.publishedVersionId = row.draftVersionId;
|
|
399
424
|
}
|
|
400
425
|
// Unpublish keeps the pointer for a later republish (spec §2).
|
|
401
426
|
row.status = status;
|
|
402
|
-
this.bump(entry, row);
|
|
427
|
+
this.bump(entry, row, by);
|
|
403
428
|
this.writeRefs(entry); // the published snapshot just became, or stopped being, a live set
|
|
404
429
|
return this.snapshot(entry);
|
|
405
430
|
}
|
|
406
|
-
async
|
|
431
|
+
async discardDraft(collection, id, locale, by) {
|
|
432
|
+
const schema = this.schema(collection);
|
|
433
|
+
const entry = this.must(collection, id);
|
|
434
|
+
const row = entryLocaleRow(entry, locale);
|
|
435
|
+
const snapshot = row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
|
|
436
|
+
if (!snapshot)
|
|
437
|
+
throw entryErrors.nothingPublished(collection, id, locale);
|
|
438
|
+
const { shared, localized } = splitFields(snapshot.fields, schema);
|
|
439
|
+
entry.fields = shared;
|
|
440
|
+
row.fields = localized;
|
|
441
|
+
const abandoned = row.draftVersionId;
|
|
442
|
+
row.draftVersionId = row.publishedVersionId;
|
|
443
|
+
if (abandoned && abandoned !== row.publishedVersionId) {
|
|
444
|
+
this.versions.delete(abandoned);
|
|
445
|
+
this.versionSeq.delete(abandoned);
|
|
446
|
+
}
|
|
447
|
+
this.bump(entry, row, by);
|
|
448
|
+
this.writeRefs(entry);
|
|
449
|
+
return this.snapshot(entry);
|
|
450
|
+
}
|
|
451
|
+
async addLocale(collection, id, locale, options, by) {
|
|
407
452
|
assertSupportedLocale(locale, this.locales);
|
|
408
453
|
const entry = this.must(collection, id);
|
|
409
454
|
// The source is read FIRST: "copy from a locale that isn't there" is
|
|
@@ -417,7 +462,7 @@ export class MemoryEntryStore {
|
|
|
417
462
|
const now = Date.now();
|
|
418
463
|
entry.locales[locale] = {
|
|
419
464
|
slug: source.slug, status: "draft", fields: structuredClone(source.fields),
|
|
420
|
-
draftVersionId: null, publishedVersionId: null, updatedAt: now,
|
|
465
|
+
draftVersionId: null, publishedVersionId: null, updatedAt: now, updatedBy: by ?? null,
|
|
421
466
|
};
|
|
422
467
|
this.recordVersion(entry, locale);
|
|
423
468
|
// Only this node's rows change: a child cannot have a locale its parent lacked.
|
|
@@ -462,9 +507,31 @@ export class MemoryEntryStore {
|
|
|
462
507
|
this.must(collection, id);
|
|
463
508
|
return [...this.versions.values()]
|
|
464
509
|
.filter((v) => v.entryId === id && v.locale === locale)
|
|
465
|
-
.sort((a, b) => b.
|
|
510
|
+
.sort((a, b) => b.savedAt - a.savedAt || this.versionSeq.get(b.id) - this.versionSeq.get(a.id))
|
|
466
511
|
.map((v) => structuredClone(v));
|
|
467
512
|
}
|
|
513
|
+
async restore(collection, id, locale, versionId, by) {
|
|
514
|
+
const schema = this.schema(collection);
|
|
515
|
+
const entry = this.must(collection, id);
|
|
516
|
+
const row = entryLocaleRow(entry, locale);
|
|
517
|
+
const version = this.versions.get(versionId);
|
|
518
|
+
if (!version || version.entryId !== id || version.locale !== locale)
|
|
519
|
+
throw entryErrors.foreignVersion(collection, id, locale, versionId);
|
|
520
|
+
if (versionId === row.publishedVersionId)
|
|
521
|
+
return this.discardDraft(collection, id, locale, by);
|
|
522
|
+
const { shared, localized } = splitFields(version.fields, schema);
|
|
523
|
+
entry.fields = shared;
|
|
524
|
+
row.fields = localized; // the slug is the row's own and never versioned
|
|
525
|
+
this.recordVersion(entry, locale);
|
|
526
|
+
this.bump(entry, row, by);
|
|
527
|
+
this.writeRefs(entry);
|
|
528
|
+
return this.snapshot(entry);
|
|
529
|
+
}
|
|
530
|
+
async getVersion(collection, versionId) {
|
|
531
|
+
this.schema(collection);
|
|
532
|
+
const v = this.versions.get(versionId);
|
|
533
|
+
return v ? structuredClone(v) : null;
|
|
534
|
+
}
|
|
468
535
|
async move(collection, id, to) {
|
|
469
536
|
if (isSharedSchema(this.schema(collection)))
|
|
470
537
|
throw entryErrors.owned(collection, "it has no tree position");
|
|
@@ -17,12 +17,12 @@ export function createPageRenderer(options) {
|
|
|
17
17
|
const entryPages = new Map(options.pages.filter((p) => p.schema.kind === "entryPage" && p.schema.collection).map((p) => [p.schema.collection, p]));
|
|
18
18
|
/** locate (uncached: the id it returns IS the cache tag) then load —
|
|
19
19
|
* cached per (locale, path) outside draft mode. */
|
|
20
|
-
async function load(locale, path, draft) {
|
|
20
|
+
async function load(locale, path, draft, version) {
|
|
21
21
|
const target = await site.locate(locale, path);
|
|
22
22
|
if (!target)
|
|
23
23
|
return null;
|
|
24
24
|
if (draft)
|
|
25
|
-
return site.load(target, locale, path, { draft: true });
|
|
25
|
+
return site.load(target, locale, path, { draft: true, ...(version !== undefined ? { version } : {}) });
|
|
26
26
|
return unstable_cache(() => site.load(target, locale, path), ["smoodly-site", locale, path], {
|
|
27
27
|
tags: [target.kind === "page" ? pageTag(target.id) : entryTag(target.id)],
|
|
28
28
|
})();
|
|
@@ -58,10 +58,14 @@ export function createPageRenderer(options) {
|
|
|
58
58
|
const locale = opts?.locale ?? site.locales.default;
|
|
59
59
|
const path = pathFor(locale);
|
|
60
60
|
const { isEnabled: draft } = await draftMode();
|
|
61
|
-
const
|
|
61
|
+
const params = await opts?.searchParams;
|
|
62
|
+
// Both parameters exist only inside draft mode: the public site never sees an editor bridge or an old version.
|
|
63
|
+
const editor = draft && params?.["smoodly-editor"] === "1";
|
|
64
|
+
const versionParam = params?.["smoodly-version"];
|
|
65
|
+
const version = draft && typeof versionParam === "string" && versionParam !== "" ? versionParam : undefined;
|
|
66
|
+
const hit = await load(locale, path, draft, version);
|
|
62
67
|
if (!hit || !accept(hit))
|
|
63
68
|
notFound();
|
|
64
|
-
const editor = draft && (await opts?.searchParams)?.["smoodly-editor"] === "1";
|
|
65
69
|
return (_jsxs(_Fragment, { children: [editor ? _jsx(EditorBridge, {}) : null, render(hit, editor, locale)] }));
|
|
66
70
|
}
|
|
67
71
|
async function metadata(pathFor, opts, accept) {
|