@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,318 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /* La Panoramica: la schermata da cui si entra nel pannello.
3
+ *
4
+ * Non è una sezione in più fra le altre — è la risposta a «com'è andata?» data prima che
5
+ * qualcuno la chieda. Da qui non si modifica niente: ogni riquadro è un numero e la porta per
6
+ * andare dov'è nato. Se ti trovi ad aggiungerci un campo modificabile, il posto giusto è la
7
+ * sezione a cui quel campo appartiene.
8
+ *
9
+ * ── Da dove vengono i numeri ───────────────────────────────────────────────────────────────
10
+ * transfer e richieste dalle liste GIÀ caricate da App.tsx per i badge della prima colonna.
11
+ * Arrivano come props apposta: una seconda query per gli stessi numeri
12
+ * vorrebbe dire due verità, e una delle due sempre vecchia.
13
+ * spazio /admin/api/storage — R2 contato, B2 letto da D1.
14
+ * visite /admin/api/analytics — Umami o Google Analytics, dietro la Function
15
+ * perché la chiave non deve passare dal browser. QUALE dei due lo dice
16
+ * la risposta (`providerLabel`): qui non si nomina nessun servizio, o
17
+ * il riquadro mente non appena si cambia.
18
+ *
19
+ * ── Il bento ───────────────────────────────────────────────────────────────────────────────
20
+ * Dodici colonne e riquadri che ne occupano 3, 4 o 8. La regola che tiene insieme la cosa: la
21
+ * RIGA DI SOPRA sono numeri singoli (una cifra, un colpo d'occhio), sotto ci sono le cose che
22
+ * si leggono (grafico, elenchi). Mescolando i due tipi si ottiene una parete di riquadri in cui
23
+ * non si sa più dove guardare per primo.
24
+ *
25
+ * Gli span NON sono fissi: dipendono da quali funzioni aggiuntive sono attive, e sono scelti
26
+ * perché le righe tornino esatte in tutte e QUATTRO le combinazioni. Con le statistiche spente
27
+ * i tre riquadri in cima passano da 3 a 4 colonne (il quarto, «Visitatori», non c'è) e il
28
+ * grafico diventa una card bloccata da 4, che si affianca ad «Archivi» e «Appuntamenti».
29
+ * Cambiando uno span, ricontrollarle tutte e quattro: `grid-auto-flow: dense` ripara i buchi
30
+ * piccoli, non una riga sbagliata.
31
+ */
32
+ import { useEffect, useRef, useState } from "react";
33
+ import { EXTRAS } from "./SiteSettingsEditor";
34
+ import { useAnalytics, useStorage, } from "../lib/overview";
35
+ import { Icon } from "../ui/icons";
36
+ import { site } from "../site.js";
37
+ const RANGE_LABEL = {
38
+ "24h": "24 ore", "7d": "7 giorni", "30d": "30 giorni",
39
+ };
40
+ export function Overview({ deliveries, expiring, requests, pending, features, bookings, bookingsPending, bookingsError, go, }) {
41
+ const storage = useStorage(true);
42
+ /* Le visite non si chiedono nemmeno quando la funzione non è attiva: sarebbe una chiamata a
43
+ un servizio esterno per un riquadro che mostrerà comunque «Attiva». */
44
+ const analytics = useAnalytics(features?.analytics === true);
45
+ const active = deliveries?.filter((d) => d.status === "active") ?? [];
46
+ const uploading = deliveries?.filter((d) => d.status === "uploading") ?? [];
47
+ /* Gli ultimi transfer, non i più grossi né i più scaricati: la domanda vera è «quello che ho
48
+ mandato giovedì, l'hanno preso?», e si risponde guardando in cima a un elenco in ordine di
49
+ tempo. Gli archiviati restano — il link vive ancora, e i download continuano a contare. */
50
+ const recent = [...(deliveries ?? [])]
51
+ .filter((d) => d.status === "active" || d.status === "archived")
52
+ .sort((a, b) => (b.activatedAt || b.updatedAt).localeCompare(a.activatedAt || a.updatedAt))
53
+ .slice(0, 5);
54
+ const downloads = (deliveries ?? []).reduce((n, d) => n + d.downloadCount, 0);
55
+ /* Il cestino è dentro il totale: quei file sono ancora nel bucket e si pagano. Escluderlo
56
+ darebbe un numero più bello e sbagliato, e soprattutto farebbe sembrare inutile svuotarlo. */
57
+ const bytes = (storage.data?.r2.bytes ?? 0) + (storage.data?.r2.trashBytes ?? 0)
58
+ + (storage.data?.b2.bytes ?? 0);
59
+ const totals = analytics.data?.totals;
60
+ /* Una funzione è "spenta" solo quando lo sappiamo davvero: finché `features` è `null` le
61
+ impostazioni non sono ancora arrivate, e mostrare la card bloccata in quel momento
62
+ farebbe lampeggiare «Attiva» a chi la funzione ce l'ha. */
63
+ const known = features !== null;
64
+ const hasAnalytics = features?.analytics === true;
65
+ const hasBookings = features?.appointments === true;
66
+ // Senza «Visitatori» la riga in cima ha tre riquadri, che devono coprire le dodici colonne.
67
+ const statSpan = hasAnalytics ? 3 : 4;
68
+ return (_jsxs("div", { className: "ov", children: [_jsxs("div", { className: "ov-bar", children: [_jsx("p", { className: "ov-bar-hi", children: "Tutto quello che \u00E8 successo mentre non guardavi." }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: storage.busy || analytics.busy, onClick: () => { void storage.load(true); analytics.reload(); }, children: storage.busy || analytics.busy ? "Aggiorno…" : "Aggiorna" })] }), _jsxs("div", { className: "ov-grid", children: [hasAnalytics && (_jsx(Stat, { span: 3, label: "Visitatori", icon: Icon.eye, tone: "blue", hint: RANGE_LABEL[analytics.range], value: totals ? int(totals.visitors.value) : null, quiet: analytics.data && !analytics.data.configured ? "Statistiche non collegate" : undefined, delta: totals?.visitors })), _jsx(Stat, { span: statSpan, label: "Transfer attivi", icon: Icon.delivery, tone: "accent", hint: uploading.length ? `+${uploading.length} in caricamento` : `${downloads} download`, value: deliveries ? String(active.length) : null, onClick: () => go("/deliveries") }), _jsx(Stat, { span: statSpan, label: "Spazio in uso", icon: Icon.archive, tone: "quiet", hint: storage.data ? `${int(storage.data.r2.objects)} foto · ${int(storage.data.b2.files)} file consegnati` : "R2 + B2", value: storage.data ? size(bytes) : null, quiet: storage.error ?? undefined }), _jsxs("button", { className: "ov-tile ov-cta", "data-span": statSpan, onClick: () => go("/deliveries"), children: [_jsxs("span", { className: "ov-cta-copy", children: [_jsx("strong", { children: "Consegne" }), _jsx("span", { children: "Manda le foto a un cliente" })] }), _jsx("span", { className: "ov-cta-go", "aria-hidden": "true", children: _jsx(Icon.delivery, {}) }), _jsx("span", { className: "ov-cta-bars", "aria-hidden": "true", children: BARS.map((h, i) => _jsx("i", { style: { height: `${h}%` } }, i)) }), expiring > 0 && _jsxs("span", { className: "ov-cta-badge", children: [expiring, " in scadenza"] })] }), !hasAnalytics ? (known && _jsx(LockedCard, { feature: "analytics", go: go })) : (_jsxs("section", { className: "ov-tile ov-chart", "data-span": "8", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Visite al sito" }), _jsxs("p", { children: ["Pagine viste e sessioni", analytics.data?.providerLabel ? `, da ${analytics.data.providerLabel}` : ""] })] }), _jsx("div", { className: "ov-range", role: "group", "aria-label": "Periodo", children: Object.keys(RANGE_LABEL).map((r) => (_jsx("button", { className: "ov-range-btn" + (analytics.range === r ? " is-on" : ""), onClick: () => analytics.setRange(r), children: RANGE_LABEL[r] }, r))) })] }), !analytics.data ? (_jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })) : !analytics.data.configured ? (_jsx(NotConnected, { providers: analytics.data.providers ?? [] })) : (_jsxs(_Fragment, { children: [analytics.error && _jsx("p", { className: "ak-error ov-pad", children: analytics.error }), _jsxs("div", { className: "ov-chart-figures", children: [_jsx(Figure, { label: "Pagine viste", metric: analytics.data.totals?.pageviews }), _jsx(Figure, { label: "Sessioni", metric: analytics.data.totals?.visits }), _jsx(Figure, { label: "Visitatori", metric: analytics.data.totals?.visitors }), _jsx(Figure, { label: "Tempo medio", metric: averageTime(analytics.data.totals), format: seconds })] }), _jsx(Chart, { points: analytics.data.series ?? [], unit: analytics.data.unit ?? "day" })] }))] })), _jsxs("section", { className: "ov-tile", "data-span": "4", children: [_jsx("header", { className: "ov-tile-head", children: _jsxs("div", { children: [_jsx("h2", { children: "Archivi" }), _jsx("p", { children: "Quanto occupano le due librerie" })] }) }), storage.error ? _jsx("p", { className: "ak-error ov-pad", children: storage.error })
69
+ : !storage.data ? _jsx("p", { className: "ak-hint ov-pad", children: "Conto gli oggetti\u2026" })
70
+ : (_jsxs("div", { className: "ov-store", children: [_jsxs("div", { className: "ov-store-rows", children: [_jsx(StoreRow, { name: "Media del sito", where: "R2", bytes: storage.data.r2.bytes, total: bytes, detail: `${int(storage.data.r2.objects)} file`, onClick: () => go("/media") }), _jsx(StoreRow, { name: "Consegne", where: "B2", bytes: storage.data.b2.bytes, total: bytes, detail: `${int(storage.data.b2.packages)} pacchetti · ${int(storage.data.b2.files)} file`, onClick: () => go("/deliveries") }), _jsx(StoreRow, { name: "Cestino", where: "R2", bytes: storage.data.r2.trashBytes, total: bytes, detail: storage.data.r2.trashObjects
71
+ ? `${int(storage.data.r2.trashObjects)} file recuperabili — svuotalo per liberare spazio`
72
+ : "vuoto", onClick: () => go("/trash") })] }), _jsxs("dl", { className: "ov-store-notes", children: [storage.data.b2.pendingBytes > 0 && (_jsxs(_Fragment, { children: [_jsx("dt", { children: "In cancellazione" }), _jsxs("dd", { children: [size(storage.data.b2.pendingBytes), ", spariscono da soli entro sette giorni"] })] })), storage.data.r2.partial && (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Attenzione" }), _jsx("dd", { children: "Il conteggio dei media si \u00E8 fermato ai primi 20.000 oggetti: il totale \u00E8 per difetto." })] }))] })] }))] }), !hasBookings
73
+ ? known && _jsx(LockedCard, { feature: "appointments", go: go })
74
+ : _jsx(BookingsCard, { rows: bookings, pending: bookingsPending, error: bookingsError, go: go }), _jsxs("section", { className: "ov-tile", "data-span": "8", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Ultimi transfer" }), _jsx("p", { children: "Quante volte \u00E8 stato scaricato ognuno" })] }), _jsx("button", { className: "ov-more", onClick: () => go("/deliveries"), children: "Tutti" })] }), !deliveries ? _jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })
75
+ : recent.length === 0 ? _jsx("p", { className: "ak-hint ov-pad", children: "Nessun transfer spedito, per ora." })
76
+ : (_jsxs("table", { className: "ov-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Transfer" }), _jsx("th", { children: "Spedito" }), _jsx("th", { children: "Peso" }), _jsx("th", { className: "ov-num", children: "Download" }), _jsx("th", {})] }) }), _jsx("tbody", { children: recent.map((d) => (_jsxs("tr", { onClick: () => go(`/deliveries/${d.id}`), children: [_jsxs("td", { children: [_jsx("span", { className: "ov-row-main", children: d.title || "Senza nome" }), _jsx("span", { className: "ov-row-sub", children: d.clientName || "—" })] }), _jsx("td", { className: "ov-when", children: day(d.activatedAt || d.updatedAt) }), _jsx("td", { className: "ov-when", children: size(d.storedBytes) }), _jsx("td", { className: "ov-num" + (d.downloadCount ? "" : " is-zero"), children: d.downloadCount || "—" }), _jsx("td", { className: "ov-state", children: _jsx(StatusPill, { delivery: d }) })] }, d.id))) })] }))] }), _jsxs("section", { className: "ov-tile", "data-span": "4", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Ultima richiesta" }), _jsx("p", { children: pending > 0 ? `${pending} da gestire` : "Dal form del sito" })] }), _jsx("button", { className: "ov-more", onClick: () => go("/requests"), children: "Tutte" })] }), !requests ? _jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })
77
+ : requests.length === 0 ? _jsx("p", { className: "ak-hint ov-pad", children: "Nessuna richiesta, per ora." })
78
+ : _jsx(LastRequest, { row: requests[0], go: go })] }), hasAnalytics && _jsx(Top, { span: 4, title: "Pagine pi\u00F9 viste", note: RANGE_LABEL[analytics.range], rows: analytics.data?.pages ?? [], ready: !!analytics.data?.configured, label: (name) => name === "/" ? "/ (home)" : name }), hasAnalytics && _jsx(Top, { span: 4, title: "Da dove arrivano", note: "Siti che portano al tuo", rows: analytics.data?.referrers ?? [], ready: !!analytics.data?.configured, label: (name) => name.replace(/^https?:\/\//, "") })] })] }));
79
+ }
80
+ /* ---- I pezzi ------------------------------------------------------------------------- */
81
+ /* La card di una funzione non attiva su questo sito.
82
+ *
83
+ * Mostra il minimo: come si chiama, che non c'è, e come si accende. NON descrive la funzione —
84
+ * quello lo fa la sezione «Funzioni aggiuntive» delle impostazioni, che è dove porta il
85
+ * pulsante: una card che spiega e insieme vende diventa un volantino in mezzo agli strumenti
86
+ * di lavoro, e questa schermata la si apre venti volte al giorno.
87
+ *
88
+ * Il titolo e il testo arrivano da `EXTRAS`, lo stesso elenco che disegna le voci nelle
89
+ * impostazioni: il nome della funzione è scritto in UN posto, quindi le due schermate non
90
+ * possono chiamarla in due modi diversi.
91
+ *
92
+ * `#/settings/funzioni` invece di `#/settings`: il secondo pezzo della rotta fa scorrere la
93
+ * sezione sotto gli occhi. Senza, si atterra in cima a una pagina lunga e la cosa promessa dal
94
+ * pulsante è fuori schermo — cioè il pulsante ha mentito. */
95
+ function LockedCard({ feature, go }) {
96
+ const extra = EXTRAS.find((e) => e.key === feature);
97
+ return (_jsxs("section", { className: "ov-tile ov-locked", "data-span": "4", children: [_jsx("header", { className: "ov-tile-head", children: _jsx("div", { children: _jsx("h2", { children: extra?.card ?? "" }) }) }), _jsxs("div", { className: "ov-locked-body", children: [_jsx("p", { children: "Non \u00E8 attiva su questo sito." }), _jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => go("/settings/funzioni"), children: "Attiva" })] })] }));
98
+ }
99
+ /* Gli appuntamenti, quando la funzione c'è: quanti aspettano una risposta e i prossimi in
100
+ arrivo. Le date PASSATE non si mostrano — un appuntamento di ieri non è una cosa da fare — e
101
+ quelli annullati o rifiutati nemmeno. */
102
+ function BookingsCard({ rows, pending, error, go }) {
103
+ const now = new Date().toISOString();
104
+ const next = [...(rows ?? [])]
105
+ .filter((b) => b.startAt > now && ["pending", "confirming", "confirmed"].includes(b.status))
106
+ .sort((a, b) => a.startAt.localeCompare(b.startAt))
107
+ .slice(0, 4);
108
+ return (_jsxs("section", { className: "ov-tile", "data-span": "4", children: [_jsxs("header", { className: "ov-tile-head", children: [_jsxs("div", { children: [_jsx("h2", { children: "Appuntamenti" }), _jsx("p", { children: pending > 0 ? `${pending} da confermare` : "I prossimi in calendario" })] }), _jsx("button", { className: "ov-more", onClick: () => go("/bookings"), children: "Tutti" })] }), error ? _jsx("p", { className: "ak-error ov-pad", children: error })
109
+ : !rows ? _jsx("p", { className: "ak-hint ov-pad", children: "Carico\u2026" })
110
+ : next.length === 0 ? _jsx("p", { className: "ak-hint ov-pad", children: "Nessun appuntamento in arrivo." })
111
+ : (_jsx("ul", { className: "ov-bookings", children: next.map((b) => (_jsx("li", { children: _jsxs("button", { onClick: () => go(`/bookings/${b.id}`), children: [_jsx("span", { className: "ov-book-when", children: slot(b.startAt) }), _jsx("span", { className: "ov-book-who", children: b.visitorName || "Senza nome" }), _jsx("span", { className: "ov-book-what", children: b.typeName }), b.status !== "confirmed" && _jsx("span", { className: "dl-pill is-expiring", children: "da confermare" })] }) }, b.id))) }))] }));
112
+ }
113
+ /* Il riquadro con UNA cifra. `onClick` è opzionale apposta: se una cifra non porta da nessuna
114
+ parte non deve nemmeno sembrare che lo faccia, quindi diventa un `div` invece di un bottone
115
+ spento. `tone` colora lo squircle dell'icona con la stessa palette già usata per gli stati
116
+ delle consegne (`.dl-pill`): niente colori nuovi, solo un posto in più dove si vedono. */
117
+ function Stat({ span: cols, label, value, hint, delta, quiet, onClick, icon: IconC, tone = "accent" }) {
118
+ const inner = (_jsxs(_Fragment, { children: [_jsx("span", { className: "ov-stat-icon is-" + tone, children: _jsx(IconC, {}) }), _jsx("span", { className: "ov-stat-label", children: label }), _jsx("strong", { className: "ov-stat-value", children: value ?? "—" }), _jsx("span", { className: "ov-stat-foot", children: quiet ? _jsx("em", { className: "ov-stat-quiet", children: quiet }) : _jsxs(_Fragment, { children: [delta && _jsx(Delta, { metric: delta }), hint && _jsx("span", { children: hint })] }) })] }));
119
+ return onClick
120
+ ? _jsx("button", { className: "ov-tile ov-stat is-link", "data-span": cols, onClick: onClick, children: inner })
121
+ : _jsx("div", { className: "ov-tile ov-stat", "data-span": cols, children: inner });
122
+ }
123
+ /* La variazione rispetto al periodo precedente. Senza `prev` non si disegna niente: una
124
+ pillola «0%» direbbe "stabile" quando invece vuol dire "non lo sappiamo". */
125
+ function Delta({ metric }) {
126
+ if (metric.prev === null || metric.prev === 0)
127
+ return null;
128
+ const change = ((metric.value - metric.prev) / metric.prev) * 100;
129
+ if (!Number.isFinite(change))
130
+ return null;
131
+ const up = change >= 0;
132
+ return (_jsxs("em", { className: "ov-delta" + (up ? " is-up" : " is-down"), children: [up ? "▲" : "▼", " ", Math.abs(change).toFixed(Math.abs(change) < 10 ? 1 : 0), "%"] }));
133
+ }
134
+ function Figure({ label, metric, format }) {
135
+ return (_jsxs("div", { className: "ov-figure", children: [_jsx("span", { children: label }), _jsx("strong", { children: metric ? (format ? format(metric.value) : int(metric.value)) : "—" }), metric && _jsx(Delta, { metric: metric })] }));
136
+ }
137
+ /* Il grafico. Disegnato a mano come le icone: una libreria di chart porta con sé un albero di
138
+ componenti, un tema e un altro modo di dire "colore" che non passa dalle custom property —
139
+ per due curve e una griglia.
140
+
141
+ Il contenitore si MISURA (un ResizeObserver), invece di lasciare che un `viewBox` fisso scali
142
+ l'SVG con la larghezza. Il primo tentativo era quello, ed è sbagliato per una ragione che si
143
+ vede solo dopo: la griglia collassa a una colonna sotto i 1240px, il riquadro passa da 780 a
144
+ 1400 pixel, e un disegno che mantiene le proporzioni diventa alto quattrocento pixel — metà
145
+ schermata per sette numeri. Misurando, un'unità dell'SVG è un pixel: l'altezza resta quella
146
+ che si è deciso, il testo non si allarga e i tratti sono nitidi senza `vector-effect`. */
147
+ const H = 200, PAD_T = 14, PAD_B = 26, PAD_L = 32;
148
+ function Chart({ points, unit }) {
149
+ const [at, setAt] = useState(null);
150
+ const [width, setWidth] = useState(640);
151
+ const box = useRef(null);
152
+ /* Gli hook stanno PRIMA dell'uscita anticipata per la serie vuota: React li vuole sempre
153
+ nello stesso ordine, e una `return` in mezzo li salterebbe al primo caricamento. */
154
+ useEffect(() => {
155
+ const el = box.current;
156
+ if (!el || typeof ResizeObserver === "undefined")
157
+ return;
158
+ const ro = new ResizeObserver(([entry]) => {
159
+ setWidth(Math.max(260, Math.round(entry.contentRect.width)));
160
+ });
161
+ ro.observe(el);
162
+ return () => ro.disconnect();
163
+ }, []);
164
+ if (points.length < 2) {
165
+ return _jsx("p", { className: "ak-hint ov-pad", children: "Non ci sono ancora abbastanza dati per un grafico." });
166
+ }
167
+ const top = Math.max(1, ...points.map((p) => p.pageviews), ...points.map((p) => p.sessions));
168
+ const x = (i) => PAD_L + (i / (points.length - 1)) * (width - PAD_L);
169
+ const y = (v) => PAD_T + (1 - v / top) * (H - PAD_T - PAD_B);
170
+ const line = (get) => points.map((p, i) => `${i ? "L" : "M"}${x(i).toFixed(1)} ${y(get(p)).toFixed(1)}`).join(" ");
171
+ const area = `${line((p) => p.pageviews)} L${width} ${H - PAD_B} L${PAD_L} ${H - PAD_B} Z`;
172
+ const hovered = at === null ? null : points[at];
173
+ return (_jsxs("div", { className: "ov-plot", ref: box, children: [_jsxs("svg", { className: "ov-plot-svg", width: width, height: H, viewBox: `0 0 ${width} ${H}`, role: "img", "aria-label": "Andamento delle visite", onMouseLeave: () => setAt(null), onMouseMove: (e) => {
174
+ const rect = e.currentTarget.getBoundingClientRect();
175
+ const ratio = (e.clientX - rect.left - PAD_L) / (rect.width - PAD_L);
176
+ setAt(Math.max(0, Math.min(points.length - 1, Math.round(ratio * (points.length - 1)))));
177
+ }, children: [_jsx("defs", { children: _jsxs("linearGradient", { id: "ov-fill", x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: "var(--ak-accent)", stopOpacity: ".22" }), _jsx("stop", { offset: "100%", stopColor: "var(--ak-accent)", stopOpacity: "0" })] }) }), [0, 0.5, 1].map((f) => (_jsxs("g", { children: [_jsx("line", { className: "ov-plot-grid", x1: PAD_L, x2: width, y1: y(top * f), y2: y(top * f) }), _jsx("text", { className: "ov-plot-tick", x: PAD_L - 6, y: y(top * f) + 3, textAnchor: "end", children: int(Math.round(top * f)) })] }, f))), _jsx("path", { d: area, fill: "url(#ov-fill)" }), _jsx("path", { className: "ov-plot-line", d: line((p) => p.pageviews) }), _jsx("path", { className: "ov-plot-line is-second", d: line((p) => p.sessions) }), at !== null && (_jsxs("g", { children: [_jsx("line", { className: "ov-plot-cursor", x1: x(at), x2: x(at), y1: PAD_T, y2: H - PAD_B }), _jsx("circle", { className: "ov-plot-dot", cx: x(at), cy: y(points[at].pageviews), r: "3.5" })] })), points.map((p, i) => (tickEvery(points.length, i) && (_jsx("text", { className: "ov-plot-tick", x: x(i), y: H - 7, textAnchor: i === 0 ? "start" : i === points.length - 1 ? "end" : "middle", children: axisLabel(p.date, unit) }, p.date))))] }), hovered && (_jsxs("div", { className: "ov-plot-tip", style: {
178
+ left: `${x(at)}px`,
179
+ transform: `translateX(-${tipAt(at, points.length)}%)`,
180
+ }, children: [_jsx("strong", { children: fullLabel(hovered.date, unit) }), _jsxs("span", { children: [int(hovered.pageviews), " viste \u00B7 ", int(hovered.sessions), " sessioni"] })] })), _jsxs("p", { className: "ov-plot-legend", children: [_jsx("span", { className: "ov-key", children: "Pagine viste" }), _jsx("span", { className: "ov-key is-second", children: "Sessioni" })] })] }));
181
+ }
182
+ /* Quanto spostare il riquadretto del tooltip: la stessa percentuale della sua posizione nel
183
+ grafico, così ai due estremi si allinea al bordo invece di sporgere. */
184
+ function tipAt(index, total) { return (index / (total - 1)) * 100; }
185
+ function StoreRow({ name, where, bytes, total, detail, onClick }) {
186
+ const share = total > 0 ? (bytes / total) * 100 : 0;
187
+ return (_jsxs("button", { className: "ov-store-row", onClick: onClick, children: [_jsxs("span", { className: "ov-store-top", children: [_jsxs("span", { className: "ov-store-name", children: [name, _jsx("em", { children: where })] }), _jsx("strong", { children: size(bytes) })] }), _jsx("span", { className: "ov-store-track", children: _jsx("i", { style: { width: `${share}%`, minWidth: share > 0 ? undefined : 0 } }) }), _jsxs("span", { className: "ov-store-detail", children: [_jsx("span", { children: detail }), _jsx("em", { children: percent(share) })] })] }));
188
+ }
189
+ function LastRequest({ row, go }) {
190
+ return (_jsxs("div", { className: "ov-req" + (row.handled_at ? " is-handled" : ""), children: [_jsxs("div", { className: "ov-req-head", children: [_jsx("span", { className: "rq-avatar", children: initials(row.name) }), _jsxs("span", { className: "ov-req-who", children: [_jsx("strong", { children: row.name }), _jsx("a", { href: `mailto:${row.email}`, onClick: (e) => e.stopPropagation(), children: row.email })] }), _jsx("span", { className: "ov-req-when", children: when(row.created_at) })] }), _jsx("p", { className: "ov-req-msg", children: row.message }), _jsxs("div", { className: "ov-req-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => go(`/requests/${row.id}`), children: "Apri" }), _jsx("a", { className: "ak-btn ak-btn-primary", href: `mailto:${row.email}`, children: "Rispondi" }), row.handled_at && _jsx("span", { className: "ak-hint", children: "Gi\u00E0 gestita" })] })] }));
191
+ }
192
+ function Top({ span: cols, title, note, rows, ready, label }) {
193
+ const top = Math.max(1, ...rows.map((r) => r.count));
194
+ return (_jsxs("section", { className: "ov-tile", "data-span": cols, children: [_jsx("header", { className: "ov-tile-head", children: _jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("p", { children: note })] }) }), !ready ? _jsx("p", { className: "ak-hint ov-pad", children: "Servono le statistiche collegate." })
195
+ : rows.length === 0 ? _jsx("p", { className: "ak-hint ov-pad", children: "Nessun dato in questo periodo." })
196
+ : (_jsx("ol", { className: "ov-top", children: rows.map((r) => (_jsxs("li", { children: [_jsx("span", { className: "ov-top-name", title: r.name, children: label(r.name) }), _jsx("span", { className: "ov-top-track", children: _jsx("i", { style: { width: `${(r.count / top) * 100}%` } }) }), _jsx("span", { className: "ov-top-count", children: int(r.count) })] }, r.name))) }))] }));
197
+ }
198
+ /* Non collegato non è un errore: è una funzione che non è stata ancora accesa. Il riquadro
199
+ elenca i servizi possibili e, per ognuno, cosa manca — perché la domanda vera di chi guarda
200
+ non è «cos'è rotto» ma «quale scelgo e cosa devo fare». Le voci arrivano dalla Function:
201
+ aggiungendo un terzo servizio ai driver, compare qui da solo. */
202
+ function NotConnected({ providers }) {
203
+ return (_jsxs("div", { className: "ov-setup", children: [_jsx("p", { children: _jsx("strong", { children: "Le visite non sono ancora collegate." }) }), _jsxs("p", { children: ["Il pannello legge le statistiche da uno di questi servizi. Il codice di tracciamento sul sito \u00E8 un\u2019altra cosa e si incolla in ", _jsx("em", { children: "Impostazioni \u2192 Sito \u2192 Tag Analytics" }), "."] }), _jsx("div", { className: "ov-setup-list", children: providers.map((p) => (_jsxs("div", { className: "ov-setup-item", children: [_jsx("strong", { children: p.label }), p.missing.length === 0
204
+ ? _jsx("span", { className: "ov-setup-ok", children: "pronto \u2014 scegli questo con ANALYTICS_PROVIDER" })
205
+ : _jsx("pre", { children: p.missing.map(recipe).join("\n") })] }, p.id))) })] }));
206
+ }
207
+ /* Una variabile si scrive in `wrangler.toml`; un secret si mette con un comando e non deve
208
+ MAI finire in un file committato. Sono due gesti diversi e il riquadro mostra quello giusto,
209
+ invece di un elenco di nomi da cercare altrove. */
210
+ function recipe({ name, secret }) {
211
+ return secret
212
+ ? `npx wrangler pages secret put ${name} \\\n --project-name ${site().pagesProject}`
213
+ : `# in wrangler.toml, fra le [vars]\n${name} = "…"`;
214
+ }
215
+ function StatusPill({ delivery }) {
216
+ const now = new Date().toISOString();
217
+ const expired = !!delivery.filesExpiresAt && delivery.filesExpiresAt <= now;
218
+ if (delivery.status === "archived")
219
+ return _jsx("span", { className: "dl-pill", children: "Archiviato" });
220
+ if (expired)
221
+ return _jsx("span", { className: "dl-pill", children: "Scaduto" });
222
+ if (delivery.downloadCount > 0)
223
+ return _jsx("span", { className: "dl-pill is-downloaded", children: "Scaricato" });
224
+ return _jsx("span", { className: "dl-pill is-ready", children: "Attivo" });
225
+ }
226
+ /* ---- Formati ------------------------------------------------------------------------- */
227
+ const BARS = [30, 52, 38, 66, 44, 82, 58, 96, 70, 46, 62, 34];
228
+ const NUM = new Intl.NumberFormat("it-IT");
229
+ function int(n) { return NUM.format(Math.round(n)); }
230
+ function size(bytes) {
231
+ if (!bytes)
232
+ return "0 B";
233
+ const units = ["B", "kB", "MB", "GB", "TB"];
234
+ const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
235
+ const v = bytes / 1024 ** i;
236
+ return `${v.toLocaleString("it-IT", { maximumFractionDigits: v < 10 && i > 0 ? 1 : 0 })} ${units[i]}`;
237
+ }
238
+ /* Umami manda il tempo TOTALE in secondi, non la media: da solo non dice niente, perché
239
+ cresce col traffico. Diviso per le sessioni diventa «quanto si è fermato uno», che è la
240
+ domanda — e la stessa divisione va fatta sul periodo precedente, altrimenti la variazione
241
+ confronta due cose diverse. Senza il dato di prima, `prev` resta null e la pillola non
242
+ compare: meglio niente che una percentuale che parla d'altro. */
243
+ function averageTime(totals) {
244
+ if (!totals || !totals.visits.value)
245
+ return undefined;
246
+ const prev = totals.totaltime.prev !== null && totals.visits.prev
247
+ ? totals.totaltime.prev / totals.visits.prev
248
+ : null;
249
+ return { value: totals.totaltime.value / totals.visits.value, prev };
250
+ }
251
+ function seconds(value) {
252
+ const s = Math.round(value);
253
+ return s >= 60 ? `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s` : `${s}s`;
254
+ }
255
+ /* Gli estremi si scrivono con un segno, non arrotondati. Uno «0%» accanto a 7 MB di fotografie
256
+ vere direbbe che non c'è niente, e la differenza fra "poco" e "niente" è tutta la ragione
257
+ della riga; un «100%» sulla riga accanto direbbe che le altre due non esistono — e le tre
258
+ percentuali arrotondate farebbero 101. */
259
+ function percent(share) {
260
+ if (share > 0 && share < 1)
261
+ return "<1%";
262
+ if (share > 99 && share < 100)
263
+ return ">99%";
264
+ return `${share.toLocaleString("it-IT", { maximumFractionDigits: share < 10 ? 1 : 0 })}%`;
265
+ }
266
+ function day(iso) {
267
+ return new Date(iso).toLocaleDateString("it-IT", { day: "numeric", month: "short" });
268
+ }
269
+ /* Giorno e ora di un appuntamento. «oggi»/«domani» invece della data quando capita: è come se
270
+ lo direbbe una persona, ed è anche l'unica distinzione che conta guardando un elenco di
271
+ cose in arrivo. */
272
+ function slot(iso) {
273
+ const d = new Date(iso);
274
+ const hhmm = d.toLocaleTimeString("it-IT", { hour: "2-digit", minute: "2-digit" });
275
+ const today = new Date();
276
+ const sameDay = (a, b) => a.toDateString() === b.toDateString();
277
+ const tomorrow = new Date(today.getTime() + 86_400_000);
278
+ if (sameDay(d, today))
279
+ return `oggi · ${hhmm}`;
280
+ if (sameDay(d, tomorrow))
281
+ return `domani · ${hhmm}`;
282
+ return `${d.toLocaleDateString("it-IT", { day: "numeric", month: "short" })} · ${hhmm}`;
283
+ }
284
+ function when(iso) {
285
+ const d = new Date(iso);
286
+ const hours = (Date.now() - d.getTime()) / 3_600_000;
287
+ if (hours < 1)
288
+ return "poco fa";
289
+ if (hours < 24)
290
+ return `${Math.floor(hours)} ore fa`;
291
+ if (hours < 48)
292
+ return "ieri";
293
+ return d.toLocaleDateString("it-IT", { day: "numeric", month: "long" });
294
+ }
295
+ /* Umami dà "2026-07-28 00:00:00": non è ISO (manca la T) e Safari lo rifiuta, quindi la
296
+ sostituzione non è cosmetica. */
297
+ function parse(x) { return new Date(x.replace(" ", "T")); }
298
+ function axisLabel(x, unit) {
299
+ const d = parse(x);
300
+ return unit === "hour"
301
+ ? d.toLocaleTimeString("it-IT", { hour: "2-digit" })
302
+ : d.toLocaleDateString("it-IT", { weekday: "short" });
303
+ }
304
+ function fullLabel(x, unit) {
305
+ const d = parse(x);
306
+ return unit === "hour"
307
+ ? d.toLocaleString("it-IT", { hour: "2-digit", minute: "2-digit" })
308
+ : d.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" });
309
+ }
310
+ /* Con trenta giorni le etichette si sovrappongono: se ne stampa una ogni tot, tenendo sempre
311
+ la prima e l'ultima — sono le due che dicono qual è il periodo. */
312
+ function tickEvery(total, i) {
313
+ const step = Math.ceil(total / 8);
314
+ return i === 0 || i === total - 1 || i % step === 0;
315
+ }
316
+ function initials(name) {
317
+ return name.trim().split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
318
+ }
@@ -0,0 +1,12 @@
1
+ import type { PageModel } from "../lib/pages";
2
+ export declare function NewPageButton({ pages, onCreate }: {
3
+ pages: PageModel[];
4
+ onCreate: (p: PageModel) => void;
5
+ }): import("react").JSX.Element;
6
+ export declare function RowMenu({ page, pages, onChange, onRemove, onDuplicate, }: {
7
+ page: PageModel;
8
+ pages: PageModel[];
9
+ onChange: (next: PageModel) => void;
10
+ onRemove: () => void;
11
+ onDuplicate: () => void;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Creazione, rinomina, duplicazione, eliminazione.
3
+ *
4
+ * Tutte e quattro sono operazioni sull'ELENCO, non chiamate al server: diventano vere al
5
+ * salvataggio, in un solo commit, come tutto il resto. È il contratto che pages.js ha sempre
6
+ * avuto — riceve l'elenco desiderato e riconcilia — e conviene non romperlo: un'eliminazione
7
+ * immediata sarebbe un commit a sé, non annullabile, e scollegata dal resto delle modifiche.
8
+ *
9
+ * `_orig` è il meccanismo dei rename: dice al server da quale cartella partire per conservare
10
+ * corpo e campi non gestiti. Vale null per le pagine nuove — non c'è niente da conservare.
11
+ *
12
+ * Il menu e la rinomina sono due POPOVER ancorati alla riga, non sostituzioni del suo contenuto:
13
+ * in una colonna larga 270px la riga non ha spazio per diventare un modulo, e vedere la riga
14
+ * scomparire mentre la si rinomina fa perdere il segno di dove si era.
15
+ */
16
+ import { useMemo, useState } from "react";
17
+ import { ui } from "../../index.js";
18
+ import { site } from "../site.js";
19
+ import { slugify, uniqueSlug } from "../lib/slug";
20
+ import { Icon } from "../ui/icons";
21
+ /* I tipi che si possono creare vengono DALLO SCHEMA — sono le stesse opzioni che il form della
22
+ * pagina mostra alla voce "Tipo" (`Page.type`, registro `ui`). Qui c'era un elenco scritto a
23
+ * mano, ed era già indietro di due tipi: mancavano `portfolio` e `workshop`, quindi dall'admin
24
+ * si potevano modificare ma non creare — con l'unico rimedio di crearne una di un altro tipo e
25
+ * cambiarlo dopo dal form. È esattamente la divergenza che i due registri esistono per togliere
26
+ * (vedi l'intestazione di src/registries.ts): un tipo nuovo si dichiara una volta nello schema
27
+ * del sito e compare da solo anche qui.
28
+ *
29
+ * ⚠️ DENTRO il componente, non come costante di modulo. Lo schema ora arriva dal sito che monta
30
+ * il kit (`site()`), e un `const` di modulo verrebbe calcolato all'IMPORT — cioè prima che
31
+ * `mountAdmin` abbia impostato la configurazione, con un'eccezione al caricamento del pannello.
32
+ * Quando era un import diretto dallo schema il valore era disponibile subito e la costante di
33
+ * modulo andava bene: è il genere di cosa che cambia quando una dipendenza smette di essere
34
+ * statica e diventa iniettata. */
35
+ function tipiDisponibili() {
36
+ const tipi = ui.get(site().page.type)?.options ?? [];
37
+ /* La preselezione: "Pagina" se c'è (è la scelta di gran lunga più frequente), altrimenti la
38
+ * prima opzione — così questo elenco non può restare senza un valore valido. */
39
+ const iniziale = tipi.some((t) => t.value === "page") ? "page" : (tipi[0]?.value ?? "page");
40
+ return { tipi, iniziale };
41
+ }
42
+ export function NewPageButton({ pages, onCreate }) {
43
+ const { tipi: TIPI, iniziale: TIPO_INIZIALE } = useMemo(tipiDisponibili, []);
44
+ const [open, setOpen] = useState(false);
45
+ const [title, setTitle] = useState("");
46
+ const [type, setType] = useState(TIPO_INIZIALE);
47
+ const create = () => {
48
+ const t = title.trim();
49
+ if (!t)
50
+ return;
51
+ onCreate({
52
+ slug: uniqueSlug(t, pages.map((p) => p.slug)),
53
+ title: t,
54
+ type,
55
+ weight: 0, // lo ricalcola positionWeight dalla posizione
56
+ published: true,
57
+ isHome: false,
58
+ inMenu: false,
59
+ r2Prefix: "",
60
+ _orig: null, // pagina nuova: nessuna sorgente da cui conservare
61
+ });
62
+ setTitle("");
63
+ setType(TIPO_INIZIALE);
64
+ setOpen(false);
65
+ };
66
+ if (!open) {
67
+ return (_jsxs("button", { className: "ak-btn ak-btn-primary ak-btn-block", onClick: () => setOpen(true), children: [_jsx(Icon.plus, {}), " Nuova pagina"] }));
68
+ }
69
+ return (_jsxs("div", { className: "pa-new", children: [_jsx("input", { className: "ak-input", autoFocus: true, placeholder: "Titolo della pagina", value: title, onChange: (e) => setTitle(e.target.value), onKeyDown: (e) => { if (e.key === "Enter")
70
+ create(); if (e.key === "Escape")
71
+ setOpen(false); } }), _jsx("select", { className: "ak-input", value: type, onChange: (e) => setType(e.target.value), children: TIPI.map((t) => _jsx("option", { value: t.value, children: t.label }, t.value)) }), title.trim() && (_jsxs("span", { className: "ak-hint", children: ["indirizzo: /", uniqueSlug(title, pages.map((p) => p.slug)), "/"] })), _jsxs("div", { className: "pa-row", children: [_jsx("button", { className: "ak-btn ak-btn-primary", onClick: create, disabled: !title.trim(), children: "Crea" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setOpen(false), children: "Annulla" })] })] }));
72
+ }
73
+ export function RowMenu({ page, pages, onChange, onRemove, onDuplicate, }) {
74
+ const [open, setOpen] = useState(false);
75
+ const [renaming, setRenaming] = useState(false);
76
+ const [title, setTitle] = useState(String(page.title ?? ""));
77
+ const taken = pages.filter((p) => p.slug !== page.slug).map((p) => p.slug);
78
+ const nuovo = slugify(title);
79
+ const cambia = nuovo !== page.slug;
80
+ return (_jsxs("span", { className: "pa", children: [_jsx("button", { className: "pa-more", onClick: () => { setOpen(!open); setRenaming(false); }, title: "Altro", children: _jsx(Icon.more, {}) }), open && !renaming && (_jsxs("div", { className: "pa-menu", onMouseLeave: () => setOpen(false), children: [_jsx("button", { onClick: () => { setTitle(String(page.title ?? "")); setRenaming(true); }, children: "Rinomina\u2026" }), _jsx("button", { onClick: () => { onChange({ ...page, inMenu: !page.inMenu }); setOpen(false); }, children: page.inMenu ? "Togli dal menu" : "Aggiungi al menu" }), _jsx("button", { onClick: () => { onChange({ ...page, published: page.published === false }); setOpen(false); }, children: page.published === false ? "Pubblica" : "Nascondi" }), _jsx("button", { onClick: () => { onDuplicate(); setOpen(false); }, children: "Duplica" }), _jsx("button", { className: "pa-danger", onClick: () => {
81
+ if (confirm(`Eliminare "${page.title}"?\n\nLa pagina e il suo testo spariscono dal sito al prossimo salvataggio. ` +
82
+ `Le foto restano nel bucket.`))
83
+ onRemove();
84
+ setOpen(false);
85
+ }, children: "Elimina\u2026" })] })), open && renaming && (_jsxs("div", { className: "pa-menu pa-menu-form", children: [_jsx("input", { className: "ak-input", autoFocus: true, value: title, onChange: (e) => setTitle(e.target.value), onKeyDown: (e) => { if (e.key === "Escape") {
86
+ setRenaming(false);
87
+ setOpen(false);
88
+ } } }), cambia && (_jsxs("span", { className: "ak-hint", children: ["l'indirizzo diventa /", uniqueSlug(title, taken), "/ \u2014 i vecchi link non funzioneranno pi\u00F9"] })), _jsxs("div", { className: "pa-row", children: [_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => {
89
+ onChange({ ...page, title: title.trim(), slug: cambia ? uniqueSlug(title, taken) : page.slug });
90
+ setRenaming(false);
91
+ setOpen(false);
92
+ }, children: "Rinomina" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => { setRenaming(false); setOpen(false); }, children: "Annulla" })] })] }))] }));
93
+ }
@@ -0,0 +1,23 @@
1
+ import type { AssetFile } from "../../index.js";
2
+ import type { PageModel } from "../lib/pages";
3
+ export declare function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, }: {
4
+ page: PageModel;
5
+ pages: PageModel[];
6
+ onChange: (next: PageModel) => void;
7
+ seoActive: boolean;
8
+ onSeoToggle: () => void;
9
+ }): import("react").JSX.Element;
10
+ export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, }: {
11
+ page: PageModel;
12
+ onChange: (next: PageModel) => void;
13
+ /** Sostituisce la pagina con quella letta dal repo, senza contare come modifica dell'utente. */
14
+ onRefresh: (next: PageModel) => void;
15
+ /** Falso se in questa sessione la pagina è già stata modificata: allora non si rilegge. */
16
+ pristine: boolean;
17
+ pickMedia?: (o: {
18
+ multiple: boolean;
19
+ }) => Promise<string[] | null>;
20
+ pickMediaFiles?: (o: {
21
+ multiple: boolean;
22
+ }) => Promise<AssetFile[] | null>;
23
+ }): import("react").JSX.Element;