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
|
@@ -2,24 +2,30 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// One Globals item (shared content, spec 2026-09-07 §6), one locale: the
|
|
4
4
|
// entry form's shape without what a singleton never has — no slug, no
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// the name of each that is not .localized(), and for a visual
|
|
8
|
-
// Styles group edited under the reserved `$styles` key.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
5
|
+
// delete, no add/remove locale (every supported locale exists by
|
|
6
|
+
// construction), no site URL. The fields in schema order, a globe-off
|
|
7
|
+
// mark after the name of each that is not .localized(), and for a visual
|
|
8
|
+
// item a Styles group edited under the reserved `$styles` key. Since
|
|
9
|
+
// 2026-09-14 it autosaves like the entry form: no Save button, the top
|
|
10
|
+
// bar carries the indicator and the one publish control, publish
|
|
11
|
+
// validates the draft and lists the fields in an amber banner. "Used on
|
|
12
|
+
// N pages" is the refs index's answer.
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
14
|
import { STYLES_KEY } from "../../shared.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
+
import { revealField } from "../forms/reveal.js";
|
|
16
|
+
import { useDraftAutosave } from "../editor/autosave.js";
|
|
17
|
+
import { updatedLine } from "../ui/format.js";
|
|
15
18
|
import { LocaleSwitcher } from "../ui/LocaleSwitcher.js";
|
|
16
|
-
import { Banner, Breadcrumb,
|
|
19
|
+
import { Banner, Breadcrumb, FieldErrorList, InfoRow, SaveIndicator, SectionLabel, TopBar } from "../ui/primitives.js";
|
|
20
|
+
import { PublishControl } from "../ui/PublishControl.js";
|
|
21
|
+
import { HistoryModal, historyRows } from "../ui/HistoryModal.js";
|
|
22
|
+
import { localeStatusOf } from "./pages-tree.js";
|
|
17
23
|
import { ResizeHandle } from "../ui/ResizeHandle.js";
|
|
18
24
|
import { entryTitle } from "./entries-list.js";
|
|
19
25
|
import { BASE } from "./base.js";
|
|
26
|
+
import { SharedFields } from "./SharedFields.js";
|
|
20
27
|
import { localeFromUrl, writeLocaleToUrl } from "../editor/locale-url.js";
|
|
21
28
|
import { useLeaveGuard } from "./router.js";
|
|
22
|
-
const STATUSES = ["Draft", "Published"];
|
|
23
29
|
export function SharedForm({ ops, registry, name }) {
|
|
24
30
|
const item = registry.shared.find((s) => s.name === name);
|
|
25
31
|
const multilingual = registry.locales.supported.length > 1;
|
|
@@ -28,10 +34,29 @@ export function SharedForm({ ops, registry, name }) {
|
|
|
28
34
|
const [fields, setFields] = useState(null);
|
|
29
35
|
const [usage, setUsage] = useState([]);
|
|
30
36
|
const [refOptions, setRefOptions] = useState({});
|
|
31
|
-
const [
|
|
37
|
+
const [notice, setNotice] = useState(null);
|
|
32
38
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
33
39
|
const [dirty, setDirty] = useState(false);
|
|
34
|
-
|
|
40
|
+
const [touched, setTouched] = useState(false);
|
|
41
|
+
const [busy, setBusy] = useState(null);
|
|
42
|
+
const [published, setPublished] = useState(false);
|
|
43
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
44
|
+
const [history, setHistory] = useState(null);
|
|
45
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
46
|
+
const formRef = useRef(null);
|
|
47
|
+
const fieldsRef = useRef(fields);
|
|
48
|
+
fieldsRef.current = fields;
|
|
49
|
+
const { saveState, mark, flush } = useDraftAutosave({
|
|
50
|
+
read: () => fieldsRef.current,
|
|
51
|
+
persist: async (value) => {
|
|
52
|
+
const r = await ops.shared.save(name, locale, value);
|
|
53
|
+
if (r.ok)
|
|
54
|
+
setRecord(r.data);
|
|
55
|
+
return r;
|
|
56
|
+
},
|
|
57
|
+
onSaved: () => setDirty(false),
|
|
58
|
+
});
|
|
59
|
+
useLeaveGuard(dirty || saveState !== "saved");
|
|
35
60
|
const refTargets = useMemo(() => {
|
|
36
61
|
const out = {};
|
|
37
62
|
for (const [key, d] of Object.entries(item?.fields ?? {})) {
|
|
@@ -40,11 +65,12 @@ export function SharedForm({ ops, registry, name }) {
|
|
|
40
65
|
}
|
|
41
66
|
return out;
|
|
42
67
|
}, [item]);
|
|
43
|
-
const localeKeys = useMemo(() => Object.entries(item?.fields ?? {}).filter(([, d]) => d.localized === true).map(([key]) => key), [item]);
|
|
44
68
|
useEffect(() => {
|
|
45
69
|
let cancelled = false;
|
|
46
70
|
setFields(null);
|
|
47
71
|
setFieldErrors({});
|
|
72
|
+
setDirty(false);
|
|
73
|
+
setTouched(false);
|
|
48
74
|
ops.shared.get(name, locale).then((r) => {
|
|
49
75
|
if (cancelled)
|
|
50
76
|
return;
|
|
@@ -52,14 +78,13 @@ export function SharedForm({ ops, registry, name }) {
|
|
|
52
78
|
setRecord(r.data.record);
|
|
53
79
|
setFields(r.data.fields);
|
|
54
80
|
setUsage(r.data.usage);
|
|
55
|
-
setDirty(false);
|
|
56
81
|
}
|
|
57
82
|
else
|
|
58
|
-
|
|
83
|
+
setNotice({ tone: "error", text: r.message });
|
|
59
84
|
}).catch(() => { if (!cancelled)
|
|
60
|
-
|
|
85
|
+
setNotice({ tone: "error", text: "Couldn't reach the server." }); });
|
|
61
86
|
return () => { cancelled = true; };
|
|
62
|
-
}, [ops, name, locale]);
|
|
87
|
+
}, [ops, name, locale, reloadKey]);
|
|
63
88
|
// Ref options in the locale being edited — the entry form's rule.
|
|
64
89
|
useEffect(() => {
|
|
65
90
|
let cancelled = false;
|
|
@@ -78,57 +103,146 @@ export function SharedForm({ ops, registry, name }) {
|
|
|
78
103
|
}, [ops, refTargets, registry, locale]);
|
|
79
104
|
if (!item)
|
|
80
105
|
return _jsx("p", { style: { padding: 32 }, children: "No such shared item." });
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
const row = record?.locales[locale];
|
|
107
|
+
const status = row ? localeStatusOf(row) : null;
|
|
108
|
+
const fail = (r) => {
|
|
109
|
+
if (r.code === "validation" && r.fields && r.fields.length > 0) {
|
|
110
|
+
setFieldErrors(Object.fromEntries(r.fields.map((f) => [f.field, f.message])));
|
|
111
|
+
setNotice({ tone: "warn", text: r.message, fields: r.fields });
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
setNotice({ tone: "error", text: r.message });
|
|
90
115
|
};
|
|
91
|
-
const
|
|
92
|
-
|
|
116
|
+
const publish = async () => {
|
|
117
|
+
setNotice(null);
|
|
118
|
+
setBusy("publish");
|
|
119
|
+
if (!(await flush())) {
|
|
120
|
+
setBusy(null);
|
|
121
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — publish cancelled." });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
let r;
|
|
125
|
+
try {
|
|
126
|
+
r = await ops.shared.setStatus(name, locale, "published");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
130
|
+
}
|
|
131
|
+
setBusy(null);
|
|
132
|
+
if (!r.ok) {
|
|
133
|
+
fail(r);
|
|
93
134
|
return;
|
|
94
|
-
|
|
95
|
-
|
|
135
|
+
}
|
|
136
|
+
setRecord(r.data);
|
|
137
|
+
setFieldErrors({});
|
|
138
|
+
setPublished(true);
|
|
139
|
+
setTimeout(() => setPublished(false), 1600);
|
|
140
|
+
};
|
|
141
|
+
const unpublish = async () => {
|
|
142
|
+
setNotice(null);
|
|
143
|
+
setBusy("unpublish");
|
|
144
|
+
let r;
|
|
145
|
+
try {
|
|
146
|
+
r = await ops.shared.setStatus(name, locale, "draft");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
150
|
+
}
|
|
151
|
+
setBusy(null);
|
|
152
|
+
if (r.ok)
|
|
96
153
|
setRecord(r.data);
|
|
97
|
-
|
|
154
|
+
else
|
|
155
|
+
fail(r);
|
|
156
|
+
};
|
|
157
|
+
const discard = async () => {
|
|
158
|
+
if (!window.confirm("Discard your changes and restore the published version?"))
|
|
159
|
+
return;
|
|
160
|
+
setNotice(null);
|
|
161
|
+
setBusy("discard");
|
|
162
|
+
if (!(await flush())) {
|
|
163
|
+
setBusy(null);
|
|
164
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — discard cancelled." });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let r;
|
|
168
|
+
try {
|
|
169
|
+
r = await ops.shared.discardDraft(name, locale);
|
|
98
170
|
}
|
|
171
|
+
catch {
|
|
172
|
+
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
173
|
+
}
|
|
174
|
+
setBusy(null);
|
|
175
|
+
if (!r.ok) {
|
|
176
|
+
fail(r);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
setRecord(r.data);
|
|
180
|
+
setReloadKey((k) => k + 1);
|
|
181
|
+
setNotice({ tone: "ok", text: "Changes discarded." });
|
|
99
182
|
};
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const setPublished = async (next) => {
|
|
103
|
-
if (next === status)
|
|
183
|
+
const openHistory = async () => {
|
|
184
|
+
if (!row)
|
|
104
185
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
186
|
+
if (!(await flush())) {
|
|
187
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — history not opened." });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const r = await ops.shared.listVersions(name, locale);
|
|
191
|
+
if (!r.ok) {
|
|
192
|
+
fail(r);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
setSnapshot(null);
|
|
196
|
+
setHistory(historyRows(r.data, row));
|
|
197
|
+
};
|
|
198
|
+
const loadSnapshot = async (versionId) => {
|
|
199
|
+
const r = await ops.shared.getVersion(name, versionId);
|
|
200
|
+
if (r.ok)
|
|
201
|
+
setSnapshot({ id: r.data.id, fields: r.data.fields });
|
|
202
|
+
else
|
|
203
|
+
fail(r);
|
|
204
|
+
};
|
|
205
|
+
const restore = async (versionId) => {
|
|
206
|
+
const r = await ops.shared.restore(name, locale, versionId);
|
|
207
|
+
if (!r.ok) {
|
|
208
|
+
fail(r);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
setRecord(r.data);
|
|
212
|
+
setReloadKey((k) => k + 1);
|
|
213
|
+
setNotice({ tone: "ok", text: "Version restored." });
|
|
214
|
+
return true;
|
|
108
215
|
};
|
|
109
|
-
const switchLocale = (next) => {
|
|
216
|
+
const switchLocale = async (next) => {
|
|
110
217
|
if (next === locale)
|
|
111
218
|
return;
|
|
112
|
-
if (
|
|
219
|
+
if (!(await flush())) {
|
|
220
|
+
setNotice({ tone: "error", text: "Couldn't save your latest changes — stayed on this locale." });
|
|
113
221
|
return;
|
|
222
|
+
}
|
|
114
223
|
writeLocaleToUrl(next);
|
|
115
|
-
|
|
224
|
+
setNotice(null);
|
|
116
225
|
setLocale(next);
|
|
117
226
|
};
|
|
118
|
-
const styles = (fields?.[STYLES_KEY] ?? {});
|
|
119
227
|
const pages = new Set(usage.filter((u) => u.sourceKind === "page").map((u) => u.sourceId));
|
|
120
|
-
const
|
|
228
|
+
const fieldEntries = Object.entries(item.fields).map(([key, descriptor]) => ({ key, ns: "fields", descriptor }));
|
|
229
|
+
const styleEntries = item.section !== undefined
|
|
230
|
+
? Object.entries(item.styles).map(([key, descriptor]) => ({ key, ns: "styles", descriptor }))
|
|
231
|
+
: [];
|
|
232
|
+
const onChange = (ns, key, value) => {
|
|
233
|
+
setFields((f) => ns === "fields"
|
|
234
|
+
? { ...(f ?? {}), [key]: value }
|
|
235
|
+
: { ...(f ?? {}), [STYLES_KEY]: { ...(f?.[STYLES_KEY] ?? {}), [key]: value } });
|
|
236
|
+
setDirty(true);
|
|
237
|
+
setTouched(true);
|
|
238
|
+
mark();
|
|
239
|
+
};
|
|
121
240
|
return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx(_Fragment, { children: _jsx(Breadcrumb, { backHref: `${BASE}/shared`, items: [
|
|
122
241
|
{
|
|
123
242
|
label: "Globals", href: `${BASE}/shared`,
|
|
124
|
-
trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: registry.locales.supported, parentLocales: [], value: locale, adding: false, onSwitch: switchLocale, onAdd: () => undefined })),
|
|
243
|
+
trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: registry.locales.supported, parentLocales: [], value: locale, adding: false, onSwitch: (l) => { void switchLocale(l); }, onAdd: () => undefined })),
|
|
125
244
|
},
|
|
126
245
|
{ label: item.title },
|
|
127
|
-
] }) }), right:
|
|
128
|
-
|
|
129
|
-
setDirty(true);
|
|
130
|
-
} }, key)))] }))] })) : !banner && _jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" })] }), _jsx(ResizeHandle, {}), _jsxs("aside", { className: "sm-inspector", children: [_jsxs("div", { className: "sm-card", children: [_jsxs(SectionLabel, { children: ["Status", multilingual ? ` · ${locale}` : ""] }), _jsx(Segmented, { options: STATUSES, stretch: true, disabled: !row, value: status === "published" ? "Published" : "Draft", onChange: (v) => { void setPublished(v === "Published" ? "published" : "draft"); } })] }), record && row && (_jsx(_Fragment, { children: _jsxs("div", { className: "sm-card", children: [_jsx(SectionLabel, { children: "Document info" }), _jsx(InfoRow, { label: "Kind", value: item.section !== undefined ? `Visual · ${item.section}` : "Fields" }), _jsx(InfoRow, { label: "Updated", value: relativeTime(row.updatedAt) }), _jsx(InfoRow, { label: "ID", value: record.id, mono: true })] }) }))] })] })] }));
|
|
131
|
-
}
|
|
132
|
-
function InfoRow({ label, value, mono }) {
|
|
133
|
-
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: { color: "var(--text)", fontFamily: mono ? "var(--font-mono)" : undefined, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: value })] }));
|
|
246
|
+
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error") && _jsx(SaveIndicator, { state: saveState }), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsxs("fieldset", { disabled: true, className: "sm-history__form", children: [_jsx(SharedFields, { item: item, entries: fieldEntries, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } }), styleEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Styles" }), _jsx(SharedFields, { item: item, entries: styleEntries, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } })] }))] })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsxs("div", { className: "sm-editor", children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
|
|
247
|
+
revealField(formRef.current, path); } })] })), record && (_jsxs("p", { className: "sm-hint", children: [pages.size === 0 ? "Not placed on any page yet." : `Used on ${pages.size} page${pages.size === 1 ? "" : "s"}: `, [...pages].map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("a", { href: `${BASE}/pages/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`, children: id })] }, id)))] })), fields !== null ? (_jsxs(_Fragment, { children: [_jsx(SharedFields, { item: item, entries: fieldEntries, fields: fields, errors: fieldErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange }), styleEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Styles" }), _jsx(SharedFields, { item: item, entries: styleEntries, fields: fields, errors: fieldErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange })] }))] })) : !notice && _jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" })] }), _jsx(ResizeHandle, {}), _jsx("aside", { className: "sm-inspector", children: record && row && (_jsxs("div", { className: "sm-card", children: [_jsx(SectionLabel, { children: "Document info" }), _jsx(InfoRow, { label: "Kind", value: item.section !== undefined ? `Visual · ${item.section}` : "Fields" }), _jsx(InfoRow, { label: "Updated", value: updatedLine(row.updatedAt, row.updatedBy) }), _jsx(InfoRow, { label: "ID", value: record.id, mono: true })] })) })] })] }));
|
|
134
248
|
}
|
|
@@ -8,6 +8,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
import { useCallback, useEffect, useState } from "react";
|
|
9
9
|
import { ListView } from "./ListView.js";
|
|
10
10
|
import { sharedRows } from "./shared-list.js";
|
|
11
|
+
import { statusLabel } from "../ui/status-label.js";
|
|
11
12
|
import { StatusBadge } from "../ui/primitives.js";
|
|
12
13
|
import { LocalePicker } from "../ui/LocaleSwitcher.js";
|
|
13
14
|
import { relativeTime } from "../ui/format.js";
|
|
@@ -50,7 +51,7 @@ export function SharedList({ ops, registry }) {
|
|
|
50
51
|
{ label: "Title", width: "minmax(160px,1fr)" },
|
|
51
52
|
{ label: "Kind", width: "90px" },
|
|
52
53
|
{ label: "Updated", width: "minmax(110px,160px)" },
|
|
53
|
-
{ label: "Status", width: "
|
|
54
|
+
{ label: "Status", width: "170px" },
|
|
54
55
|
], empty: items ? "No shared content is registered." : "Loading…", rows: rows.map((row) => ({
|
|
55
56
|
key: row.name,
|
|
56
57
|
href: `${BASE}/shared/${encodeURIComponent(row.name)}?locale=${encodeURIComponent(locale)}`,
|
|
@@ -58,7 +59,7 @@ export function SharedList({ ops, registry }) {
|
|
|
58
59
|
_jsx("span", { className: "sm-cell sm-cell--title", children: row.title }, "t"),
|
|
59
60
|
_jsx("span", { className: "sm-cell sm-cell--muted", children: row.flavour }, "k"),
|
|
60
61
|
_jsx("span", { className: "sm-cell sm-cell--muted", children: relativeTime(row.updatedAt) }, "u"),
|
|
61
|
-
_jsx(StatusBadge, { status: row.status }, "s"),
|
|
62
|
+
_jsx(StatusBadge, { status: statusLabel(row.status) }, "s"),
|
|
62
63
|
],
|
|
63
64
|
})) }));
|
|
64
65
|
}
|
|
@@ -9,8 +9,8 @@ export type EntryRow = {
|
|
|
9
9
|
/** The locale row's updatedAt when present, else the node's. */
|
|
10
10
|
updatedAt: number;
|
|
11
11
|
};
|
|
12
|
-
/** The
|
|
13
|
-
*
|
|
12
|
+
/** The entry's title in the selected locale, else the default locale's, else any locale's;
|
|
13
|
+
* "Untitled" when every one is empty (spec 2026-09-14 §4). Display only — a shared
|
|
14
14
|
* title field reads the same from every locale. */
|
|
15
15
|
export declare function entryTitle(record: EntryRecord, meta: AdminCollection, registry: AdminRegistry, locale: string): string;
|
|
16
16
|
/** The locale an "Add <locale>" copies from: the default when the record has it, else its first. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { localeStatusOf } from "./pages-tree.js";
|
|
2
|
-
/** The
|
|
3
|
-
*
|
|
2
|
+
/** The entry's title in the selected locale, else the default locale's, else any locale's;
|
|
3
|
+
* "Untitled" when every one is empty (spec 2026-09-14 §4). Display only — a shared
|
|
4
4
|
* title field reads the same from every locale. */
|
|
5
5
|
export function entryTitle(record, meta, registry, locale) {
|
|
6
6
|
const key = meta.titleField;
|
|
@@ -11,7 +11,7 @@ export function entryTitle(record, meta, registry, locale) {
|
|
|
11
11
|
const value = key in row.fields ? row.fields[key] : record.fields[key];
|
|
12
12
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
13
13
|
};
|
|
14
|
-
return from(locale) ?? from(registry.locales.default) ?? Object.keys(record.locales).map(from).find((t) => t !== undefined) ??
|
|
14
|
+
return from(locale) ?? from(registry.locales.default) ?? Object.keys(record.locales).map(from).find((t) => t !== undefined) ?? "Untitled";
|
|
15
15
|
}
|
|
16
16
|
/** The locale an "Add <locale>" copies from: the default when the record has it, else its first. */
|
|
17
17
|
export function entrySourceLocale(record, registry) {
|
|
@@ -11,7 +11,7 @@ export function supabaseAdminAuth(input) {
|
|
|
11
11
|
return {
|
|
12
12
|
async verify(token) {
|
|
13
13
|
const res = await auth.auth.getUser(token);
|
|
14
|
-
return res.data.user ? { userId: res.data.user.id } : null;
|
|
14
|
+
return res.data.user ? { userId: res.data.user.id, email: res.data.user.email ?? null } : null;
|
|
15
15
|
},
|
|
16
16
|
async isEditor(userId) {
|
|
17
17
|
const res = await input.db.from("editors").select("user_id").eq("user_id", userId).limit(1);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ActionMenuItem = {
|
|
2
|
+
label: string;
|
|
3
|
+
run(): void;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
danger?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function ActionMenu({ label, items, disabled }: {
|
|
8
|
+
label: string;
|
|
9
|
+
items: ActionMenuItem[];
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// A round three-dot button and its popover of actions (spec 2026-09-13
|
|
4
|
+
// publish flow §2). Items are always rendered and disabled when they do
|
|
5
|
+
// not apply — the layout never shifts and the editor learns what exists.
|
|
6
|
+
// The popover is the Pages list's row menu shape (`.sm-picker`), anchored
|
|
7
|
+
// under the button instead of at the pointer.
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { cx } from "./primitives.js";
|
|
10
|
+
export function ActionMenu({ label, items, disabled }) {
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!open)
|
|
15
|
+
return;
|
|
16
|
+
const onDown = (e) => { if (!ref.current?.contains(e.target))
|
|
17
|
+
setOpen(false); };
|
|
18
|
+
const onKey = (e) => { if (e.key === "Escape")
|
|
19
|
+
setOpen(false); };
|
|
20
|
+
document.addEventListener("pointerdown", onDown);
|
|
21
|
+
document.addEventListener("keydown", onKey);
|
|
22
|
+
return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
|
|
23
|
+
}, [open]);
|
|
24
|
+
return (_jsxs("div", { ref: ref, style: { position: "relative", flex: "none" }, children: [_jsx("button", { type: "button", className: "sm-dotmenu", "aria-label": label, title: label, "aria-haspopup": "menu", "aria-expanded": open, disabled: disabled, onClick: () => setOpen((o) => !o), children: "\u00B7\u00B7\u00B7" }), open && (_jsx("div", { role: "menu", className: "sm-picker sm-picker--menu", children: items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: cx("sm-picker__item", item.danger && "sm-picker__item--danger"), disabled: item.disabled, onClick: () => { setOpen(false); item.run(); }, children: item.label }, item.label))) }))] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { PageLocale } from "../../store.ts";
|
|
3
|
+
import type { VersionSummary } from "../ops.ts";
|
|
4
|
+
export type HistoryTag = "Draft" | "Live" | null;
|
|
5
|
+
export type HistoryRow = VersionSummary & {
|
|
6
|
+
tag: HistoryTag;
|
|
7
|
+
};
|
|
8
|
+
export declare const RESTORE_CONFIRM = "Restore this version? The current draft will be replaced. Nothing goes live until you publish.";
|
|
9
|
+
/** The rows for a locale's versions: the unpublished draft is Draft, the
|
|
10
|
+
* row the site serves Live, an older publish carries nothing. */
|
|
11
|
+
export declare function historyRows(versions: VersionSummary[], row: Pick<PageLocale, "status" | "draftVersionId" | "publishedVersionId">): HistoryRow[];
|
|
12
|
+
export declare function HistoryModal({ rows, draftId, render, onSelect, onRestore, onClose }: {
|
|
13
|
+
rows: HistoryRow[];
|
|
14
|
+
/** The current draft's version id: Restore is disabled on it. */
|
|
15
|
+
draftId: string | null;
|
|
16
|
+
/** The main area for the selected version. */
|
|
17
|
+
render(versionId: string): ReactNode;
|
|
18
|
+
/** The selection changed — fired once for the initial row too; the entry screens load the snapshot here. */
|
|
19
|
+
onSelect?(versionId: string): void;
|
|
20
|
+
/** Resolves true when the restore landed; the modal then closes itself. */
|
|
21
|
+
onRestore(versionId: string): Promise<boolean>;
|
|
22
|
+
onClose(): void;
|
|
23
|
+
}): ReactElement;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
// The History modal (spec 2026-09-13 history §5): the locale's versions on
|
|
4
|
+
// the left — newest first, the unpublished draft tagged Draft and the row
|
|
5
|
+
// the site serves Live — and the selected version on the right, rendered
|
|
6
|
+
// by the screen that opened it (an iframe of the site's draft route for a
|
|
7
|
+
// page, a disabled form for an entry or a shared item). Restore confirms,
|
|
8
|
+
// hands the id to the screen, and closes once the restore landed.
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { versionTime } from "./format.js";
|
|
11
|
+
import { Modal } from "./Modal.js";
|
|
12
|
+
import { Button, cx } from "./primitives.js";
|
|
13
|
+
export const RESTORE_CONFIRM = "Restore this version? The current draft will be replaced. Nothing goes live until you publish.";
|
|
14
|
+
/** The rows for a locale's versions: the unpublished draft is Draft, the
|
|
15
|
+
* row the site serves Live, an older publish carries nothing. */
|
|
16
|
+
export function historyRows(versions, row) {
|
|
17
|
+
const live = row.status === "published";
|
|
18
|
+
return versions.map((v) => ({
|
|
19
|
+
...v,
|
|
20
|
+
tag: v.id === row.draftVersionId && (v.id !== row.publishedVersionId || !live) ? "Draft"
|
|
21
|
+
: v.id === row.publishedVersionId && live ? "Live"
|
|
22
|
+
: null,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function HistoryModal({ rows, draftId, render, onSelect, onRestore, onClose }) {
|
|
26
|
+
const [selected, setSelected] = useState(rows[0]?.id ?? null);
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
useEffect(() => { if (selected)
|
|
29
|
+
onSelect?.(selected); }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
30
|
+
const restore = async () => {
|
|
31
|
+
if (!selected || !window.confirm(RESTORE_CONFIRM))
|
|
32
|
+
return;
|
|
33
|
+
setBusy(true);
|
|
34
|
+
const landed = await onRestore(selected);
|
|
35
|
+
setBusy(false);
|
|
36
|
+
if (landed)
|
|
37
|
+
onClose();
|
|
38
|
+
};
|
|
39
|
+
return (_jsx(Modal, { title: "History", width: 1200, height: "85vh", flush: true, onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: onClose, children: "Close" }), _jsx(Button, { disabled: busy || !selected || selected === draftId, onClick: () => { void restore(); }, children: busy ? "Restoring…" : "Restore" })] }), children: _jsxs("div", { className: "sm-history", children: [_jsxs("div", { className: "sm-history__list", role: "listbox", "aria-label": "Versions", children: [rows.length === 0 && _jsx("span", { className: "sm-hint", style: { padding: 12 }, children: "No versions yet." }), rows.map((r) => (_jsxs("button", { type: "button", role: "option", "aria-selected": r.id === selected, className: cx("sm-history__row", r.id === selected && "sm-history__row--active"), onClick: () => setSelected(r.id), children: [_jsx("span", { children: versionTime(r.savedAt) }), r.tag && _jsx("span", { className: cx("sm-history__tag", r.tag === "Live" && "sm-history__tag--live"), children: r.tag })] }, r.id)))] }), _jsx("div", { className: "sm-history__main", children: selected ? render(selected) : null })] }) }));
|
|
40
|
+
}
|
package/dist/admin/ui/Modal.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
-
export declare function Modal({ title, width, onClose, children, footer }: {
|
|
2
|
+
export declare function Modal({ title, width, height, flush, onClose, children, footer }: {
|
|
3
3
|
title: string;
|
|
4
4
|
width?: number;
|
|
5
|
+
/** A fixed panel height (e.g. `"85vh"`) for a modal whose body must fill it; default: the content's. */
|
|
6
|
+
height?: number | string;
|
|
7
|
+
/** Drops the body padding for a modal whose body is a two-pane layout (History). */
|
|
8
|
+
flush?: boolean;
|
|
5
9
|
onClose(): void;
|
|
6
10
|
children: ReactNode;
|
|
7
11
|
footer?: ReactNode;
|
package/dist/admin/ui/Modal.js
CHANGED
|
@@ -8,13 +8,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// overflow the same way. Static rendering (no document) keeps it inline.
|
|
9
9
|
import { useEffect, useState } from "react";
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
11
|
-
import { IconButton } from "./primitives.js";
|
|
11
|
+
import { cx, IconButton } from "./primitives.js";
|
|
12
12
|
function adminRoot() {
|
|
13
13
|
if (typeof document === "undefined")
|
|
14
14
|
return null;
|
|
15
15
|
return document.querySelector(".sm-admin") ?? document.body;
|
|
16
16
|
}
|
|
17
|
-
export function Modal({ title, width = 720, onClose, children, footer }) {
|
|
17
|
+
export function Modal({ title, width = 720, height, flush = false, onClose, children, footer }) {
|
|
18
18
|
const [root] = useState(adminRoot);
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
const onKey = (e) => { if (e.key === "Escape")
|
|
@@ -22,6 +22,6 @@ export function Modal({ title, width = 720, onClose, children, footer }) {
|
|
|
22
22
|
window.addEventListener("keydown", onKey);
|
|
23
23
|
return () => window.removeEventListener("keydown", onKey);
|
|
24
24
|
}, [onClose]);
|
|
25
|
-
const modal = (_jsx("div", { className: "sm-modal", onClick: onClose, children: _jsxs("div", { className: "sm-modal__panel", role: "dialog", "aria-label": title, style: { width }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "sm-modal__head", children: [_jsx("span", { className: "sm-title", children: title }), _jsx(IconButton, { label: "Close", onClick: onClose, children: "\u00D7" })] }), _jsx("div", { className: "sm-modal__body", children: children }), footer && _jsx("div", { className: "sm-modal__foot", children: footer })] }) }));
|
|
25
|
+
const modal = (_jsx("div", { className: "sm-modal", onClick: onClose, children: _jsxs("div", { className: "sm-modal__panel", role: "dialog", "aria-label": title, style: { width, ...(height !== undefined ? { height } : {}) }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "sm-modal__head", children: [_jsx("span", { className: "sm-title", children: title }), _jsx(IconButton, { label: "Close", onClick: onClose, children: "\u00D7" })] }), _jsx("div", { className: cx("sm-modal__body", flush && "sm-modal__body--flush"), children: children }), footer && _jsx("div", { className: "sm-modal__foot", children: footer })] }) }));
|
|
26
26
|
return root ? createPortal(modal, root) : modal;
|
|
27
27
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LocaleStatus } from "../shell/pages-tree.ts";
|
|
2
|
+
import { type ActionMenuItem } from "./ActionMenu.tsx";
|
|
3
|
+
export type PublishBusy = "publish" | "discard" | "unpublish" | null;
|
|
4
|
+
/** The three-dot menu's items: History first when the screen offers it (spec 2026-09-13 history §5;
|
|
5
|
+
* every collection since 2026-09-14), then the two publish-flow actions — always rendered,
|
|
6
|
+
* disabled when they do not apply. */
|
|
7
|
+
export declare function publishMenu(opts: {
|
|
8
|
+
can: {
|
|
9
|
+
discard: boolean;
|
|
10
|
+
unpublish: boolean;
|
|
11
|
+
};
|
|
12
|
+
onDiscard(): void;
|
|
13
|
+
onUnpublish(): void;
|
|
14
|
+
onHistory?(): void;
|
|
15
|
+
}): ActionMenuItem[];
|
|
16
|
+
export declare function PublishControl({ status, pending, busy, flash, disabled, onPublish, onDiscard, onUnpublish, onHistory, }: {
|
|
17
|
+
/** The locale's stored state; null while the document loads or before it exists. */
|
|
18
|
+
status: LocaleStatus | null;
|
|
19
|
+
/** The editor holds a change the store has not confirmed (dirty, or a save pending / in flight / failed). */
|
|
20
|
+
pending?: boolean;
|
|
21
|
+
busy?: PublishBusy;
|
|
22
|
+
/** The moment after a publish: the button reads "Published ✓". */
|
|
23
|
+
flash?: boolean;
|
|
24
|
+
/** No document to act on (loading, a new unsaved entry). */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
onPublish(): void;
|
|
27
|
+
onDiscard(): void;
|
|
28
|
+
onUnpublish(): void;
|
|
29
|
+
/** Opens History (spec 2026-09-13 history §5). Absent = no History item (the canvas card). */
|
|
30
|
+
onHistory?(): void;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ActionMenu } from "./ActionMenu.js";
|
|
4
|
+
import { Button, StatusBadge } from "./primitives.js";
|
|
5
|
+
import { publishActions, statusLabel } from "./status-label.js";
|
|
6
|
+
const BUSY_WORD = {
|
|
7
|
+
publish: "Publishing…", discard: "Discarding…", unpublish: "Unpublishing…",
|
|
8
|
+
};
|
|
9
|
+
/** The three-dot menu's items: History first when the screen offers it (spec 2026-09-13 history §5;
|
|
10
|
+
* every collection since 2026-09-14), then the two publish-flow actions — always rendered,
|
|
11
|
+
* disabled when they do not apply. */
|
|
12
|
+
export function publishMenu(opts) {
|
|
13
|
+
return [
|
|
14
|
+
...(opts.onHistory ? [{ label: "History", run: opts.onHistory }] : []),
|
|
15
|
+
{ label: "Discard changes", danger: true, disabled: !opts.can.discard, run: opts.onDiscard },
|
|
16
|
+
{ label: "Unpublish", disabled: !opts.can.unpublish, run: opts.onUnpublish },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
export function PublishControl({ status, pending = false, busy = null, flash = false, disabled = false, onPublish, onDiscard, onUnpublish, onHistory, }) {
|
|
20
|
+
const can = publishActions(status, pending);
|
|
21
|
+
const inert = disabled || busy !== null;
|
|
22
|
+
const word = busy ? BUSY_WORD[busy] : flash ? "Published ✓" : "Publish";
|
|
23
|
+
return (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [status && _jsx(StatusBadge, { status: statusLabel(status) }), _jsx(Button, { variant: flash && !busy ? "ok" : "primary", disabled: inert || !can.publish, onClick: onPublish, children: word }), _jsx(ActionMenu, { label: "More actions", disabled: inert || status === null, items: publishMenu({ can, onDiscard, onUnpublish, onHistory }) })] }));
|
|
24
|
+
}
|
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export declare function relativeTime(ms: number, now?: number): string;
|
|
2
|
+
/** A version's saved time: an absolute date and time — History compares moments, not distances. */
|
|
3
|
+
export declare function versionTime(ms: number): string;
|
|
4
|
+
/** The "Updated" info line: the relative time, then who, when the row knows (spec 2026-09-13 history §2). */
|
|
5
|
+
export declare function updatedLine(updatedAt: number, updatedBy: string | null, now?: number): string;
|
package/dist/admin/ui/format.js
CHANGED
|
@@ -23,3 +23,12 @@ export function relativeTime(ms, now = Date.now()) {
|
|
|
23
23
|
month: "short", day: "numeric", ...(sameYear ? {} : { year: "numeric" }),
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
/** A version's saved time: an absolute date and time — History compares moments, not distances. */
|
|
27
|
+
export function versionTime(ms) {
|
|
28
|
+
return new Date(ms).toLocaleString(undefined, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
29
|
+
}
|
|
30
|
+
/** The "Updated" info line: the relative time, then who, when the row knows (spec 2026-09-13 history §2). */
|
|
31
|
+
export function updatedLine(updatedAt, updatedBy, now = Date.now()) {
|
|
32
|
+
const when = relativeTime(updatedAt, now);
|
|
33
|
+
return updatedBy ? `${when} · ${updatedBy}` : when;
|
|
34
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, CSSProperties, ReactNode } from "react";
|
|
2
2
|
import type { SaveState } from "../editor/autosave.ts";
|
|
3
|
+
import type { FieldError } from "../validate.ts";
|
|
3
4
|
/** The wordmark, inlined: a package can't ship a file into the host app's
|
|
4
5
|
* /public. Its colour is the --logo-color token (accent in the warm look). */
|
|
5
6
|
export declare function Logo({ height }: {
|
|
@@ -115,10 +116,22 @@ export declare function SectionLabel({ children, action, mono, style }: {
|
|
|
115
116
|
export declare function Divider({ bleed }: {
|
|
116
117
|
bleed?: number;
|
|
117
118
|
}): import("react").JSX.Element;
|
|
119
|
+
/** The lines under a publish-validation banner: one per field, each focusing its widget
|
|
120
|
+
* when the screen can reveal it (spec 2026-09-14 §2). */
|
|
121
|
+
export declare function FieldErrorList({ fields, onReveal }: {
|
|
122
|
+
fields: FieldError[];
|
|
123
|
+
onReveal?(path: string): void;
|
|
124
|
+
}): import("react").JSX.Element | null;
|
|
118
125
|
export declare function Banner({ tone, children, onDismiss }: {
|
|
119
126
|
tone: "ok" | "error" | "warn";
|
|
120
127
|
children: ReactNode;
|
|
121
128
|
onDismiss?: () => void;
|
|
122
129
|
}): import("react").JSX.Element;
|
|
123
130
|
export declare function cx(...parts: (string | false | null | undefined)[]): string;
|
|
131
|
+
/** One line of a form's info block: a muted label, the value right-aligned and clipped. */
|
|
132
|
+
export declare function InfoRow({ label, value, mono }: {
|
|
133
|
+
label: string;
|
|
134
|
+
value: string;
|
|
135
|
+
mono?: boolean;
|
|
136
|
+
}): import("react").JSX.Element;
|
|
124
137
|
export {};
|