@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,124 @@
1
+ /* LO SMISTATORE: l'ingresso `./functions` del kit.
2
+ *
3
+ * Cloudflare Pages instrada le Functions PER FILE, e un pacchetto npm non può mettere file
4
+ * dentro `functions/`. Quindi il sito ne tiene due, entrambi da una riga — un acchiappatutto su
5
+ * `/admin/api/*` e il controllo d'accesso — e tutto il resto vive qui.
6
+ *
7
+ * functions/admin/api/[[path]].js -> handleAdminApi(context, config)
8
+ * functions/admin/_middleware.js -> adminAccess
9
+ *
10
+ * L'acchiappatutto è ristretto a `…/api/*` di proposito: uno un livello più in alto passerebbe
11
+ * per lo smistatore anche i file statici del pannello. Restano invece sotto il middleware — che
12
+ * è giusto, perché l'HTML e il JS del pannello devono stare dietro Access esattamente come le
13
+ * sue API (verificato: senza le variabili di Access, anche `/admin/index.html` risponde 503).
14
+ *
15
+ * COSA QUESTO SOSTITUISCE, e cosa no. Rifà la corrispondenza fra percorso e gestore, che è
16
+ * l'unica cosa che serviva. NON rifà i segmenti variabili (`[id].js`): oggi nessuna rotta ne ha
17
+ * — l'unica che li usava era Prenotazioni, che in v1.0 resta al sito. Aggiungerli adesso
18
+ * sarebbe macchinario per un caso che non esiste; si aggiungono quando arriva.
19
+ *
20
+ * ⚠️ Un sito può tenersi endpoint PROPRI accanto a questi: Cloudflare instrada prima le rotte
21
+ * più specifiche, quindi un `functions/admin/api/booking/index.js` del sito vince
22
+ * sull'acchiappatutto (verificato, non dedotto). È il motivo per cui Prenotazioni può restare
23
+ * dov'è senza inventare nessun punto di innesto.
24
+ */
25
+ import { setServerConfig, moduleEnabled } from "./site.js";
26
+
27
+ import * as analytics from "./api/analytics.js";
28
+ import * as assets from "./api/assets.js";
29
+ import * as assetsDelete from "./api/assets/delete.js";
30
+ import * as assetsFolder from "./api/assets/folder.js";
31
+ import * as assetsFolders from "./api/assets/folders.js";
32
+ import * as assetsMove from "./api/assets/move.js";
33
+ import * as assetsPreview from "./api/assets/preview.js";
34
+ import * as assetsTrash from "./api/assets/trash.js";
35
+ import * as assetsTrashDelete from "./api/assets/trash/delete.js";
36
+ import * as assetsTrashEmpty from "./api/assets/trash/empty.js";
37
+ import * as assetsTrashRestore from "./api/assets/trash/restore.js";
38
+ import * as assetsUpload from "./api/assets/upload.js";
39
+ import * as assetsUsage from "./api/assets/usage.js";
40
+ import * as deliveries from "./api/deliveries.js";
41
+ import * as deliveryHero from "./api/delivery/hero.js";
42
+ import * as deliveryImage from "./api/delivery/image.js";
43
+ import * as deliveryMultipart from "./api/delivery/multipart.js";
44
+ import * as pages from "./api/pages.js";
45
+ import * as pagesGet from "./api/pages/get.js";
46
+ import * as publish from "./api/publish.js";
47
+ import * as requests from "./api/requests.js";
48
+ import * as settings from "./api/settings.js";
49
+ import * as singleton from "./api/singleton.js";
50
+ import * as storage from "./api/storage.js";
51
+
52
+ export { adminAccess } from "./_middleware.js";
53
+ export { setServerConfig } from "./site.js";
54
+
55
+ /* La tabella. `module` dice a quale interruttore appartiene una rotta: `null` = sempre presente.
56
+ *
57
+ * Oggi l'unico modulo che spegne le proprie rotte è Consegne, ed è deliberato: spegnerne altre
58
+ * cambierebbe come si comporta un pannello già in uso, e la v1.0 ha una lista ENUMERATA di
59
+ * cambiamenti di comportamento a cui attenersi. Gli altri interruttori arrivano quando c'è modo
60
+ * di verificarli uno per uno. */
61
+ const ROUTES = {
62
+ "analytics": { mod: null, h: analytics },
63
+ "assets": { mod: null, h: assets },
64
+ "assets/delete": { mod: null, h: assetsDelete },
65
+ "assets/folder": { mod: null, h: assetsFolder },
66
+ "assets/folders": { mod: null, h: assetsFolders },
67
+ "assets/move": { mod: null, h: assetsMove },
68
+ "assets/preview": { mod: null, h: assetsPreview },
69
+ "assets/trash": { mod: null, h: assetsTrash },
70
+ "assets/trash/delete": { mod: null, h: assetsTrashDelete },
71
+ "assets/trash/empty": { mod: null, h: assetsTrashEmpty },
72
+ "assets/trash/restore": { mod: null, h: assetsTrashRestore },
73
+ "assets/upload": { mod: null, h: assetsUpload },
74
+ "assets/usage": { mod: null, h: assetsUsage },
75
+ "deliveries": { mod: "delivery", h: deliveries },
76
+ "delivery/hero": { mod: "delivery", h: deliveryHero },
77
+ "delivery/image": { mod: "delivery", h: deliveryImage },
78
+ "delivery/multipart": { mod: "delivery", h: deliveryMultipart },
79
+ "pages": { mod: null, h: pages },
80
+ "pages/get": { mod: null, h: pagesGet },
81
+ "publish": { mod: null, h: publish },
82
+ "requests": { mod: null, h: requests },
83
+ "settings": { mod: null, h: settings },
84
+ "singleton": { mod: null, h: singleton },
85
+ "storage": { mod: null, h: storage },
86
+ };
87
+
88
+ const PREFIX = "/admin/api/";
89
+
90
+ function json(data, status) {
91
+ return new Response(JSON.stringify(data), {
92
+ status,
93
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
94
+ });
95
+ }
96
+
97
+ export async function handleAdminApi(context, config) {
98
+ setServerConfig(config);
99
+
100
+ const url = new URL(context.request.url);
101
+ if (!url.pathname.startsWith(PREFIX)) return json({ ok: false, error: "rotta fuori da /admin/api/" }, 404);
102
+
103
+ /* Le barre di troppo e finali non fanno una rotta diversa: `/assets/` e `/assets` sono la
104
+ * stessa cosa per chi la scrive, e farle divergere produce un 404 che sembra un guasto. */
105
+ const path = url.pathname.slice(PREFIX.length).split("/").filter(Boolean).join("/");
106
+ const route = ROUTES[path];
107
+ if (!route) return json({ ok: false, error: "rotta sconosciuta" }, 404);
108
+
109
+ /* Un modulo spento risponde 404, non 403: dall'esterno quella funzione non esiste, e un 403
110
+ * racconterebbe che esiste ma è vietata. */
111
+ if (route.mod && !moduleEnabled(route.mod)) {
112
+ return json({ ok: false, error: `Funzione "${route.mod}" non attiva.`, code: "feature_inactive" }, 404);
113
+ }
114
+
115
+ const method = context.request.method.toUpperCase();
116
+ const handler = route.h[`onRequest${method.charAt(0)}${method.slice(1).toLowerCase()}`] ?? route.h.onRequest;
117
+ if (!handler) {
118
+ /* 405 e non 404: la rotta c'è, è il verbo a non esserci. La differenza conta quando si
119
+ * cerca il guasto — «non esiste» e «non così» portano a cercare in due posti diversi. */
120
+ return json({ ok: false, error: "metodo non ammesso" }, 405);
121
+ }
122
+
123
+ return handler({ ...context, config });
124
+ }
@@ -0,0 +1,38 @@
1
+ /* LA GIUNTURA lato Worker: ciò che gli endpoint non possono sapere da soli.
2
+ *
3
+ * Gemella di `src/app/site.ts`, che fa la stessa cosa per l'interfaccia. Sono due perché sono
4
+ * due mondi diversi — uno gira nel browser e conosce React, l'altro gira in un Worker e non
5
+ * deve nemmeno vederlo — ma il sito le riempie **dalla stessa dichiarazione**: `site.config`
6
+ * è dati puri, quindi lo importano entrambi senza che React finisca dentro il Worker.
7
+ *
8
+ * Prima queste cose arrivavano con un `import` diretto dallo schema del cliente (e, in un caso,
9
+ * con un `import` del suo `data/site.json` — cioè un percorso che usciva dal pacchetto, che in
10
+ * un pacchetto pubblicato semplicemente non esiste).
11
+ *
12
+ * ⚠️ Provvisoria quanto la sua gemella: in v1.1, con gli schemi per tipo, `page` diventa una
13
+ * mappa e metà di questi campi sparisce. Vedi l'intestazione di `src/app/site.ts`.
14
+ */
15
+
16
+ let current = null;
17
+
18
+ /** Chiamata dallo smistatore a ogni richiesta: il valore è costante per deploy, riscriverlo
19
+ * è idempotente e costa niente. */
20
+ export function setServerConfig(config) {
21
+ current = config;
22
+ }
23
+
24
+ export function site() {
25
+ if (!current) {
26
+ throw new Error(
27
+ "adminkit: configurazione del sito non impostata. " +
28
+ "Gli endpoint si servono passando da `handleAdminApi(context, config)`.",
29
+ );
30
+ }
31
+ return current;
32
+ }
33
+
34
+ /** Un modulo è acceso? Ciò che non è dichiarato è SPENTO: un interruttore dimenticato deve
35
+ * produrre una funzione assente, non una accesa per sbaglio. */
36
+ export function moduleEnabled(name) {
37
+ return current?.modules?.[name] === true;
38
+ }
@@ -0,0 +1,28 @@
1
+ export type GalleryImage = {
2
+ src: string;
3
+ w?: number | null;
4
+ h?: number | null;
5
+ hidden?: boolean;
6
+ };
7
+ export type SectionType = "header" | "text" | "image" | "gallery";
8
+ /** Una sezione della pagina gallery. Ha TUTTI i campi qualunque sia il tipo: Zod e React vogliono
9
+ * una forma stabile, e l'encoder emette poi solo quelli pertinenti (vedi `sections` più sotto). */
10
+ export interface PageSection {
11
+ id: string;
12
+ type: SectionType;
13
+ eyebrow: string;
14
+ heading: string;
15
+ lead: string;
16
+ paragraphs: string[];
17
+ image: string;
18
+ w: number | null;
19
+ h: number | null;
20
+ alt: string;
21
+ caption: string;
22
+ layout: "wide" | "contained";
23
+ images: GalleryImage[];
24
+ }
25
+ export declare const GALLERY_BLOCK_ID = "gallery";
26
+ /** Una sezione ai valori di riposo. Un posto solo in cui aggiungere un campo, invece di due
27
+ * (qui e nell'admin) che nessuno modifica insieme. */
28
+ export declare function emptySection(type: SectionType, id: string): PageSection;
@@ -0,0 +1,18 @@
1
+ /* L'id del blocco gallery quando non ce l'ha ancora: lo usano la migrazione qui sotto e l'admin,
2
+ * che il blocco lo mostra sempre anche quando il front matter non lo contiene.
3
+ * È COSTANTE, e non generato come gli altri (`Date.now()` + random): un id nuovo a ogni render
4
+ * darebbe a dnd-kit una chiave nuova ogni volta, la card verrebbe rimontata di continuo e
5
+ * trascinarla diventerebbe impossibile. */
6
+ export const GALLERY_BLOCK_ID = "gallery";
7
+ /** Una sezione ai valori di riposo. Un posto solo in cui aggiungere un campo, invece di due
8
+ * (qui e nell'admin) che nessuno modifica insieme. */
9
+ export function emptySection(type, id) {
10
+ return {
11
+ id, type,
12
+ eyebrow: "", heading: "", lead: "",
13
+ paragraphs: [],
14
+ image: "", w: null, h: null, alt: "", caption: "",
15
+ layout: "wide",
16
+ images: [],
17
+ };
18
+ }
@@ -0,0 +1,10 @@
1
+ export * from "./config.js";
2
+ export * from "./registries.js";
3
+ export * from "./frontmatter.js";
4
+ export * from "./yaml.js";
5
+ export * from "./serialize.js";
6
+ export * from "./model.js";
7
+ export * from "./api.js";
8
+ export * from "./prose.js";
9
+ export * from "./gallery.js";
10
+ export * from "./normalize.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./config.js";
2
+ export * from "./registries.js";
3
+ export * from "./frontmatter.js";
4
+ export * from "./yaml.js";
5
+ export * from "./serialize.js";
6
+ export * from "./model.js";
7
+ export * from "./api.js";
8
+ export * from "./prose.js";
9
+ export * from "./gallery.js";
10
+ export * from "./normalize.js";
@@ -0,0 +1,2 @@
1
+ import type { Shape, Model } from "./serialize.js";
2
+ export declare function modelFromRaw(raw: Record<string, unknown>, shape: Shape, extra?: Model): Model;
package/dist/model.js ADDED
@@ -0,0 +1,47 @@
1
+ /* Costruisce il modello di un elemento dal front matter GREZZO, senza passare dal markdown.
2
+ * È la via che serve al manifest: Hugo emette i parametri così come sono nel file, e la
3
+ * conversione — il flip visible/published, la presenza del blocco `menu` — la fa lo schema.
4
+ * È l'unico modo per avere quella conversione in un posto solo: i template Hugo non possono
5
+ * importare TypeScript, quindi devono smettere di conoscerla del tutto. */
6
+ import { fm } from "./registries.js";
7
+ export function modelFromRaw(raw, shape, extra = {}) {
8
+ const normalized = withSchemaCasing(raw, shape);
9
+ const model = { ...extra };
10
+ for (const [name, field] of Object.entries(shape)) {
11
+ const meta = fm.get(field);
12
+ if (!meta)
13
+ continue;
14
+ const v = meta.decode(normalized, { raw: normalized });
15
+ if (v !== undefined)
16
+ model[name] = v;
17
+ }
18
+ return model;
19
+ }
20
+ /* HUGO MINUSCOLIZZA LE CHIAVI DEI PARAMETRI. `r2Prefix` nel file diventa `r2prefix` in
21
+ * `.Params`, `isHome` diventa `ishome`. Le `decode` sono scritte con la grafia del FILE — che è
22
+ * quella giusta, ed è quella che vede il serializzatore — quindi leggendo dal manifest non
23
+ * troverebbero niente e restituirebbero un valore vuoto.
24
+ *
25
+ * Non è teorico: `r2Prefix` spariva da tutte e quattro le gallery. E `isHome` sembrava
26
+ * funzionare solo perché vale `false` ovunque — con una pagina a `true` avrebbe sbagliato in
27
+ * silenzio, che è il modo peggiore.
28
+ *
29
+ * Qui si rimettono le chiavi nella grafia dichiarata dallo schema. Solo per quelle dichiarate:
30
+ * non è una normalizzazione generale, è una riconciliazione fra due sorgenti che chiamano la
31
+ * stessa cosa in due modi. */
32
+ function withSchemaCasing(raw, shape) {
33
+ const lower = new Map();
34
+ for (const [k, v] of Object.entries(raw))
35
+ lower.set(k.toLowerCase(), v);
36
+ const out = { ...raw };
37
+ for (const field of Object.values(shape)) {
38
+ for (const key of fm.get(field)?.keys ?? []) {
39
+ if (Object.prototype.hasOwnProperty.call(out, key))
40
+ continue;
41
+ const hit = lower.get(key.toLowerCase());
42
+ if (hit !== undefined)
43
+ out[key] = hit;
44
+ }
45
+ }
46
+ return out;
47
+ }
@@ -0,0 +1,50 @@
1
+ export type Credit = {
2
+ label: string;
3
+ name: string;
4
+ href: string;
5
+ };
6
+ export declare function normalizeCredits(value: unknown): Credit[];
7
+ export type WorkCardType = "image" | "text" | "frosted";
8
+ export type WorkInfo = {
9
+ label: string;
10
+ value: string;
11
+ };
12
+ export type WorkCard = {
13
+ type: WorkCardType;
14
+ image: string;
15
+ alt: string;
16
+ kicker: string;
17
+ heading: string;
18
+ body: string[];
19
+ };
20
+ export type Work = {
21
+ title: string;
22
+ cover: string;
23
+ description: string[];
24
+ info: WorkInfo[];
25
+ cards: WorkCard[];
26
+ };
27
+ export declare const CARD_KINDS: {
28
+ type: WorkCardType;
29
+ label: string;
30
+ hint: string;
31
+ }[];
32
+ export declare function emptyWork(): Work;
33
+ export declare function emptyCard(type: WorkCardType): WorkCard;
34
+ export declare function normalizeWorks(value: unknown): Work[];
35
+ export type WorkshopBlock = {
36
+ heading: string;
37
+ body: string[];
38
+ };
39
+ export type Workshop = {
40
+ image: string;
41
+ kicker: string;
42
+ title: string;
43
+ lead: string;
44
+ blocks: WorkshopBlock[];
45
+ };
46
+ export declare function emptyWorkshop(): Workshop;
47
+ export declare function normalizeWorkshops(value: unknown): Workshop[];
48
+ import { type PageSection } from "./gallery.js";
49
+ export declare function normalizeSections(value: unknown): PageSection[];
50
+ export declare function withGalleryBlock(sections: PageSection[]): PageSection[];
@@ -0,0 +1,120 @@
1
+ /* I NORMALIZZATORI degli editor per tipo di pagina, e i tipi che portano con sé.
2
+ *
3
+ * Stavano dentro i rispettivi componenti React. Sono qui per due ragioni, e la seconda è quella
4
+ * che conta:
5
+ *
6
+ * 1. Sono logica pura — nessun JSX, nessuno stato, nessun hook — e stare dentro un file
7
+ * `.tsx` non le rendeva tali, le nascondeva soltanto.
8
+ *
9
+ * 2. SERVONO FUORI DALL'INTERFACCIA. Ogni sito deve poter verificare che normalizzare i
10
+ * PROPRI contenuti veri non li cambi: l'editor rimanda indietro la lista normalizzata, non
11
+ * quella che ha ricevuto, quindi se normalizzare alterasse anche un carattere, la prima
12
+ * modifica a un campo qualunque riscriverebbe in silenzio tutte le altre voci — e nel diff
13
+ * comparirebbero righe che nessuno ha toccato. Quella verifica gira in Node, dove React non
14
+ * c'è: importarla da un `.tsx` trascinerebbe l'intera interfaccia dentro una suite di test.
15
+ *
16
+ * Il modo in cui quella garanzia veniva ottenuta prima è istruttivo: il test del sito APRIVA il
17
+ * file `.tsx` dell'editor e ne ritagliava la funzione col testo, «perché una copia è un posto in
18
+ * più in cui divergere». Aveva ragione sul problema e non aveva alternative sulla soluzione.
19
+ * Con un pacchetto l'alternativa c'è, ed è questa.
20
+ */
21
+ const str = (value) => (typeof value === "string" ? value : "");
22
+ const list = (value) => (Array.isArray(value) ? value : []);
23
+ const obj = (value) => (value ?? {});
24
+ export function normalizeCredits(value) {
25
+ return list(value).map((raw) => {
26
+ const item = obj(raw);
27
+ return { label: str(item.label), name: str(item.name), href: str(item.href) };
28
+ });
29
+ }
30
+ export const CARD_KINDS = [
31
+ { type: "image", label: "Immagine", hint: "Una fotografia a tutta larghezza" },
32
+ { type: "text", label: "Testo", hint: "Titolo e paragrafi" },
33
+ { type: "frosted", label: "Testo su immagine", hint: "Parole sopra una fotografia" },
34
+ ];
35
+ export function emptyWork() {
36
+ return { title: "", cover: "", description: [], info: [], cards: [] };
37
+ }
38
+ /* Il tipo di una card nuova è SEMPRE esplicito, mai stringa vuota: un `select` mostra comunque
39
+ * la prima opzione quando il valore non combacia con nessuna, quindi una card salvata senza
40
+ * toccare il campo finirebbe nel file con `type: ""` — un valore che non corrisponde a nessun
41
+ * ramo del template, cioè una card vuota sul sito. È già successo. */
42
+ export function emptyCard(type) {
43
+ return { type, image: "", alt: "", kicker: "", heading: "", body: [] };
44
+ }
45
+ export function normalizeWorks(value) {
46
+ return list(value).map((raw) => {
47
+ const item = obj(raw);
48
+ return {
49
+ title: str(item.title),
50
+ cover: str(item.cover),
51
+ description: list(item.description).map(str).filter(Boolean),
52
+ info: list(item.info).map((entry) => {
53
+ const o = obj(entry);
54
+ return { label: str(o.label), value: str(o.value) };
55
+ }),
56
+ cards: list(item.cards).map((entry) => {
57
+ const o = obj(entry);
58
+ const type = String(o.type);
59
+ return {
60
+ ...emptyCard(CARD_KINDS.some((k) => k.type === type) ? type : "image"),
61
+ image: str(o.image),
62
+ alt: str(o.alt),
63
+ kicker: str(o.kicker),
64
+ heading: str(o.heading),
65
+ body: list(o.body).map(str),
66
+ };
67
+ }),
68
+ };
69
+ });
70
+ }
71
+ export function emptyWorkshop() {
72
+ return { image: "", kicker: "", title: "", lead: "", blocks: [] };
73
+ }
74
+ export function normalizeWorkshops(value) {
75
+ return list(value).map((raw) => {
76
+ const item = obj(raw);
77
+ return {
78
+ image: str(item.image),
79
+ kicker: str(item.kicker),
80
+ title: str(item.title),
81
+ lead: str(item.lead),
82
+ blocks: list(item.blocks).map((entry) => {
83
+ const block = obj(entry);
84
+ return { heading: str(block.heading), body: list(block.body).map(str) };
85
+ }),
86
+ };
87
+ });
88
+ }
89
+ /* ---- Gallery: le sezioni ------------------------------------------------------------------- */
90
+ import { emptySection, GALLERY_BLOCK_ID } from "./gallery.js";
91
+ export function normalizeSections(value) {
92
+ if (!Array.isArray(value))
93
+ return [];
94
+ return value
95
+ .map((raw, index) => {
96
+ const item = (raw ?? {});
97
+ if (!["header", "text", "image", "gallery"].includes(String(item.type)))
98
+ return null;
99
+ return {
100
+ ...emptySection(item.type, String(item.id || `section-${index + 1}`)),
101
+ ...item,
102
+ id: String(item.id || `section-${index + 1}`),
103
+ paragraphs: Array.isArray(item.paragraphs) ? item.paragraphs.map(String) : [],
104
+ w: Number(item.w) > 0 ? Number(item.w) : null,
105
+ h: Number(item.h) > 0 ? Number(item.h) : null,
106
+ layout: item.layout === "contained" ? "contained" : "wide",
107
+ images: Array.isArray(item.images) ? item.images : [],
108
+ };
109
+ })
110
+ .filter((item) => !!item);
111
+ }
112
+ /* Il blocco delle foto c'è sempre. Se il front matter non lo contiene ancora — una gallery mai
113
+ * salvata dal pannello, o una pagina appena diventata gallery — lo si aggiunge in coda, che è
114
+ * dove la griglia usciva prima che questo blocco esistesse. Vive solo nella vista finché
115
+ * l'utente non tocca qualcosa: aprire una pagina non deve sporcarla. */
116
+ export function withGalleryBlock(sections) {
117
+ return sections.some((section) => section.type === "gallery")
118
+ ? sections
119
+ : [...sections, emptySection("gallery", GALLERY_BLOCK_ID)];
120
+ }
@@ -0,0 +1,3 @@
1
+ /** Vero solo su un'anteprima locale. In produzione la scheda non va nemmeno emessa. */
2
+ export declare function isLocalPreview(): boolean;
3
+ export declare function Preview(): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Pannello "Anteprima": mostra dentro l'admin lo stato dell'anteprima locale gestita da Regia.
3
+ *
4
+ * Due cose da tenere a mente, perché sembrano mancanze e non lo sono:
5
+ *
6
+ * 1. Da qui si può solo FERMARE e RIAVVIARE. Non accendere: per vedere questa pagina il sito
7
+ * deve già girare, quindi l'accensione non può che vivere nel cruscotto di Regia. È un
8
+ * limite del mezzo, e va scritto nell'interfaccia o sembra una funzione dimenticata.
9
+ * 2. Regia è facoltativa. Se il demone non c'è il pannello lo dice e basta: niente errori
10
+ * rumorosi, niente riprove, e soprattutto nessuna dipendenza dell'admin da uno strumento
11
+ * che su un'altra macchina potrebbe non essere installato.
12
+ */
13
+ import { useCallback, useEffect, useState } from "react";
14
+ const DAEMON_PORT = 7391;
15
+ const DAEMON = `http://127.0.0.1:${DAEMON_PORT}`;
16
+ /** Vero solo su un'anteprima locale. In produzione la scheda non va nemmeno emessa. */
17
+ export function isLocalPreview() {
18
+ if (typeof location === "undefined")
19
+ return false;
20
+ return location.hostname === "localhost" || location.hostname === "127.0.0.1";
21
+ }
22
+ const STATE_TEXT = {
23
+ running: "attiva",
24
+ starting: "in avvio…",
25
+ stopped: "ferma",
26
+ crashed: "caduta",
27
+ failed: "avvio fallito",
28
+ };
29
+ export function Preview() {
30
+ const [status, setStatus] = useState({ kind: "loading" });
31
+ const [busy, setBusy] = useState(false);
32
+ const load = useCallback(async () => {
33
+ const port = location.port || (location.protocol === "https:" ? "443" : "80");
34
+ try {
35
+ const res = await fetch(`${DAEMON}/api/whoami?port=${encodeURIComponent(port)}`);
36
+ if (res.status === 404)
37
+ return setStatus({ kind: "unmanaged" });
38
+ if (!res.ok)
39
+ return setStatus({ kind: "absent" });
40
+ const data = (await res.json());
41
+ setStatus({ kind: "site", site: data.site, dashboard: data.dashboard });
42
+ }
43
+ catch {
44
+ // Il demone non risponde: non è un errore dell'admin, è uno strumento assente.
45
+ setStatus({ kind: "absent" });
46
+ }
47
+ }, []);
48
+ useEffect(() => {
49
+ void load();
50
+ }, [load]);
51
+ const act = async (action) => {
52
+ if (status.kind !== "site")
53
+ return;
54
+ if (action === "stop" && !confirm("Fermare l'anteprima? Questa pagina smetterà di rispondere."))
55
+ return;
56
+ setBusy(true);
57
+ try {
58
+ await fetch(`${DAEMON}/api/sites/${encodeURIComponent(status.site.id)}/${action}`, { method: "POST" });
59
+ }
60
+ catch {
61
+ /* se il riavvio uccide la connessione va bene così: si ricarica sotto */
62
+ }
63
+ setBusy(false);
64
+ if (action === "restart") {
65
+ // Dopo un riavvio il server torna su con qualche secondo di ritardo.
66
+ setTimeout(() => void load(), 2500);
67
+ }
68
+ else {
69
+ void load();
70
+ }
71
+ };
72
+ if (status.kind === "loading")
73
+ return _jsx("p", { className: "ak-hint", children: "Controllo\u2026" });
74
+ if (status.kind === "absent") {
75
+ return (_jsx("div", { className: "rg", children: _jsxs("p", { className: "ak-hint", children: ["Regia non \u00E8 in esecuzione, quindi questa anteprima non \u00E8 pilotabile da qui. Apri l'icona ", _jsx("strong", { children: "Regia" }), ", oppure il cruscotto: ", _jsxs("a", { href: `${DAEMON}/`, children: [DAEMON, "/"] })] }) }));
76
+ }
77
+ if (status.kind === "unmanaged") {
78
+ return (_jsx("div", { className: "rg", children: _jsx("p", { className: "ak-hint", children: "Questa anteprima non \u00E8 gestita da Regia: \u00E8 stata avviata in un altro modo, quindi va fermata dove \u00E8 stata accesa." }) }));
79
+ }
80
+ const { site, dashboard } = status;
81
+ return (_jsxs("div", { className: "rg", children: [_jsxs("header", { className: "rg-top", children: [_jsx("span", { className: `rg-dot rg-dot-${site.state}`, "aria-hidden": "true" }), _jsx("span", { className: "rg-name", children: site.name }), _jsxs("span", { className: "ak-hint", children: ["anteprima ", STATE_TEXT[site.state], " \u00B7 porta ", site.port] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void act("restart"), children: "Riavvia" }), _jsx("button", { className: "ak-btn", disabled: busy, onClick: () => void act("stop"), children: "Ferma" })] }), site.message && _jsx("p", { className: "ak-hint", children: site.message }), _jsxs("p", { className: "ak-hint", children: ["Da qui si pu\u00F2 solo fermare o riavviare: per accendere un'anteprima serve il cruscotto, ", _jsx("a", { href: dashboard, children: dashboard })] }), site.lan && site.urls.lan.length > 0 ? (_jsxs("div", { className: "rg-lan", children: [_jsxs("div", { className: "rg-lan-urls", children: [_jsxs("p", { className: "ak-hint", children: ["Dal telefono, sulla stessa rete \u2014 si vede ", _jsx("strong", { children: "il sito" }), ": questo pannello no, perch\u00E9 la via d'uscita di Access vale solo su un hostname locale."] }), site.urls.lan.map((l) => (_jsxs("div", { children: [_jsx("a", { href: l.url, children: l.url }), l.kind === "mdns" && _jsx("span", { className: "ak-hint", children: " \u2014 se il nome non risponde, usa l'indirizzo numerico" })] }, l.url)))] }), _jsx("img", { className: "rg-qr", alt: "Codice QR dell'indirizzo di rete", src: `${DAEMON}/api/sites/${encodeURIComponent(site.id)}/qr.svg?url=${encodeURIComponent(site.urls.lan[0].url)}` })] })) : (_jsx("p", { className: "ak-hint", children: "Questa anteprima \u00E8 visibile solo da questo Mac. Per aprirla al telefono, accendi \u00ABSulla rete locale\u00BB nel cruscotto." }))] }));
82
+ }
@@ -0,0 +1,17 @@
1
+ /** Un testo e la porzione selezionata dentro di lui. */
2
+ export interface Selection {
3
+ text: string;
4
+ start: number;
5
+ end: number;
6
+ }
7
+ /** Il markdown come lo mostra il campo: un `<p>` per paragrafo, `<br>` per l'a capo singolo. */
8
+ export declare function mdToHtml(md: string): string;
9
+ /** Gli indirizzi che si possono scrivere in un collegamento.
10
+ *
11
+ * `javascript:` è la ragione per cui questo controllo esiste: il markdown dell'admin finisce in
12
+ * pagine renderizzate con `unsafe = true`, quindi un indirizzo non è un dato inerte. Si
13
+ * accettano gli schemi che servono e i percorsi interni; tutto il resto è rifiutato invece di
14
+ * essere ripulito, perché "ripulire" un indirizzo ostile è un gioco che si perde. */
15
+ export declare function isSafeHref(href: string): boolean;
16
+ /** Il testo del campo com'è da salvare: markdown. */
17
+ export declare function htmlToMd(html: string): string;