@cardinali-studio/adminkit 2.2.25 → 2.2.34

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 (59) hide show
  1. package/dist/api.d.ts +19 -0
  2. package/dist/api.js +3 -0
  3. package/dist/app/App.js +22 -8
  4. package/dist/app/app.css +3 -3
  5. package/dist/app/delivery.d.ts +11 -0
  6. package/dist/app/delivery.js +46 -0
  7. package/dist/app/index.d.ts +4 -1
  8. package/dist/app/index.js +3 -2
  9. package/dist/app/lib/bookings.js +5 -0
  10. package/dist/app/lib/deliveries.d.ts +8 -0
  11. package/dist/app/lib/deliveries.js +7 -0
  12. package/dist/app/lib/delivery-network.d.ts +25 -0
  13. package/dist/app/lib/delivery-network.js +159 -0
  14. package/dist/app/lib/delivery-upload.d.ts +29 -11
  15. package/dist/app/lib/delivery-upload.js +408 -131
  16. package/dist/app/lib/home.js +7 -4
  17. package/dist/app/lib/pages.js +2 -1
  18. package/dist/app/lib/publish.d.ts +5 -0
  19. package/dist/app/lib/publish.js +61 -4
  20. package/dist/app/lib/requests.js +6 -0
  21. package/dist/app/lib/settings.js +7 -3
  22. package/dist/app/lib/upload-queue.d.ts +21 -3
  23. package/dist/app/lib/upload-queue.js +76 -19
  24. package/dist/app/routes/Deliveries.js +108 -29
  25. package/dist/app/routes/PageActions.d.ts +1 -1
  26. package/dist/app/routes/PageActions.js +5 -2
  27. package/dist/app/routes/PageEditor.js +6 -4
  28. package/dist/app/routes/PageSeoEditor.js +6 -1
  29. package/dist/app/routes/PagesList.js +5 -1
  30. package/dist/app/routes/UploadDock.js +6 -2
  31. package/dist/app/site.d.ts +13 -0
  32. package/dist/direct-publication.d.ts +20 -0
  33. package/dist/direct-publication.js +75 -0
  34. package/dist/functions/_shared/b2.js +22 -3
  35. package/dist/functions/_shared/delivery-crypto.js +5 -0
  36. package/dist/functions/_shared/delivery-limits.d.ts +9 -0
  37. package/dist/functions/_shared/delivery-limits.js +29 -0
  38. package/dist/functions/_shared/delivery-manifest-v2.sql +49 -0
  39. package/dist/functions/_shared/delivery-manifest.js +392 -0
  40. package/dist/functions/_shared/delivery-purge.js +17 -1
  41. package/dist/functions/api/deliveries.js +177 -49
  42. package/dist/functions/api/delivery/image.js +11 -6
  43. package/dist/functions/api/delivery/multipart.js +196 -64
  44. package/dist/functions/delivery-public/assets/client.js +246 -0
  45. package/dist/functions/delivery-public/assets/delivery.css +297 -0
  46. package/dist/functions/delivery-public/assets/gallery.js +243 -0
  47. package/dist/functions/delivery-public/assets/stage.js +107 -0
  48. package/dist/functions/delivery-public/index.js +637 -0
  49. package/dist/functions/delivery-public/migrations/0002_deliveries.sql +128 -0
  50. package/dist/functions/delivery-public/migrations/0003_delivery_quick_hero.sql +34 -0
  51. package/dist/functions/delivery-public/migrations/0004_delivery_file_expiry.sql +9 -0
  52. package/dist/functions/delivery-public/migrations/0006_delivery_manifest_v2.sql +49 -0
  53. package/dist/functions/delivery-public/migrations/0007_delivery_link_aliases.sql +8 -0
  54. package/dist/functions/delivery-public/migrations/0008_delivery_web_format.sql +2 -0
  55. package/dist/functions/delivery-public/migrations/0009_delivery_download_labels.sql +2 -0
  56. package/dist/functions/index.js +3 -0
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +1 -0
  59. package/package.json +10 -3
package/dist/api.d.ts CHANGED
@@ -71,6 +71,8 @@ export type ContentDocumentPublishTarget = {
71
71
  documentId: string;
72
72
  expectedDraftRevisionId: string | null;
73
73
  includeSiteAccess?: boolean;
74
+ expectedBaseReleaseId?: string;
75
+ previewChecksum?: string;
74
76
  };
75
77
  export type ContentMediaPublishTarget = {
76
78
  mediaOperationId: string;
@@ -204,6 +206,23 @@ export declare const api: {
204
206
  enabled: boolean;
205
207
  } | null;
206
208
  activeReleaseId?: string | null;
209
+ previousReleaseId?: string | null;
210
+ }>;
211
+ contentPreview: (target: ContentDocumentPublishTarget) => Promise<{
212
+ ok: true;
213
+ previewUrl: string;
214
+ expectedBaseReleaseId: string;
215
+ expectedDraftRevisionId: string;
216
+ previewChecksum: string;
217
+ }>;
218
+ contentRollback: (expectedCurrentReleaseId: string, targetReleaseId: string) => Promise<{
219
+ ok: true;
220
+ releaseId: string;
221
+ }>;
222
+ contentVerification: (releaseId: string) => Promise<{
223
+ ok: true;
224
+ publicOriginVerified: boolean;
225
+ reason: string;
207
226
  }>;
208
227
  contentPublish: (target: ContentPublishTarget) => Promise<{
209
228
  ok: true;
package/dist/api.js CHANGED
@@ -73,6 +73,9 @@ export const api = {
73
73
  /** D1 publication is site-owned: the site chooses which documents it can
74
74
  * materialize, while the panel only displays the pending immutable inputs. */
75
75
  contentPublishState: () => call("/admin/api/content/publish"),
76
+ contentPreview: (target) => post("/admin/api/content/preview", target),
77
+ contentRollback: (expectedCurrentReleaseId, targetReleaseId) => post("/admin/api/content/rollback", { expectedCurrentReleaseId, targetReleaseId }),
78
+ contentVerification: (releaseId) => call("/admin/api/publication-status?releaseId=" + encodeURIComponent(releaseId)),
76
79
  contentPublish: (target) => post("/admin/api/content/publish", target),
77
80
  contentPublishMedia: (mediaOperationId) => post("/admin/api/content/publish", { mediaOperationId }),
78
81
  // ---- pubblicazione
package/dist/app/App.js CHANGED
@@ -20,8 +20,10 @@ import { useEffect, useMemo, useState } from "react";
20
20
  import "../theme.css";
21
21
  import "./app.css";
22
22
  import "./next.css";
23
- import { MediaFolders, MediaGrid, ProsePages, Trash, useMediaLibrary, usePicker } from "../form/index.js";
23
+ import { MediaFolders, MediaGrid, ProsePages, Trash, SchemaForm, useMediaLibrary, usePicker } from "../form/index.js";
24
24
  import { Preview, isLocalPreview } from "../preview.js";
25
+ import { api } from "../api.js";
26
+ import { ui } from "../registries.js";
25
27
  import { contentPersistenceMode, site } from "./site.js";
26
28
  import { useHashRoute } from "./lib/router";
27
29
  import { usePages, legacyDraft, legacyDraftTitles, discardLegacyDraft } from "./lib/pages";
@@ -71,6 +73,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
71
73
  const pages = usePages();
72
74
  const home = useHome(section === "home");
73
75
  const HomeEditorView = site().homeEditor ?? HomeEditor;
76
+ const MediaEditorView = site().mediaEditor;
74
77
  /* Anche la Panoramica: le sue card devono sapere quali funzioni aggiuntive sono attive, e
75
78
  lo stato è quello — non una seconda copia. È una lettura sola, e la Panoramica è la
76
79
  schermata da cui si entra, quindi arriva prima che serva. */
@@ -78,7 +81,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
78
81
  /* La libreria media vive QUI, non dentro un componente: le cartelle stanno nella seconda
79
82
  colonna e la griglia nella terza, e sono due punti dello schermo lontani. Uno stato per
80
83
  ciascuno vorrebbe dire due idee diverse di quale cartella si sta guardando. */
81
- const media = useMediaLibrary({ mode: "manage", active: section === "media" });
84
+ const media = useMediaLibrary({ mode: "manage", active: section === "media" && !MediaEditorView });
82
85
  const mediaSettingsOpen = view === "media" && route[1] === "settings";
83
86
  /* Le richieste si caricano SUBITO, non entrando nella sezione: il numero «da gestire» nella
84
87
  prima colonna è il motivo per cui uno ci entra, e un badge che compare solo dopo essere
@@ -258,7 +261,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
258
261
  || (section === "requests" && !openReq)
259
262
  || (section === "bookings" && !openBooking)
260
263
  || (section === "media" && (view === "trash" || mediaSettingsOpen || mediaCrumbs.length === 0));
261
- return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), "aria-label": n.label, disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", "aria-label": "Vedi il sito", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, "aria-label": theme === "dark" ? "Tema chiaro" : "Tema scuro", children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), "aria-label": "Impostazioni", disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Pagine" }), list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) })] }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
264
+ return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !(section === "media" && MediaEditorView) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), "aria-label": n.label, disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", "aria-label": "Vedi il sito", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, "aria-label": theme === "dark" ? "Tema chiaro" : "Tema scuro", children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), "aria-label": "Impostazioni", disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !(section === "media" && MediaEditorView) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Pagine" }), list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) })] }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
262
265
  : !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
263
266
  : _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
264
267
  if (editing?.slug === from)
@@ -283,13 +286,24 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
283
286
  : deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
284
287
  : _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
285
288
  : !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
286
- : _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen && mediaCrumbs.length > 0 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : !redundantPaneContext ? (_jsx("h1", { className: "pane-title", children: heading })) : null, view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
289
+ : _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen && mediaCrumbs.length > 0 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : !redundantPaneContext ? (_jsx("h1", { className: "pane-title", children: heading })) : null, view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && !MediaEditorView && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
287
290
  /* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
288
291
  rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
289
292
  il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
290
293
  qualcosa da pubblicare. */
291
- onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), contentPersistenceMode() === "d1" && d1ContextPending && otherPendingCount > 0 && (_jsx("span", { className: "pane-count", title: "Queste modifiche restano in bozza", children: otherPendingCount === 1 ? "1 altra bozza" : `${otherPendingCount} altre bozze` })), ((contentPersistenceMode() === "d1" && d1ContextPending)
292
- || (contentPersistenceMode() !== "d1" && publish.ahead > 0)) && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing, onClick: () => void publish.publish(d1PublishTarget ?? undefined), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
294
+ onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), contentPersistenceMode() === "d1" && d1ContextPending && otherPendingCount > 0 && (_jsx("span", { className: "pane-count", title: "Queste modifiche restano in bozza", children: otherPendingCount === 1 ? "1 altra bozza" : `${otherPendingCount} altre bozze` })), site().directPublication && _jsxs(_Fragment, { children: [d1ContextPending && d1PublishTarget && _jsx("button", { className: "ak-btn", disabled: publish.publishing || !!saver?.dirty || !!saver?.saving, onClick: () => void publish.preview(d1PublishTarget), children: "Anteprima" }), d1ContextPending && d1PublishTarget && _jsx("button", { className: "ak-btn", disabled: publish.publishing || !!saver?.dirty || !!saver?.saving, onClick: () => void api.discardContent(d1PublishTarget.documentId, d1PublishTarget.expectedDraftRevisionId, null).then(async () => { if (section === "pages")
295
+ await pages.load(); if (section === "home")
296
+ await home.load(); if (section === "settings")
297
+ await settings.load(); await publish.reload(); }), children: "Scarta bozza" }), _jsx("button", { className: "ak-btn", onClick: () => void publish.verify(), children: "Verifica sito pubblico" }), publish.previousReleaseId && _jsx("button", { className: "ak-btn", disabled: publish.publishing || pages.dirty || home.dirty || settings.dirty, onClick: () => { if (confirm("Ripristinare la release precedente del sito? Le bozze restano salvate."))
298
+ void publish.rollback().then(async () => { await pages.load(); await home.load(); await settings.load(); }); }, children: "Ripristina precedente" })] }), ((contentPersistenceMode() === "d1" && d1ContextPending)
299
+ || (contentPersistenceMode() !== "d1" && publish.ahead > 0)) && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing || !!saver?.dirty || !!saver?.saving || (site().directPublication === true && !publish.previewApproved(d1PublishTarget)), onClick: () => void publish.publish(d1PublishTarget ?? undefined).then(async () => { if (!saver?.dirty) {
300
+ if (section === "pages")
301
+ await pages.load();
302
+ if (section === "home")
303
+ await home.load();
304
+ if (section === "settings")
305
+ await settings.load();
306
+ } }), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
293
307
  : contentPersistenceMode() === "d1" ? contextualPublishLabel
294
308
  : publish.ahead === 1 ? "Pubblica (1 modifica)"
295
309
  : `Pubblica (${publish.ahead} modifiche)` }))] }), offModule ? _jsx(FeatureOff, { info: offModule }) : _jsxs(_Fragment, { children: [draft !== null && (_jsxs("div", { className: "app-legacy", children: [_jsxs("p", { className: "app-legacy-row", children: [_jsxs("span", { children: ["Questo browser conserva una bozza del ", _jsx("strong", { children: "vecchio" }), " pannello (", draft, " pagine). Non viene usata: quello che si vede qui \u00E8 il sito pubblicato."] }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setDraftOpen((v) => !v), children: draftOpen ? "Nascondi" : "Cosa contiene" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => { discardLegacyDraft(); setDraft(null); }, children: "Butta" })] }), draftOpen && _jsx("p", { className: "app-legacy-list", children: legacyDraftTitles().join(" · ") })] })), _jsxs("div", { className: "pane-body" + (wide ? " is-wide" : ""), children: [section === "overview" && (
@@ -304,9 +318,9 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
304
318
  se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
305
319
  home.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: home.error }), _jsx("button", { className: "ak-btn", onClick: () => void home.load(), children: "Riprova" })] }))
306
320
  : home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
307
- : _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (settingsView === "site" ? (_jsx(SiteSettingsUnavailable, { error: settings.error, onRetry: () => void settings.load(), adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface })) : (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] }))) : settings.data ? (settingsView === "seo"
321
+ : _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (settingsView === "site" ? (_jsx(SiteSettingsUnavailable, { error: settings.error, onRetry: () => void settings.load(), adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface })) : (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] }))) : settings.data ? site().settingsShape ? _jsx(SchemaForm, { shape: site().settingsShape, omit: Object.entries(site().settingsShape).filter(([, field]) => settingsView === "seo" ? ui.get(field)?.group !== "seo" : ui.get(field)?.group === "seo").map(([key]) => key), value: settings.data, onChange: (next) => settings.change(next), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles }) : (settingsView === "seo"
308
322
  ? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
309
- : _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
323
+ : _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (MediaEditorView ? _jsx(MediaEditorView, {}) : mediaSettingsOpen
310
324
  ? _jsx(MediaSettingsEditor, { lib: media })
311
325
  : _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
312
326
  : openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
package/dist/app/app.css CHANGED
@@ -1216,14 +1216,14 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
1216
1216
  border-bottom: 1px solid var(--ak-line);
1217
1217
  }
1218
1218
  .dl-cover-head p { margin: 3px 0 0; color: var(--ak-ink-quiet); font-size: 11px; }
1219
- .dl-cover-actions { display: flex; gap: 6px; flex: none; }
1220
- .dl-cover-grid { padding: 14px; display: grid; grid-template-columns: repeat(auto-fill,minmax(116px,1fr)); gap: 8px; }
1219
+ .dl-cover-actions { display: flex; flex-wrap: wrap; gap: 6px; flex: none; }
1220
+ .dl-cover-grid { padding: 14px; display: grid; grid-template-columns: repeat(auto-fill,minmax(min(150px,100%),1fr)); align-items: start; gap: 10px; }
1221
1221
  .dl-cover-grid button {
1222
1222
  position: relative; min-width: 0; padding: 0; overflow: hidden; cursor: pointer;
1223
1223
  border: 2px solid transparent; border-radius: 6px; background: var(--ak-bg);
1224
1224
  }
1225
1225
  .dl-cover-grid button.is-chosen { border-color: var(--ak-accent); }
1226
- .dl-cover-grid img { width: 100%; aspect-ratio: 3/2; display: block; object-fit: cover; }
1226
+ .dl-cover-grid img { width: 100%; height: auto; display: block; object-fit: contain; }
1227
1227
  .dl-cover-grid em {
1228
1228
  position: absolute; top: 5px; right: 5px; width: 18px; height: 18px; display: grid; place-items: center;
1229
1229
  border-radius: 50%; background: var(--ak-accent); color: #fff; font: 600 11px/1 var(--ak-font); font-style: normal;
@@ -0,0 +1,11 @@
1
+ import "../theme.css";
2
+ import "./app.css";
3
+ import "./next.css";
4
+ export interface DeliveryAdminOptions {
5
+ name: string;
6
+ storagePrefix: string;
7
+ root?: HTMLElement | null;
8
+ websiteUrl?: string;
9
+ }
10
+ /** Delivery-only entry: no editorial, GitHub, booking or R2 hooks are mounted. */
11
+ export declare function mountDeliveryAdmin(options: DeliveryAdminOptions): void;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StrictMode, useEffect, useState } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { setStoragePrefix } from "../index.js";
5
+ import { applyTheme, initialTheme, useTheme } from "./lib/theme.js";
6
+ import { useHashRoute } from "./lib/router.js";
7
+ import { useDeliveries } from "./lib/deliveries.js";
8
+ import { useUploadCompletions } from "./lib/upload-queue.js";
9
+ import { DeliveriesList, DeliveriesTotal, DeliveryWorkspace, NewDeliveryButton } from "./routes/Deliveries.js";
10
+ import { UploadDock } from "./routes/UploadDock.js";
11
+ import "../theme.css";
12
+ import "./app.css";
13
+ import "./next.css";
14
+ /** Delivery-only entry: no editorial, GitHub, booking or R2 hooks are mounted. */
15
+ export function mountDeliveryAdmin(options) {
16
+ setStoragePrefix(options.storagePrefix);
17
+ applyTheme(initialTheme());
18
+ const root = options.root ?? document.getElementById("root");
19
+ if (!root)
20
+ throw new Error("adminkit: elemento di montaggio non trovato");
21
+ createRoot(root).render(_jsx(StrictMode, { children: _jsx(DeliveryAdmin, { ...options }) }));
22
+ }
23
+ function DeliveryAdmin({ name, websiteUrl }) {
24
+ const [route, go] = useHashRoute();
25
+ const id = route[0] === "deliveries" ? route[1] : undefined;
26
+ const api = useDeliveries(id);
27
+ const { theme, toggle } = useTheme();
28
+ const completions = useUploadCompletions();
29
+ const [draftKey, setDraftKey] = useState(0);
30
+ const [selectedId, setSelectedId] = useState();
31
+ useEffect(() => { if (completions > 0)
32
+ void api.load(); }, [completions, api.load]);
33
+ useEffect(() => {
34
+ const prevent = (e) => e.preventDefault();
35
+ window.addEventListener("dragover", prevent);
36
+ window.addEventListener("drop", prevent);
37
+ return () => { window.removeEventListener("dragover", prevent); window.removeEventListener("drop", prevent); };
38
+ }, []);
39
+ const open = (value) => { setSelectedId(undefined); go(`/deliveries/${value}`); };
40
+ // Never render the previous transfer while the requested detail is loading.
41
+ const detail = id && api.detail?.id === id ? api.detail : null;
42
+ return _jsxs("div", { className: "app", children: [_jsxs("nav", { className: "rail", "aria-label": "Navigazione", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: name }), _jsx("span", { className: "rail-brand-role", children: "Consegne" })] }), _jsx("button", { className: "rail-item is-on", onClick: () => go("/deliveries"), children: "Transfer" }), _jsxs("div", { className: "rail-foot", children: [websiteUrl && _jsx("a", { className: "rail-item", href: websiteUrl, target: "_blank", rel: "noreferrer", children: "Sito dello studio" }), _jsx("button", { className: "rail-item", onClick: toggle, children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] })] }), _jsxs("aside", { className: "col", "aria-label": "Elenco transfer", children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Transfer" }), _jsx(NewDeliveryButton, { onCreate: () => { setSelectedId(undefined); setDraftKey(v => v + 1); go("/deliveries"); } })] }), _jsx("div", { className: "col-body", children: !api.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" }) : api.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
43
+ : _jsx(DeliveriesList, { rows: api.rows, current: id || selectedId, onOpen: open }) }), _jsx("div", { className: "dl-col-foot", children: api.rows && _jsx(DeliveriesTotal, { rows: api.rows }) })] }), _jsx("main", { className: "pane", children: _jsx("div", { className: "pane-body", children: api.error ? _jsxs("div", { role: "alert", children: [_jsx("p", { className: "ak-error", children: api.error }), _jsx("button", { className: "ak-btn", onClick: () => void api.refresh(), children: "Riprova" })] })
44
+ : id && !detail ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
45
+ : _jsx(DeliveryWorkspace, { delivery: detail, busy: api.busy, onAction: api.action, onRefresh: api.refresh, onReload: api.load, onRead: api.read, onDeleted: () => go("/deliveries"), onOpen: open, onSelected: setSelectedId }, id || `new-${draftKey}`) }) }), _jsx(UploadDock, {})] });
46
+ }
@@ -1,3 +1,4 @@
1
+ import { type ComponentType } from "react";
1
2
  import { type SiteAdminConfig } from "./site.js";
2
3
  export type { SiteAdminConfig } from "./site.js";
3
4
  export interface MountOptions {
@@ -5,6 +6,8 @@ export interface MountOptions {
5
6
  site: SiteAdminConfig;
6
7
  /** L'elemento in cui montare. Default: `#root`. */
7
8
  root?: HTMLElement | null;
9
+ /** Site-owned editorial workspace, sharing the kit bootstrap and media library. */
10
+ workspace?: ComponentType;
8
11
  /**
9
12
  * Namespace delle preferenze salvate nel browser (ordinamento dei media, ultima cartella
10
13
  * aperta nel selettore).
@@ -15,4 +18,4 @@ export interface MountOptions {
15
18
  */
16
19
  storagePrefix: string;
17
20
  }
18
- export declare function mountAdmin({ site, root, storagePrefix }: MountOptions): void;
21
+ export declare function mountAdmin({ site, root, storagePrefix, workspace }: MountOptions): void;
package/dist/app/index.js CHANGED
@@ -18,7 +18,7 @@ import { setStoragePrefix } from "../index.js";
18
18
  import { App } from "./App.js";
19
19
  import { applyTheme, initialTheme } from "./lib/theme.js";
20
20
  import { setSiteConfig } from "./site.js";
21
- export function mountAdmin({ site, root, storagePrefix }) {
21
+ export function mountAdmin({ site, root, storagePrefix, workspace }) {
22
22
  setSiteConfig(site);
23
23
  setStoragePrefix(storagePrefix);
24
24
  /* SINCRONO, prima del render: se il tema lo applicasse solo l'effetto dentro `App`, ci sarebbe
@@ -29,5 +29,6 @@ export function mountAdmin({ site, root, storagePrefix }) {
29
29
  const el = root ?? document.getElementById("root");
30
30
  if (!el)
31
31
  throw new Error("adminkit: elemento di montaggio non trovato (atteso #root)");
32
- createRoot(el).render(_jsx(StrictMode, { children: _jsx(App, {}) }));
32
+ const Workspace = workspace ?? App;
33
+ createRoot(el).render(_jsx(StrictMode, { children: _jsx(Workspace, {}) }));
33
34
  }
@@ -1,3 +1,4 @@
1
+ import { site } from "../site.js";
1
2
  import { useCallback, useEffect, useState } from "react";
2
3
  async function call(path, init) {
3
4
  const response = await fetch(`/admin/api/booking${path}`, {
@@ -17,6 +18,10 @@ export function useBookings() {
17
18
  loading: true, busy: false, error: null, notice: null,
18
19
  });
19
20
  const load = useCallback(async () => {
21
+ if (site().modules?.booking !== true) {
22
+ setState(value => ({ ...value, rows: [], pending: 0, upcoming: 0, loading: false, error: null }));
23
+ return;
24
+ }
20
25
  setState((value) => ({ ...value, loading: true, error: null }));
21
26
  try {
22
27
  const [list, types, settings, availability, google, activation, blocks] = await Promise.all([
@@ -7,6 +7,8 @@ export interface DeliverySummary {
7
7
  licenseNotes: string;
8
8
  ctaEnabled: boolean;
9
9
  ctaLabel: string;
10
+ downloadOriginalsLabel?: string;
11
+ downloadWebLabel?: string;
10
12
  ctaUrl: string;
11
13
  expiresAt: string | null;
12
14
  filesExpiresAt: string | null;
@@ -21,12 +23,15 @@ export interface DeliverySummary {
21
23
  downloadCount: number;
22
24
  }
23
25
  export interface DeliveryPackage {
26
+ manifestVersion?: 1 | 2;
27
+ nextCursor?: number | null;
24
28
  id: string;
25
29
  deliveryId: string;
26
30
  label: string;
27
31
  position: number;
28
32
  galleryEnabled: boolean;
29
33
  webEnabled: boolean;
34
+ webFormat?: "webp" | "jpg";
30
35
  state: "uploading" | "ready" | "failed";
31
36
  sourceFingerprint: string;
32
37
  fileCount: number;
@@ -37,6 +42,8 @@ export interface DeliveryPackage {
37
42
  images: DeliveryPackageImage[];
38
43
  }
39
44
  export interface DeliveryPackageImage {
45
+ width?: number;
46
+ height?: number;
40
47
  index: number;
41
48
  name: string;
42
49
  previewUrl: string;
@@ -72,6 +79,7 @@ export interface DeliveryLink {
72
79
  lastDownloadAt: string | null;
73
80
  }
74
81
  export interface Delivery extends DeliverySummary {
82
+ canShortenLinks?: boolean;
75
83
  packages: DeliveryPackage[];
76
84
  links: DeliveryLink[];
77
85
  heroes: DeliveryHero[];
@@ -1,3 +1,4 @@
1
+ import { site } from "../site.js";
1
2
  import { useCallback, useEffect, useState } from "react";
2
3
  import { ApiError } from "../../index.js";
3
4
  export function useDeliveries(activeId) {
@@ -8,6 +9,12 @@ export function useDeliveries(activeId) {
8
9
  const [error, setError] = useState(null);
9
10
  const [busy, setBusy] = useState(false);
10
11
  const load = useCallback(async () => {
12
+ if (site().modules?.delivery !== true) {
13
+ setRows([]);
14
+ setExpiring(0);
15
+ setError(null);
16
+ return;
17
+ }
11
18
  try {
12
19
  const out = await getJson("/admin/api/deliveries");
13
20
  setRows(out.deliveries);
@@ -0,0 +1,25 @@
1
+ /** Richieste recuperabili delle consegne. Usare retry solo per operazioni idempotenti. */
2
+ export declare class UploadCancelled extends Error {
3
+ constructor();
4
+ }
5
+ export interface UploadControl {
6
+ writerToken?: string;
7
+ gate(): Promise<void>;
8
+ signal?: AbortSignal;
9
+ networkWaiting?(waiting: boolean): void;
10
+ }
11
+ export declare class DeliveryRequestError extends Error {
12
+ readonly retryable: boolean;
13
+ readonly retryAfter: number;
14
+ constructor(message: string, retryable?: boolean, retryAfter?: number);
15
+ }
16
+ export declare function checkCancelled(control?: UploadControl): void;
17
+ export declare function deliveryGate(control?: UploadControl): Promise<void>;
18
+ export declare function waitForRetry(ms: number, signal?: AbortSignal): Promise<void>;
19
+ export declare function retryDelivery<T>(operation: () => Promise<T>, control?: UploadControl, policy?: {
20
+ maxElapsedMs: number;
21
+ baseDelayMs: number;
22
+ }): Promise<T>;
23
+ export declare function deliveryJson<T>(url: string, body: Record<string, unknown>, control?: UploadControl, retry?: boolean): Promise<T>;
24
+ /** La callback viene richiamata a ogni tentativo: le firme scadute non vengono riusate. */
25
+ export declare function deliveryPut(sign: () => Promise<string>, body: BodyInit, contentType: string, control?: UploadControl): Promise<string>;
@@ -0,0 +1,159 @@
1
+ /** Richieste recuperabili delle consegne. Usare retry solo per operazioni idempotenti. */
2
+ export class UploadCancelled extends Error {
3
+ constructor() { super("Caricamento annullato"); this.name = "UploadCancelled"; }
4
+ }
5
+ export class DeliveryRequestError extends Error {
6
+ retryable;
7
+ retryAfter;
8
+ constructor(message, retryable = false, retryAfter = 0) {
9
+ super(message);
10
+ this.retryable = retryable;
11
+ this.retryAfter = retryAfter;
12
+ }
13
+ }
14
+ export function checkCancelled(control) {
15
+ if (control?.signal?.aborted)
16
+ throw new UploadCancelled();
17
+ }
18
+ export async function deliveryGate(control) {
19
+ checkCancelled(control);
20
+ if (!control)
21
+ return;
22
+ let abort;
23
+ try {
24
+ await Promise.race([
25
+ control.gate(),
26
+ new Promise((_, reject) => {
27
+ abort = () => reject(new UploadCancelled());
28
+ control.signal?.addEventListener("abort", abort, { once: true });
29
+ if (control.signal?.aborted)
30
+ abort();
31
+ }),
32
+ ]);
33
+ checkCancelled(control);
34
+ }
35
+ finally {
36
+ if (abort)
37
+ control.signal?.removeEventListener("abort", abort);
38
+ }
39
+ }
40
+ export function waitForRetry(ms, signal) {
41
+ return new Promise((resolve, reject) => {
42
+ if (signal?.aborted) {
43
+ reject(new UploadCancelled());
44
+ return;
45
+ }
46
+ const timer = setTimeout(() => { signal?.removeEventListener("abort", abort); resolve(); }, ms);
47
+ function abort() { clearTimeout(timer); reject(new UploadCancelled()); }
48
+ signal?.addEventListener("abort", abort, { once: true });
49
+ });
50
+ }
51
+ export async function retryDelivery(operation, control, policy = { maxElapsedMs: 30 * 60_000, baseDelayMs: 1000 }) {
52
+ const started = Date.now();
53
+ let waiting = false;
54
+ try {
55
+ for (let attempt = 0;; attempt++) {
56
+ checkCancelled(control);
57
+ await deliveryGate(control);
58
+ try {
59
+ const result = await operation();
60
+ checkCancelled(control);
61
+ return result;
62
+ }
63
+ catch (error) {
64
+ checkCancelled(control);
65
+ if (!(error instanceof DeliveryRequestError) || !error.retryable ||
66
+ Date.now() - started >= policy.maxElapsedMs)
67
+ throw error;
68
+ if (!waiting) {
69
+ waiting = true;
70
+ control?.networkWaiting?.(true);
71
+ }
72
+ const delay = error.retryAfter || Math.min(30_000, policy.baseDelayMs * 2 ** Math.min(attempt, 5));
73
+ await waitForRetry(delay + Math.floor(Math.random() * policy.baseDelayMs / 4), control?.signal);
74
+ }
75
+ }
76
+ }
77
+ finally {
78
+ if (waiting)
79
+ control?.networkWaiting?.(false);
80
+ }
81
+ }
82
+ /** Il timeout comprende anche la lettura della risposta, non solo gli header. */
83
+ async function request(url, init, read, control, timeoutMs = 60_000) {
84
+ checkCancelled(control);
85
+ const controller = new AbortController();
86
+ const abort = () => controller.abort();
87
+ control?.signal?.addEventListener("abort", abort, { once: true });
88
+ const timer = setTimeout(abort, timeoutMs);
89
+ try {
90
+ let response;
91
+ try {
92
+ response = await fetch(url, { ...init, signal: controller.signal });
93
+ }
94
+ catch {
95
+ checkCancelled(control);
96
+ throw new DeliveryRequestError("Connessione interrotta. Attendo e riprovo…", true);
97
+ }
98
+ return await read(response);
99
+ }
100
+ catch (error) {
101
+ checkCancelled(control);
102
+ if (controller.signal.aborted)
103
+ throw new DeliveryRequestError("La richiesta non risponde. Riprovo…", true);
104
+ if (error instanceof TypeError)
105
+ throw new DeliveryRequestError("Risposta interrotta. Riprovo…", true);
106
+ throw error;
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ control?.signal?.removeEventListener("abort", abort);
111
+ }
112
+ }
113
+ function httpError(res, message, renewSignature = false) {
114
+ const transient = [408, 429, 500, 502, 503, 504].includes(res.status) || (renewSignature && res.status === 403);
115
+ const header = res.headers.get("retry-after");
116
+ const seconds = Number(header);
117
+ const retryAfter = header ? (Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - Date.now()) : 0;
118
+ return new DeliveryRequestError(message || `Richiesta non riuscita (HTTP ${res.status})`, transient, Number.isFinite(retryAfter) ? Math.max(0, Math.min(60_000, retryAfter)) : 0);
119
+ }
120
+ export function deliveryJson(url, body, control, retry = true) {
121
+ const operation = () => request(url, {
122
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(control?.writerToken ? { ...body, writerToken: control.writerToken } : body),
123
+ }, async (res) => {
124
+ let value;
125
+ try {
126
+ value = await res.json();
127
+ }
128
+ catch (error) {
129
+ if (!res.ok)
130
+ throw httpError(res);
131
+ if (error instanceof TypeError || res.headers.get("content-type")?.includes("application/json")) {
132
+ throw new DeliveryRequestError("La risposta si è interrotta. Riprovo…", true);
133
+ }
134
+ throw new DeliveryRequestError("Risposta non valida. Verifica l’accesso al pannello e riprendi il caricamento.");
135
+ }
136
+ if (!res.ok || value.ok !== true) {
137
+ const error = httpError(res, value.error);
138
+ throw value.retryable === false ? new DeliveryRequestError(error.message) : error;
139
+ }
140
+ return value;
141
+ }, control);
142
+ return retry ? retryDelivery(operation, control) : operation();
143
+ }
144
+ /** La callback viene richiamata a ogni tentativo: le firme scadute non vengono riusate. */
145
+ export function deliveryPut(sign, body, contentType, control) {
146
+ return retryDelivery(async () => {
147
+ const url = await sign();
148
+ await deliveryGate(control);
149
+ return request(url, { method: "PUT", body, headers: { "Content-Type": contentType } }, async (res) => {
150
+ if (!res.ok) {
151
+ await res.body?.cancel();
152
+ throw httpError(res, undefined, true);
153
+ }
154
+ const etag = res.headers.get("etag") || "";
155
+ await res.body?.cancel();
156
+ return etag;
157
+ }, control, 30 * 60_000);
158
+ }, control);
159
+ }
@@ -1,3 +1,5 @@
1
+ import { type UploadControl } from "./delivery-network.js";
2
+ export { UploadCancelled, type UploadControl } from "./delivery-network.js";
1
3
  export interface DeliveryUploadProgress {
2
4
  phase: "originals" | "derivatives" | "finalizing";
3
5
  file: string;
@@ -20,7 +22,7 @@ export interface DeliveryManifestEntry {
20
22
  height?: number;
21
23
  }
22
24
  export interface DeliveryManifest {
23
- version: 1;
25
+ version: 1 | 2;
24
26
  createdAt: string;
25
27
  entries: DeliveryManifestEntry[];
26
28
  skipped: {
@@ -33,9 +35,11 @@ interface ZipMember {
33
35
  length: number;
34
36
  crc32: number;
35
37
  }
36
- interface UploadOptions {
38
+ export interface UploadOptions {
39
+ manifestVersion?: 1 | 2;
37
40
  galleryEnabled: boolean;
38
41
  webEnabled: boolean;
42
+ webFormat?: "webp" | "jpg";
39
43
  }
40
44
  interface UploadResult {
41
45
  manifest: DeliveryManifest;
@@ -43,28 +47,43 @@ interface UploadResult {
43
47
  sourceBytes: number;
44
48
  storedBytes: number;
45
49
  }
46
- export interface UploadControl {
47
- gate(): Promise<void>;
48
- }
49
- export declare class UploadCancelled extends Error {
50
- constructor();
50
+ interface MemberCheckpoint {
51
+ name: string;
52
+ size: number;
53
+ read: number;
54
+ crc: number;
55
+ chain: string;
56
+ localOffset: number;
57
+ dataOffset: number;
58
+ time: number;
59
+ date: number;
51
60
  }
52
61
  export declare function uploadDeliveryPackage(deliveryId: string, packageId: string, selected: File[], options: UploadOptions, onProgress: (progress: DeliveryUploadProgress) => void, control?: UploadControl): Promise<UploadResult>;
53
62
  interface ZipSink {
63
+ gate?(): Promise<void>;
54
64
  write(data: Uint8Array): Promise<void>;
55
65
  close(): Promise<void>;
56
66
  }
57
67
  export declare class ZipStream {
68
+ private utc;
58
69
  private sink;
59
70
  private offset;
60
71
  private entries;
61
- constructor(sink: ZipSink);
62
- add(nameValue: string, blob: Blob, progress?: (read: number) => void, modified?: Date): Promise<ZipMember>;
72
+ constructor(sink: ZipSink, utc?: boolean);
73
+ restore(entries: DeliveryManifestEntry[], files: File[], offset: number): void;
74
+ add(nameValue: string, blob: Blob, progress?: (read: number) => void, modified?: Date, checkpoint?: (current: MemberCheckpoint, offset: number) => Promise<void>, resume?: MemberCheckpoint): Promise<ZipMember>;
63
75
  close(): Promise<void>;
76
+ private sinkGate;
64
77
  private push;
65
78
  }
66
79
  export declare function setDeliveryRelativePath(file: File, path: string): void;
67
80
  export declare function deliveryRelativeName(file: File): string;
81
+ /** Solo metadati: nessuna lettura dei file o richiesta di rete prima del rifiuto. */
82
+ export declare function preflightDeliverySelection(files: File[], options?: UploadOptions): {
83
+ sourceBytes: number;
84
+ archiveBytes: number;
85
+ manifestBytes: number;
86
+ };
68
87
  export declare function deliverySelectionName(files: File[]): string;
69
88
  export declare function uploadDeliveryHeroObject(deliveryId: string, packageId: string, file: File): Promise<{
70
89
  type: "object";
@@ -74,5 +93,4 @@ export declare function uploadDeliveryHeroObject(deliveryId: string, packageId:
74
93
  height: number;
75
94
  bytes: number;
76
95
  }>;
77
- export declare function deliverySourceFingerprint(files: File[]): Promise<string>;
78
- export {};
96
+ export declare function deliverySourceFingerprint(files: File[], version?: 1 | 2): Promise<string>;