@cardinali-studio/adminkit 2.2.18 → 2.2.20

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.
package/dist/api.d.ts CHANGED
@@ -46,6 +46,10 @@ export interface RepoPage {
46
46
  url: string;
47
47
  /** Lo `sha` del blob su GitHub: identifica la VERSIONE di questo file, non il suo contenuto. */
48
48
  sha: string;
49
+ /** Identità D1 site-owned. Assenti nella corsia Git/Hugo. */
50
+ documentId?: string;
51
+ draftRevisionId?: string | null;
52
+ publishedRevisionId?: string | null;
49
53
  page: Record<string, unknown>;
50
54
  /** Le sibling dichiarate dal sito, senza caricare il loro contenuto nell'elenco. */
51
55
  translations?: Record<string, {
@@ -53,6 +57,18 @@ export interface RepoPage {
53
57
  sha?: string;
54
58
  }>;
55
59
  }
60
+ export type ContentPublishTarget = {
61
+ documentId: string;
62
+ expectedDraftRevisionId: string | null;
63
+ includeSiteAccess?: boolean;
64
+ };
65
+ export type ContentPublishDocument = {
66
+ documentId: string;
67
+ kind: string;
68
+ updatedAt: number;
69
+ draftRevisionId: string;
70
+ publishedRevisionId: string | null;
71
+ };
56
72
  export type ContentSaveCommand = {
57
73
  revisionId: string;
58
74
  documentId: string;
@@ -122,6 +138,11 @@ export declare const api: {
122
138
  files: number;
123
139
  skipped: string[];
124
140
  shas?: Record<string, string>;
141
+ documents?: Record<string, {
142
+ documentId: string;
143
+ draftRevisionId: string | null;
144
+ publishedRevisionId: string | null;
145
+ }>;
125
146
  }>;
126
147
  contentState: () => Promise<{
127
148
  ok: true;
@@ -163,23 +184,24 @@ export declare const api: {
163
184
  ok: true;
164
185
  configured: true;
165
186
  ahead: number;
166
- documents: {
167
- documentId: string;
168
- kind: string;
169
- updatedAt: number;
170
- }[];
187
+ documents: ContentPublishDocument[];
171
188
  mediaOperations?: MediaOperationStatus[];
172
189
  siteAccess?: {
173
190
  enabled: boolean;
174
191
  } | null;
175
192
  activeReleaseId?: string | null;
176
193
  }>;
177
- contentPublish: () => Promise<{
194
+ contentPublish: (target: ContentPublishTarget) => Promise<{
178
195
  ok: true;
179
196
  published: boolean;
180
197
  releaseId?: string;
181
198
  attemptId?: string;
182
199
  message?: string;
200
+ verified?: boolean;
201
+ observedReleaseId?: string | null;
202
+ documents?: number;
203
+ dependentDocuments?: number;
204
+ remainingAhead?: number;
183
205
  }>;
184
206
  publishState: () => Promise<{
185
207
  ok: true;
@@ -288,6 +310,9 @@ export declare const api: {
288
310
  trashed: number;
289
311
  pagesUpdated: string[];
290
312
  refsToken?: string;
313
+ d1?: boolean;
314
+ deferred?: boolean;
315
+ operationId?: string;
291
316
  }>;
292
317
  requests: () => Promise<{
293
318
  ok: true;
package/dist/api.js CHANGED
@@ -66,7 +66,7 @@ export const api = {
66
66
  /** D1 publication is site-owned: the site chooses which documents it can
67
67
  * materialize, while the panel only displays the pending immutable inputs. */
68
68
  contentPublishState: () => call("/admin/api/content/publish"),
69
- contentPublish: () => post("/admin/api/content/publish", {}),
69
+ contentPublish: (target) => post("/admin/api/content/publish", target),
70
70
  // ---- pubblicazione
71
71
  /* Salvare scrive sulla bozza e non fa partire nessuna build; è questo che manda online. Due
72
72
  * gesti separati, quindi due chiamate: una chiede quanto c'è di non pubblicato, l'altra lo
package/dist/app/App.js CHANGED
@@ -21,7 +21,7 @@ import "../theme.css";
21
21
  import "./app.css";
22
22
  import { MediaFolders, MediaGrid, ProsePages, Trash, useMediaLibrary, usePicker } from "../form/index.js";
23
23
  import { Preview, isLocalPreview } from "../preview.js";
24
- import { site } from "./site.js";
24
+ import { contentPersistenceMode, site } from "./site.js";
25
25
  import { useHashRoute } from "./lib/router";
26
26
  import { usePages, legacyDraft, legacyDraftTitles, discardLegacyDraft } from "./lib/pages";
27
27
  import { useHome } from "./lib/home";
@@ -185,14 +185,32 @@ export function App() {
185
185
  /* Chi risponde del «Salva»: le pagine, la home, o nessuno (media e richieste si scrivono nel
186
186
  * momento in cui si fa il gesto — un bottone di salvataggio lì sarebbe una promessa falsa).
187
187
  *
188
- * «Pubblica» invece NON passa di qui: è globale. Salvare scrive sulla bozza, e pubblicare
189
- * manda online tutto ciò che c'è sulla bozza — pagine, home e impostazioni insieme — non la
190
- * sezione che si sta guardando. Metterlo dentro `saver` avrebbe voluto dire tre pulsanti che
191
- * fanno la stessa identica cosa. */
188
+ * In D1 «Pubblica» usa invece l'identità del contenuto aperto: le altre bozze rimangono
189
+ * salvate. La corsia Git/Hugo conserva il suo publish globale. */
192
190
  const saver = section === "pages" ? pages
193
191
  : section === "home" ? home
194
192
  : section === "settings" ? settings
195
193
  : null;
194
+ const removedPageTarget = section === "pages" && route[1]
195
+ ? pages.removedPublishTargets[route[1]] ?? null : null;
196
+ const d1PublishTarget = section === "pages" && editing?._documentId
197
+ ? { documentId: editing._documentId, expectedDraftRevisionId: editing._draftRevisionId ?? null }
198
+ : section === "pages" && removedPageTarget ? removedPageTarget
199
+ : section === "home" ? home.publishTarget
200
+ : section === "settings" ? settings.publishTarget
201
+ : null;
202
+ const d1DocumentPending = d1PublishTarget
203
+ ? publish.documents.some((document) => document.documentId === d1PublishTarget.documentId
204
+ && document.draftRevisionId === d1PublishTarget.expectedDraftRevisionId)
205
+ : false;
206
+ const d1ContextPending = Boolean(d1PublishTarget && (d1DocumentPending
207
+ || (section === "settings" && publish.siteAccessPending)));
208
+ const contextualPublishLabel = section === "pages" ? (removedPageTarget ? "Pubblica eliminazione" : "Pubblica questa pagina")
209
+ : section === "home" ? "Pubblica la Home"
210
+ : "Pubblica le impostazioni";
211
+ const contextualPendingCount = (d1DocumentPending ? 1 : 0)
212
+ + (section === "settings" && publish.siteAccessPending ? 1 : 0);
213
+ const otherPendingCount = Math.max(0, publish.ahead - contextualPendingCount);
196
214
  const heading = section === "overview" ? "Panoramica"
197
215
  : section === "pages" ? (editing ? String(editing.title ?? editing.slug) : "Pagine")
198
216
  : section === "home" ? (homeGroup || "Home")
@@ -219,7 +237,7 @@ export function App() {
219
237
  const p = all.slice(0, i + 1).join("/") + "/";
220
238
  return { prefix: p, label: media.labelOf(p) };
221
239
  });
222
- 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" : ""), 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", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, 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" : ""), 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: [_jsx("div", { className: "col-head", children: 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 })
240
+ 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: [_jsx("div", { className: "col-head", children: 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 })
223
241
  : !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
224
242
  : _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
225
243
  if (editing?.slug === from)
@@ -249,9 +267,11 @@ export function App() {
249
267
  rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
250
268
  il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
251
269
  qualcosa da pubblicare. */
252
- onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), publish.ahead > 0 && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing, onClick: () => void publish.publish(), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
253
- : publish.ahead === 1 ? "Pubblica (1 modifica)"
254
- : `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" && (
270
+ 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)
271
+ || (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…"
272
+ : contentPersistenceMode() === "d1" ? contextualPublishLabel
273
+ : publish.ahead === 1 ? "Pubblica (1 modifica)"
274
+ : `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" && (
255
275
  /* Transfer e richieste arrivano da QUI, già caricati per i badge della prima
256
276
  colonna: la Panoramica non li richiede una seconda volta, altrimenti lo stesso
257
277
  numero avrebbe due sorgenti e una delle due sarebbe sempre indietro. */
package/dist/app/app.css CHANGED
@@ -745,49 +745,63 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
745
745
  }
746
746
  .pe-photo-overlay img { max-width: 100%; max-height: 100%; display: block; }
747
747
 
748
- /* La modalità masonry replica il flusso del sito: le foto riempiono ogni colonna dall'alto
749
- verso il basso e conservano il proprio rapporto d'aspetto. Le regole base qui sopra restano
750
- quindi intatte per tutte le gallery con layout ordinario. */
751
- .pe-gallery.is-masonry > .pe-photo-grid {
748
+ /* Il masonry verticale usa il flusso CSS a colonne: riempie una colonna dall'alto verso il
749
+ basso prima di passare alla successiva. `.is-masonry` resta come alias visuale per markup
750
+ legacy; i nuovi consumer ricevono sempre il nome esplicito. */
751
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid {
752
752
  display: block; column-count: 3; column-gap: 1.5vw;
753
753
  padding: 4px 2px 40px;
754
754
  }
755
- .pe-gallery.is-masonry > .pe-photo-grid > .pe-photo {
755
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid > .pe-photo {
756
756
  width: 100%; margin: 0 0 1.5vw; padding: 0;
757
757
  display: inline-block; break-inside: avoid; -webkit-column-break-inside: avoid;
758
758
  vertical-align: top;
759
759
  }
760
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo-frame {
760
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo-frame {
761
761
  width: 100%; height: auto; display: block; overflow: hidden;
762
762
  }
763
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo-frame img {
763
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo-frame img {
764
764
  width: 100%; height: auto; max-width: none; max-height: none;
765
765
  }
766
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo-actions {
766
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo-actions {
767
767
  top: 8px; transform: translate(-50%, 0);
768
768
  }
769
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:hover .pe-photo-actions,
770
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:focus-within .pe-photo-actions {
769
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:hover .pe-photo-actions,
770
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:focus-within .pe-photo-actions {
771
771
  transform: translate(-50%, -2px);
772
772
  }
773
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo figcaption {
773
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo figcaption {
774
774
  position: absolute; z-index: 2; right: 8px; bottom: 8px; left: 8px;
775
775
  padding: 6px 8px; border-radius: 4px; background: rgba(20,20,22,.72);
776
776
  color: #fff; text-align: left; opacity: 0; pointer-events: none;
777
777
  transition: opacity .12s;
778
778
  }
779
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:hover figcaption,
780
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo:focus-within figcaption { opacity: 1; }
781
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-before::before,
782
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-after::after {
779
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:hover figcaption,
780
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo:focus-within figcaption { opacity: 1; }
781
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-before::before,
782
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-after::after {
783
783
  right: 0; left: 0; width: auto; height: 3px;
784
784
  }
785
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-before::before {
785
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-before::before {
786
786
  top: -7px; bottom: auto;
787
787
  }
788
- .pe-gallery.is-masonry > .pe-photo-grid .pe-photo.is-insert-after::after {
788
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid .pe-photo.is-insert-after::after {
789
789
  top: auto; bottom: -7px;
790
790
  }
791
+
792
+ /* Il masonry orizzontale mostra l'ordine editoriale da sinistra a destra. Sei slot rendono
793
+ ogni riga dell'admin equivalente a due battute della gallery pubblica a tre colonne; il
794
+ canvas uniforme mantiene stabile il drag e `contain` rende leggibile l'orientamento. */
795
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid {
796
+ display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
797
+ gap: 20px 12px; column-count: auto; padding: 4px 2px 40px;
798
+ }
799
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid .pe-photo-frame {
800
+ height: clamp(112px, 8.5vw, 150px); aspect-ratio: auto;
801
+ }
802
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid .pe-photo-frame img {
803
+ width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain;
804
+ }
791
805
  .pe-hidden-gallery {
792
806
  display: flex; flex-direction: column; gap: 18px; padding: 24px 2px 8px;
793
807
  border-top: 1px solid var(--ak-line);
@@ -2127,11 +2141,13 @@ body:has(.uk) .pane-body { padding-bottom: 190px; }
2127
2141
  .seo-row > .seo-field,
2128
2142
  .seo-row > .seo-help { grid-column: 2; }
2129
2143
  .seo-help { padding-top: 0; }
2144
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2130
2145
  }
2131
2146
 
2132
2147
  @media (max-width: 900px) {
2133
- .pe-gallery.is-masonry > .pe-photo-grid { column-count: 2; column-gap: 3vw; }
2134
- .pe-gallery.is-masonry > .pe-photo-grid > .pe-photo { margin-bottom: 3vw; }
2148
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid { column-count: 2; column-gap: 3vw; }
2149
+ .pe-gallery:is(.is-masonry, .is-masonry-vertical) > .pe-photo-grid > .pe-photo { margin-bottom: 3vw; }
2150
+ .pe-gallery.is-masonry-horizontal > .pe-photo-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2135
2151
  .settings-editor { padding-inline: 6px; gap: 34px; }
2136
2152
  .st-row { grid-template-columns: minmax(0, 1fr); gap: 15px; padding: 22px 0; }
2137
2153
  .seo-editor { padding-inline: 6px; }
@@ -0,0 +1,8 @@
1
+ export type GalleryLayout = "grid" | "masonry-vertical" | "masonry-horizontal";
2
+ /**
3
+ * `masonry` era il solo valore speciale supportato dal kit e descriveva il
4
+ * flusso CSS a colonne, quindi resta un alias del layout verticale. I siti
5
+ * possono migrare ai nomi espliciti senza cambiare di colpo documenti già
6
+ * pubblicati.
7
+ */
8
+ export declare function normalizeGalleryLayout(value: unknown): GalleryLayout;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `masonry` era il solo valore speciale supportato dal kit e descriveva il
3
+ * flusso CSS a colonne, quindi resta un alias del layout verticale. I siti
4
+ * possono migrare ai nomi espliciti senza cambiare di colpo documenti già
5
+ * pubblicati.
6
+ */
7
+ export function normalizeGalleryLayout(value) {
8
+ if (value === "masonry" || value === "masonry-vertical")
9
+ return "masonry-vertical";
10
+ if (value === "masonry-horizontal")
11
+ return "masonry-horizontal";
12
+ return "grid";
13
+ }
@@ -8,4 +8,8 @@ export declare function useHome(active: boolean): {
8
8
  change: (next: Record<string, unknown>) => void;
9
9
  save: () => Promise<void>;
10
10
  setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
11
+ publishTarget: {
12
+ documentId: string;
13
+ expectedDraftRevisionId: string | null;
14
+ } | null;
11
15
  };
@@ -89,5 +89,9 @@ export function useHome(active) {
89
89
  }
90
90
  }, [data, revision]);
91
91
  const change = useCallback((next) => { setData(next); setDirty(true); }, []);
92
- return { data, error, dirty, saving, notice, load, change, save, setNotice };
92
+ const mapped = site().homeDocument;
93
+ return { data, error, dirty, saving, notice, load, change, save, setNotice,
94
+ publishTarget: contentPersistenceMode() === "d1" && mapped && revision
95
+ ? { documentId: mapped.documentId, expectedDraftRevisionId: revision.expected }
96
+ : null };
93
97
  }
@@ -4,6 +4,10 @@ export type PageModel = Record<string, unknown> & {
4
4
  /** Versione del file da cui questa pagina è stata letta. Torna al server nel salvataggio: se
5
5
  * nel frattempo il file è cambiato, il server rifiuta invece di sovrascrivere. */
6
6
  _sha?: string;
7
+ /** Metadati D1 usati soltanto per pubblicare la pagina aperta. */
8
+ _documentId?: string;
9
+ _draftRevisionId?: string | null;
10
+ _publishedRevisionId?: string | null;
7
11
  /** Presenza/versione delle sibling dichiarate; il loro contenuto è caricato solo aprendole. */
8
12
  translations?: Record<string, {
9
13
  exists: boolean;
@@ -28,6 +32,10 @@ export declare function usePages(): {
28
32
  isPristine: (slug: string) => boolean;
29
33
  save: () => Promise<void>;
30
34
  setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
35
+ removedPublishTargets: Record<string, {
36
+ documentId: string;
37
+ expectedDraftRevisionId: string | null;
38
+ }>;
31
39
  };
32
40
  export declare function legacyDraft(): number | null;
33
41
  /** I titoli che la bozza contiene, per poterla guardare prima di buttarla. */
@@ -23,6 +23,7 @@ export function usePages() {
23
23
  const [dirty, setDirty] = useState(false);
24
24
  const [saving, setSaving] = useState(false);
25
25
  const [notice, setNotice] = useState(null);
26
+ const [removedPublishTargets, setRemovedPublishTargets] = useState({});
26
27
  const load = useCallback(async () => {
27
28
  try {
28
29
  const d = await api.pages();
@@ -31,10 +32,13 @@ export function usePages() {
31
32
  * andava riconciliato con `modelFromRaw` — un passaggio in meno, e una grafia in meno da
32
33
  * far combaciare. L'ordine per peso lo dà già il server. */
33
34
  const list = (d.pages ?? []).map((p) => ({
34
- ...p.page, slug: p.slug, url: p.url, _orig: p.slug, _sha: p.sha, translations: p.translations,
35
+ ...p.page, slug: p.slug, url: p.url, _orig: p.slug, _sha: p.sha,
36
+ _documentId: p.documentId, _draftRevisionId: p.draftRevisionId,
37
+ _publishedRevisionId: p.publishedRevisionId, translations: p.translations,
35
38
  }));
36
39
  base.current = new Map(list.map((p) => [p.slug, p]));
37
40
  setPages(list);
41
+ setRemovedPublishTargets({});
38
42
  setDirty(false);
39
43
  setError(null);
40
44
  }
@@ -89,8 +93,16 @@ export function usePages() {
89
93
  pages.forEach((p) => {
90
94
  p._orig = p.slug;
91
95
  const s = r.shas?.[p.slug];
92
- if (s)
96
+ if (s) {
93
97
  p._sha = s;
98
+ p._draftRevisionId = s;
99
+ }
100
+ const document = r.documents?.[p.slug];
101
+ if (document) {
102
+ p._documentId = document.documentId;
103
+ p._draftRevisionId = document.draftRevisionId;
104
+ p._publishedRevisionId = document.publishedRevisionId;
105
+ }
94
106
  /* Le sibling hanno lo stesso ciclo di vita del file base: se non si aggiorna il loro
95
107
  SHA qui, la modifica EN appena salvata parte già da una versione che non esiste più
96
108
  e il Worker la rifiuta come conflitto al click successivo. */
@@ -108,6 +120,13 @@ export function usePages() {
108
120
  p._translations[code]._sha = translationSha;
109
121
  }
110
122
  });
123
+ const liveSlugs = new Set(pages.map((page) => page.slug));
124
+ setRemovedPublishTargets(Object.fromEntries(Object.entries(r.documents ?? {})
125
+ .filter(([slug]) => !liveSlugs.has(slug))
126
+ .map(([slug, document]) => [slug, {
127
+ documentId: document.documentId,
128
+ expectedDraftRevisionId: document.draftRevisionId,
129
+ }])));
111
130
  // Da qui in poi il file contiene ciò che hanno queste pagine: tornano tutte "non toccate".
112
131
  base.current = new Map(pages.map((p) => [p.slug, p]));
113
132
  }
@@ -124,7 +143,8 @@ export function usePages() {
124
143
  setSaving(false);
125
144
  }
126
145
  }, [pages]);
127
- return { pages, error, dirty, saving, notice, load, update, refresh, isPristine, save, setNotice };
146
+ return { pages, error, dirty, saving, notice, load, update, refresh, isPristine, save, setNotice,
147
+ removedPublishTargets };
128
148
  }
129
149
  /* Bozza lasciata dal VECCHIO pannello nel localStorage di questo browser: si mostra, non si usa.
130
150
  *
@@ -1,3 +1,4 @@
1
+ import { type ContentPublishDocument, type ContentPublishTarget } from "../../index.js";
1
2
  export interface PublishState {
2
3
  /** Quanti commit ci sono sulla bozza che non sono ancora online. */
3
4
  ahead: number;
@@ -6,13 +7,20 @@ export interface PublishState {
6
7
  message: string;
7
8
  date: string | null;
8
9
  }[];
10
+ documents: ContentPublishDocument[];
11
+ siteAccessPending: boolean;
9
12
  /** Falso se il branch di bozza non esiste ancora (passo manuale di configurazione). */
10
13
  configured: boolean;
11
14
  publishing: boolean;
12
15
  notice: string | null;
13
16
  error: string | null;
14
17
  reload(): Promise<void>;
15
- publish(): Promise<void>;
18
+ publish(target?: ContentPublishTarget): Promise<void>;
16
19
  clearNotice(): void;
17
20
  }
21
+ export declare function contentPublicationNotice(result: {
22
+ published: boolean;
23
+ message?: string;
24
+ verified?: boolean;
25
+ }): string;
18
26
  export declare function usePublish(): PublishState;
@@ -8,10 +8,20 @@
8
8
  import { useCallback, useEffect, useState } from "react";
9
9
  import { api, ApiError } from "../../index.js";
10
10
  import { contentPersistenceMode } from "../site.js";
11
+ export function contentPublicationNotice(result) {
12
+ if (result.published === false) {
13
+ return result.message ?? "Non c'erano modifiche salvate da pubblicare.";
14
+ }
15
+ return result.message ?? (result.verified === true
16
+ ? "Pubblicato e verificato online."
17
+ : "Pubblicato. Verifica online in attesa.");
18
+ }
11
19
  export function usePublish() {
12
20
  const [ahead, setAhead] = useState(0);
13
21
  const [commits, setCommits] = useState([]);
14
22
  const [configured, setConfigured] = useState(true);
23
+ const [documents, setDocuments] = useState([]);
24
+ const [siteAccessPending, setSiteAccessPending] = useState(false);
15
25
  const [publishing, setPublishing] = useState(false);
16
26
  const [notice, setNotice] = useState(null);
17
27
  const [error, setError] = useState(null);
@@ -20,6 +30,8 @@ export function usePublish() {
20
30
  try {
21
31
  const d = await api.contentPublishState();
22
32
  setAhead(d.ahead ?? 0);
33
+ setDocuments(d.documents ?? []);
34
+ setSiteAccessPending(Boolean(d.siteAccess));
23
35
  setCommits((d.documents ?? []).map((document) => ({
24
36
  message: `Modifica salvata (${document.kind})`, date: document.updatedAt ? new Date(document.updatedAt).toISOString() : null,
25
37
  })).concat((d.mediaOperations ?? [])
@@ -42,6 +54,8 @@ export function usePublish() {
42
54
  }
43
55
  try {
44
56
  const d = await api.publishState();
57
+ setDocuments([]);
58
+ setSiteAccessPending(false);
45
59
  setAhead(d.ahead ?? 0);
46
60
  setCommits(d.commits ?? []);
47
61
  setConfigured(d.configured !== false);
@@ -57,15 +71,15 @@ export function usePublish() {
57
71
  * per cui uno guarda quel pulsante, e un conteggio che compare solo dopo esserci passati sopra
58
72
  * non serve a niente. È la stessa ragione dei badge di Richieste e Consegne. */
59
73
  useEffect(() => { void reload(); }, [reload]);
60
- const publish = useCallback(async () => {
74
+ const publish = useCallback(async (target) => {
61
75
  if (contentPersistenceMode() === "d1") {
62
76
  setPublishing(true);
63
77
  setNotice(null);
64
78
  try {
65
- const r = await api.contentPublish();
66
- setNotice(r.published === false
67
- ? r.message ?? "Non c'erano modifiche salvate da pubblicare."
68
- : "Pubblicazione avviata. Il sito si aggiorna fra qualche minuto.");
79
+ if (!target)
80
+ throw new ApiError("Apri il contenuto che vuoi pubblicare.", "invalid_request", 400);
81
+ const r = await api.contentPublish(target);
82
+ setNotice(contentPublicationNotice(r));
69
83
  await reload();
70
84
  }
71
85
  catch (e) {
@@ -101,7 +115,7 @@ export function usePublish() {
101
115
  }
102
116
  }, [reload]);
103
117
  return {
104
- ahead, commits, configured, publishing, notice, error,
118
+ ahead, commits, documents, siteAccessPending, configured, publishing, notice, error,
105
119
  reload, publish, clearNotice: () => setNotice(null),
106
120
  };
107
121
  }
@@ -28,4 +28,9 @@ export declare function useSiteSettings(active: boolean): {
28
28
  setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
29
29
  accessDraftPending: boolean;
30
30
  discardSiteAccess: () => Promise<void>;
31
+ publishTarget: {
32
+ documentId: string;
33
+ expectedDraftRevisionId: string | null;
34
+ includeSiteAccess: boolean;
35
+ } | null;
31
36
  };
@@ -152,5 +152,10 @@ export function useSiteSettings(active) {
152
152
  }
153
153
  }, [accessState, editorialBaseline]);
154
154
  return { data, error, dirty, saving, notice, load, change, save, setNotice,
155
- accessDraftPending: Boolean(accessState?.draft), discardSiteAccess };
155
+ accessDraftPending: Boolean(accessState?.draft), discardSiteAccess,
156
+ publishTarget: contentPersistenceMode() === "d1" && site().siteSettingsDocument && revision
157
+ ? { documentId: site().siteSettingsDocument.documentId,
158
+ expectedDraftRevisionId: revision.expected,
159
+ includeSiteAccess: Boolean(accessState?.draft) }
160
+ : null };
156
161
  }
@@ -15,6 +15,7 @@ import { CSS } from "@dnd-kit/utilities";
15
15
  import { ProseField, SchemaForm } from "../../form/index.js";
16
16
  import { api } from "../../index.js";
17
17
  import { pageType } from "../site.js";
18
+ import { normalizeGalleryLayout } from "../lib/gallery-layout";
18
19
  import { uniqueSlug } from "../lib/slug";
19
20
  import { proseStats } from "../lib/text";
20
21
  import { Icon } from "../ui/icons";
@@ -100,7 +101,7 @@ function imageName(src) {
100
101
  return raw;
101
102
  }
102
103
  }
103
- function GalleryCard({ item, masonry = false, sortable = true, activeId, overId, activeIndex, overIndex, onReplace, onToggle, onRemove, }) {
104
+ function GalleryCard({ item, verticalMasonry = false, sortable = true, activeId, overId, activeIndex, overIndex, onReplace, onToggle, onRemove, }) {
104
105
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
105
106
  id: item.src,
106
107
  disabled: !sortable,
@@ -108,7 +109,7 @@ function GalleryCard({ item, masonry = false, sortable = true, activeId, overId,
108
109
  const isTarget = sortable && overId === item.src && activeId !== item.src;
109
110
  const insertAfter = isTarget && activeIndex < overIndex;
110
111
  const insertBefore = isTarget && activeIndex > overIndex;
111
- const frameStyle = masonry && item.w && item.h
112
+ const frameStyle = verticalMasonry && item.w && item.h
112
113
  ? { aspectRatio: `${item.w} / ${item.h}` }
113
114
  : undefined;
114
115
  return (_jsxs("figure", { ref: setNodeRef, className: [
@@ -120,8 +121,10 @@ function GalleryCard({ item, masonry = false, sortable = true, activeId, overId,
120
121
  insertAfter ? "is-insert-after" : "",
121
122
  ].filter(Boolean).join(" "), style: sortable ? { transform: CSS.Transform.toString(transform), transition } : undefined, children: [_jsxs("div", { className: "pe-photo-frame", style: frameStyle, ...(sortable ? attributes : {}), ...(sortable ? listeners : {}), children: [_jsx("img", { src: item.src, alt: imageName(item.src), draggable: false }), item.hidden && _jsx("span", { className: "pe-hidden-label", children: "Nascosta" })] }), _jsxs("div", { className: "pe-photo-actions", children: [_jsx("button", { type: "button", onClick: onReplace, title: "Cambia immagine", "aria-label": "Cambia immagine", children: _jsx(Icon.replace, {}) }), _jsx("button", { type: "button", onClick: onToggle, title: item.hidden ? "Rimetti online" : "Nascondi dalla gallery", "aria-label": item.hidden ? "Rimetti online" : "Nascondi dalla gallery", children: item.hidden ? _jsx(Icon.eye, {}) : _jsx(Icon.eyeOff, {}) }), _jsx("button", { type: "button", className: "is-danger", onClick: onRemove, title: "Rimuovi dalla gallery", "aria-label": "Rimuovi dalla gallery", children: _jsx(Icon.trash, {}) })] }), _jsx("figcaption", { title: imageName(item.src), children: imageName(item.src) })] }));
122
123
  }
123
- function GalleryEditor({ value, onChange, pickMediaFiles, masonry = false, }) {
124
+ function GalleryEditor({ value, onChange, pickMediaFiles, layout = "grid", }) {
124
125
  const list = Array.isArray(value) ? value : [];
126
+ const masonry = layout !== "grid";
127
+ const verticalMasonry = layout === "masonry-vertical";
125
128
  const shownItems = masonry ? list.filter((item) => !item.hidden) : list;
126
129
  const hiddenItems = masonry ? list.filter((item) => item.hidden) : [];
127
130
  const [activeId, setActiveId] = useState(null);
@@ -203,9 +206,9 @@ function GalleryEditor({ value, onChange, pickMediaFiles, masonry = false, }) {
203
206
  ? { ...item, hidden: item.hidden ? undefined : true }
204
207
  : item));
205
208
  const remove = (src) => onChange(list.filter((item) => item.src !== src));
206
- return (_jsxs("section", { className: `pe-gallery${masonry ? " is-masonry" : ""}`, "aria-label": "Foto della gallery", children: [_jsxs("div", { className: "pe-gallery-bar", children: [_jsxs("div", { className: "pe-gallery-summary", children: [_jsxs("strong", { children: [list.length, " ", list.length === 1 ? "foto" : "foto"] }), _jsx("span", { children: list.length === 0
209
+ return (_jsxs("section", { className: `pe-gallery${masonry ? ` is-${layout}` : ""}`, "aria-label": "Foto della gallery", children: [_jsxs("div", { className: "pe-gallery-bar", children: [_jsxs("div", { className: "pe-gallery-summary", children: [_jsxs("strong", { children: [list.length, " ", list.length === 1 ? "foto" : "foto"] }), _jsx("span", { children: list.length === 0
207
210
  ? "Scegli le immagini da mostrare nella pagina."
208
- : `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), masonry && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })) : (_jsxs("div", { className: "pe-add-group", ref: addPositionRef, children: [_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi in cima ", _jsx(Icon.plus, {})] }), _jsx("button", { type: "button", className: "pe-add-position", "aria-label": "Scegli dove aggiungere le foto", "aria-expanded": addPositionOpen, "aria-controls": "gallery-add-position", onClick: () => setAddPositionOpen((open) => !open), children: "\u2304" }), addPositionOpen && (_jsx("div", { className: "pe-add-menu", id: "gallery-add-position", children: _jsx("button", { type: "button", onClick: () => void add("end"), children: "Aggiungi in fondo" }) }))] }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add("start"), children: [_jsx(Icon.gallery, {}), _jsx("strong", { children: "La gallery \u00E8 vuota" }), _jsx("span", { children: "Aggiungi le prime fotografie" })] })) : (_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: ({ active }) => setActiveId(String(active.id)), onDragOver: ({ over }) => setOverId(over ? String(over.id) : null), onDragCancel: () => { setActiveId(null); setOverId(null); }, onDragEnd: finishDrag, children: [_jsx(SortableContext, { items: shownItems.map((item) => item.src), strategy: rectSortingStrategy, children: _jsx("div", { className: "pe-photo-grid", children: shownItems.map((item) => (_jsx(GalleryCard, { item: item, masonry: masonry, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) }), masonry && hiddenItems.length > 0 && (_jsxs("section", { className: "pe-hidden-gallery", "aria-label": "Foto nascoste", children: [_jsxs("div", { className: "pe-hidden-gallery-head", children: [_jsx("strong", { children: "Nascoste" }), _jsx("span", { children: "Non occupano spazio nella gallery pubblica." })] }), _jsx("div", { className: "pe-hidden-grid", children: hiddenItems.map((item) => (_jsx(GalleryCard, { item: item, sortable: false, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) })] }))] }))] }));
211
+ : `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` }), layout === "masonry-vertical" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry verticale \u00B7 3 colonne \u00B7 ordine dall\u2019alto verso il basso" })), layout === "masonry-horizontal" && list.length > 0 && (_jsx("span", { className: "pe-gallery-layout-note", children: "Masonry orizzontale \u00B7 6 colonne \u00B7 ordine da sinistra a destra" }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })) : (_jsxs("div", { className: "pe-add-group", ref: addPositionRef, children: [_jsxs("button", { type: "button", className: "pe-add", onClick: () => void add("start"), children: ["Aggiungi in cima ", _jsx(Icon.plus, {})] }), _jsx("button", { type: "button", className: "pe-add-position", "aria-label": "Scegli dove aggiungere le foto", "aria-expanded": addPositionOpen, "aria-controls": "gallery-add-position", onClick: () => setAddPositionOpen((open) => !open), children: "\u2304" }), addPositionOpen && (_jsx("div", { className: "pe-add-menu", id: "gallery-add-position", children: _jsx("button", { type: "button", onClick: () => void add("end"), children: "Aggiungi in fondo" }) }))] }))] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add("start"), children: [_jsx(Icon.gallery, {}), _jsx("strong", { children: "La gallery \u00E8 vuota" }), _jsx("span", { children: "Aggiungi le prime fotografie" })] })) : (_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: ({ active }) => setActiveId(String(active.id)), onDragOver: ({ over }) => setOverId(over ? String(over.id) : null), onDragCancel: () => { setActiveId(null); setOverId(null); }, onDragEnd: finishDrag, children: [_jsx(SortableContext, { items: shownItems.map((item) => item.src), strategy: rectSortingStrategy, children: _jsx("div", { className: "pe-photo-grid", children: shownItems.map((item) => (_jsx(GalleryCard, { item: item, verticalMasonry: verticalMasonry, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) }), masonry && hiddenItems.length > 0 && (_jsxs("section", { className: "pe-hidden-gallery", "aria-label": "Foto nascoste", children: [_jsxs("div", { className: "pe-hidden-gallery-head", children: [_jsx("strong", { children: "Nascoste" }), _jsx("span", { children: "Non occupano spazio nella gallery pubblica." })] }), _jsx("div", { className: "pe-hidden-grid", children: hiddenItems.map((item) => (_jsx(GalleryCard, { item: item, sortable: false, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(itemIndex(item.src)), onToggle: () => toggle(item.src), onRemove: () => remove(item.src) }, item.src))) })] }))] }))] }));
209
212
  }
210
213
  export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }) {
211
214
  const [bodyState, setBodyState] = useState("idle");
@@ -342,7 +345,7 @@ export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pic
342
345
  (`sections[].images`) e dove la griglia esce sul sito. Non c'è più un pannello staccato in
343
346
  fondo — c'era finché le foto stavano in un campo della pagina, e insieme a lui è sparito
344
347
  il caso in cui la stessa griglia si vedeva due volte. */
345
- return (_jsxs("div", { className: "ed ed-gallery", children: [bodyState === "error" && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati della pagina dal sito." })), _jsx(GallerySectionsEditor, { value: page.sections, onChange: (sections) => onChange({ ...page, sections }), renderGallery: (images, onImages) => (_jsx(GalleryEditor, { value: images, onChange: onImages, pickMediaFiles: pickMediaFiles, masonry: page.galleryLayout === "masonry" })), pickMediaFiles: pickMediaFiles })] }));
348
+ return (_jsxs("div", { className: "ed ed-gallery", children: [bodyState === "error" && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati della pagina dal sito." })), _jsx(GallerySectionsEditor, { value: page.sections, onChange: (sections) => onChange({ ...page, sections }), renderGallery: (images, onImages) => (_jsx(GalleryEditor, { value: images, onChange: onImages, pickMediaFiles: pickMediaFiles, layout: normalizeGalleryLayout(page.galleryLayout) })), pickMediaFiles: pickMediaFiles })] }));
346
349
  }
347
350
  return (_jsxs("div", { className: "ed", children: [translationActive && (_jsx("p", { className: "pe-translation-note", children: page.translations?.[activeLanguage]?.exists || page._translations?.[activeLanguage]
348
351
  ? `Stai modificando ${activeLanguage.toUpperCase()}. I campi non traducibili restano visibili e vengono copiati dalla lingua predefinita al salvataggio.`
@@ -1,6 +1,13 @@
1
- import { type AssetFile, type AssetFolder } from "../../api.js";
1
+ import { api, type AssetFile, type AssetFolder } from "../../api.js";
2
2
  import type { MediaOperationStatus } from "../../media.js";
3
3
  import { type MediaUploadJob } from "./media-upload-queue.js";
4
+ /** D1 registra l'intera cancellazione nel preflight: non esistono né il token Git né i
5
+ * successivi gruppi R2 della corsia legacy. Tenere qui la biforcazione evita che il client
6
+ * annunci un falso errore dopo che il server ha già accettato l'operazione. */
7
+ export declare function deleteMediaKeys(keys: string[], remove: typeof api.remove, onProgress?: (done: number) => void): Promise<{
8
+ done: number;
9
+ refsToken?: string;
10
+ }>;
4
11
  export type MediaOperationState = "idle" | "planning" | "awaiting_confirmation" | "moving" | "deleting" | "renaming" | "creating_folder" | "journal";
5
12
  export type SortKey = "uploaded" | "capturedAt" | "name" | "size";
6
13
  export declare const SORT_LABELS: Record<SortKey, string>;
@@ -21,6 +21,28 @@ import { executeMediaMovePlan, mediaMoveConfirmation, mediaMoveNeedsConfirmation
21
21
  * pensa che sia bloccato. Alzarlo fa tornare il difetto: richieste lunghe che muoiono a metà. */
22
22
  const CHUNK = 10;
23
23
  const MOVE_CONFIRM_FILE_THRESHOLD = 2;
24
+ /** D1 registra l'intera cancellazione nel preflight: non esistono né il token Git né i
25
+ * successivi gruppi R2 della corsia legacy. Tenere qui la biforcazione evita che il client
26
+ * annunci un falso errore dopo che il server ha già accettato l'operazione. */
27
+ export async function deleteMediaKeys(keys, remove, onProgress = () => { }) {
28
+ const preflight = await remove({ keys }, { refsOnly: true });
29
+ if (preflight.d1 === true) {
30
+ const done = preflight.trashed ?? keys.length;
31
+ onProgress(done);
32
+ return { done };
33
+ }
34
+ if (!preflight.refsToken) {
35
+ throw new Error("Il server non ha confermato la verifica dei riferimenti.");
36
+ }
37
+ let done = 0;
38
+ for (let i = 0; i < keys.length; i += CHUNK) {
39
+ const group = keys.slice(i, i + CHUNK);
40
+ const result = await remove({ keys: group }, { skipRefs: true, refsToken: preflight.refsToken });
41
+ done += result.trashed ?? group.length;
42
+ onProgress(done);
43
+ }
44
+ return { done, refsToken: preflight.refsToken };
45
+ }
24
46
  export const SORT_LABELS = {
25
47
  uploaded: "caricamento", capturedAt: "scatto", name: "nome", size: "peso",
26
48
  };
@@ -410,17 +432,9 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
410
432
  let refsToken;
411
433
  if (keys.length) {
412
434
  setProgress({ done: 0, total: keys.length, verb: "Cestino" });
413
- const preflight = await api.remove({ keys }, { refsOnly: true });
414
- if (!preflight.refsToken)
415
- throw new Error("Il server non ha confermato la verifica dei riferimenti.");
416
- refsToken = preflight.refsToken;
417
- let done = 0;
418
- for (let i = 0; i < keys.length; i += CHUNK) {
419
- const group = keys.slice(i, i + CHUNK);
420
- const r = await api.remove({ keys: group }, { skipRefs: true, refsToken: preflight.refsToken });
421
- done += r.trashed ?? group.length;
422
- setProgress({ done, total: keys.length, verb: "Cestino" });
423
- }
435
+ const result = await deleteMediaKeys(keys, api.remove, (done) => setProgress({ done, total: keys.length, verb: "Cestino" }));
436
+ refsToken = result.refsToken;
437
+ const done = result.done;
424
438
  if (done < keys.length) {
425
439
  failure = `Cestinate ${done} foto su ${keys.length}: le altre sono ancora lì, riprova.`;
426
440
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.18",
3
+ "version": "2.2.20",
4
4
  "type": "module",
5
5
  "description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
6
6
  "license": "UNLICENSED",