@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,18 @@
1
+ export declare function OpenCard({ id, index, total, open, thumb, title, empty, meta, kind, onToggle, actions, children, }: {
2
+ id: string;
3
+ index: number;
4
+ total: number;
5
+ open: boolean;
6
+ /** L'immagine della miniatura. Vuota: al suo posto l'icona del segnaposto. */
7
+ thumb: string;
8
+ title: string;
9
+ /** Cosa scrivere quando il titolo non c'è ancora. */
10
+ empty: string;
11
+ /** La riga sotto il titolo: quanto contenuto c'è, o dove e quando. */
12
+ meta?: string;
13
+ /** Come si chiama questa cosa, per chi naviga a tastiera o con un lettore di schermo. */
14
+ kind: string;
15
+ onToggle: () => void;
16
+ actions?: React.ReactNode;
17
+ children?: React.ReactNode;
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Una card di elenco che si apre: maniglia, miniatura, titolo, e il contenuto dentro.
3
+ *
4
+ * È il guscio dei lavori del portfolio e delle edizioni dei workshop. Sono la stessa cosa a
5
+ * guardarle — una COSA con una sua immagine e un suo nome, che si riconosce dall'elenco e si
6
+ * apre per modificarla — e finché il guscio stava dentro l'editor dei lavori la seconda pagina
7
+ * l'avrebbe ricopiato. Qui dentro non c'è niente che sappia cosa siano un lavoro o un'edizione:
8
+ * riceve cosa mostrare e cosa fare, il resto lo mette chi la usa.
9
+ *
10
+ * Una alla volta la decide chi possiede l'elenco (`openAt`): con più card aperte insieme
11
+ * l'elenco perde la sua funzione, che è far vedere l'ordine — e l'ordine è il contenuto della
12
+ * pagina, non un dettaglio.
13
+ */
14
+ import { useSortable } from "@dnd-kit/sortable";
15
+ import { CSS } from "@dnd-kit/utilities";
16
+ import { Icon } from "./icons";
17
+ export function OpenCard({ id, index, total, open, thumb, title, empty, meta, kind, onToggle, actions, children, }) {
18
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
19
+ return (_jsxs("article", { ref: setNodeRef, className: ["pw-work", open ? "is-open" : "", isDragging ? "is-dragging" : ""].filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("div", { className: "pw-work-head", children: [_jsx("button", { type: "button", className: "pw-grip", "aria-label": `Riordina ${title || empty.toLowerCase()}, posizione ${index + 1} di ${total}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsxs("button", { type: "button", className: "pw-work-open", "aria-expanded": open, onClick: onToggle, children: [_jsx("span", { className: "pw-thumb", children: thumb ? _jsx("img", { src: thumb, alt: "" }) : _jsx("span", { className: "pw-thumb-empty", children: _jsx(Icon.media, {}) }) }), _jsxs("span", { className: "pw-work-title", children: [_jsx("strong", { className: title ? "" : "is-empty", children: title || empty }), !!meta && _jsx("small", { children: meta })] }), _jsx("span", { className: "pw-chevron", "aria-hidden": "true", children: _jsx(Icon.chevron, {}) })] }), _jsx("div", { className: "pw-work-actions", "aria-label": `Azioni su ${kind}`, children: actions })] }), open && _jsx("div", { className: "pw-work-body", children: children })] }));
20
+ }
@@ -0,0 +1,8 @@
1
+ export declare const toParagraphs: (value: string) => string[];
2
+ export declare function ParagraphEditor({ value, onChange, rows, placeholder, className, }: {
3
+ value: string[];
4
+ onChange: (next: string[]) => void;
5
+ rows?: number;
6
+ placeholder?: string;
7
+ className?: string;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* Un testo su più capoversi, scritto come si scrive un testo.
3
+ *
4
+ * Il modello sotto è un ARRAY di paragrafi — nel front matter una stringa non può contenere un
5
+ * a capo (`yamlStr` si ferma apposta, vedi ADMIN.md §6), quindi una descrizione su più
6
+ * capoversi non sarebbe salvabile come stringa unica. Chi scrive però non deve saperlo: qui
7
+ * batte una riga vuota, come ovunque.
8
+ *
9
+ * Lo stato locale è la ragione d'essere del componente. Il valore di fuori è già spezzato e
10
+ * ripulito, quindi rileggerlo a ogni tasto cancellerebbe la riga vuota appena battuta —
11
+ * `toParagraphs` la scarta — e sarebbe impossibile andare a capo due volte. Il testo locale si
12
+ * risincronizza solo quando quello di fuori è DAVVERO un altro (confronto fra forme
13
+ * normalizzate), cioè quando a cambiarlo è stato qualcun altro: un annulla, o l'apertura di un
14
+ * altro elemento.
15
+ *
16
+ * Vive in `ui/` e non dentro un editor perché lo usano sia le sezioni delle gallery sia la
17
+ * descrizione dei lavori del portfolio. Era in `GallerySectionsEditor.tsx`: la seconda copia
18
+ * sarebbe stata la prima occasione per farle divergere.
19
+ */
20
+ import { useEffect, useState } from "react";
21
+ /* La textarea è `ProseField`: sopra la selezione compare la barra con grassetto, corsivo e
22
+ collegamento. Il valore che entra e che esce è la stessa stringa di prima — la barra scrive
23
+ markdown nel testo, non introduce una seconda rappresentazione. */
24
+ import { ProseField } from "../../form/index.js";
25
+ export const toParagraphs = (value) => value.split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
26
+ export function ParagraphEditor({ value, onChange, rows = 8, placeholder, className, }) {
27
+ const [text, setText] = useState(() => value.join("\n\n"));
28
+ useEffect(() => {
29
+ const outside = value.join("\n\n");
30
+ if (toParagraphs(text).join("\n\n") !== outside)
31
+ setText(outside);
32
+ }, [value]);
33
+ return (_jsx(ProseField, { className: className, rows: rows, value: text, placeholder: placeholder ?? "Scrivi il testo della sezione.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (next) => {
34
+ setText(next);
35
+ onChange(toParagraphs(next));
36
+ } }));
37
+ }
@@ -0,0 +1 @@
1
+ export declare function useDragSensors(): import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
@@ -0,0 +1,11 @@
1
+ /* I sensori del trascinamento, uguali in ogni elenco del pannello.
2
+ *
3
+ * I sei pixel di soglia non sono una preferenza: senza, il click su una maniglia per aprire una
4
+ * card verrebbe interpretato come l'inizio di un trascinamento. Il sensore da tastiera è quello
5
+ * che tiene gli elenchi riordinabili anche senza mouse.
6
+ */
7
+ import { KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
8
+ import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
9
+ export function useDragSensors() {
10
+ return useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
11
+ }
@@ -0,0 +1,26 @@
1
+ export declare const Icon: {
2
+ grid: () => import("react").JSX.Element;
3
+ home: () => import("react").JSX.Element;
4
+ pages: () => import("react").JSX.Element;
5
+ media: () => import("react").JSX.Element;
6
+ delivery: () => import("react").JSX.Element;
7
+ calendar: () => import("react").JSX.Element;
8
+ settings: () => import("react").JSX.Element;
9
+ mail: () => import("react").JSX.Element;
10
+ play: () => import("react").JSX.Element;
11
+ out: () => import("react").JSX.Element;
12
+ trash: () => import("react").JSX.Element;
13
+ gallery: () => import("react").JSX.Element;
14
+ doc: () => import("react").JSX.Element;
15
+ grip: () => import("react").JSX.Element;
16
+ more: () => import("react").JSX.Element;
17
+ plus: () => import("react").JSX.Element;
18
+ chevron: () => import("react").JSX.Element;
19
+ replace: () => import("react").JSX.Element;
20
+ search: () => import("react").JSX.Element;
21
+ eye: () => import("react").JSX.Element;
22
+ eyeOff: () => import("react").JSX.Element;
23
+ archive: () => import("react").JSX.Element;
24
+ sun: () => import("react").JSX.Element;
25
+ moon: () => import("react").JSX.Element;
26
+ };
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Icone della navigazione. Disegnate qui e non prese da una libreria: sono otto tratti, e una
3
+ * dipendenza di icone porta con sé un albero di componenti, un tema e un altro modo di dire
4
+ * "colore" che non passa dalle custom property.
5
+ *
6
+ * Tutte 16×16, tratto 1.5, `currentColor`: ereditano il colore della voce che le contiene, così
7
+ * lo stato attivo si scrive una volta sola nel CSS. */
8
+ const P = { fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
9
+ function Svg({ children }) {
10
+ return _jsx("svg", { className: "ico", viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", children: children });
11
+ }
12
+ export const Icon = {
13
+ /* La Panoramica: quattro riquadri di dimensioni diverse, cioè il bento che si trova
14
+ entrando. Un grafico o un cruscotto direbbero "statistiche", che è solo un pezzo. */
15
+ grid: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "2.5", width: "5", height: "6.5", rx: "1" }), _jsx("rect", { ...P, x: "9", y: "2.5", width: "4.5", height: "4", rx: "1" }), _jsx("rect", { ...P, x: "2.5", y: "10.5", width: "5", height: "3", rx: "1" }), _jsx("rect", { ...P, x: "9", y: "8", width: "4.5", height: "5.5", rx: "1" })] }),
16
+ home: () => _jsx(Svg, { children: _jsx("path", { ...P, d: "M2.5 6.8 8 2.5l5.5 4.3V13a.5.5 0 0 1-.5.5h-3v-4h-4v4H3a.5.5 0 0 1-.5-.5V6.8Z" }) }),
17
+ pages: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M3.5 2.5h6l3 3v8h-9v-11Z" }), _jsx("path", { ...P, d: "M9.5 2.5v3h3" })] }),
18
+ media: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "9", rx: "1" }), _jsx("path", { ...P, d: "m2.5 10 3-2.5 3 2.5 2-1.5 3 2.5" }), _jsx("circle", { ...P, cx: "6", cy: "6", r: "1" })] }),
19
+ delivery: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M8 2.5v7" }), _jsx("path", { ...P, d: "m5.5 7 2.5 2.5L10.5 7" }), _jsx("path", { ...P, d: "M3 11v2.5h10V11" })] }),
20
+ calendar: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "10", rx: "1" }), _jsx("path", { ...P, d: "M5 2.5v2M11 2.5v2M2.5 6.5h11" }), _jsx("path", { ...P, d: "M5.2 9h.1M8 9h.1M10.8 9h.1M5.2 11.3h.1M8 11.3h.1" })] }),
21
+ settings: () => _jsxs(Svg, { children: [_jsx("circle", { ...P, cx: "8", cy: "8", r: "2.2" }), _jsx("path", { ...P, d: "M8 2.5v1.2M8 12.3v1.2M2.5 8h1.2M12.3 8h1.2M4.1 4.1l.9.9M11 11l.9.9M11.9 4.1l-.9.9M5 11l-.9.9" })] }),
22
+ mail: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "9", rx: "1" }), _jsx("path", { ...P, d: "m2.5 5 5.5 3.5L13.5 5" })] }),
23
+ play: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2", y: "3", width: "12", height: "8.5", rx: "1" }), _jsx("path", { ...P, d: "M5.5 14h5" }), _jsx("path", { ...P, d: "M7 6.2 9.6 7.7 7 9.2v-3Z" })] }),
24
+ out: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M7 3.5H3.5v9h9V9" }), _jsx("path", { ...P, d: "M9.5 2.5h4v4" }), _jsx("path", { ...P, d: "m13 3-5 5" })] }),
25
+ trash: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M3.5 4.5h9" }), _jsx("path", { ...P, d: "M5.5 4.5V3h5v1.5" }), _jsx("path", { ...P, d: "M4.5 4.5 5 13h6l.5-8.5" })] }),
26
+ gallery: () => _jsxs(Svg, { children: [_jsx("rect", { ...P, x: "2.5", y: "3.5", width: "11", height: "9", rx: "1" }), _jsx("path", { ...P, d: "M6 3.5v9M10 3.5v9" })] }),
27
+ doc: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M3.5 2.5h9v11h-9v-11Z" }), _jsx("path", { ...P, d: "M5.5 5.5h5M5.5 8h5M5.5 10.5h3" })] }),
28
+ grip: () => _jsxs(Svg, { children: [_jsx("circle", { cx: "6", cy: "4", r: "1", fill: "currentColor" }), _jsx("circle", { cx: "10", cy: "4", r: "1", fill: "currentColor" }), _jsx("circle", { cx: "6", cy: "8", r: "1", fill: "currentColor" }), _jsx("circle", { cx: "10", cy: "8", r: "1", fill: "currentColor" }), _jsx("circle", { cx: "6", cy: "12", r: "1", fill: "currentColor" }), _jsx("circle", { cx: "10", cy: "12", r: "1", fill: "currentColor" })] }),
29
+ more: () => _jsxs(Svg, { children: [_jsx("circle", { cx: "4", cy: "8", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "8", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "12", cy: "8", r: "1.1", fill: "currentColor" })] }),
30
+ plus: () => _jsx(Svg, { children: _jsx("path", { ...P, d: "M8 3.5v9M3.5 8h9" }) }),
31
+ /* La freccia dell'apri/chiudi. Una sola, che RUOTA con una transizione: due glifi diversi
32
+ scattano da uno stato all'altro e non raccontano che la card si sta aprendo. */
33
+ chevron: () => _jsx(Svg, { children: _jsx("path", { ...P, d: "m4.5 6.5 3.5 3.5 3.5-3.5" }) }),
34
+ replace: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M2.5 5h10" }), _jsx("path", { ...P, d: "m10 2.5 2.5 2.5L10 7.5" }), _jsx("path", { ...P, d: "M13.5 11h-10" }), _jsx("path", { ...P, d: "M6 8.5 3.5 11 6 13.5" })] }),
35
+ search: () => _jsxs(Svg, { children: [_jsx("circle", { ...P, cx: "7", cy: "7", r: "4" }), _jsx("path", { ...P, d: "m10 10 3.5 3.5" })] }),
36
+ eye: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M2.5 8s2.2-3.5 5.5-3.5S13.5 8 13.5 8s-2.2 3.5-5.5 3.5S2.5 8 2.5 8Z" }), _jsx("circle", { ...P, cx: "8", cy: "8", r: "1.7" })] }),
37
+ eyeOff: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M2.5 8s2.2-3.5 5.5-3.5S13.5 8 13.5 8s-2.2 3.5-5.5 3.5S2.5 8 2.5 8Z" }), _jsx("path", { ...P, d: "m3 3 10 10" })] }),
38
+ /* Il pozzetto "Spazio in uso" della Panoramica: non è uno stato buono/cattivo come le altre
39
+ card, quindi non ha un colore — solo la sua icona. */
40
+ archive: () => _jsxs(Svg, { children: [_jsx("path", { ...P, d: "M2.5 4.5 4 2.5h8l1.5 2" }), _jsx("rect", { ...P, x: "2.5", y: "4.5", width: "11", height: "8.5", rx: "1" }), _jsx("path", { ...P, d: "M6.3 8h3.4" })] }),
41
+ /* Il toggle del tema: mostra la destinazione del click, non lo stato corrente — si preme il
42
+ sole per ANDARE al chiaro. */
43
+ sun: () => _jsxs(Svg, { children: [_jsx("circle", { ...P, cx: "8", cy: "8", r: "3" }), _jsx("path", { ...P, d: "M8 1.8v1.6M8 12.6v1.6M14.2 8h-1.6M3.4 8H1.8M12.2 3.8l-1.1 1.1M4.9 11.1l-1.1 1.1M12.2 12.2l-1.1-1.1M4.9 4.9 3.8 3.8" })] }),
44
+ moon: () => _jsx(Svg, { children: _jsx("path", { ...P, d: "M10.6 2.7A6 6 0 1 0 13.3 10.4 5 5 0 0 1 10.6 2.7Z" }) }),
45
+ };
@@ -0,0 +1,5 @@
1
+ /** Da chiamare al bootstrap. Un sito già in produzione passa il prefisso che usava, altrimenti
2
+ * le preferenze salvate nei browser di chi lo usa tutti i giorni si azzerano in silenzio. */
3
+ export declare function setStoragePrefix(value: string): void;
4
+ /** La chiave completa di `localStorage` per un nome logico: `storageKey("sortKey")`. */
5
+ export declare function storageKey(name: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ /* La poca configurazione che il motore riceve dal sito che lo monta.
2
+ *
3
+ * Esiste per una ragione sola, e vale la pena scriverla: `packages/adminkit` è il motore
4
+ * condiviso, e non deve contenere niente di un cliente specifico — è la voce §10.1 della
5
+ * checklist in `ADMIN.md`, e c'è un test che la difende (`no-site-specifics.test.js`). Le
6
+ * chiavi di `localStorage` erano l'ultima cosa rimasta: erano scritte con le iniziali del
7
+ * cliente incorporate nel nome, cioè portavano un sito dentro il motore di tutti.
8
+ *
9
+ * Perché una configurazione a livello di MODULO e non una prop: `useMediaLibrary` viene
10
+ * chiamato anche da `MediaBrowser`, che sta dentro il kit. Passare il prefisso come prop
11
+ * avrebbe voluto dire farlo attraversare un componente che non ha nessun motivo di conoscerlo,
12
+ * e ricordarsi di passarlo in ogni punto futuro — cioè un modo per dimenticarselo.
13
+ *
14
+ * Si imposta UNA volta al bootstrap, prima del render (vedi `admin/src/main.tsx`).
15
+ */
16
+ /** Il default è neutro apposta: un sito che non configura niente non eredita il nome di un altro. */
17
+ let prefix = "adminkit";
18
+ /** Da chiamare al bootstrap. Un sito già in produzione passa il prefisso che usava, altrimenti
19
+ * le preferenze salvate nei browser di chi lo usa tutti i giorni si azzerano in silenzio. */
20
+ export function setStoragePrefix(value) {
21
+ prefix = value;
22
+ }
23
+ /** La chiave completa di `localStorage` per un nome logico: `storageKey("sortKey")`. */
24
+ export function storageKey(name) {
25
+ return `${prefix}_${name}`;
26
+ }
@@ -0,0 +1,13 @@
1
+ import type { AssetFile } from "../api.js";
2
+ export interface MediaBrowserProps {
3
+ /** "manage" = tutto (upload, sposta, cancella). "pick" = solo scelta. */
4
+ mode: "manage" | "pick";
5
+ multiple?: boolean;
6
+ onPick?: (files: AssetFile[]) => void;
7
+ onCancel?: () => void;
8
+ }
9
+ export declare function MediaBrowser({ mode, multiple, onPick, onCancel }: MediaBrowserProps): import("react").JSX.Element;
10
+ export { useMediaLibrary } from "./media/useMediaLibrary.js";
11
+ export type { MediaLibrary, SortKey } from "./media/useMediaLibrary.js";
12
+ export { MediaFolders } from "./media/MediaFolders.js";
13
+ export { MediaGrid } from "./media/MediaGrid.js";
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Il browser dei media in UN pezzo solo: cartelle a sinistra, griglia a destra.
3
+ *
4
+ * È la forma che serve dentro il modale di scelta e ai widget r2Image/r2Gallery, dove non c'è un
5
+ * guscio a tre colonne in cui incastrarsi. La vista «Media» dell'admin invece usa gli stessi
6
+ * pezzi separati — `useMediaLibrary` + `MediaFolders` + `MediaGrid` — perché le sue due metà
7
+ * stanno in due colonne diverse dello schermo.
8
+ *
9
+ * Il vecchio admin aveva DUE browser indipendenti (`loadAssets` per la vista, `openMediaPicker`
10
+ * per il modale) con ~250 righe duplicate che divergevano lentamente: il picker filtrava solo le
11
+ * immagini, la vista no; l'ordinamento c'era di là e non di qua. Qui la differenza è una prop,
12
+ * `mode`, e tutto il resto è condiviso per costruzione.
13
+ */
14
+ import { useMediaLibrary } from "./media/useMediaLibrary.js";
15
+ import { MediaFolders } from "./media/MediaFolders.js";
16
+ import { MediaGrid } from "./media/MediaGrid.js";
17
+ export function MediaBrowser({ mode, multiple = true, onPick, onCancel }) {
18
+ // `remember`: il modale riapre nella cartella dell'ultima volta (vedi useMediaLibrary).
19
+ const lib = useMediaLibrary({ mode, multiple, remember: mode === "pick" });
20
+ return (_jsxs("div", { className: "mb mb-" + mode, children: [_jsx("aside", { className: "mb-side", children: _jsx(MediaFolders, { lib: lib }) }), _jsx("section", { className: "mb-main", children: _jsx(MediaGrid, { lib: lib, onPick: onPick, onCancel: onCancel }) })] }));
21
+ }
22
+ export { useMediaLibrary } from "./media/useMediaLibrary.js";
23
+ export { MediaFolders } from "./media/MediaFolders.js";
24
+ export { MediaGrid } from "./media/MediaGrid.js";
@@ -0,0 +1,10 @@
1
+ import type { AssetFile } from "../api.js";
2
+ export declare function usePicker(): {
3
+ pickMedia: (opts: {
4
+ multiple: boolean;
5
+ }) => Promise<string[] | null>;
6
+ pickMediaFiles: (opts: {
7
+ multiple: boolean;
8
+ }) => Promise<AssetFile[] | null>;
9
+ pickerElement: import("react").JSX.Element | null;
10
+ };
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* Il modale di scelta: lo STESSO MediaBrowser in modalità "pick".
3
+ * `usePicker` restituisce una promessa, così i widget lo usano come una funzione: la vecchia
4
+ * versione passava una callback e ogni chiamante gestiva la chiusura a modo suo. */
5
+ import { useCallback, useState } from "react";
6
+ import { MediaBrowser } from "./MediaBrowser.js";
7
+ export function usePicker() {
8
+ const [req, setReq] = useState(null);
9
+ /* Due funzioni sulla stessa scelta. `pickMediaFiles` dà i file interi — dimensioni comprese —
10
+ * e serve a chi costruisce una gallery; `pickMedia` dà i soli percorsi, che è tutto ciò che
11
+ * vogliono sapere r2Image e r2Gallery. Tenere il caso semplice semplice evita che ogni widget
12
+ * debba conoscere la forma di un AssetFile per salvare una stringa. */
13
+ const pickMediaFiles = useCallback((opts) => new Promise((resolve) => setReq({ multiple: opts.multiple, resolve })), []);
14
+ const pickMedia = useCallback((opts) => pickMediaFiles(opts).then((fs) => (fs ? fs.map((f) => f.url) : null)), [pickMediaFiles]);
15
+ const element = req ? (_jsx("div", { className: "mb-modal", onClick: (e) => { if (e.target === e.currentTarget) {
16
+ req.resolve(null);
17
+ setReq(null);
18
+ } }, children: _jsx("div", { className: "mb-modal-panel", children: _jsx(MediaBrowser, { mode: "pick", multiple: req.multiple, onPick: (files) => { req.resolve(files); setReq(null); }, onCancel: () => { req.resolve(null); setReq(null); } }) }) })) : null;
19
+ return { pickMedia, pickMediaFiles, pickerElement: element };
20
+ }
@@ -0,0 +1,21 @@
1
+ import { type Resolver } from "react-hook-form";
2
+ import * as z from "zod";
3
+ import type { AssetFile } from "../api.js";
4
+ import "./fields/index.js";
5
+ export type Shape = Record<string, z.ZodType>;
6
+ export declare function zodResolver(schema: z.ZodType): Resolver<any>;
7
+ export interface SchemaFormProps {
8
+ shape: Shape;
9
+ value: Record<string, unknown>;
10
+ onChange: (next: Record<string, unknown>) => void;
11
+ pickMedia?: (opts: {
12
+ multiple: boolean;
13
+ }) => Promise<string[] | null>;
14
+ /** Come sopra ma con i file interi (dimensioni comprese): lo vuole `r2Curated`. */
15
+ pickMediaFiles?: (opts: {
16
+ multiple: boolean;
17
+ }) => Promise<AssetFile[] | null>;
18
+ /** Campi da non mostrare, anche se dichiarati (es. quelli già gestiti dall'elenco). */
19
+ omit?: readonly string[];
20
+ }
21
+ export declare function SchemaForm({ shape, value, onChange, pickMedia, pickMediaFiles, omit }: SchemaFormProps): import("react").JSX.Element;
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Il form generato dallo schema.
3
+ *
4
+ * Non c'è un layout scritto a mano da nessuna parte: i campi escono nell'ordine di
5
+ * dichiarazione, raggruppati per `ui.group`. Aggiungere un campo allo schema lo fa comparire
6
+ * qui senza toccare questo file — che è tutto il punto dell'operazione.
7
+ */
8
+ import { useMemo } from "react";
9
+ import { useForm, Controller } from "react-hook-form";
10
+ import * as z from "zod";
11
+ import { ui } from "../registries.js";
12
+ import { fieldFor } from "./registry.js";
13
+ import "./fields/index.js"; // registra gli otto widget
14
+ /* Resolver per React Hook Form scritto a mano invece di usare @hookform/resolvers: quel
15
+ * pacchetto trascina un conflitto di peer dependency (valibot) che si risolverebbe solo con
16
+ * --legacy-peer-deps, cioè nascondendolo finché non esplode nella build di Cloudflare.
17
+ * L'interfaccia è questa, e sono dieci righe. */
18
+ export function zodResolver(schema) {
19
+ return async (values) => {
20
+ const r = schema.safeParse(values);
21
+ if (r.success)
22
+ return { values: r.data, errors: {} };
23
+ const errors = {};
24
+ for (const issue of r.error.issues) {
25
+ const key = issue.path.join(".") || "_root";
26
+ if (!errors[key])
27
+ errors[key] = { type: "validation", message: issue.message };
28
+ }
29
+ return { values: {}, errors: errors };
30
+ };
31
+ }
32
+ export function SchemaForm({ shape, value, onChange, pickMedia, pickMediaFiles, omit = [] }) {
33
+ const object = useMemo(() => z.object(shape), [shape]);
34
+ const { control, formState } = useForm({
35
+ values: value,
36
+ resolver: zodResolver(object),
37
+ mode: "onChange",
38
+ });
39
+ // Gruppi nell'ordine in cui compaiono le loro prime dichiarazioni: l'ordine dello schema è
40
+ // anche l'ordine del file su disco, quindi il form si legge come si legge il contenuto.
41
+ const groups = [];
42
+ for (const [name, field] of Object.entries(shape)) {
43
+ if (omit.includes(name))
44
+ continue;
45
+ const meta = ui.get(field);
46
+ if (!meta || meta.widget === "hidden")
47
+ continue;
48
+ const g = meta.group ?? "";
49
+ let bucket = groups.find((x) => x.name === g);
50
+ if (!bucket) {
51
+ bucket = { name: g, fields: [] };
52
+ groups.push(bucket);
53
+ }
54
+ bucket.fields.push(name);
55
+ }
56
+ return (_jsx("div", { className: "ak-form", children: groups.map((g) => (_jsxs("fieldset", { className: "ak-group", children: [g.name && _jsx("legend", { className: "ak-legend", children: g.name }), g.fields.map((name) => {
57
+ const meta = ui.get(shape[name]);
58
+ const Field = fieldFor(meta.widget);
59
+ return (_jsx(Controller, { name: name, control: control, render: ({ field }) => (_jsx(Field, { name: name,
60
+ /* `schema` serve a `objectList`, l'unico widget che deve scendere nei figli.
61
+ Passarlo a tutti costa nulla e evita un canale a parte per un caso solo. */
62
+ meta: { ...meta, schema: shape[name] }, value: field.value, error: formState.errors?.[name]?.message, pickMedia: pickMedia, pickMediaFiles: pickMediaFiles, onChange: (v) => {
63
+ field.onChange(v);
64
+ onChange({ ...value, [name]: v });
65
+ } })) }, name));
66
+ })] }, g.name || "_"))) }));
67
+ }
@@ -0,0 +1 @@
1
+ export declare function Trash(): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* Cestino. Vale la pena ricordare perché esiste ed è l'unica rete: R2 NON ha versioning, e
3
+ * `_trash/` è il solo posto da cui si può tornare indietro dopo una cancellazione. */
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { api, ApiError } from "../api.js";
6
+ const fmtSize = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${Math.round(n / 1024)} KB` : `${(n / 1048576).toFixed(1)} MB`);
7
+ /* Nessun pulsante «indietro»: il cestino è una voce della colonna delle cartelle, e da lì si
8
+ * torna dove si vuole con un click. Un secondo modo di uscire, dentro il pannello, era solo un
9
+ * bottone in più che diceva la stessa cosa. */
10
+ export function Trash() {
11
+ const [items, setItems] = useState(null);
12
+ const [error, setError] = useState(null);
13
+ const load = useCallback(async () => {
14
+ try {
15
+ setItems((await api.trash()).items ?? []);
16
+ setError(null);
17
+ }
18
+ catch (e) {
19
+ setError(e instanceof ApiError ? e.message : String(e));
20
+ }
21
+ }, []);
22
+ useEffect(() => { void load(); }, [load]);
23
+ return (_jsxs("div", { className: "tr", children: [_jsxs("header", { className: "mb-top", children: [_jsx("span", { className: "ak-spacer" }), !!items?.length && (_jsx("button", { className: "ak-btn", onClick: async () => {
24
+ if (!confirm(`Svuotare il cestino? ${items.length} file cancellati per sempre.`))
25
+ return;
26
+ await api.trashEmpty();
27
+ await load();
28
+ }, children: "Svuota il cestino" }))] }), error && _jsx("p", { className: "ak-error", children: error }), !items && _jsx("p", { className: "ak-hint", children: "Carico\u2026" }), items?.length === 0 && _jsx("p", { className: "ak-hint", children: "Il cestino \u00E8 vuoto." }), _jsx("ul", { className: "tr-list", children: items?.map((it) => (_jsxs("li", { className: "tr-row", children: [_jsx("img", { src: it.previewUrl, alt: "", className: "tr-thumb", loading: "lazy" }), _jsx("span", { className: "tr-name", children: it.name }), _jsx("code", { className: "tr-orig", children: it.originalKey }), _jsx("span", { className: "ak-hint", children: fmtSize(it.size) }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: async () => {
29
+ const r = await api.trashRestore([it.trashKey]);
30
+ if (r.conflicts?.length) {
31
+ alert(`Esisteva già un file con quel nome: ripristinato come ${r.conflicts[0].restoredAs}.`);
32
+ }
33
+ // NB: il ripristino NON rimette i riferimenti nelle pagine — li aveva tolti la
34
+ // cancellazione. Va rifatto a mano, ed è documentato nell'endpoint.
35
+ await load();
36
+ }, children: "Ripristina" }), _jsx("button", { className: "ak-btn", onClick: async () => {
37
+ if (!confirm(`Cancellare ${it.name} per sempre? Non è più recuperabile.`))
38
+ return;
39
+ await api.trashDelete([it.trashKey]);
40
+ await load();
41
+ }, children: "Elimina" })] }, it.trashKey))) })] }));
42
+ }
@@ -0,0 +1 @@
1
+ import "./objectList.js";
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* I widget. Piccoli di proposito: il valore del sistema sta nello schema, non qui.
3
+ * Nessuno di questi conosce il sito — etichette, opzioni e aiuti arrivano da `ui`. */
4
+ import { useEffect, useState } from "react";
5
+ import { registerField } from "../registry.js";
6
+ import { ProseField } from "../prose/ProseField.js";
7
+ function Wrap({ meta, error, children }) {
8
+ return (_jsxs("label", { className: "ak-field", children: [_jsx("span", { className: "ak-label", children: meta.label }), children, meta.hint && _jsx("span", { className: "ak-hint", children: meta.hint }), error && _jsx("span", { className: "ak-error", children: error })] }));
9
+ }
10
+ registerField("text", ({ meta, value, onChange, error }) => (_jsx(Wrap, { meta: meta, error: error, children: _jsx("input", { className: "ak-input", type: "text", value: String(value ?? ""), placeholder: meta.placeholder, maxLength: meta.maxLength, onChange: (e) => onChange(e.target.value) }) })));
11
+ /* `ProseField` e non una `textarea` nuda: è prosa, quindi ha la barra di formattazione sopra la
12
+ selezione — grassetto, corsivo e collegamento. Il valore resta la stessa stringa di prima. */
13
+ registerField("textarea", ({ meta, value, onChange, error }) => (_jsx(Wrap, { meta: meta, error: error, children: _jsx(ProseField, { className: "ak-input ak-textarea", rows: 8, value: String(value ?? ""), placeholder: meta.placeholder, maxLength: meta.maxLength, onChange: onChange }) })));
14
+ /* Prosa: si scrive in una textarea, si salva come ELENCO di paragrafi.
15
+ *
16
+ * I due mondi non combaciano e questo widget è la cerniera. Da una parte chi scrive vuole
17
+ * digitare, andare a capo due volte e avere un paragrafo nuovo. Dall'altra il front matter non
18
+ * può contenere un a capo dentro una stringa: `yamlStr` si ferma apposta invece di indovinare
19
+ * (vedi ADMIN.md §6), quindi un testo su più righe non era proprio salvabile — il salvataggio
20
+ * falliva, ed è il difetto che questo widget chiude.
21
+ *
22
+ * La forma su disco è quella che il sito già usa per la prosa: un array di stringhe, un
23
+ * paragrafo per elemento, come `about.paragraphs` nella home. La differenza è solo che lì si
24
+ * aggiunge una riga per volta con «+ Aggiungi», e per scrivere due paragrafi di descrizione è
25
+ * un attrezzo sbagliato.
26
+ *
27
+ * Riga vuota = paragrafo nuovo. Un a capo singolo resta dentro il paragrafo e si perde nel
28
+ * rendering: è la convenzione di Markdown, che è quella che conosce chiunque scriva su un
29
+ * computer. */
30
+ const toText = (v) => (Array.isArray(v) ? v.join("\n\n") : String(v ?? ""));
31
+ const toParagraphs = (text) => text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
32
+ /* IL TESTO GREZZO STA NELLO STATO LOCALE, e non è un dettaglio: la textarea mostra quello che
33
+ * hai scritto, non il risultato della conversione. Rileggendo il valore dall'elenco a ogni
34
+ * battuta, la riga vuota appena premuta verrebbe cancellata dal render successivo — «Invio,
35
+ * Invio» non aprirebbe mai un paragrafo nuovo, cioè proprio il gesto per cui il widget esiste.
36
+ * Il valore di fuori vince solo quando dice qualcosa di DIVERSO da ciò che si sta scrivendo:
37
+ * è il caso del caricamento della pagina, non quello della digitazione. */
38
+ function ParagraphsField({ meta, value, onChange, error }) {
39
+ const [text, setText] = useState(() => toText(value));
40
+ useEffect(() => {
41
+ const fuori = Array.isArray(value) ? value : toParagraphs(String(value ?? ""));
42
+ if (fuori.join("\n\n") !== toParagraphs(text).join("\n\n"))
43
+ setText(fuori.join("\n\n"));
44
+ }, [value]);
45
+ return (_jsx(Wrap, { meta: meta, error: error, children: _jsx(ProseField, { className: "ak-input ak-textarea", rows: 10, value: text, placeholder: meta.placeholder, onChange: (next) => { setText(next); onChange(toParagraphs(next)); } }) }));
46
+ }
47
+ registerField("paragraphs", ParagraphsField);
48
+ registerField("number", ({ meta, value, onChange, error }) => (_jsx(Wrap, { meta: meta, error: error, children: _jsx("input", { className: "ak-input", type: "number", value: Number(value ?? 0), onChange: (e) => onChange(e.target.value === "" ? 0 : Number(e.target.value)) }) })));
49
+ registerField("boolean", ({ meta, value, onChange, error }) => (_jsxs("label", { className: "ak-field ak-field-inline", children: [_jsx("input", { type: "checkbox", checked: !!value, onChange: (e) => onChange(e.target.checked) }), _jsx("span", { className: "ak-label", children: meta.label }), meta.hint && _jsx("span", { className: "ak-hint", children: meta.hint }), error && _jsx("span", { className: "ak-error", children: error })] })));
50
+ registerField("select", ({ meta, value, onChange, error }) => (_jsx(Wrap, { meta: meta, error: error, children: _jsx("select", { className: "ak-input", value: String(value ?? ""), onChange: (e) => onChange(e.target.value), children: (meta.options ?? []).map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) }) })));
51
+ /* `menu` esiste come widget a sé (invece di riusare `boolean`) perché l'appartenenza al menu
52
+ * non è una proprietà qualsiasi: cambia dove la pagina compare nell'elenco. Per ora si comporta
53
+ * come una casella; averlo separato permette di arricchirlo senza toccare gli altri. */
54
+ registerField("menu", ({ meta, value, onChange, error }) => (_jsxs("label", { className: "ak-field ak-field-inline", children: [_jsx("input", { type: "checkbox", checked: !!value, onChange: (e) => onChange(e.target.checked) }), _jsx("span", { className: "ak-label", children: meta.label }), error && _jsx("span", { className: "ak-error", children: error })] })));
55
+ /** Nascosto: il campo esiste nel modello ma non si mostra (es. il peso, che è derivato). */
56
+ registerField("hidden", () => null);
57
+ registerField("r2Image", ({ meta, value, onChange, error, pickMedia }) => {
58
+ const url = String(value ?? "");
59
+ return (_jsx(Wrap, { meta: meta, error: error, children: _jsxs("div", { className: "ak-media-one", children: [url ? _jsx("img", { src: url, alt: "", className: "ak-thumb" }) : _jsx("div", { className: "ak-thumb ak-thumb-empty" }), _jsxs("div", { className: "ak-media-actions", children: [_jsx("button", { type: "button", className: "ak-btn", onClick: async () => {
60
+ const picked = await pickMedia?.({ multiple: false });
61
+ if (picked?.length)
62
+ onChange(picked[0]);
63
+ }, children: "Scegli\u2026" }), url && _jsx("button", { type: "button", className: "ak-btn ak-btn-quiet", onClick: () => onChange(""), children: "Rimuovi" })] })] }) }));
64
+ });
65
+ registerField("r2Gallery", ({ meta, value, onChange, error, pickMedia }) => {
66
+ const list = Array.isArray(value) ? value : [];
67
+ const move = (from, to) => {
68
+ if (to < 0 || to >= list.length)
69
+ return;
70
+ const next = list.slice();
71
+ const [x] = next.splice(from, 1);
72
+ next.splice(to, 0, x);
73
+ onChange(next);
74
+ };
75
+ return (_jsxs(Wrap, { meta: meta, error: error, children: [_jsx("div", { className: "ak-media-grid", children: list.map((u, i) => (_jsxs("figure", { className: "ak-media-item", children: [_jsx("img", { src: u, alt: "", className: "ak-thumb" }), _jsxs("figcaption", { className: "ak-media-item-bar", children: [_jsx("button", { type: "button", className: "ak-icon", title: "Sposta indietro", onClick: () => move(i, i - 1), children: "\u2039" }), _jsx("button", { type: "button", className: "ak-icon", title: "Rimuovi", onClick: () => onChange(list.filter((_, j) => j !== i)), children: "\u2715" }), _jsx("button", { type: "button", className: "ak-icon", title: "Sposta avanti", onClick: () => move(i, i + 1), children: "\u203A" })] })] }, `${u}-${i}`))) }), _jsx("button", { type: "button", className: "ak-btn", onClick: async () => {
76
+ const picked = await pickMedia?.({ multiple: true });
77
+ if (picked?.length)
78
+ onChange([...list, ...picked]);
79
+ }, children: "+ Aggiungi immagini" })] }));
80
+ });
81
+ registerField("r2Curated", ({ meta, value, onChange, error, pickMediaFiles }) => {
82
+ const list = Array.isArray(value) ? value : [];
83
+ const patch = (i, up) => onChange(list.map((it, j) => (j === i ? { ...it, ...up } : it)));
84
+ const move = (from, to) => {
85
+ if (to < 0 || to >= list.length)
86
+ return;
87
+ const next = list.slice();
88
+ const [x] = next.splice(from, 1);
89
+ next.splice(to, 0, x);
90
+ onChange(next);
91
+ };
92
+ const visible = list.filter((it) => !it.hidden).length;
93
+ return (_jsxs(Wrap, { meta: meta, error: error, children: [_jsx("p", { className: "ak-hint", children: list.length === 0 ? "Nessuna foto." : `${visible} online${list.length > visible ? `, ${list.length - visible} nascoste` : ""}` }), _jsx("div", { className: "ak-media-grid", children: list.map((it, i) => (_jsxs("figure", { className: "ak-media-item" + (it.hidden ? " is-hidden" : ""), children: [_jsx("img", { src: it.src, alt: "", className: "ak-thumb" }), _jsxs("figcaption", { className: "ak-media-item-bar", children: [_jsx("button", { type: "button", className: "ak-icon", title: "Sposta indietro", onClick: () => move(i, i - 1), children: "\u2039" }), _jsx("button", { type: "button", className: "ak-icon", title: it.hidden ? "Rimetti online" : "Nascondi (resta nell'elenco)", onClick: () => patch(i, { hidden: it.hidden ? undefined : true }), children: it.hidden ? "◎" : "◉" }), _jsx("button", { type: "button", className: "ak-icon", title: "Togli dall'elenco", onClick: () => onChange(list.filter((_, j) => j !== i)), children: "\u2715" }), _jsx("button", { type: "button", className: "ak-icon", title: "Sposta avanti", onClick: () => move(i, i + 1), children: "\u203A" })] })] }, `${it.src}-${i}`))) }), _jsx("button", { type: "button", className: "ak-btn", onClick: async () => {
94
+ const picked = await pickMediaFiles?.({ multiple: true });
95
+ if (!picked?.length)
96
+ return;
97
+ // Niente doppioni: una foto già in elenco che viene riscelta resta dov'è invece di
98
+ // comparire due volte in fondo — succede di continuo usando "seleziona tutte" dopo
99
+ // aver già aggiunto qualcosa.
100
+ const have = new Set(list.map((it) => it.src));
101
+ const add = picked
102
+ .filter((f) => !have.has(f.url))
103
+ .map((f) => ({ src: f.url, w: f.w ?? null, h: f.h ?? null }));
104
+ if (add.length)
105
+ onChange([...list, ...add]);
106
+ }, children: "+ Aggiungi foto" })] }));
107
+ });
108
+ /* `objectList` sta in un file suo: è l'unico widget ricorsivo, e tenerlo separato rende
109
+ evidente quando qualcuno prova ad aggiungerne un secondo. */
110
+ import "./objectList.js";
@@ -0,0 +1 @@
1
+ export {};