@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 +31 -6
- package/dist/api.js +1 -1
- package/dist/app/App.js +29 -9
- package/dist/app/app.css +35 -19
- package/dist/app/lib/gallery-layout.d.ts +8 -0
- package/dist/app/lib/gallery-layout.js +13 -0
- package/dist/app/lib/home.d.ts +4 -0
- package/dist/app/lib/home.js +5 -1
- package/dist/app/lib/pages.d.ts +8 -0
- package/dist/app/lib/pages.js +23 -3
- package/dist/app/lib/publish.d.ts +9 -1
- package/dist/app/lib/publish.js +20 -6
- package/dist/app/lib/settings.d.ts +5 -0
- package/dist/app/lib/settings.js +6 -1
- package/dist/app/routes/PageEditor.js +9 -6
- package/dist/form/media/useMediaLibrary.d.ts +8 -1
- package/dist/form/media/useMediaLibrary.js +25 -11
- package/package.json +1 -1
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
|
|
189
|
-
*
|
|
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" })] })),
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
/*
|
|
749
|
-
|
|
750
|
-
|
|
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
|
+
}
|
package/dist/app/lib/home.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/app/lib/home.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/app/lib/pages.d.ts
CHANGED
|
@@ -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. */
|
package/dist/app/lib/pages.js
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/app/lib/publish.js
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
};
|
package/dist/app/lib/settings.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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,
|
|
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 ?
|
|
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,
|
|
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,
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
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.
|
|
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",
|