@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 +28 -6
- package/dist/api.js +1 -1
- package/dist/app/App.d.ts +1 -0
- package/dist/app/App.js +64 -23
- package/dist/app/NextAdminApp.d.ts +8 -0
- package/dist/app/NextAdminApp.js +3 -0
- package/dist/app/app.css +35 -19
- package/dist/app/lib/admin-interface.d.ts +11 -0
- package/dist/app/lib/admin-interface.js +45 -0
- package/dist/app/lib/gallery-layout.d.ts +8 -0
- package/dist/app/lib/gallery-layout.js +13 -0
- package/dist/app/lib/home.d.ts +4 -0
- package/dist/app/lib/home.js +5 -1
- package/dist/app/lib/pages.d.ts +8 -0
- package/dist/app/lib/pages.js +23 -3
- package/dist/app/lib/publish.d.ts +9 -1
- package/dist/app/lib/publish.js +20 -6
- package/dist/app/lib/settings.d.ts +5 -0
- package/dist/app/lib/settings.js +6 -1
- package/dist/app/next.css +826 -0
- package/dist/app/routes/AdminInterfaceSettings.d.ts +12 -0
- package/dist/app/routes/AdminInterfaceSettings.js +14 -0
- package/dist/app/routes/Deliveries.js +1 -1
- package/dist/app/routes/HomeEditor.js +2 -2
- package/dist/app/routes/Overview.js +4 -4
- package/dist/app/routes/PageEditor.js +9 -6
- package/dist/app/routes/Requests.js +2 -2
- package/dist/app/routes/SiteSettingsEditor.d.ts +4 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/ui/MediaSlot.js +1 -1
- package/dist/app/ui/OpenCard.js +22 -1
- package/dist/app/ui/icons.js +5 -2
- package/package.json +2 -2
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
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: "
|
|
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
|
|
189
|
-
*
|
|
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" : "
|
|
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
|
-
|
|
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: [
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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" })] })),
|
|
253
|
-
|
|
254
|
-
|
|
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: "
|
|
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
|
|
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à
|
|
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;
|
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
|
-
/*
|
|
749
|
-
|
|
750
|
-
|
|
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
|
+
}
|
package/dist/app/lib/home.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/app/lib/home.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/app/lib/pages.d.ts
CHANGED
|
@@ -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. */
|
package/dist/app/lib/pages.js
CHANGED
|
@@ -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,
|
|
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
|
*
|