@cardinali-studio/adminkit 2.2.20 → 2.2.22

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.
Files changed (48) hide show
  1. package/dist/api.d.ts +38 -2
  2. package/dist/api.js +13 -0
  3. package/dist/app/App.d.ts +1 -0
  4. package/dist/app/App.js +36 -15
  5. package/dist/app/NextAdminApp.d.ts +8 -0
  6. package/dist/app/NextAdminApp.js +3 -0
  7. package/dist/app/lib/admin-interface.d.ts +11 -0
  8. package/dist/app/lib/admin-interface.js +45 -0
  9. package/dist/app/next.css +826 -0
  10. package/dist/app/routes/AdminInterfaceSettings.d.ts +12 -0
  11. package/dist/app/routes/AdminInterfaceSettings.js +14 -0
  12. package/dist/app/routes/Deliveries.js +1 -1
  13. package/dist/app/routes/GallerySectionsEditor.d.ts +1 -1
  14. package/dist/app/routes/GallerySectionsEditor.js +14 -2
  15. package/dist/app/routes/HomeEditor.js +2 -2
  16. package/dist/app/routes/Overview.js +4 -4
  17. package/dist/app/routes/PageEditor.js +12 -5
  18. package/dist/app/routes/PortfolioWorksEditor.d.ts +1 -1
  19. package/dist/app/routes/PortfolioWorksEditor.js +11 -3
  20. package/dist/app/routes/Requests.js +2 -2
  21. package/dist/app/routes/SiteSettingsEditor.d.ts +4 -1
  22. package/dist/app/routes/SiteSettingsEditor.js +3 -2
  23. package/dist/app/ui/MediaSlot.js +1 -1
  24. package/dist/app/ui/OpenCard.js +22 -1
  25. package/dist/app/ui/icons.js +5 -2
  26. package/dist/form/fields/index.js +2 -1
  27. package/dist/form/media/Lightbox.js +33 -3
  28. package/dist/form/media/MediaGrid.js +9 -6
  29. package/dist/form/media/media-model.d.ts +2 -0
  30. package/dist/form/media/media-model.js +8 -0
  31. package/dist/form/media/useMediaLibrary.d.ts +11 -2
  32. package/dist/form/media/useMediaLibrary.js +89 -24
  33. package/dist/functions/_shared/d1-media.js +105 -91
  34. package/dist/functions/_shared/media-alt.js +61 -0
  35. package/dist/functions/api/assets/delete.js +20 -3
  36. package/dist/functions/api/assets/media.js +57 -2
  37. package/dist/functions/api/assets/move.js +5 -1
  38. package/dist/functions/api/assets/upload.js +3 -1
  39. package/dist/functions/api/assets.js +5 -2
  40. package/dist/gallery.d.ts +2 -0
  41. package/dist/media.d.ts +19 -0
  42. package/dist/media.js +132 -0
  43. package/dist/normalize.d.ts +1 -0
  44. package/dist/normalize.js +2 -1
  45. package/dist/persistence.d.ts +8 -0
  46. package/dist/persistence.js +78 -6
  47. package/dist/theme.css +42 -0
  48. package/package.json +2 -2
package/dist/api.d.ts CHANGED
@@ -6,6 +6,12 @@ export declare class ApiError extends Error {
6
6
  details: unknown;
7
7
  constructor(message: string, code: string, status: number, details?: unknown);
8
8
  }
9
+ export interface AssetAltText {
10
+ /** Vero significa che il sito deve emettere deliberatamente alt="". */
11
+ decorative: boolean;
12
+ /** Chiave "default" nei siti monolingui, codice lingua nei siti multilingui. */
13
+ texts: Record<string, string>;
14
+ }
9
15
  export interface AssetFile {
10
16
  key: string;
11
17
  name: string;
@@ -14,9 +20,13 @@ export interface AssetFile {
14
20
  uploaded?: string;
15
21
  capturedAt?: string;
16
22
  type?: string;
23
+ alt?: AssetAltText;
17
24
  w?: number | null;
18
25
  h?: number | null;
19
26
  }
27
+ /** Risolve il testo Media da copiare nel singolo utilizzo editoriale. Il campo della pagina
28
+ * resta un override: cambiare in seguito il default del file non riscrive contenuti esistenti. */
29
+ export declare function assetAltText(file: Pick<AssetFile, "alt">, locale?: string): string;
20
30
  export interface AssetFolder {
21
31
  prefix: string;
22
32
  name: string;
@@ -57,11 +67,15 @@ export interface RepoPage {
57
67
  sha?: string;
58
68
  }>;
59
69
  }
60
- export type ContentPublishTarget = {
70
+ export type ContentDocumentPublishTarget = {
61
71
  documentId: string;
62
72
  expectedDraftRevisionId: string | null;
63
73
  includeSiteAccess?: boolean;
64
74
  };
75
+ export type ContentMediaPublishTarget = {
76
+ mediaOperationId: string;
77
+ };
78
+ export type ContentPublishTarget = ContentDocumentPublishTarget | ContentMediaPublishTarget;
65
79
  export type ContentPublishDocument = {
66
80
  documentId: string;
67
81
  kind: string;
@@ -203,6 +217,17 @@ export declare const api: {
203
217
  dependentDocuments?: number;
204
218
  remainingAhead?: number;
205
219
  }>;
220
+ contentPublishMedia: (mediaOperationId: string) => Promise<{
221
+ ok: true;
222
+ published: boolean;
223
+ releaseId?: string;
224
+ attemptId?: string;
225
+ message?: string;
226
+ verified?: boolean;
227
+ observedReleaseId?: string | null;
228
+ documents?: number;
229
+ remainingAhead?: number;
230
+ }>;
206
231
  publishState: () => Promise<{
207
232
  ok: true;
208
233
  configured: boolean;
@@ -257,6 +282,9 @@ export declare const api: {
257
282
  error: string;
258
283
  }[];
259
284
  warning?: string;
285
+ operationId?: string;
286
+ requiresPublication?: boolean;
287
+ completed?: number;
260
288
  }>;
261
289
  /** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
262
290
  moveApplyRefs: (applyRefs: Record<string, string>) => Promise<{
@@ -274,7 +302,12 @@ export declare const api: {
274
302
  ok: true;
275
303
  operations: MediaOperationStatus[];
276
304
  }>;
277
- resumeMediaOperation: (operationId: string, releaseId: string) => Promise<{
305
+ saveMediaAlt: (key: string, alt: AssetAltText) => Promise<{
306
+ ok: true;
307
+ key: string;
308
+ alt: AssetAltText;
309
+ }>;
310
+ resumeMediaOperation: (operationId: string, releaseId: string | null) => Promise<{
278
311
  ok: true;
279
312
  completed: number;
280
313
  failed: number;
@@ -313,6 +346,9 @@ export declare const api: {
313
346
  d1?: boolean;
314
347
  deferred?: boolean;
315
348
  operationId?: string;
349
+ requiresPublication?: boolean;
350
+ completed?: number;
351
+ failed?: number;
316
352
  }>;
317
353
  requests: () => Promise<{
318
354
  ok: true;
package/dist/api.js CHANGED
@@ -34,6 +34,13 @@ const post = (url, data) => call(url, {
34
34
  headers: { "Content-Type": "application/json" },
35
35
  body: JSON.stringify(data),
36
36
  });
37
+ /** Risolve il testo Media da copiare nel singolo utilizzo editoriale. Il campo della pagina
38
+ * resta un override: cambiare in seguito il default del file non riscrive contenuti esistenti. */
39
+ export function assetAltText(file, locale = "default") {
40
+ if (file.alt?.decorative)
41
+ return "";
42
+ return file.alt?.texts[locale] ?? file.alt?.texts.default ?? "";
43
+ }
37
44
  /** Builds the only D1 Save wire shape. The Worker recomputes these hashes too,
38
45
  * so retries are deterministic rather than client-trusted. */
39
46
  export async function contentSaveRequest(command) {
@@ -67,6 +74,7 @@ export const api = {
67
74
  * materialize, while the panel only displays the pending immutable inputs. */
68
75
  contentPublishState: () => call("/admin/api/content/publish"),
69
76
  contentPublish: (target) => post("/admin/api/content/publish", target),
77
+ contentPublishMedia: (mediaOperationId) => post("/admin/api/content/publish", { mediaOperationId }),
70
78
  // ---- pubblicazione
71
79
  /* Salvare scrive sulla bozza e non fa partire nessuna build; è questo che manda online. Due
72
80
  * gesti separati, quindi due chiamate: una chiede quanto c'è di non pubblicato, l'altra lo
@@ -99,6 +107,11 @@ export const api = {
99
107
  /** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
100
108
  moveApplyRefs: (applyRefs) => post("/admin/api/assets/move", { applyRefs }),
101
109
  mediaOperations: () => call("/admin/api/assets/media"),
110
+ saveMediaAlt: (key, alt) => call("/admin/api/assets/media", {
111
+ method: "PATCH",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ key, alt }),
114
+ }),
102
115
  resumeMediaOperation: (operationId, releaseId) => post("/admin/api/assets/media", { action: "resume", operationId, releaseId }),
103
116
  cancelMediaOperation: (operationId) => post("/admin/api/assets/media", { action: "cancel", operationId }),
104
117
  usage: (target) => post("/admin/api/assets/usage", target),
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,6 +19,7 @@ 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
25
  import { contentPersistenceMode, site } from "./site.js";
@@ -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 }] : []),
@@ -221,7 +232,7 @@ export function App() {
221
232
  /* Consegne non ripete il titolo qui: il nome del transfer sta nella sua schermata, dove
222
233
  si modifica, e si aggiorna mentre lo si scrive. Averlo in due posti significa averne
223
234
  uno dei due sempre vecchio. */
224
- : section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "Consegne")
235
+ : section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "Transfer")
225
236
  : section === "bookings" ? (bookingSettingsOpen ? "Impostazioni prenotazioni" : openBooking ? openBooking.visitorName || openBooking.referenceCode : "Prenotazioni")
226
237
  : section === "requests" ? (openReq ? openReq.name : "Richieste")
227
238
  : section === "settings" ? (settingsView === "seo" ? "SEO" : "Sito")
@@ -237,7 +248,17 @@ export function App() {
237
248
  const p = all.slice(0, i + 1).join("/") + "/";
238
249
  return { prefix: p, label: media.labelOf(p) };
239
250
  });
240
- 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: [_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 })
241
262
  : !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
242
263
  : _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
243
264
  if (editing?.slug === from)
@@ -253,16 +274,16 @@ export function App() {
253
274
  else
254
275
  next.add(id);
255
276
  return next;
256
- }) }) })] })), section === "deliveries" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx(NewDeliveryButton, { onCreate: () => {
257
- setQuickSelectedId(undefined);
258
- setQuickKey((value) => value + 1);
259
- go("/deliveries");
260
- } }) }), _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 })
261
282
  : !deliveries.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
262
283
  : deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
263
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 })
264
285
  : !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
265
- : _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,
266
287
  /* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
267
288
  rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
268
289
  il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
@@ -283,9 +304,9 @@ export function App() {
283
304
  se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
284
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" })] }))
285
306
  : home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
286
- : _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"
287
308
  ? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
288
- : _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
289
310
  ? _jsx(MediaSettingsEditor, { lib: media })
290
311
  : _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
291
312
  : openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
@@ -317,10 +338,10 @@ function hasList(section) {
317
338
  const OPTIONAL_SECTIONS = {
318
339
  deliveries: {
319
340
  module: "delivery",
320
- title: "Consegne",
341
+ title: "Transfer",
321
342
  lead: "Manda le fotografie a un cliente con un link privato, senza allegati e senza servizi di terze parti.",
322
343
  points: [
323
- "Un indirizzo riservato per ogni consegna, che scade da solo",
344
+ "Un indirizzo riservato per ogni Transfer, che scade da solo",
324
345
  "Il cliente scarica tutto in una volta, o solo le foto che gli servono",
325
346
  "Vedi quante volte è stato scaricato, e da quando",
326
347
  ],
@@ -341,7 +362,7 @@ const OPTIONAL_SECTIONS = {
341
362
  lead: "Le richieste che arrivano dal modulo di contatto del sito, raccolte qui invece che in una casella di posta.",
342
363
  points: [
343
364
  "Nessuna richiesta persa fra le altre email",
344
- "Segni quali hai già gestito",
365
+ "Segni quali hai già letto",
345
366
  "Rispondi con un click, dall'indirizzo di sempre",
346
367
  ],
347
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
+ }
@@ -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
+ }