@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,585 @@
1
+ /* Motore browser della consegna.
2
+ *
3
+ * - ZIP STORE/ZIP64: le fotografie sono già compresse, ricomprimerle spreca CPU.
4
+ * - memoria limitata: file a slice da 4 MiB, parti B2 da 16 MiB, una foto derivata alla volta.
5
+ * - upload diretto a B2 tramite URL PUT prefirmati; i byte non passano dalla Function.
6
+ * - il manifest conserva gli offset dei membri STORE, così la Function pubblica può
7
+ * servire una singola foto con un GET Range senza duplicarla come oggetto.
8
+ */
9
+ const PART = 16 * 1024 * 1024;
10
+ const READ = 4 * 1024 * 1024;
11
+ const UPLOAD_CONCURRENCY = 4;
12
+ const IMAGE = /\.(jpe?g|png|webp)$/i;
13
+ const enc = new TextEncoder();
14
+ const droppedPaths = new WeakMap();
15
+ export class UploadCancelled extends Error {
16
+ constructor() { super("Caricamento annullato"); this.name = "UploadCancelled"; }
17
+ }
18
+ export async function uploadDeliveryPackage(deliveryId, packageId, selected, options, onProgress, control) {
19
+ const startedAt = Date.now();
20
+ const files = [...selected].sort((a, b) => deliveryRelativeName(a).localeCompare(deliveryRelativeName(b), undefined, { numeric: true, sensitivity: "base" }));
21
+ if (!files.length)
22
+ throw new Error("Nessun file selezionato");
23
+ const sourceBytes = files.reduce((n, f) => n + f.size, 0);
24
+ const entries = [];
25
+ let processed = 0;
26
+ let currentOriginal = "";
27
+ const originalsSink = new B2Sink(deliveryId, packageId, "originals", reportOriginals, control);
28
+ const originals = new ZipStream(originalsSink);
29
+ function reportOriginals() {
30
+ onProgress({
31
+ phase: "originals", file: currentOriginal, doneFiles: processed, totalFiles: files.length,
32
+ sourceBytes, uploadedBytes: originalsSink.uploadedBytes,
33
+ transferredBytes: originalsSink.transferredBytes, startedAt,
34
+ transferStartedAt: originalsSink.transferStartedAt || undefined,
35
+ });
36
+ }
37
+ for (const file of files) {
38
+ if (control)
39
+ await control.gate();
40
+ const name = safePath(deliveryRelativeName(file));
41
+ currentOriginal = name;
42
+ const member = await originals.add(name, file, reportOriginals, new Date(file.lastModified));
43
+ entries.push({ name, type: file.type || mimeFromName(name), original: member });
44
+ processed++;
45
+ reportOriginals();
46
+ }
47
+ await originals.close();
48
+ const wantWebArchive = options.webEnabled || options.galleryEnabled;
49
+ let webSink = null;
50
+ let thumbsSink = null;
51
+ let webZip = null;
52
+ let thumbsZip = null;
53
+ const skipped = [];
54
+ if (wantWebArchive) {
55
+ webSink = new B2Sink(deliveryId, packageId, "web", undefined, control);
56
+ webZip = new ZipStream(webSink);
57
+ }
58
+ if (options.galleryEnabled) {
59
+ thumbsSink = new B2Sink(deliveryId, packageId, "thumbs", undefined, control);
60
+ thumbsZip = new ZipStream(thumbsSink);
61
+ }
62
+ if (webZip) {
63
+ let imageDone = 0;
64
+ const imageFiles = files.filter((f) => IMAGE.test(f.name));
65
+ for (const file of imageFiles) {
66
+ if (control)
67
+ await control.gate();
68
+ const sourceName = safePath(deliveryRelativeName(file));
69
+ try {
70
+ const variants = await makeVariants(file, !!thumbsZip);
71
+ const outputName = sourceName.replace(/\.[^.\/]+$/, "") + ".webp";
72
+ const modified = new Date(file.lastModified);
73
+ const web = await webZip.add(outputName, variants.display, undefined, modified);
74
+ const entry = entries.find((it) => it.name === sourceName);
75
+ if (entry) {
76
+ entry.web = web;
77
+ entry.width = variants.width;
78
+ entry.height = variants.height;
79
+ if (thumbsZip && variants.thumb) {
80
+ entry.thumb = await thumbsZip.add(outputName, variants.thumb, undefined, modified);
81
+ }
82
+ }
83
+ }
84
+ catch (e) {
85
+ skipped.push({ name: sourceName, reason: e instanceof Error ? e.message : String(e) });
86
+ }
87
+ imageDone++;
88
+ onProgress({
89
+ phase: "derivatives", file: sourceName, doneFiles: imageDone,
90
+ totalFiles: imageFiles.length, sourceBytes,
91
+ uploadedBytes: originalsSink.uploadedBytes + (webSink?.uploadedBytes || 0) +
92
+ (thumbsSink?.uploadedBytes || 0),
93
+ transferredBytes: originalsSink.transferredBytes + (webSink?.transferredBytes || 0) +
94
+ (thumbsSink?.transferredBytes || 0),
95
+ startedAt,
96
+ transferStartedAt: firstStartedAt(originalsSink, webSink, thumbsSink),
97
+ note: skipped.length ? `${skipped.length} senza anteprima` : undefined,
98
+ });
99
+ }
100
+ await webZip.close();
101
+ if (thumbsZip)
102
+ await thumbsZip.close();
103
+ }
104
+ onProgress({
105
+ phase: "finalizing", file: "", doneFiles: files.length, totalFiles: files.length,
106
+ sourceBytes, uploadedBytes: originalsSink.uploadedBytes + (webSink?.uploadedBytes || 0) +
107
+ (thumbsSink?.uploadedBytes || 0),
108
+ transferredBytes: originalsSink.transferredBytes + (webSink?.transferredBytes || 0) +
109
+ (thumbsSink?.transferredBytes || 0),
110
+ startedAt,
111
+ transferStartedAt: firstStartedAt(originalsSink, webSink, thumbsSink),
112
+ });
113
+ return {
114
+ manifest: { version: 1, createdAt: new Date().toISOString(), entries, skipped },
115
+ fileCount: files.length,
116
+ sourceBytes,
117
+ storedBytes: originalsSink.totalBytes + (webSink?.totalBytes || 0) + (thumbsSink?.totalBytes || 0),
118
+ };
119
+ }
120
+ class B2Sink {
121
+ deliveryId;
122
+ packageId;
123
+ variant;
124
+ uploadedBytes = 0;
125
+ transferredBytes = 0;
126
+ transferStartedAt = 0;
127
+ totalBytes = 0;
128
+ chunks = [];
129
+ buffered = 0;
130
+ session = null;
131
+ partNumber = 0;
132
+ priorParts = new Map();
133
+ inFlight = new Set();
134
+ failure = null;
135
+ onTransfer;
136
+ control;
137
+ constructor(deliveryId, packageId, variant, onTransfer, control) {
138
+ this.deliveryId = deliveryId;
139
+ this.packageId = packageId;
140
+ this.variant = variant;
141
+ this.onTransfer = onTransfer;
142
+ this.control = control;
143
+ }
144
+ async write(input) {
145
+ if (!input.byteLength)
146
+ return;
147
+ this.chunks.push(input);
148
+ this.buffered += input.byteLength;
149
+ this.totalBytes += input.byteLength;
150
+ while (this.buffered >= PART)
151
+ await this.queuePart(this.take(PART));
152
+ }
153
+ async close() {
154
+ if (this.session?.state === "completed") {
155
+ const rest = this.take(this.buffered);
156
+ this.markUploaded(rest.byteLength, false);
157
+ await this.drain();
158
+ return;
159
+ }
160
+ if (!this.session) {
161
+ const body = this.take(this.buffered);
162
+ const signed = await postJson({
163
+ action: "putPresign", id: this.deliveryId, packageId: this.packageId, variant: this.variant,
164
+ });
165
+ this.startTransfer();
166
+ const res = await fetch(signed.url, {
167
+ method: "PUT", body: body.buffer, headers: { "Content-Type": "application/zip" },
168
+ });
169
+ if (!res.ok)
170
+ throw new Error(`Caricamento non riuscito (${res.status}). Riprova fra poco.`);
171
+ await postJson({
172
+ action: "putComplete", id: this.deliveryId, packageId: this.packageId,
173
+ variant: this.variant, bytes: body.byteLength,
174
+ });
175
+ this.markUploaded(body.byteLength, true);
176
+ return;
177
+ }
178
+ if (this.buffered)
179
+ await this.queuePart(this.take(this.buffered));
180
+ await this.drain();
181
+ await postJson({ action: "complete", uploadId: this.session.id });
182
+ }
183
+ async ensureSession() {
184
+ if (this.session)
185
+ return this.session;
186
+ const out = await postJson({
187
+ action: "create", id: this.deliveryId, packageId: this.packageId, variant: this.variant,
188
+ });
189
+ this.session = out.upload;
190
+ if (out.upload.state === "completed")
191
+ return out.upload;
192
+ const reconciled = await postJson({
193
+ action: "reconcile", uploadId: out.upload.id,
194
+ });
195
+ this.priorParts = new Map(reconciled.parts.map((part) => [part.partNumber, part]));
196
+ return out.upload;
197
+ }
198
+ async queuePart(body) {
199
+ /* Qui passa OGNI parte: è il punto in cui pausa e annullamento arrivano davvero, anche
200
+ dentro un file solo da venti giga. */
201
+ if (this.control)
202
+ await this.control.gate();
203
+ const session = await this.ensureSession();
204
+ const partNumber = ++this.partNumber;
205
+ await this.failIfNeeded();
206
+ let task;
207
+ task = this.uploadPart(body, partNumber, session)
208
+ .catch((error) => {
209
+ if (this.failure == null)
210
+ this.failure = error;
211
+ })
212
+ .finally(() => this.inFlight.delete(task));
213
+ this.inFlight.add(task);
214
+ if (this.inFlight.size >= UPLOAD_CONCURRENCY) {
215
+ await Promise.race(this.inFlight);
216
+ await this.failIfNeeded();
217
+ }
218
+ }
219
+ async uploadPart(body, partNumber, session) {
220
+ if (session.state === "completed") {
221
+ this.markUploaded(body.byteLength, false);
222
+ return;
223
+ }
224
+ const digest = await digestHex(body);
225
+ const prior = this.priorParts.get(partNumber);
226
+ if (prior?.size === body.byteLength && prior.digest === digest) {
227
+ this.markUploaded(body.byteLength, false);
228
+ return;
229
+ }
230
+ const signed = await postJson({
231
+ action: "presign", uploadId: session.id, partNumber,
232
+ });
233
+ let res = null;
234
+ let failure = "errore di rete o CORS";
235
+ for (let attempt = 0; attempt < 3; attempt++) {
236
+ this.startTransfer();
237
+ res = await fetch(signed.url, {
238
+ method: "PUT", body: body.buffer, headers: { "Content-Type": "application/octet-stream" },
239
+ }).catch(() => null);
240
+ if (res?.ok)
241
+ break;
242
+ if (res) {
243
+ const detail = (await res.text().catch(() => "")).replace(/\s+/g, " ").slice(0, 180);
244
+ failure = `HTTP ${res.status}${detail ? ` · ${detail}` : ""}`;
245
+ }
246
+ await delay(500 * 2 ** attempt);
247
+ }
248
+ if (!res?.ok) {
249
+ throw new Error(`Un pezzo del caricamento non è passato (${failure}). Riprova.`);
250
+ }
251
+ const etag = res.headers.get("etag");
252
+ if (!etag) {
253
+ throw new Error("L’archivio non espone l’header ETag: manca la configurazione CORS del bucket (vedi DELIVERY.md)");
254
+ }
255
+ let recorded = false;
256
+ for (let attempt = 0; attempt < 3 && !recorded; attempt++) {
257
+ try {
258
+ await postJson({
259
+ action: "record", uploadId: session.id, partNumber, etag,
260
+ digest, size: body.byteLength,
261
+ });
262
+ recorded = true;
263
+ }
264
+ catch {
265
+ if (attempt === 2)
266
+ throw new Error("Un pezzo del caricamento non è stato registrato. Riprova.");
267
+ await delay(400 * 2 ** attempt);
268
+ }
269
+ }
270
+ this.markUploaded(body.byteLength, true);
271
+ }
272
+ async drain() {
273
+ await Promise.all([...this.inFlight]);
274
+ await this.failIfNeeded();
275
+ }
276
+ async failIfNeeded() {
277
+ if (this.failure == null)
278
+ return;
279
+ await Promise.all([...this.inFlight]);
280
+ const error = this.failure;
281
+ this.failure = null;
282
+ throw error;
283
+ }
284
+ startTransfer() {
285
+ if (!this.transferStartedAt)
286
+ this.transferStartedAt = Date.now();
287
+ }
288
+ markUploaded(size, transferred) {
289
+ this.uploadedBytes += size;
290
+ if (transferred)
291
+ this.transferredBytes += size;
292
+ this.onTransfer?.();
293
+ }
294
+ take(size) {
295
+ const out = new Uint8Array(size);
296
+ let offset = 0;
297
+ while (offset < size) {
298
+ const first = this.chunks[0];
299
+ if (!first)
300
+ throw new Error("buffer ZIP incoerente");
301
+ const count = Math.min(first.byteLength, size - offset);
302
+ out.set(first.subarray(0, count), offset);
303
+ offset += count;
304
+ if (count === first.byteLength)
305
+ this.chunks.shift();
306
+ else
307
+ this.chunks[0] = first.subarray(count);
308
+ }
309
+ this.buffered -= size;
310
+ return out;
311
+ }
312
+ }
313
+ function firstStartedAt(...sinks) {
314
+ const values = sinks.map((sink) => sink?.transferStartedAt || 0).filter(Boolean);
315
+ return values.length ? Math.min(...values) : undefined;
316
+ }
317
+ export class ZipStream {
318
+ sink;
319
+ offset = 0;
320
+ entries = [];
321
+ constructor(sink) { this.sink = sink; }
322
+ async add(nameValue, blob, progress, modified = new Date()) {
323
+ const name = enc.encode(safePath(nameValue));
324
+ const size = blob.size;
325
+ const localOffset = this.offset;
326
+ const zip64 = size >= 0xffffffff;
327
+ const extra = zip64 ? zip64Extra([size, size]) : new Uint8Array();
328
+ const local = bytes(30 + name.length + extra.length);
329
+ const v = view(local);
330
+ v.setUint32(0, 0x04034b50, true);
331
+ v.setUint16(4, zip64 ? 45 : 20, true);
332
+ v.setUint16(6, 0x0808, true); // descriptor + nomi UTF-8
333
+ v.setUint16(8, 0, true); // STORE
334
+ const [time, date] = dosDate(modified);
335
+ v.setUint16(10, time, true);
336
+ v.setUint16(12, date, true);
337
+ v.setUint32(18, zip64 ? 0xffffffff : 0, true);
338
+ v.setUint32(22, zip64 ? 0xffffffff : 0, true);
339
+ v.setUint16(26, name.length, true);
340
+ v.setUint16(28, extra.length, true);
341
+ local.set(name, 30);
342
+ local.set(extra, 30 + name.length);
343
+ await this.push(local);
344
+ const dataOffset = this.offset;
345
+ let crc = 0xffffffff;
346
+ let read = 0;
347
+ while (read < size) {
348
+ const chunk = new Uint8Array(await blob.slice(read, Math.min(size, read + READ)).arrayBuffer());
349
+ crc = crc32Update(crc, chunk);
350
+ read += chunk.byteLength;
351
+ await this.push(chunk);
352
+ progress?.(read);
353
+ }
354
+ crc = (crc ^ 0xffffffff) >>> 0;
355
+ const descriptor = bytes(zip64 ? 24 : 16);
356
+ const d = view(descriptor);
357
+ d.setUint32(0, 0x08074b50, true);
358
+ d.setUint32(4, crc, true);
359
+ if (zip64) {
360
+ setU64(d, 8, size);
361
+ setU64(d, 16, size);
362
+ }
363
+ else {
364
+ d.setUint32(8, size, true);
365
+ d.setUint32(12, size, true);
366
+ }
367
+ await this.push(descriptor);
368
+ this.entries.push({ name, size, crc, localOffset, time, date });
369
+ return { offset: dataOffset, length: size, crc32: crc };
370
+ }
371
+ async close() {
372
+ const centralOffset = this.offset;
373
+ for (const entry of this.entries) {
374
+ const large = entry.size >= 0xffffffff || entry.localOffset >= 0xffffffff;
375
+ const extra = large ? zip64Extra([entry.size, entry.size, entry.localOffset]) : new Uint8Array();
376
+ const central = bytes(46 + entry.name.length + extra.length);
377
+ const v = view(central);
378
+ v.setUint32(0, 0x02014b50, true);
379
+ v.setUint16(4, 45, true);
380
+ v.setUint16(6, large ? 45 : 20, true);
381
+ v.setUint16(8, 0x0808, true);
382
+ v.setUint16(10, 0, true);
383
+ v.setUint16(12, entry.time, true);
384
+ v.setUint16(14, entry.date, true);
385
+ v.setUint32(16, entry.crc, true);
386
+ v.setUint32(20, large ? 0xffffffff : entry.size, true);
387
+ v.setUint32(24, large ? 0xffffffff : entry.size, true);
388
+ v.setUint16(28, entry.name.length, true);
389
+ v.setUint16(30, extra.length, true);
390
+ v.setUint32(42, large ? 0xffffffff : entry.localOffset, true);
391
+ central.set(entry.name, 46);
392
+ central.set(extra, 46 + entry.name.length);
393
+ await this.push(central);
394
+ }
395
+ const centralSize = this.offset - centralOffset;
396
+ const needs64 = this.entries.length >= 0xffff || centralOffset >= 0xffffffff || centralSize >= 0xffffffff;
397
+ if (needs64) {
398
+ const zip64Offset = this.offset;
399
+ const end64 = bytes(56);
400
+ const e = view(end64);
401
+ e.setUint32(0, 0x06064b50, true);
402
+ setU64(e, 4, 44);
403
+ e.setUint16(12, 45, true);
404
+ e.setUint16(14, 45, true);
405
+ setU64(e, 24, this.entries.length);
406
+ setU64(e, 32, this.entries.length);
407
+ setU64(e, 40, centralSize);
408
+ setU64(e, 48, centralOffset);
409
+ await this.push(end64);
410
+ const locator = bytes(20);
411
+ const l = view(locator);
412
+ l.setUint32(0, 0x07064b50, true);
413
+ setU64(l, 8, zip64Offset);
414
+ l.setUint32(16, 1, true);
415
+ await this.push(locator);
416
+ }
417
+ const end = bytes(22);
418
+ const e = view(end);
419
+ e.setUint32(0, 0x06054b50, true);
420
+ e.setUint16(8, Math.min(this.entries.length, 0xffff), true);
421
+ e.setUint16(10, Math.min(this.entries.length, 0xffff), true);
422
+ e.setUint32(12, Math.min(centralSize, 0xffffffff), true);
423
+ e.setUint32(16, Math.min(centralOffset, 0xffffffff), true);
424
+ await this.push(end);
425
+ await this.sink.close();
426
+ }
427
+ async push(data) {
428
+ await this.sink.write(data);
429
+ this.offset += data.byteLength;
430
+ }
431
+ }
432
+ async function makeVariants(file, wantThumb) {
433
+ const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
434
+ try {
435
+ const displaySize = fit(bitmap.width, bitmap.height, 2048);
436
+ const display = await drawWebp(bitmap, displaySize.width, displaySize.height, .8);
437
+ let thumb;
438
+ if (wantThumb) {
439
+ const thumbSize = fit(bitmap.width, bitmap.height, 640);
440
+ thumb = await drawWebp(bitmap, thumbSize.width, thumbSize.height, .72);
441
+ }
442
+ return { display, thumb, width: displaySize.width, height: displaySize.height };
443
+ }
444
+ finally {
445
+ bitmap.close();
446
+ }
447
+ }
448
+ async function drawWebp(bitmap, width, height, quality) {
449
+ if (typeof OffscreenCanvas !== "undefined") {
450
+ const canvas = new OffscreenCanvas(width, height);
451
+ const ctx = canvas.getContext("2d", { alpha: false });
452
+ if (!ctx)
453
+ throw new Error("Canvas 2D non disponibile");
454
+ ctx.drawImage(bitmap, 0, 0, width, height);
455
+ return canvas.convertToBlob({ type: "image/webp", quality });
456
+ }
457
+ const canvas = document.createElement("canvas");
458
+ canvas.width = width;
459
+ canvas.height = height;
460
+ const ctx = canvas.getContext("2d", { alpha: false });
461
+ if (!ctx)
462
+ throw new Error("Canvas 2D non disponibile");
463
+ ctx.drawImage(bitmap, 0, 0, width, height);
464
+ return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("Conversione WebP non riuscita")), "image/webp", quality));
465
+ }
466
+ function fit(width, height, edge) {
467
+ const scale = Math.min(1, edge / Math.max(width, height));
468
+ return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)) };
469
+ }
470
+ async function postJson(body) {
471
+ const res = await fetch("/admin/api/delivery/multipart", {
472
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
473
+ });
474
+ const answer = await res.json().catch(() => ({}));
475
+ if (!res.ok || answer.ok === false)
476
+ throw new Error(answer.error || `HTTP ${res.status}`);
477
+ return answer;
478
+ }
479
+ export function setDeliveryRelativePath(file, path) {
480
+ droppedPaths.set(file, safePath(path));
481
+ }
482
+ export function deliveryRelativeName(file) {
483
+ return droppedPaths.get(file) || file.webkitRelativePath || file.name;
484
+ }
485
+ export function deliverySelectionName(files) {
486
+ const first = files[0];
487
+ if (!first)
488
+ return "";
489
+ const path = deliveryRelativeName(first).replace(/\\/g, "/");
490
+ const slash = path.indexOf("/");
491
+ return slash > 0 ? path.slice(0, slash) : first.name.replace(/\.[^.]+$/, "") || first.name;
492
+ }
493
+ export async function uploadDeliveryHeroObject(deliveryId, packageId, file) {
494
+ const variant = await makeHeroVariant(file);
495
+ const signed = await postJson({
496
+ action: "heroPresign", id: deliveryId, packageId,
497
+ });
498
+ const response = await fetch(signed.url, {
499
+ method: "PUT", body: variant.blob, headers: { "Content-Type": "image/webp" },
500
+ });
501
+ if (!response.ok)
502
+ throw new Error(`Copertina non caricata (${response.status}). Riprova.`);
503
+ return {
504
+ type: "object", id: signed.heroId, sourceName: deliveryRelativeName(file),
505
+ width: variant.width, height: variant.height, bytes: variant.blob.size,
506
+ };
507
+ }
508
+ async function makeHeroVariant(file) {
509
+ if (!IMAGE.test(file.name))
510
+ throw new Error("La copertina dev’essere un’immagine JPEG, PNG o WebP");
511
+ const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
512
+ try {
513
+ const size = fit(bitmap.width, bitmap.height, 2560);
514
+ return {
515
+ blob: await drawWebp(bitmap, size.width, size.height, .82),
516
+ width: size.width, height: size.height,
517
+ };
518
+ }
519
+ finally {
520
+ bitmap.close();
521
+ }
522
+ }
523
+ export async function deliverySourceFingerprint(files) {
524
+ const lines = [...files]
525
+ .sort((a, b) => deliveryRelativeName(a).localeCompare(deliveryRelativeName(b), undefined, {
526
+ numeric: true, sensitivity: "base",
527
+ }))
528
+ .map((file) => `${safePath(deliveryRelativeName(file))}\0${file.size}\0${file.lastModified}`)
529
+ .join("\n");
530
+ return digestHex(enc.encode(lines));
531
+ }
532
+ function safePath(input) {
533
+ const parts = String(input).replace(/\\/g, "/").split("/")
534
+ .filter((part) => part && part !== "." && part !== "..");
535
+ return parts.join("/") || "file";
536
+ }
537
+ function mimeFromName(name) {
538
+ const ext = name.split(".").pop()?.toLowerCase();
539
+ return { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp",
540
+ tif: "image/tiff", tiff: "image/tiff", pdf: "application/pdf" }[ext || ""] ||
541
+ "application/octet-stream";
542
+ }
543
+ function bytes(size) { return new Uint8Array(size); }
544
+ function view(value) { return new DataView(value.buffer, value.byteOffset, value.byteLength); }
545
+ function setU64(v, offset, value) {
546
+ v.setUint32(offset, value >>> 0, true);
547
+ v.setUint32(offset + 4, Math.floor(value / 0x100000000), true);
548
+ }
549
+ function zip64Extra(values) {
550
+ const out = bytes(4 + values.length * 8);
551
+ const v = view(out);
552
+ v.setUint16(0, 0x0001, true);
553
+ v.setUint16(2, values.length * 8, true);
554
+ values.forEach((value, i) => setU64(v, 4 + i * 8, value));
555
+ return out;
556
+ }
557
+ function dosDate(date) {
558
+ const year = Math.max(1980, date.getFullYear());
559
+ return [
560
+ (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
561
+ ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
562
+ ];
563
+ }
564
+ const CRC_TABLE = (() => {
565
+ const table = new Uint32Array(256);
566
+ for (let n = 0; n < 256; n++) {
567
+ let c = n;
568
+ for (let k = 0; k < 8; k++)
569
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
570
+ table[n] = c >>> 0;
571
+ }
572
+ return table;
573
+ })();
574
+ function crc32Update(crc, data) {
575
+ let c = crc;
576
+ for (let i = 0; i < data.length; i++)
577
+ c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
578
+ return c >>> 0;
579
+ }
580
+ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
581
+ async function digestHex(data) {
582
+ const digest = await crypto.subtle.digest("SHA-256", data.buffer);
583
+ return [...new Uint8Array(digest)]
584
+ .map((byte) => byte.toString(16).padStart(2, "0")).join("");
585
+ }
@@ -0,0 +1,9 @@
1
+ import type * as z from "zod";
2
+ type Shape = Record<string, z.ZodType>;
3
+ /** I nomi dei gruppi, nell'ordine di dichiarazione. Salta i campi nascosti. */
4
+ export declare function groupsOf(shape: Shape): string[];
5
+ /** I campi che NON appartengono al gruppo: è la forma che vuole `omit` di SchemaForm. */
6
+ export declare function fieldsOutside(shape: Shape, group: string): string[];
7
+ /** Nome del gruppo -> pezzo di indirizzo. Serve solo a poter tornare su una sezione col link. */
8
+ export declare function groupSlug(group: string): string;
9
+ export {};
@@ -0,0 +1,32 @@
1
+ /* I gruppi di uno schema, letti dal registro `ui`.
2
+ *
3
+ * Servono alla seconda colonna della home: le sue sezioni non sono un elenco scritto a mano da
4
+ * qualche parte, sono i `group` dichiarati nello schema — nell'ordine in cui compaiono, che è
5
+ * anche l'ordine del file su disco. Aggiungere una sezione allo schema la fa comparire nella
6
+ * colonna senza toccare niente qui, esattamente come succede al form.
7
+ */
8
+ import { ui } from "../../index.js";
9
+ /** I nomi dei gruppi, nell'ordine di dichiarazione. Salta i campi nascosti. */
10
+ export function groupsOf(shape) {
11
+ const out = [];
12
+ for (const field of Object.values(shape)) {
13
+ const meta = ui.get(field);
14
+ if (!meta || meta.widget === "hidden")
15
+ continue;
16
+ const g = meta.group ?? "";
17
+ if (!out.includes(g))
18
+ out.push(g);
19
+ }
20
+ return out;
21
+ }
22
+ /** I campi che NON appartengono al gruppo: è la forma che vuole `omit` di SchemaForm. */
23
+ export function fieldsOutside(shape, group) {
24
+ return Object.entries(shape)
25
+ .filter(([, field]) => (ui.get(field)?.group ?? "") !== group)
26
+ .map(([name]) => name);
27
+ }
28
+ /** Nome del gruppo -> pezzo di indirizzo. Serve solo a poter tornare su una sezione col link. */
29
+ export function groupSlug(group) {
30
+ return group.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
31
+ .replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "generale";
32
+ }
@@ -0,0 +1,11 @@
1
+ export declare function useHome(active: boolean): {
2
+ data: Record<string, unknown> | null;
3
+ error: string | null;
4
+ dirty: boolean;
5
+ saving: boolean;
6
+ notice: string | null;
7
+ load: () => Promise<void>;
8
+ change: (next: Record<string, unknown>) => void;
9
+ save: () => Promise<void>;
10
+ setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
11
+ };