@cardinali-studio/adminkit 1.0.0

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 (181) hide show
  1. package/dist/api.d.ts +195 -0
  2. package/dist/api.js +118 -0
  3. package/dist/app/App.d.ts +3 -0
  4. package/dist/app/App.js +320 -0
  5. package/dist/app/app.css +2068 -0
  6. package/dist/app/index.d.ts +18 -0
  7. package/dist/app/index.js +33 -0
  8. package/dist/app/lib/bookings.d.ts +132 -0
  9. package/dist/app/lib/bookings.js +79 -0
  10. package/dist/app/lib/deliveries.d.ts +95 -0
  11. package/dist/app/lib/deliveries.js +104 -0
  12. package/dist/app/lib/delivery-drop.d.ts +1 -0
  13. package/dist/app/lib/delivery-drop.js +72 -0
  14. package/dist/app/lib/delivery-upload.d.ts +78 -0
  15. package/dist/app/lib/delivery-upload.js +585 -0
  16. package/dist/app/lib/groups.d.ts +9 -0
  17. package/dist/app/lib/groups.js +32 -0
  18. package/dist/app/lib/home.d.ts +11 -0
  19. package/dist/app/lib/home.js +67 -0
  20. package/dist/app/lib/overview.d.ts +82 -0
  21. package/dist/app/lib/overview.js +68 -0
  22. package/dist/app/lib/pages.d.ts +24 -0
  23. package/dist/app/lib/pages.js +156 -0
  24. package/dist/app/lib/publish.d.ts +18 -0
  25. package/dist/app/lib/publish.js +63 -0
  26. package/dist/app/lib/reorder.d.ts +3 -0
  27. package/dist/app/lib/reorder.js +31 -0
  28. package/dist/app/lib/requests.d.ts +10 -0
  29. package/dist/app/lib/requests.js +67 -0
  30. package/dist/app/lib/router.d.ts +1 -0
  31. package/dist/app/lib/router.js +14 -0
  32. package/dist/app/lib/settings.d.ts +29 -0
  33. package/dist/app/lib/settings.js +82 -0
  34. package/dist/app/lib/slug.d.ts +3 -0
  35. package/dist/app/lib/slug.js +23 -0
  36. package/dist/app/lib/text.d.ts +1 -0
  37. package/dist/app/lib/text.js +13 -0
  38. package/dist/app/lib/theme.d.ts +7 -0
  39. package/dist/app/lib/theme.js +43 -0
  40. package/dist/app/lib/upload-queue.d.ts +35 -0
  41. package/dist/app/lib/upload-queue.js +167 -0
  42. package/dist/app/routes/AboutEditor.d.ts +15 -0
  43. package/dist/app/routes/AboutEditor.js +122 -0
  44. package/dist/app/routes/Bookings.d.ts +21 -0
  45. package/dist/app/routes/Bookings.js +134 -0
  46. package/dist/app/routes/Deliveries.d.ts +37 -0
  47. package/dist/app/routes/Deliveries.js +563 -0
  48. package/dist/app/routes/GallerySectionsEditor.d.ts +12 -0
  49. package/dist/app/routes/GallerySectionsEditor.js +107 -0
  50. package/dist/app/routes/HomeEditor.d.ts +10 -0
  51. package/dist/app/routes/HomeEditor.js +188 -0
  52. package/dist/app/routes/Overview.d.ts +17 -0
  53. package/dist/app/routes/Overview.js +318 -0
  54. package/dist/app/routes/PageActions.d.ts +12 -0
  55. package/dist/app/routes/PageActions.js +93 -0
  56. package/dist/app/routes/PageEditor.d.ts +23 -0
  57. package/dist/app/routes/PageEditor.js +233 -0
  58. package/dist/app/routes/PageSeoEditor.d.ts +13 -0
  59. package/dist/app/routes/PageSeoEditor.js +102 -0
  60. package/dist/app/routes/PagesList.d.ts +10 -0
  61. package/dist/app/routes/PagesList.js +76 -0
  62. package/dist/app/routes/PortfolioWorksEditor.d.ts +12 -0
  63. package/dist/app/routes/PortfolioWorksEditor.js +139 -0
  64. package/dist/app/routes/Requests.d.ts +13 -0
  65. package/dist/app/routes/Requests.js +30 -0
  66. package/dist/app/routes/SiteSettingsEditor.d.ts +21 -0
  67. package/dist/app/routes/SiteSettingsEditor.js +66 -0
  68. package/dist/app/routes/UploadDock.d.ts +1 -0
  69. package/dist/app/routes/UploadDock.js +66 -0
  70. package/dist/app/routes/WorkshopEditionsEditor.d.ts +12 -0
  71. package/dist/app/routes/WorkshopEditionsEditor.js +101 -0
  72. package/dist/app/site.d.ts +37 -0
  73. package/dist/app/site.js +18 -0
  74. package/dist/app/ui/MediaSlot.d.ts +10 -0
  75. package/dist/app/ui/MediaSlot.js +20 -0
  76. package/dist/app/ui/OpenCard.d.ts +18 -0
  77. package/dist/app/ui/OpenCard.js +20 -0
  78. package/dist/app/ui/ParagraphEditor.d.ts +8 -0
  79. package/dist/app/ui/ParagraphEditor.js +37 -0
  80. package/dist/app/ui/dnd.d.ts +1 -0
  81. package/dist/app/ui/dnd.js +11 -0
  82. package/dist/app/ui/icons.d.ts +26 -0
  83. package/dist/app/ui/icons.js +45 -0
  84. package/dist/config.d.ts +5 -0
  85. package/dist/config.js +26 -0
  86. package/dist/form/MediaBrowser.d.ts +13 -0
  87. package/dist/form/MediaBrowser.js +24 -0
  88. package/dist/form/MediaPicker.d.ts +10 -0
  89. package/dist/form/MediaPicker.js +20 -0
  90. package/dist/form/SchemaForm.d.ts +21 -0
  91. package/dist/form/SchemaForm.js +67 -0
  92. package/dist/form/Trash.d.ts +1 -0
  93. package/dist/form/Trash.js +42 -0
  94. package/dist/form/fields/index.d.ts +1 -0
  95. package/dist/form/fields/index.js +110 -0
  96. package/dist/form/fields/objectList.d.ts +1 -0
  97. package/dist/form/fields/objectList.js +99 -0
  98. package/dist/form/index.d.ts +8 -0
  99. package/dist/form/index.js +8 -0
  100. package/dist/form/media/Lightbox.d.ts +4 -0
  101. package/dist/form/media/Lightbox.js +70 -0
  102. package/dist/form/media/MediaFolders.d.ts +20 -0
  103. package/dist/form/media/MediaFolders.js +116 -0
  104. package/dist/form/media/MediaGrid.d.ts +8 -0
  105. package/dist/form/media/MediaGrid.js +139 -0
  106. package/dist/form/media/UploadDialog.d.ts +4 -0
  107. package/dist/form/media/UploadDialog.js +37 -0
  108. package/dist/form/media/useMediaLibrary.d.ts +55 -0
  109. package/dist/form/media/useMediaLibrary.js +495 -0
  110. package/dist/form/prose/ProseBar.d.ts +21 -0
  111. package/dist/form/prose/ProseBar.js +111 -0
  112. package/dist/form/prose/ProseField.d.ts +11 -0
  113. package/dist/form/prose/ProseField.js +152 -0
  114. package/dist/form/prose/pages.d.ts +6 -0
  115. package/dist/form/prose/pages.js +13 -0
  116. package/dist/form/registry.d.ts +20 -0
  117. package/dist/form/registry.js +15 -0
  118. package/dist/frontmatter.d.ts +20 -0
  119. package/dist/frontmatter.js +202 -0
  120. package/dist/functions/_middleware.js +101 -0
  121. package/dist/functions/_shared/analytics-ga4.js +248 -0
  122. package/dist/functions/_shared/analytics-umami.js +119 -0
  123. package/dist/functions/_shared/assets.js +90 -0
  124. package/dist/functions/_shared/b2.js +300 -0
  125. package/dist/functions/_shared/delivery-crypto.js +99 -0
  126. package/dist/functions/_shared/delivery-purge.js +222 -0
  127. package/dist/functions/_shared/edge-cache.js +37 -0
  128. package/dist/functions/_shared/exif.js +53 -0
  129. package/dist/functions/_shared/github.js +138 -0
  130. package/dist/functions/_shared/imagesize.js +101 -0
  131. package/dist/functions/_shared/media-content.js +200 -0
  132. package/dist/functions/_shared/pending-media.js +98 -0
  133. package/dist/functions/_shared/refs.js +155 -0
  134. package/dist/functions/api/analytics.js +106 -0
  135. package/dist/functions/api/assets/delete.js +241 -0
  136. package/dist/functions/api/assets/folder.js +43 -0
  137. package/dist/functions/api/assets/folders.js +32 -0
  138. package/dist/functions/api/assets/move.js +253 -0
  139. package/dist/functions/api/assets/preview.js +17 -0
  140. package/dist/functions/api/assets/trash/delete.js +13 -0
  141. package/dist/functions/api/assets/trash/empty.js +16 -0
  142. package/dist/functions/api/assets/trash/restore.js +43 -0
  143. package/dist/functions/api/assets/trash.js +28 -0
  144. package/dist/functions/api/assets/upload.js +54 -0
  145. package/dist/functions/api/assets/usage.js +41 -0
  146. package/dist/functions/api/assets.js +94 -0
  147. package/dist/functions/api/deliveries.js +629 -0
  148. package/dist/functions/api/delivery/hero.js +21 -0
  149. package/dist/functions/api/delivery/image.js +35 -0
  150. package/dist/functions/api/delivery/multipart.js +261 -0
  151. package/dist/functions/api/pages/get.js +26 -0
  152. package/dist/functions/api/pages.js +354 -0
  153. package/dist/functions/api/publish.js +134 -0
  154. package/dist/functions/api/requests.js +57 -0
  155. package/dist/functions/api/settings.js +236 -0
  156. package/dist/functions/api/singleton.js +105 -0
  157. package/dist/functions/api/storage.js +125 -0
  158. package/dist/functions/index.js +124 -0
  159. package/dist/functions/site.js +38 -0
  160. package/dist/gallery.d.ts +28 -0
  161. package/dist/gallery.js +18 -0
  162. package/dist/index.d.ts +10 -0
  163. package/dist/index.js +10 -0
  164. package/dist/model.d.ts +2 -0
  165. package/dist/model.js +47 -0
  166. package/dist/normalize.d.ts +50 -0
  167. package/dist/normalize.js +120 -0
  168. package/dist/preview.d.ts +3 -0
  169. package/dist/preview.js +82 -0
  170. package/dist/prose.d.ts +17 -0
  171. package/dist/prose.js +205 -0
  172. package/dist/registries.d.ts +43 -0
  173. package/dist/registries.js +29 -0
  174. package/dist/serialize.d.ts +26 -0
  175. package/dist/serialize.js +136 -0
  176. package/dist/testing.d.ts +26 -0
  177. package/dist/testing.js +66 -0
  178. package/dist/theme.css +636 -0
  179. package/dist/yaml.d.ts +8 -0
  180. package/dist/yaml.js +90 -0
  181. package/package.json +97 -0
@@ -0,0 +1,320 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /* Il guscio del pannello: tre colonne fisse.
3
+ *
4
+ * 1. dove sono la navigazione principale — non cambia mai
5
+ * 2. cosa c'è dentro l'elenco della sezione: le pagine, le sezioni della home, i media
6
+ * 3. cosa sto facendo la modifica di UNA cosa sola
7
+ *
8
+ * Il motivo per cui non è più una barra di schede: con le schede, aprire una pagina sostituiva
9
+ * tutto lo schermo e l'elenco spariva. Qui l'elenco resta sotto gli occhi mentre si modifica, e
10
+ * passare da una pagina all'altra è un click invece di due — che è tutto il senso della seconda
11
+ * colonna, non un vezzo grafico.
12
+ *
13
+ * Il salvataggio sta in ALTO A DESTRA, uno solo, e sa a chi parlare: pagine e home hanno endpoint
14
+ * separati (una collezione e un singleton), ma da qui dentro sono lo stesso gesto e non c'è
15
+ * ragione di farli sembrare due funzioni diverse.
16
+ */
17
+ import { useEffect, useMemo, useState } from "react";
18
+ /* L'ordine di questi tre import È la separazione motore/cliente, e non va scambiato:
19
+ * i default del kit, poi l'identità di questo sito che li sovrascrive, poi l'impaginazione. */
20
+ import "../theme.css";
21
+ import "./app.css";
22
+ import { MediaFolders, MediaGrid, ProsePages, Trash, useMediaLibrary, usePicker } from "../form/index.js";
23
+ import { Preview, isLocalPreview } from "../preview.js";
24
+ import { site } from "./site.js";
25
+ import { useHashRoute } from "./lib/router";
26
+ import { usePages, legacyDraft, legacyDraftTitles, discardLegacyDraft } from "./lib/pages";
27
+ import { useHome } from "./lib/home";
28
+ import { useSiteSettings } from "./lib/settings";
29
+ import { groupsOf, groupSlug } from "./lib/groups";
30
+ import { Overview } from "./routes/Overview";
31
+ import { PagesList } from "./routes/PagesList";
32
+ import { PageEditor, PageHeader } from "./routes/PageEditor";
33
+ import { PageSeoEditor } from "./routes/PageSeoEditor";
34
+ import { SiteSettingsEditor, WebsiteSeoSettingsEditor } from "./routes/SiteSettingsEditor";
35
+ import { HomeEditor } from "./routes/HomeEditor";
36
+ import { RequestsList, RequestDetail } from "./routes/Requests";
37
+ import { useRequests } from "./lib/requests";
38
+ import { usePublish } from "./lib/publish";
39
+ import { useDeliveries } from "./lib/deliveries";
40
+ import { useBookings } from "./lib/bookings";
41
+ import { useUploadCompletions } from "./lib/upload-queue";
42
+ import { UploadDock } from "./routes/UploadDock";
43
+ import { DeliveriesList, DeliveriesTotal, DeliverySettings, DeliveryWorkspace, NewDeliveryButton, } from "./routes/Deliveries";
44
+ import { NewPageButton } from "./routes/PageActions";
45
+ import { BookingAgenda, BookingDetail, BookingsList, BookingSettingsHub, } from "./routes/Bookings";
46
+ import { Icon } from "./ui/icons";
47
+ import { useTheme } from "./lib/theme";
48
+ export function App() {
49
+ const [route, go] = useHashRoute();
50
+ const { theme, toggle: toggleTheme } = useTheme();
51
+ /* Si entra dalla Panoramica, non dalle Pagine: aprendo il pannello la domanda è quasi sempre
52
+ «è successo qualcosa?», e la risposta stava sparsa in tre sezioni da visitare una per una. */
53
+ const view = route[0] || "overview";
54
+ // Il cestino non è una voce di primo livello: è un ripostiglio dei media, e vive nella loro
55
+ // seconda colonna. Nella prima resta accesa "Media", altrimenti sembrerebbe di essere altrove.
56
+ const section = view === "trash" ? "media" : view;
57
+ // In produzione la voce non esiste: non nascosta col CSS, proprio non emessa.
58
+ const local = isLocalPreview();
59
+ const pages = usePages();
60
+ const home = useHome(section === "home");
61
+ /* Anche la Panoramica: le sue card devono sapere quali funzioni aggiuntive sono attive, e
62
+ lo stato è quello — non una seconda copia. È una lettura sola, e la Panoramica è la
63
+ schermata da cui si entra, quindi arriva prima che serva. */
64
+ const settings = useSiteSettings(section === "settings" || section === "overview" || (section === "pages" && route[2] === "seo"));
65
+ /* La libreria media vive QUI, non dentro un componente: le cartelle stanno nella seconda
66
+ colonna e la griglia nella terza, e sono due punti dello schermo lontani. Uno stato per
67
+ ciascuno vorrebbe dire due idee diverse di quale cartella si sta guardando. */
68
+ const media = useMediaLibrary({ mode: "manage", active: section === "media" });
69
+ /* Le richieste si caricano SUBITO, non entrando nella sezione: il numero «da gestire» nella
70
+ prima colonna è il motivo per cui uno ci entra, e un badge che compare solo dopo essere
71
+ andati a guardare non serve a niente. */
72
+ const requests = useRequests();
73
+ const publish = usePublish();
74
+ const deliverySettingsOpen = section === "deliveries" && route[1] === "settings";
75
+ const activeDeliveryId = section === "deliveries" && !deliverySettingsOpen ? route[1] : undefined;
76
+ /* Come Richieste, Consegne carica subito l'elenco: il badge "in scadenza" deve
77
+ esistere prima di entrare nella sezione. Il dettaglio resta pigro. */
78
+ const deliveries = useDeliveries(activeDeliveryId);
79
+ const bookings = useBookings();
80
+ /* Un caricamento che finisce cambia l'elenco (dimensione, stato, «in scadenza»), e può
81
+ finire mentre si sta guardando tutt'altro: qui ci si abbona SOLO al fatto che ne sia
82
+ finito uno, non all'avanzamento — altrimenti il pannello si ridisegnerebbe due volte al
83
+ secondo per tutta la durata dell'upload. */
84
+ const uploadCompletions = useUploadCompletions();
85
+ useEffect(() => {
86
+ if (section === "deliveries")
87
+ void deliveries.load();
88
+ }, [uploadCompletions]); // eslint-disable-line react-hooks/exhaustive-deps
89
+ const [quickKey, setQuickKey] = useState(0);
90
+ const [quickSelectedId, setQuickSelectedId] = useState();
91
+ const [pickedReq, setPickedReq] = useState(new Set());
92
+ const { pickMedia, pickMediaFiles, pickerElement } = usePicker();
93
+ // Stato e non solo lettura: l'avviso deve poter sparire, o resta lì per sempre.
94
+ const [draft, setDraft] = useState(legacyDraft);
95
+ const [draftOpen, setDraftOpen] = useState(false);
96
+ /* Un file trascinato per sbaglio FUORI da una zona di rilascio fa navigare il browser su
97
+ quel file: si esce dal pannello e si perdono le modifiche non salvate. Qui si annulla il
98
+ comportamento predefinito a livello di finestra — le zone vere hanno già gestito l'evento
99
+ prima, perché risale da loro. */
100
+ useEffect(() => {
101
+ const stop = (e) => e.preventDefault();
102
+ addEventListener("dragover", stop);
103
+ addEventListener("drop", stop);
104
+ return () => { removeEventListener("dragover", stop); removeEventListener("drop", stop); };
105
+ }, []);
106
+ const list = pages.pages;
107
+ const editing = section === "pages" && route[1] && list
108
+ ? list.find((p) => p.slug === route[1])
109
+ : undefined;
110
+ const seoEditing = !!editing && route[2] === "seo";
111
+ /* Le destinazioni interne del dialogo «Collegamento», dalle pagine già caricate: la barra di
112
+ formattazione compare dentro widget annidati anche di tre livelli, e le riceve da un
113
+ contesto invece che da una catena di prop (vedi adminkit form/prose/pages.ts). Le pagine
114
+ offline restano fuori: un link a una pagina non pubblicata è un 404 con un altro nome. */
115
+ const pageLinks = useMemo(() => (list ?? [])
116
+ .filter((page) => page.published !== false)
117
+ .map((page) => ({
118
+ label: String(page.title ?? page.slug),
119
+ href: typeof page.url === "string" && page.url ? page.url : `/${page.slug}/`,
120
+ })), [list]);
121
+ const settingsView = section === "settings" && route[1] === "seo" ? "seo" : "site";
122
+ const replace = (next) => {
123
+ if (!list || !editing)
124
+ return;
125
+ const previousSlug = editing.slug;
126
+ pages.update(list.map((p) => (p.slug === previousSlug ? next : p)));
127
+ if (next.slug !== previousSlug)
128
+ go(`/pages/${next.slug}${seoEditing ? "/seo" : ""}`);
129
+ };
130
+ const openReq = section === "requests" && route[1] && requests.rows
131
+ ? requests.rows.find((r) => String(r.id) === route[1])
132
+ : undefined;
133
+ const bookingSettingsOpen = section === "bookings" && route[1] === "settings";
134
+ const openBooking = section === "bookings" && route[1] && !bookingSettingsOpen && bookings.rows
135
+ ? bookings.rows.find((row) => row.id === route[1])
136
+ : undefined;
137
+ // Le sezioni della home sono i `group` del suo schema: nessun elenco da tenere allineato.
138
+ const homeGroups = useMemo(() => groupsOf(site().home), []);
139
+ const homeGroup = homeGroups.find((g) => groupSlug(g) === route[1]) ?? homeGroups[0] ?? "";
140
+ /* Due gruppi, non di più: con sette-otto voci un terzo aggiungerebbe rumore invece di
141
+ * struttura. Panoramica resta isolata in cima — è il punto d'ingresso, non un contenuto fra
142
+ * gli altri — e Anteprima (solo in locale) chiude Attività invece di avere un gruppo suo. */
143
+ /* Un modulo è acceso? Gemella di `moduleEnabled` lato Worker, e deve rispondere allo stesso
144
+ * modo: se le due divergessero, il pannello mostrerebbe una funzione che gli endpoint negano
145
+ * (o la nasconderebbe mentre esiste). */
146
+ const moduleOn = (name) => site().modules?.[name] === true;
147
+ /* La sezione che si sta guardando appartiene a un modulo SPENTO? In quel caso al posto del
148
+ * contenuto va la sua presentazione. `null` quando la sezione non è di un modulo o è accesa. */
149
+ const offModule = OPTIONAL_SECTIONS[section] && !moduleOn(OPTIONAL_SECTIONS[section].module)
150
+ ? OPTIONAL_SECTIONS[section]
151
+ : null;
152
+ const navGroups = [
153
+ { title: null, items: [
154
+ { id: "overview", label: "Panoramica", icon: Icon.grid },
155
+ ] },
156
+ { title: "Contenuti", items: [
157
+ { id: "home", label: "Home", icon: Icon.home },
158
+ { id: "pages", label: "Pagine", icon: Icon.pages },
159
+ { id: "media", label: "Media", icon: Icon.media },
160
+ ] },
161
+ /* Le voci di «Attività» sono MODULI, e ci sono ANCHE quando sono spenti.
162
+ *
163
+ * Nasconderle sarebbe la scelta ovvia e sarebbe sbagliata: un cliente non può chiedere una
164
+ * funzione che non sa di poter avere. Chi preme una voce spenta trova una schermata che
165
+ * spiega cosa fa e a chi scrivere per accenderla — il pannello è anche il posto in cui quelle
166
+ * funzioni si scoprono.
167
+ *
168
+ * Il badge invece sparisce: un contatore su una funzione spenta conterebbe un elenco che il
169
+ * server non serve nemmeno (le sue rotte rispondono 404). */
170
+ { title: "Attività", items: [
171
+ { id: "deliveries", label: "Consegne", icon: Icon.delivery, badge: moduleOn("delivery") ? deliveries.expiring : 0 },
172
+ { id: "bookings", label: "Prenotazioni", icon: Icon.calendar, badge: moduleOn("booking") ? bookings.pending : 0 },
173
+ { id: "requests", label: "Richieste", icon: Icon.mail, badge: moduleOn("requests") ? requests.pending : 0 },
174
+ ...(local ? [{ id: "preview", label: "Anteprima", icon: Icon.play }] : []),
175
+ ] },
176
+ ];
177
+ /* Chi risponde del «Salva»: le pagine, la home, o nessuno (media e richieste si scrivono nel
178
+ * momento in cui si fa il gesto — un bottone di salvataggio lì sarebbe una promessa falsa).
179
+ *
180
+ * «Pubblica» invece NON passa di qui: è globale. Salvare scrive sulla bozza, e pubblicare
181
+ * manda online tutto ciò che c'è sulla bozza — pagine, home e impostazioni insieme — non la
182
+ * sezione che si sta guardando. Metterlo dentro `saver` avrebbe voluto dire tre pulsanti che
183
+ * fanno la stessa identica cosa. */
184
+ const saver = section === "pages" ? pages
185
+ : section === "home" ? home
186
+ : section === "settings" ? settings
187
+ : null;
188
+ const heading = section === "overview" ? "Panoramica"
189
+ : section === "pages" ? (editing ? String(editing.title ?? editing.slug) : "Pagine")
190
+ : section === "home" ? (homeGroup || "Home")
191
+ : view === "trash" ? "Cestino"
192
+ : section === "media" ? (media.prefix ? media.labelOf(media.prefix) : "Senza cartella")
193
+ /* Consegne non ripete il titolo qui: il nome del transfer sta nella sua schermata, dove
194
+ si modifica, e si aggiorna mentre lo si scrive. Averlo in due posti significa averne
195
+ uno dei due sempre vecchio. */
196
+ : section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "Consegne")
197
+ : section === "bookings" ? (bookingSettingsOpen ? "Impostazioni prenotazioni" : openBooking ? openBooking.visitorName || openBooking.referenceCode : "Prenotazioni")
198
+ : section === "requests" ? (openReq ? openReq.name : "Richieste")
199
+ : section === "settings" ? (settingsView === "seo" ? "SEO" : "Sito")
200
+ : "Anteprima";
201
+ const wide = section === "media" || editing?.type === "gallery" ||
202
+ editing?.type === "portfolio" || editing?.type === "about" ||
203
+ editing?.type === "workshop" || seoEditing ||
204
+ section === "settings" || section === "bookings";
205
+ /* Il percorso della cartella aperta, un pezzo per volta e ognuno cliccabile. Con il solo nome
206
+ dell'ultima cartella non si capisce dove si è finiti né come tornare indietro: sotto
207
+ «Piatti 2024» c'è food, ma quel «food» non era scritto da nessuna parte. */
208
+ const mediaCrumbs = media.prefix.split("/").filter(Boolean).map((_seg, i, all) => {
209
+ const p = all.slice(0, i + 1).join("/") + "/";
210
+ return { prefix: p, label: media.labelOf(p) };
211
+ });
212
+ 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" : ""), 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" : ""), 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 })
213
+ : !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
214
+ : _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), onChange: pages.update, onRenamed: (from, to) => {
215
+ if (editing?.slug === from)
216
+ go(`/pages/${to}${seoEditing ? "/seo" : ""}`);
217
+ } }) })] })), section === "media" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Cartelle" }) }), _jsx("div", { className: "col-body", children: _jsx(MediaFolders, { lib: media, trash: { active: view === "trash", open: () => go("/trash"), leave: () => go("/media") } }) })] })), section === "requests" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: pickedReq.size === 0 ? (_jsx("span", { className: "col-title", children: requests.rows ? `${requests.rows.length} richieste` : "Richieste" })) : (_jsxs("span", { className: "rq-bulk", children: [_jsxs("span", { className: "col-title", children: [pickedReq.size, " scelte"] }), _jsx("button", { className: "mg-act", onClick: () => { void requests.mark([...pickedReq], true); setPickedReq(new Set()); }, children: "Gestite" }), _jsx("button", { className: "mg-act is-danger", onClick: () => { void requests.remove([...pickedReq]); setPickedReq(new Set()); }, children: "Elimina" }), _jsx("button", { className: "mg-act", onClick: () => setPickedReq(new Set()), children: "\u00D7" })] })) }), _jsx("div", { className: "col-body", children: requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
218
+ : !requests.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
219
+ : requests.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessuna richiesta, per ora." })
220
+ : _jsx(RequestsList, { rows: requests.rows, current: openReq?.id, picked: pickedReq, onOpen: (id) => go(`/requests/${id}`), onPick: (id) => setPickedReq((prev) => {
221
+ // Funzionale: due click ravvicinati leggerebbero lo stesso stato vecchio.
222
+ const next = new Set(prev);
223
+ if (next.has(id))
224
+ next.delete(id);
225
+ else
226
+ next.add(id);
227
+ return next;
228
+ }) }) })] })), section === "deliveries" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx(NewDeliveryButton, { onCreate: () => {
229
+ setQuickSelectedId(undefined);
230
+ setQuickKey((value) => value + 1);
231
+ go("/deliveries");
232
+ } }) }), _jsx("div", { className: "col-body", children: deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
233
+ : !deliveries.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
234
+ : deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
235
+ : _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 })
236
+ : !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
237
+ : _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"}`) })) : view === "media" ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), 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" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && (_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => media.setUploadOpen(true), 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,
238
+ /* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
239
+ rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
240
+ il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
241
+ qualcosa da pubblicare. */
242
+ 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…"
243
+ : publish.ahead === 1 ? "Pubblica (1 modifica)"
244
+ : `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" && (
245
+ /* Transfer e richieste arrivano da QUI, già caricati per i badge della prima
246
+ colonna: la Panoramica non li richiede una seconda volta, altrimenti lo stesso
247
+ numero avrebbe due sorgenti e una delle due sarebbe sempre indietro. */
248
+ _jsx(Overview, { deliveries: deliveries.rows, expiring: deliveries.expiring, requests: requests.rows, pending: requests.pending, features: settings.data?.features ?? null, bookings: bookings.rows, bookingsPending: bookings.pending, bookingsError: bookings.error, go: go })), section === "bookings" && (bookings.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: bookings.error }), _jsx("button", { className: "ak-btn", onClick: () => void bookings.load(), children: "Riprova" })] })) : !bookings.rows || !bookings.settings ? _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })
249
+ : bookingSettingsOpen ? _jsx(BookingSettingsHub, { store: bookings })
250
+ : openBooking ? _jsx(BookingDetail, { row: openBooking, store: bookings })
251
+ : _jsx(BookingAgenda, { store: bookings, onOpen: (id) => go(`/bookings/${id}`) })), section === "pages" && (pages.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: pages.error }), _jsx("button", { className: "ak-btn", onClick: () => void pages.load(), children: "Riprova" })] })) : editing ? (seoEditing ? (_jsx(PageSeoEditor, { page: editing, onChange: replace, onRefresh: pages.refresh, pristine: pages.isPristine(editing.slug), pickMediaFiles: pickMediaFiles, siteSettings: settings.data })) : (_jsx(PageEditor, { page: editing, onChange: replace, onRefresh: pages.refresh, pristine: pages.isPristine(editing.slug), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles }))) : (_jsx("p", { className: "pane-empty ak-hint", children: list ? "Scegli una pagina dall'elenco, o creane una nuova." : "Carico…" }))), section === "home" && (
252
+ /* Con un errore di caricamento c'era solo la riga rossa: nessun modo di riprovare
253
+ se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
254
+ 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" })] }))
255
+ : home.data ? _jsx(HomeEditor, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
256
+ : _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"
257
+ ? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
258
+ : _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, focus: route[1] })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && _jsx(MediaGrid, { lib: media }), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
259
+ : openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
260
+ : _jsx("p", { className: "pane-empty ak-hint", children: requests.rows?.length ? "Scegli una richiesta dall'elenco." : "" })), section === "deliveries" && (deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
261
+ : deliverySettingsOpen ? (_jsx(DeliverySettings, { settings: deliveries.settings, onAction: deliveries.action, pickMedia: pickMedia })) : activeDeliveryId && !deliveries.detail ? (_jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })) : (
262
+ /* Un transfer nuovo e uno già spedito sono la stessa schermata: cambia solo se
263
+ arriva già con dei dati. La `key` la rimonta quando si passa da un transfer
264
+ all'altro, e quando «Nuovo transfer» chiede di ricominciare da capo. */
265
+ _jsx(DeliveryWorkspace, { delivery: deliveries.detail, busy: deliveries.busy, onAction: deliveries.action, onRefresh: deliveries.refresh, onReload: deliveries.load, onRead: deliveries.read, onDeleted: () => go("/deliveries"), onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); }, onSelected: setQuickSelectedId }, deliveries.detail ? deliveries.detail.id : `nuovo-${quickKey}`))), section === "preview" && local && _jsx(Preview, {})] })] })] }), _jsx(UploadDock, {}), pickerElement] }) }));
266
+ }
267
+ /* Le sezioni che hanno una seconda colonna. Le altre lasciano la modifica a tutta larghezza.
268
+ *
269
+ * Per i media l'elenco sono le CARTELLE: il browser non se le tiene più per sé, così stanno
270
+ * dove stanno tutti gli altri elenchi del pannello — e diventano il bersaglio a cui trascinare
271
+ * le foto per spostarle. */
272
+ function hasList(section) {
273
+ return section === "pages" || section === "home" || section === "media" ||
274
+ section === "deliveries" || section === "bookings" || section === "requests" || section === "settings";
275
+ }
276
+ /* ---- Le funzioni aggiuntive, quando sono spente ---------------------------------------------
277
+ *
278
+ * Una voce spenta non sparisce dal rail: chi usa il pannello deve poter scoprire che quella
279
+ * funzione esiste. Cliccandola trova questa schermata invece di un elenco vuoto o di un errore
280
+ * — che è la differenza fra «non ce l'hai» e «non funziona».
281
+ *
282
+ * I testi stanno qui e non nella configurazione di un sito perché descrivono il MODULO, che è
283
+ * lo stesso ovunque; l'unica cosa per-sito è a chi scrivere, e quella arriva da `site().support`.
284
+ * -------------------------------------------------------------------------------------------- */
285
+ const OPTIONAL_SECTIONS = {
286
+ deliveries: {
287
+ module: "delivery",
288
+ title: "Consegne",
289
+ lead: "Manda le fotografie a un cliente con un link privato, senza allegati e senza servizi di terze parti.",
290
+ points: [
291
+ "Un indirizzo riservato per ogni consegna, che scade da solo",
292
+ "Il cliente scarica tutto in una volta, o solo le foto che gli servono",
293
+ "Vedi quante volte è stato scaricato, e da quando",
294
+ ],
295
+ },
296
+ bookings: {
297
+ module: "booking",
298
+ title: "Prenotazioni",
299
+ lead: "Fai scegliere data e ora direttamente dal sito, senza scambi di messaggi per trovare un momento.",
300
+ points: [
301
+ "Gli orari liberi vengono dal tuo calendario, non da un elenco da aggiornare a mano",
302
+ "Conferme e promemoria partono da soli",
303
+ "Gli appuntamenti li vedi qui e nel calendario insieme",
304
+ ],
305
+ },
306
+ requests: {
307
+ module: "requests",
308
+ title: "Richieste",
309
+ lead: "Le richieste che arrivano dal modulo di contatto del sito, raccolte qui invece che in una casella di posta.",
310
+ points: [
311
+ "Nessuna richiesta persa fra le altre email",
312
+ "Segni quali hai già gestito",
313
+ "Rispondi con un click, dall'indirizzo di sempre",
314
+ ],
315
+ },
316
+ };
317
+ function FeatureOff({ info }) {
318
+ const { name, email } = site().support;
319
+ return (_jsxs("section", { className: "feature-off", children: [_jsx("span", { className: "feature-off-kicker", children: "Funzione aggiuntiva" }), _jsx("h2", { children: info.title }), _jsx("p", { className: "feature-off-lead", children: info.lead }), _jsx("ul", { children: info.points.map((point) => _jsx("li", { children: point }, point)) }), _jsxs("p", { className: "feature-off-cta", children: ["Su questo sito non \u00E8 attiva. Per accenderla scrivi a ", _jsx("strong", { children: name }), ":", " ", _jsx("a", { href: `mailto:${email}?subject=${encodeURIComponent(`Attivare ${info.title} su ${site().name}`)}`, children: email }), "."] })] }));
320
+ }