@cardinali-studio/adminkit 2.2.19 → 2.2.21

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/dist/api.d.ts CHANGED
@@ -46,6 +46,10 @@ export interface RepoPage {
46
46
  url: string;
47
47
  /** Lo `sha` del blob su GitHub: identifica la VERSIONE di questo file, non il suo contenuto. */
48
48
  sha: string;
49
+ /** Identità D1 site-owned. Assenti nella corsia Git/Hugo. */
50
+ documentId?: string;
51
+ draftRevisionId?: string | null;
52
+ publishedRevisionId?: string | null;
49
53
  page: Record<string, unknown>;
50
54
  /** Le sibling dichiarate dal sito, senza caricare il loro contenuto nell'elenco. */
51
55
  translations?: Record<string, {
@@ -53,6 +57,18 @@ export interface RepoPage {
53
57
  sha?: string;
54
58
  }>;
55
59
  }
60
+ export type ContentPublishTarget = {
61
+ documentId: string;
62
+ expectedDraftRevisionId: string | null;
63
+ includeSiteAccess?: boolean;
64
+ };
65
+ export type ContentPublishDocument = {
66
+ documentId: string;
67
+ kind: string;
68
+ updatedAt: number;
69
+ draftRevisionId: string;
70
+ publishedRevisionId: string | null;
71
+ };
56
72
  export type ContentSaveCommand = {
57
73
  revisionId: string;
58
74
  documentId: string;
@@ -122,6 +138,11 @@ export declare const api: {
122
138
  files: number;
123
139
  skipped: string[];
124
140
  shas?: Record<string, string>;
141
+ documents?: Record<string, {
142
+ documentId: string;
143
+ draftRevisionId: string | null;
144
+ publishedRevisionId: string | null;
145
+ }>;
125
146
  }>;
126
147
  contentState: () => Promise<{
127
148
  ok: true;
@@ -163,23 +184,24 @@ export declare const api: {
163
184
  ok: true;
164
185
  configured: true;
165
186
  ahead: number;
166
- documents: {
167
- documentId: string;
168
- kind: string;
169
- updatedAt: number;
170
- }[];
187
+ documents: ContentPublishDocument[];
171
188
  mediaOperations?: MediaOperationStatus[];
172
189
  siteAccess?: {
173
190
  enabled: boolean;
174
191
  } | null;
175
192
  activeReleaseId?: string | null;
176
193
  }>;
177
- contentPublish: () => Promise<{
194
+ contentPublish: (target: ContentPublishTarget) => Promise<{
178
195
  ok: true;
179
196
  published: boolean;
180
197
  releaseId?: string;
181
198
  attemptId?: string;
182
199
  message?: string;
200
+ verified?: boolean;
201
+ observedReleaseId?: string | null;
202
+ documents?: number;
203
+ dependentDocuments?: number;
204
+ remainingAhead?: number;
183
205
  }>;
184
206
  publishState: () => Promise<{
185
207
  ok: true;
package/dist/api.js CHANGED
@@ -66,7 +66,7 @@ export const api = {
66
66
  /** D1 publication is site-owned: the site chooses which documents it can
67
67
  * materialize, while the panel only displays the pending immutable inputs. */
68
68
  contentPublishState: () => call("/admin/api/content/publish"),
69
- contentPublish: () => post("/admin/api/content/publish", {}),
69
+ contentPublish: (target) => post("/admin/api/content/publish", target),
70
70
  // ---- pubblicazione
71
71
  /* Salvare scrive sulla bozza e non fa partire nessuna build; è questo che manda online. Due
72
72
  * gesti separati, quindi due chiamate: una chiede quanto c'è di non pubblicato, l'altra lo
package/dist/app/App.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import "../theme.css";
2
2
  import "./app.css";
3
+ import "./next.css";
3
4
  export declare function App(): import("react").JSX.Element;
package/dist/app/App.js CHANGED
@@ -19,9 +19,10 @@ import { useEffect, useMemo, useState } from "react";
19
19
  * i default del kit, poi l'identità di questo sito che li sovrascrive, poi l'impaginazione. */
20
20
  import "../theme.css";
21
21
  import "./app.css";
22
+ import "./next.css";
22
23
  import { MediaFolders, MediaGrid, ProsePages, Trash, useMediaLibrary, usePicker } from "../form/index.js";
23
24
  import { Preview, isLocalPreview } from "../preview.js";
24
- import { site } from "./site.js";
25
+ import { contentPersistenceMode, site } from "./site.js";
25
26
  import { useHashRoute } from "./lib/router";
26
27
  import { usePages, legacyDraft, legacyDraftTitles, discardLegacyDraft } from "./lib/pages";
27
28
  import { useHome } from "./lib/home";
@@ -31,7 +32,8 @@ import { Overview } from "./routes/Overview";
31
32
  import { PagesList } from "./routes/PagesList";
32
33
  import { PageEditor, PageHeader } from "./routes/PageEditor";
33
34
  import { PageSeoEditor } from "./routes/PageSeoEditor";
34
- import { SiteSettingsEditor, WebsiteSeoSettingsEditor } from "./routes/SiteSettingsEditor";
35
+ import { SiteSettingsEditor, WebsiteSeoSettingsEditor, } from "./routes/SiteSettingsEditor";
36
+ import { SiteSettingsUnavailable } from "./routes/AdminInterfaceSettings";
35
37
  import { MediaSettingsEditor } from "./routes/MediaSettingsEditor";
36
38
  import { HomeEditor } from "./routes/HomeEditor";
37
39
  import { RequestsList, RequestDetail } from "./routes/Requests";
@@ -46,7 +48,16 @@ import { NewPageButton } from "./routes/PageActions";
46
48
  import { BookingAgenda, BookingDetail, BookingsList, BookingSettingsHub, } from "./routes/Bookings";
47
49
  import { Icon } from "./ui/icons";
48
50
  import { useTheme } from "./lib/theme";
51
+ import { useAdminInterface } from "./lib/admin-interface";
52
+ import { NextAdminApp } from "./NextAdminApp";
49
53
  export function App() {
54
+ const { adminInterface, setAdminInterface } = useAdminInterface();
55
+ const current = (_jsx(CurrentAdminApp, { adminInterface: adminInterface, setAdminInterface: setAdminInterface }));
56
+ return adminInterface === "next"
57
+ ? _jsx(NextAdminApp, { current: current, adminInterface: adminInterface, setAdminInterface: setAdminInterface })
58
+ : current;
59
+ }
60
+ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
50
61
  const [route, go] = useHashRoute();
51
62
  const { theme, toggle: toggleTheme } = useTheme();
52
63
  /* Si entra dalla Panoramica, non dalle Pagine: aprendo il pannello la domanda è quasi sempre
@@ -176,7 +187,7 @@ export function App() {
176
187
  * Il badge invece sparisce: un contatore su una funzione spenta conterebbe un elenco che il
177
188
  * server non serve nemmeno (le sue rotte rispondono 404). */
178
189
  { title: "Attività", items: [
179
- { id: "deliveries", label: "Consegne", icon: Icon.delivery, badge: moduleOn("delivery") ? deliveries.expiring : 0 },
190
+ { id: "deliveries", label: "Transfer", icon: Icon.delivery, badge: moduleOn("delivery") ? deliveries.expiring : 0 },
180
191
  { id: "bookings", label: "Prenotazioni", icon: Icon.calendar, badge: moduleOn("booking") ? bookings.pending : 0 },
181
192
  { id: "requests", label: "Richieste", icon: Icon.mail, badge: moduleOn("requests") ? requests.pending : 0 },
182
193
  ...(local ? [{ id: "preview", label: "Anteprima", icon: Icon.play }] : []),
@@ -185,14 +196,32 @@ export function App() {
185
196
  /* Chi risponde del «Salva»: le pagine, la home, o nessuno (media e richieste si scrivono nel
186
197
  * momento in cui si fa il gesto — un bottone di salvataggio lì sarebbe una promessa falsa).
187
198
  *
188
- * «Pubblica» invece NON passa di qui: è globale. Salvare scrive sulla bozza, e pubblicare
189
- * manda online tutto ciò che c'è sulla bozza — pagine, home e impostazioni insieme — non la
190
- * sezione che si sta guardando. Metterlo dentro `saver` avrebbe voluto dire tre pulsanti che
191
- * fanno la stessa identica cosa. */
199
+ * In D1 «Pubblica» usa invece l'identità del contenuto aperto: le altre bozze rimangono
200
+ * salvate. La corsia Git/Hugo conserva il suo publish globale. */
192
201
  const saver = section === "pages" ? pages
193
202
  : section === "home" ? home
194
203
  : section === "settings" ? settings
195
204
  : null;
205
+ const removedPageTarget = section === "pages" && route[1]
206
+ ? pages.removedPublishTargets[route[1]] ?? null : null;
207
+ const d1PublishTarget = section === "pages" && editing?._documentId
208
+ ? { documentId: editing._documentId, expectedDraftRevisionId: editing._draftRevisionId ?? null }
209
+ : section === "pages" && removedPageTarget ? removedPageTarget
210
+ : section === "home" ? home.publishTarget
211
+ : section === "settings" ? settings.publishTarget
212
+ : null;
213
+ const d1DocumentPending = d1PublishTarget
214
+ ? publish.documents.some((document) => document.documentId === d1PublishTarget.documentId
215
+ && document.draftRevisionId === d1PublishTarget.expectedDraftRevisionId)
216
+ : false;
217
+ const d1ContextPending = Boolean(d1PublishTarget && (d1DocumentPending
218
+ || (section === "settings" && publish.siteAccessPending)));
219
+ const contextualPublishLabel = section === "pages" ? (removedPageTarget ? "Pubblica eliminazione" : "Pubblica questa pagina")
220
+ : section === "home" ? "Pubblica la Home"
221
+ : "Pubblica le impostazioni";
222
+ const contextualPendingCount = (d1DocumentPending ? 1 : 0)
223
+ + (section === "settings" && publish.siteAccessPending ? 1 : 0);
224
+ const otherPendingCount = Math.max(0, publish.ahead - contextualPendingCount);
196
225
  const heading = section === "overview" ? "Panoramica"
197
226
  : section === "pages" ? (editing ? String(editing.title ?? editing.slug) : "Pagine")
198
227
  : section === "home" ? (homeGroup || "Home")
@@ -203,7 +232,7 @@ export function App() {
203
232
  /* Consegne non ripete il titolo qui: il nome del transfer sta nella sua schermata, dove
204
233
  si modifica, e si aggiorna mentre lo si scrive. Averlo in due posti significa averne
205
234
  uno dei due sempre vecchio. */
206
- : section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "Consegne")
235
+ : section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "Transfer")
207
236
  : section === "bookings" ? (bookingSettingsOpen ? "Impostazioni prenotazioni" : openBooking ? openBooking.visitorName || openBooking.referenceCode : "Prenotazioni")
208
237
  : section === "requests" ? (openReq ? openReq.name : "Richieste")
209
238
  : section === "settings" ? (settingsView === "seo" ? "SEO" : "Sito")
@@ -219,7 +248,17 @@ export function App() {
219
248
  const p = all.slice(0, i + 1).join("/") + "/";
220
249
  return { prefix: p, label: media.labelOf(p) };
221
250
  });
222
- return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) }) }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
251
+ /* La testata non ripete il contesto già interamente leggibile nella colonna sinistra.
252
+ * Resta invece quando porta informazione ulteriore: controlli della pagina, antenati di una
253
+ * cartella, oppure il nome di un elemento che nell'elenco può essere troncato. */
254
+ const redundantPaneContext = (section === "pages" && !editing)
255
+ || section === "home"
256
+ || section === "deliveries"
257
+ || section === "settings"
258
+ || (section === "requests" && !openReq)
259
+ || (section === "bookings" && !openBooking)
260
+ || (section === "media" && (view === "trash" || mediaSettingsOpen || mediaCrumbs.length === 0));
261
+ return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), "aria-label": n.label, disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", "aria-label": "Vedi il sito", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, "aria-label": theme === "dark" ? "Tema chiaro" : "Tema scuro", children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), "aria-label": "Impostazioni", disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Pagine" }), list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) })] }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
223
262
  : !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
224
263
  : _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
225
264
  if (editing?.slug === from)
@@ -235,23 +274,25 @@ export function App() {
235
274
  else
236
275
  next.add(id);
237
276
  return next;
238
- }) }) })] })), section === "deliveries" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx(NewDeliveryButton, { onCreate: () => {
239
- setQuickSelectedId(undefined);
240
- setQuickKey((value) => value + 1);
241
- go("/deliveries");
242
- } }) }), _jsx("div", { className: "col-body", children: deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
277
+ }) }) })] })), section === "deliveries" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Transfer" }), _jsx(NewDeliveryButton, { onCreate: () => {
278
+ setQuickSelectedId(undefined);
279
+ setQuickKey((value) => value + 1);
280
+ go("/deliveries");
281
+ } })] }), _jsx("div", { className: "col-body", children: deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
243
282
  : !deliveries.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
244
283
  : deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
245
284
  : _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
246
285
  : !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
247
- : _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
286
+ : _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen && mediaCrumbs.length > 0 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : !redundantPaneContext ? (_jsx("h1", { className: "pane-title", children: heading })) : null, view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
248
287
  /* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
249
288
  rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
250
289
  il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
251
290
  qualcosa da pubblicare. */
252
- onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), publish.ahead > 0 && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing, onClick: () => void publish.publish(), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
253
- : publish.ahead === 1 ? "Pubblica (1 modifica)"
254
- : `Pubblica (${publish.ahead} modifiche)` }))] }), offModule ? _jsx(FeatureOff, { info: offModule }) : _jsxs(_Fragment, { children: [draft !== null && (_jsxs("div", { className: "app-legacy", children: [_jsxs("p", { className: "app-legacy-row", children: [_jsxs("span", { children: ["Questo browser conserva una bozza del ", _jsx("strong", { children: "vecchio" }), " pannello (", draft, " pagine). Non viene usata: quello che si vede qui \u00E8 il sito pubblicato."] }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setDraftOpen((v) => !v), children: draftOpen ? "Nascondi" : "Cosa contiene" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => { discardLegacyDraft(); setDraft(null); }, children: "Butta" })] }), draftOpen && _jsx("p", { className: "app-legacy-list", children: legacyDraftTitles().join(" · ") })] })), _jsxs("div", { className: "pane-body" + (wide ? " is-wide" : ""), children: [section === "overview" && (
291
+ onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), contentPersistenceMode() === "d1" && d1ContextPending && otherPendingCount > 0 && (_jsx("span", { className: "pane-count", title: "Queste modifiche restano in bozza", children: otherPendingCount === 1 ? "1 altra bozza" : `${otherPendingCount} altre bozze` })), ((contentPersistenceMode() === "d1" && d1ContextPending)
292
+ || (contentPersistenceMode() !== "d1" && publish.ahead > 0)) && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing, onClick: () => void publish.publish(d1PublishTarget ?? undefined), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
293
+ : contentPersistenceMode() === "d1" ? contextualPublishLabel
294
+ : publish.ahead === 1 ? "Pubblica (1 modifica)"
295
+ : `Pubblica (${publish.ahead} modifiche)` }))] }), offModule ? _jsx(FeatureOff, { info: offModule }) : _jsxs(_Fragment, { children: [draft !== null && (_jsxs("div", { className: "app-legacy", children: [_jsxs("p", { className: "app-legacy-row", children: [_jsxs("span", { children: ["Questo browser conserva una bozza del ", _jsx("strong", { children: "vecchio" }), " pannello (", draft, " pagine). Non viene usata: quello che si vede qui \u00E8 il sito pubblicato."] }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setDraftOpen((v) => !v), children: draftOpen ? "Nascondi" : "Cosa contiene" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => { discardLegacyDraft(); setDraft(null); }, children: "Butta" })] }), draftOpen && _jsx("p", { className: "app-legacy-list", children: legacyDraftTitles().join(" · ") })] })), _jsxs("div", { className: "pane-body" + (wide ? " is-wide" : ""), children: [section === "overview" && (
255
296
  /* Transfer e richieste arrivano da QUI, già caricati per i badge della prima
256
297
  colonna: la Panoramica non li richiede una seconda volta, altrimenti lo stesso
257
298
  numero avrebbe due sorgenti e una delle due sarebbe sempre indietro. */
@@ -263,9 +304,9 @@ export function App() {
263
304
  se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
264
305
  home.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: home.error }), _jsx("button", { className: "ak-btn", onClick: () => void home.load(), children: "Riprova" })] }))
265
306
  : home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
266
- : _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] })) : settings.data ? (settingsView === "seo"
307
+ : _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (settingsView === "site" ? (_jsx(SiteSettingsUnavailable, { error: settings.error, onRetry: () => void settings.load(), adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface })) : (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] }))) : settings.data ? (settingsView === "seo"
267
308
  ? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
268
- : _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
309
+ : _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
269
310
  ? _jsx(MediaSettingsEditor, { lib: media })
270
311
  : _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
271
312
  : openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
@@ -297,10 +338,10 @@ function hasList(section) {
297
338
  const OPTIONAL_SECTIONS = {
298
339
  deliveries: {
299
340
  module: "delivery",
300
- title: "Consegne",
341
+ title: "Transfer",
301
342
  lead: "Manda le fotografie a un cliente con un link privato, senza allegati e senza servizi di terze parti.",
302
343
  points: [
303
- "Un indirizzo riservato per ogni consegna, che scade da solo",
344
+ "Un indirizzo riservato per ogni Transfer, che scade da solo",
304
345
  "Il cliente scarica tutto in una volta, o solo le foto che gli servono",
305
346
  "Vedi quante volte è stato scaricato, e da quando",
306
347
  ],
@@ -321,7 +362,7 @@ const OPTIONAL_SECTIONS = {
321
362
  lead: "Le richieste che arrivano dal modulo di contatto del sito, raccolte qui invece che in una casella di posta.",
322
363
  points: [
323
364
  "Nessuna richiesta persa fra le altre email",
324
- "Segni quali hai già gestito",
365
+ "Segni quali hai già letto",
325
366
  "Rispondi con un click, dall'indirizzo di sempre",
326
367
  ],
327
368
  },
@@ -0,0 +1,8 @@
1
+ import type { Dispatch, ReactNode, SetStateAction } from "react";
2
+ import type { AdminInterface } from "./lib/admin-interface.js";
3
+ export interface NextAdminAppProps {
4
+ current: ReactNode;
5
+ adminInterface: AdminInterface;
6
+ setAdminInterface: Dispatch<SetStateAction<AdminInterface>>;
7
+ }
8
+ export declare function NextAdminApp({ current }: NextAdminAppProps): ReactNode;
@@ -0,0 +1,3 @@
1
+ export function NextAdminApp({ current }) {
2
+ return current;
3
+ }
package/dist/app/app.css CHANGED
@@ -745,49 +745,63 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
745
745
  }
746
746
  .pe-photo-overlay img { max-width: 100%; max-height: 100%; display: block; }
747
747
 
748
- /* La modalità masonry replica il flusso del sito: le foto riempiono ogni colonna dall'alto
749
- verso il basso e conservano il proprio rapporto d'aspetto. Le regole base qui sopra restano
750
- quindi intatte per tutte le gallery con layout ordinario. */
751
- .pe-gallery.is-masonry > .pe-photo-grid {
748
+ /* Il masonry verticale usa il flusso CSS a colonne: riempie una colonna dall'alto verso il
749
+ basso prima di passare alla successiva. `.is-masonry` resta come alias visuale per markup
750
+ legacy; i nuovi consumer ricevono sempre il nome esplicito. */
751
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid {
752
752
  display: block; column-count: 3; column-gap: 1.5vw;
753
753
  padding: 4px 2px 40px;
754
754
  }
755
- .pe-gallery.is-masonry > .pe-photo-grid > .pe-photo {
755
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid > .pe-photo {
756
756
  width: 100%; margin: 0 0 1.5vw; padding: 0;
757
757
  display: inline-block; break-inside: avoid; -webkit-column-break-inside: avoid;
758
758
  vertical-align: top;
759
759
  }
760
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo-frame {
760
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo-frame {
761
761
  width: 100%; height: auto; display: block; overflow: hidden;
762
762
  }
763
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo-frame img {
763
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo-frame img {
764
764
  width: 100%; height: auto; max-width: none; max-height: none;
765
765
  }
766
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo-actions {
766
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo-actions {
767
767
  top: 8px; transform: translate(-50%, 0);
768
768
  }
769
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:hover .pe-photo-actions,
770
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:focus-within .pe-photo-actions {
769
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:hover .pe-photo-actions,
770
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:focus-within .pe-photo-actions {
771
771
  transform: translate(-50%, -2px);
772
772
  }
773
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo figcaption {
773
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo figcaption {
774
774
  position: absolute; z-index: 2; right: 8px; bottom: 8px; left: 8px;
775
775
  padding: 6px 8px; border-radius: 4px; background: rgba(20,20,22,.72);
776
776
  color: #fff; text-align: left; opacity: 0; pointer-events: none;
777
777
  transition: opacity .12s;
778
778
  }
779
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:hover figcaption,
780
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:focus-within figcaption { opacity: 1; }
781
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-before::before,
782
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-after::after {
779
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:hover figcaption,
780
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:focus-within figcaption { opacity: 1; }
781
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-before::before,
782
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-after::after {
783
783
  right: 0; left: 0; width: auto; height: 3px;
784
784
  }
785
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-before::before {
785
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-before::before {
786
786
  top: -7px; bottom: auto;
787
787
  }
788
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-after::after {
788
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-after::after {
789
789
  top: auto; bottom: -7px;
790
790
  }
791
+
792
+ /* Il masonry orizzontale mostra l'ordine editoriale da sinistra a destra. Sei slot rendono
793
+ ogni riga dell'admin equivalente a due battute della gallery pubblica a tre colonne; il
794
+ canvas uniforme mantiene stabile il drag e `contain` rende leggibile l'orientamento. */
795
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid {
796
+ display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
797
+ gap: 20px 12px; column-count: auto; padding: 4px 2px 40px;
798
+ }
799
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid .pe-photo-frame {
800
+ height: clamp(112px, 8.5vw, 150px); aspect-ratio: auto;
801
+ }
802
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid .pe-photo-frame img {
803
+ width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain;
804
+ }
791
805
  .pe-hidden-gallery {
792
806
  display: flex; flex-direction: column; gap: 18px; padding: 24px 2px 8px;
793
807
  border-top: 1px solid var(--ak-line);
@@ -2127,11 +2141,13 @@ body:has(.uk) .pane-body { padding-bottom: 190px; }
2127
2141
  .seo-row > .seo-field,
2128
2142
  .seo-row > .seo-help { grid-column: 2; }
2129
2143
  .seo-help { padding-top: 0; }
2144
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2130
2145
  }
2131
2146
 
2132
2147
  @media (max-width: 900px) {
2133
- .pe-gallery.is-masonry > .pe-photo-grid { column-count: 2; column-gap: 3vw; }
2134
- .pe-gallery.is-masonry > .pe-photo-grid > .pe-photo { margin-bottom: 3vw; }
2148
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid { column-count: 2; column-gap: 3vw; }
2149
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid > .pe-photo { margin-bottom: 3vw; }
2150
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2135
2151
  .settings-editor { padding-inline: 6px; gap: 34px; }
2136
2152
  .st-row { grid-template-columns: minmax(0, 1fr); gap: 15px; padding: 22px 0; }
2137
2153
  .seo-editor { padding-inline: 6px; }
@@ -0,0 +1,11 @@
1
+ export type AdminInterface = "current" | "next";
2
+ type StorageReader = Pick<Storage, "getItem">;
3
+ type StorageWriter = Pick<Storage, "setItem">;
4
+ export declare function parseAdminInterface(value: string | null): AdminInterface;
5
+ export declare function readAdminInterface(storage?: StorageReader): AdminInterface;
6
+ export declare function saveAdminInterface(value: AdminInterface, storage?: StorageWriter): void;
7
+ export declare function useAdminInterface(): {
8
+ adminInterface: AdminInterface;
9
+ setAdminInterface: import("react").Dispatch<import("react").SetStateAction<AdminInterface>>;
10
+ };
11
+ export {};
@@ -0,0 +1,45 @@
1
+ /* La nuova interfaccia nasce dietro una preferenza LOCALE al browser.
2
+ *
3
+ * Non è un'impostazione editoriale del sito: salvarla in D1 o in data/site.json la farebbe
4
+ * passare dalla pubblicazione e la accenderebbe anche agli altri amministratori. Qui invece
5
+ * il sito pubblico non riceve nulla. La nuova esperienza è il default, ma ogni browser può
6
+ * tornare all'interfaccia precedente senza cambiare ciò che vedono gli altri amministratori.
7
+ */
8
+ import { useEffect, useState } from "react";
9
+ import { storageKey } from "../../index.js";
10
+ export function parseAdminInterface(value) {
11
+ return value === "current" ? "current" : "next";
12
+ }
13
+ /* La chiave si calcola al momento della lettura, dopo che mountAdmin ha impostato il prefisso
14
+ * del sito. Calcolarla a livello di modulo la bloccherebbe sul prefisso neutro `adminkit`. */
15
+ export function readAdminInterface(storage) {
16
+ try {
17
+ const target = storage ?? (typeof localStorage !== "undefined" ? localStorage : undefined);
18
+ return parseAdminInterface(target?.getItem(storageKey("adminInterface")) ?? null);
19
+ }
20
+ catch {
21
+ return "next";
22
+ }
23
+ }
24
+ export function saveAdminInterface(value, storage) {
25
+ try {
26
+ const target = storage ?? (typeof localStorage !== "undefined" ? localStorage : undefined);
27
+ target?.setItem(storageKey("adminInterface"), value);
28
+ }
29
+ catch {
30
+ /* Se localStorage non è disponibile, la scelta resta valida per la sessione corrente. */
31
+ }
32
+ }
33
+ function applyAdminInterface(value) {
34
+ if (typeof document !== "undefined") {
35
+ document.documentElement.setAttribute("data-admin-interface", value);
36
+ }
37
+ }
38
+ export function useAdminInterface() {
39
+ const [adminInterface, setAdminInterface] = useState(readAdminInterface);
40
+ useEffect(() => {
41
+ applyAdminInterface(adminInterface);
42
+ saveAdminInterface(adminInterface);
43
+ }, [adminInterface]);
44
+ return { adminInterface, setAdminInterface };
45
+ }
@@ -0,0 +1,8 @@
1
+ export type GalleryLayout = "grid" | "masonry-vertical" | "masonry-horizontal";
2
+ /**
3
+ * `masonry` era il solo valore speciale supportato dal kit e descriveva il
4
+ * flusso CSS a colonne, quindi resta un alias del layout verticale. I siti
5
+ * possono migrare ai nomi espliciti senza cambiare di colpo documenti già
6
+ * pubblicati.
7
+ */
8
+ export declare function normalizeGalleryLayout(value: unknown): GalleryLayout;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `masonry` era il solo valore speciale supportato dal kit e descriveva il
3
+ * flusso CSS a colonne, quindi resta un alias del layout verticale. I siti
4
+ * possono migrare ai nomi espliciti senza cambiare di colpo documenti già
5
+ * pubblicati.
6
+ */
7
+ export function normalizeGalleryLayout(value) {
8
+ if (value === "masonry" || value === "masonry-vertical")
9
+ return "masonry-vertical";
10
+ if (value === "masonry-horizontal")
11
+ return "masonry-horizontal";
12
+ return "grid";
13
+ }
@@ -8,4 +8,8 @@ export declare function useHome(active: boolean): {
8
8
  change: (next: Record<string, unknown>) => void;
9
9
  save: () => Promise<void>;
10
10
  setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
11
+ publishTarget: {
12
+ documentId: string;
13
+ expectedDraftRevisionId: string | null;
14
+ } | null;
11
15
  };
@@ -89,5 +89,9 @@ export function useHome(active) {
89
89
  }
90
90
  }, [data, revision]);
91
91
  const change = useCallback((next) => { setData(next); setDirty(true); }, []);
92
- return { data, error, dirty, saving, notice, load, change, save, setNotice };
92
+ const mapped = site().homeDocument;
93
+ return { data, error, dirty, saving, notice, load, change, save, setNotice,
94
+ publishTarget: contentPersistenceMode() === "d1" && mapped && revision
95
+ ? { documentId: mapped.documentId, expectedDraftRevisionId: revision.expected }
96
+ : null };
93
97
  }
@@ -4,6 +4,10 @@ export type PageModel = Record<string, unknown> & {
4
4
  /** Versione del file da cui questa pagina è stata letta. Torna al server nel salvataggio: se
5
5
  * nel frattempo il file è cambiato, il server rifiuta invece di sovrascrivere. */
6
6
  _sha?: string;
7
+ /** Metadati D1 usati soltanto per pubblicare la pagina aperta. */
8
+ _documentId?: string;
9
+ _draftRevisionId?: string | null;
10
+ _publishedRevisionId?: string | null;
7
11
  /** Presenza/versione delle sibling dichiarate; il loro contenuto è caricato solo aprendole. */
8
12
  translations?: Record<string, {
9
13
  exists: boolean;
@@ -28,6 +32,10 @@ export declare function usePages(): {
28
32
  isPristine: (slug: string) => boolean;
29
33
  save: () => Promise<void>;
30
34
  setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
35
+ removedPublishTargets: Record<string, {
36
+ documentId: string;
37
+ expectedDraftRevisionId: string | null;
38
+ }>;
31
39
  };
32
40
  export declare function legacyDraft(): number | null;
33
41
  /** I titoli che la bozza contiene, per poterla guardare prima di buttarla. */
@@ -23,6 +23,7 @@ export function usePages() {
23
23
  const [dirty, setDirty] = useState(false);
24
24
  const [saving, setSaving] = useState(false);
25
25
  const [notice, setNotice] = useState(null);
26
+ const [removedPublishTargets, setRemovedPublishTargets] = useState({});
26
27
  const load = useCallback(async () => {
27
28
  try {
28
29
  const d = await api.pages();
@@ -31,10 +32,13 @@ export function usePages() {
31
32
  * andava riconciliato con `modelFromRaw` — un passaggio in meno, e una grafia in meno da
32
33
  * far combaciare. L'ordine per peso lo dà già il server. */
33
34
  const list = (d.pages ?? []).map((p) => ({
34
- ...p.page, slug: p.slug, url: p.url, _orig: p.slug, _sha: p.sha, translations: p.translations,
35
+ ...p.page, slug: p.slug, url: p.url, _orig: p.slug, _sha: p.sha,
36
+ _documentId: p.documentId, _draftRevisionId: p.draftRevisionId,
37
+ _publishedRevisionId: p.publishedRevisionId, translations: p.translations,
35
38
  }));
36
39
  base.current = new Map(list.map((p) => [p.slug, p]));
37
40
  setPages(list);
41
+ setRemovedPublishTargets({});
38
42
  setDirty(false);
39
43
  setError(null);
40
44
  }
@@ -89,8 +93,16 @@ export function usePages() {
89
93
  pages.forEach((p) => {
90
94
  p._orig = p.slug;
91
95
  const s = r.shas?.[p.slug];
92
- if (s)
96
+ if (s) {
93
97
  p._sha = s;
98
+ p._draftRevisionId = s;
99
+ }
100
+ const document = r.documents?.[p.slug];
101
+ if (document) {
102
+ p._documentId = document.documentId;
103
+ p._draftRevisionId = document.draftRevisionId;
104
+ p._publishedRevisionId = document.publishedRevisionId;
105
+ }
94
106
  /* Le sibling hanno lo stesso ciclo di vita del file base: se non si aggiorna il loro
95
107
  SHA qui, la modifica EN appena salvata parte già da una versione che non esiste più
96
108
  e il Worker la rifiuta come conflitto al click successivo. */
@@ -108,6 +120,13 @@ export function usePages() {
108
120
  p._translations[code]._sha = translationSha;
109
121
  }
110
122
  });
123
+ const liveSlugs = new Set(pages.map((page) => page.slug));
124
+ setRemovedPublishTargets(Object.fromEntries(Object.entries(r.documents ?? {})
125
+ .filter(([slug]) => !liveSlugs.has(slug))
126
+ .map(([slug, document]) => [slug, {
127
+ documentId: document.documentId,
128
+ expectedDraftRevisionId: document.draftRevisionId,
129
+ }])));
111
130
  // Da qui in poi il file contiene ciò che hanno queste pagine: tornano tutte "non toccate".
112
131
  base.current = new Map(pages.map((p) => [p.slug, p]));
113
132
  }
@@ -124,7 +143,8 @@ export function usePages() {
124
143
  setSaving(false);
125
144
  }
126
145
  }, [pages]);
127
- return { pages, error, dirty, saving, notice, load, update, refresh, isPristine, save, setNotice };
146
+ return { pages, error, dirty, saving, notice, load, update, refresh, isPristine, save, setNotice,
147
+ removedPublishTargets };
128
148
  }
129
149
  /* Bozza lasciata dal VECCHIO pannello nel localStorage di questo browser: si mostra, non si usa.
130
150
  *